File Coverage

blib/lib/HTML/Transmorgify.pm
Criterion Covered Total %
statement 479 588 81.4
branch 151 268 56.3
condition 31 56 55.3
subroutine 86 106 81.1
pod 29 45 64.4
total 776 1063 73.0


line stmt bran cond sub pod time code
1              
2             #
3             # Setup and initialization is done with objects, but execution
4             # proceedural code using local() variables for state. This
5             # imposes a recusion model on the control flow, but allows
6             # previous states to automatically resume.
7             #
8              
9             package HTML::Transmorgify;
10              
11 8     8   265496 use strict;
  8         21  
  8         385  
12 8     8   41 use warnings;
  8         14  
  8         270  
13              
14 8     8   43 use List::Util qw(first);
  8         17  
  8         1157  
15 8     8   8754 use Image::Size;
  8         46837  
  8         636  
16 8     8   80 use Scalar::Util qw(reftype blessed);
  8         16  
  8         964  
17 8     8   14302 use File::Slurp;
  8         204411  
  8         759  
18 8     8   101 use Digest::MD5 qw(md5_hex);
  8         1580  
  8         520  
19 8     8   10534 use Data::Dumper;
  8         92888  
  8         669  
20             require Exporter;
21 8     8   6848 use Module::Load;
  8         8747  
  8         52  
22 8     8   5932 use HTML::Transmorgify::Symbols;
  8         21  
  8         24142  
23              
24             our $VERSION = 0.12;
25              
26             our @ISA = qw(Exporter);
27             our @EXPORT = qw(dangling);
28             our @EXPORT_OK = qw(
29             dstring
30             run
31             compile
32             dangling
33             continue_compile
34             capture_compile
35             queue_intercept
36             queue_capture
37             allocate_result_type
38             eat_cr
39             rbuf
40             postbuf
41             module_bits
42             boolean
43             bomb
44             %variables
45             %transformations
46             %dispatch
47             %priorities
48             @post_intercept_push
49             $xml_quoting
50             $textref
51             $rbuf
52             $debug
53             $tagset
54             $input_file
55             $input_line
56             $modules
57             $result
58             $query_param
59             $original_file
60             $original_line
61             $invocation_options
62             $process_text_ref
63             );
64              
65             our $tagset;
66             our $textref;
67             our $result;
68             our %variables;
69             our $rbuf;
70             our $modules;
71             our $debug = 0;
72             our %compiled; # cache of compiled text -> $rbuf
73             our $intercept_okay = 0;
74             our $input_file;
75             our $input_line;
76             our $original_file;
77             our $original_line;
78             our $xml_quoting = 0;
79             our @result_array;
80             our %dispatch;
81             our %priorities;
82             our %queued_intercepts;
83             our @queued_captures;
84             our @post_intercept_push;
85             our $invocation_options;
86             our $wrap_compile_cb;
87             our $process_text_ref;
88              
89             our %result_index = ( text => 0, script => 1 );
90             our %reverse_result_index = reverse %result_index;
91             our $result_key_count = 2;
92              
93             our $query_param;
94              
95             my %base_tags;
96              
97             #### PUBLIC FUNCTIONS
98              
99             sub allocate_result_type
100             {
101 4     4 1 8 my ($type) = @_;
102 4 50       12 return $result_index{$type} if defined $result_index{$type};
103 4         10 $result_index{$type} = $result_key_count;
104 4         15 $reverse_result_index{$result_key_count} = $type;
105 4         11 return $result_key_count++;
106             }
107              
108             sub rbuf
109             {
110 210 50   210 1 709 die if grep { ref($_) && reftype($_) ne 'CODE' } @_;
  210 50       1645  
111 210         1268 push (@$rbuf, @_);
112             }
113              
114             sub postbuf
115             {
116 2     2 1 5 push (@post_intercept_push, @_);
117             }
118              
119             #
120             # True if defined and true or defined and empty
121             # False if 0 or
122             #
123             sub boolean
124             {
125 286     286 1 360 my ($b, $default) = @_;
126 286 100       1886 return $default unless defined $b;
127 11         29 $b = lc($b);
128 11 50       33 return 0 if $b eq 'false';
129 11 50       36 return 0 if $b eq 'no';
130 11 50       30 return 0 if $b eq 'off';
131 11 50       27 return 1 if $b eq '';
132 11 100       65 return 1 if $b;
133 2         11 return 0;
134             }
135              
136             #### METHODS
137              
138             sub new
139             {
140 7     7 0 1581 my ($pkg, %opts) = @_;
141 7         56 my $self = bless {
142             tagset => new_hash(%base_tags),
143             modules => 1,
144             packages => {},
145             modules => '',
146             options => \%opts,
147             pre_compile_cb => [],
148             }, $pkg;
149 7         28 return $self;
150             }
151              
152             my $module_count = 0;
153             my %module_bits;
154              
155             sub module_bits
156             {
157 19     19 0 50 my ($pkg) = @_;
158 19 100       80 $pkg = ref($pkg) if ref($pkg);
159 19 100       77 return $module_bits{$pkg} if defined $module_bits{$pkg};
160             #print STDERR "# Allocating module bits for $pkg (at $module_count)\n";
161 15         43 $module_bits{$pkg} = '';
162 15         73 vec($module_bits{$pkg}, $module_count++, 1) = 1;
163 15         62 return $module_bits{$pkg};
164             }
165              
166             sub intercept_exclusive
167             {
168 4     4 0 49 my ($self, $tobj, $tag_pkg, $priority, %tags) = @_;
169 4         39 $self->intercept($tobj, $tag_pkg, %tags);
170 4         77 for my $t (keys %tags) {
171 68 50       145 if (! $dispatch{$t}) {
    0          
172 68         161 $dispatch{$t} = HTML::Transmorgify::Exclusive->new($tag_pkg);
173             } elsif ($dispatch{$t}->exclusive) {
174 0         0 $dispatch{$t} = HTML::Transmorgify::MutuallyExclusive->more($tag_pkg);
175             } else {
176 0         0 die;
177             }
178             }
179             }
180              
181             sub intercept_shared
182             {
183 7     7 0 40 my ($self, $tobj, $tag_pkg, $priority, %tags) = @_;
184 7 50 33     53 die if $priorities{$tag_pkg} && $priorities{$tag_pkg} != $priority;
185 7         22 $priorities{$tag_pkg} = $priority;
186 7         63 $self->intercept($tobj, $tag_pkg, %tags);
187 7         26 for my $t (keys %tags) {
188 29 50       74 if (! $dispatch{$t}) {
    0          
189 29         98 $dispatch{$t} = HTML::Transmorgify::Stack->new($tag_pkg);
190             } elsif ($dispatch{$t}->exclusive) {
191 0         0 die;
192             } else {
193 0         0 $dispatch{$t} = HTML::Transmorgify::Stack->more($tag_pkg);
194             }
195             }
196             }
197              
198             sub intercept_pre_compile
199             {
200 4     4 0 9 my ($self, $cb) = @_;
201 4         11 push(@{$self->{pre_compile_cb}}, $cb);
  4         20  
202             }
203              
204             sub queue_capture
205             {
206 8     8 1 13 my ($cb) = @_;
207 8         34 push(@queued_captures, $cb);
208             }
209              
210             sub queue_intercept
211             {
212 11     11 1 55 my ($tag_pkg, %new) = @_;
213 37         106 my @k = $tag_pkg
214 11 50       48 ? (map { "$tag_pkg $_" } keys %new)
215             : (keys %new);
216 11         92 @queued_intercepts{@k} = values %new;
217             }
218              
219             sub intercept
220             {
221 11     11 0 55 my ($self, $tobj, $tag_pkg, %new) = @_;
222 11         20 my %opts;
223 11 100       61 if (ref $_[0]) {
224 8         35 %opts = %{shift(@_)};
  8         73  
225             }
226 11         21 my $ts;
227 11 50       40 if (ref $tobj) {
    0          
228 11         77 $tobj->{modules} |= $self->module_bits;
229 11         32 $ts = $tobj->{tagset};
230             } elsif ($intercept_okay) {
231 0         0 $modules |= $self->module_bits;
232 0         0 $ts = $tagset;
233             } else {
234 0         0 die;
235             }
236 97         219 my @k = $tag_pkg
237 11 50       103 ? (map { "$tag_pkg $_" } keys %new)
238             : (keys %new);
239 11         40 my %old = map { $_ => $ts->{$_} } @k;
  97         257  
240 11         97 @$ts{@k} = values %new;
241 11         89 return %old;
242             }
243              
244 0     0 0 0 sub add_tags { die "must redefine" }
245              
246             sub mixin
247             {
248 7     7 0 51 my ($self, $module) = @_;
249 7         44 load $module;
250 7         623 $module->add_tags($self);
251             }
252              
253             sub process
254             {
255 37     37 0 98839 my $self = shift;
256 37 50       260 die unless blessed $self;
257 37         128 local($tagset) = $self->{tagset};
258 37         124 local($modules) = $self->{modules};
259 37         95 local($process_text_ref) = \$_[0];
260 37         92 local($intercept_okay) = 1;
261 37         53 shift;
262 37         79 local($invocation_options) = {};
263 37 50       137 $invocation_options = shift if ref $_[0];
264 37         307 local(%variables) = @_;
265 37   50     265 local($query_param) = $invocation_options->{query_param} || {};
266 37   33     223 local($original_file) = local($input_file) = $invocation_options->{input_file} || (caller())[1];
267 37   33     187 local($original_line) = local($input_line) = $invocation_options->{input_line} || (caller())[2];
268 37         164 local($xml_quoting) = first_key('xml_quoting', 0, $invocation_options, $self->{options});
269 37         159 $_->($self) for @{$self->{pre_compile_cb}};
  37         206  
270 37         134 my $buf = compile($modules, $process_text_ref);
271 37         117 local(@result_array) = ( '' );
272             #print Dumper([__FILE__, __LINE__, $rbuf]) if $debug;
273 37         98 run($buf);
274 37 50       116 return map { $_ => $result_array[$result_index{$_}] } keys %result_index
  0         0  
275             if wantarray;
276 37         404 return $result_array[0];
277             }
278              
279              
280             #### (SEMI)PRIVATE FUNCTIONS
281              
282             sub run
283             {
284 306     306 1 388 my $buf = shift;
285              
286 306 100       961 return run($buf, \@result_array) unless $_[0];
287              
288 180         254 local $result = shift;
289              
290 180         336 for my $i (@$buf) {
291 581 100       1630 if (ref $i) {
292 8     8   80 use Data::Dumper;
  8         16  
  8         1871  
293 280 50       864 die Dumper($buf) unless reftype($i) eq 'CODE';
294 280         881 $i->();
295             } else {
296 301 50       576 printf STDERR "# Appending %s\n", dstring($i) if $debug;
297 301         1356 $result->[0] .= $i;
298             }
299             }
300             }
301              
302             sub first_key
303             {
304 37     37 0 105 my ($key, $default, @hashes) = @_;
305 37         136 for my $h (@hashes) {
306 74 100       285 next unless exists $h->{$key};
307 6         31 return $h->{$key};
308             }
309 31         95 return $default;
310             }
311              
312             sub dstring
313             {
314 8     8   45 use Carp qw(confess);
  8         17  
  8         39468  
315 0     0 0 0 my ($s, @pos) = @_;
316 0 0       0 return "UNDEF" unless defined $s;
317 0 0       0 confess() if grep { $_ > length($s) } @pos; # XXX
  0         0  
318 0         0 substr($s, $_, 0) = "*##*" for (reverse sort { $a <=> $b } @pos);
  0         0  
319 0         0 $s =~ s/\n/\\n/g;
320 0         0 return $s;
321             }
322              
323             sub eat_cr
324             {
325 89     89 1 165 my $o = pos($$textref);
326 89         674 $$textref =~ /\G\n/gcs;
327 89         136 my $n = pos($$textref);
328 89 50       412 printf STDERR "# EAT_CR %s\n", dstring($$textref, $o, $n) if $debug;
329             }
330              
331             sub compile
332             {
333 73     73 1 565 my $cacheline = shift;
334 73         111 local $textref = shift;
335              
336 73 50       201 printf STDERR "# Invoking compile(%s, %s) for %s\n", tobits($cacheline), scalar(%$tagset), dstring($$textref, 0) if $debug;
337 73         103 my $md5;
338 73 50       197 confess() unless defined $$textref;
339 73         407 $md5 = md5_hex($$textref);
340 73         196 my $cached = $compiled{$cacheline}{$md5};
341 73 100       178 if ($cached) {
342 1 50       4 print STDERR "# returning cached result\n" if $debug;
343 1         4 return $cached;
344             }
345 72         130 local($rbuf) = \my @rbuf;
346 72         225 pos($$textref) = 0;
347             my $ccb = sub {
348 72     72   187 continue_compile(undef, undef, undef);
349 72         297 };
350 72 50       165 if ($wrap_compile_cb) {
351 0         0 local($wrap_compile_cb);
352 0         0 $wrap_compile_cb->($ccb);
353             } else {
354 72         136 $ccb->();
355             }
356 72         274 $compiled{$cacheline}{$md5} = \@rbuf;
357 72 50       185 printf STDERR "# Done compile(%s, %s) now at %d\n", tobits($cacheline), scalar(%$tagset), pos($$textref) if $debug;
358 72         627 return $rbuf;
359             }
360              
361             sub capture_compile
362             {
363 65     65 1 125 my $onetag = $_[0];
364 65 50       167 die unless $onetag;
365 65         346 local($dispatch{"/$onetag"}) = HTML::Transmorgify::Deferred->new($dispatch{"/$onetag"});
366 65         164 my $buf = [];
367             {
368 65         87 local($rbuf) = $buf;
  65         103  
369 65         280 continue_compile(@_);
370             }
371 65 100       3368 return $buf unless wantarray;
372 10         36 return ($buf, $dispatch{"/$onetag"});
373             }
374              
375             my $no_opts = {};
376              
377             sub continue_compile
378             {
379 148     148 1 441 my ($onetag, $starting_attr, $opts, %tags) = @_;
380 148   66     523 $opts ||= $no_opts;
381 106         357 my @ks = $opts->{tag_package}
382 148 100       648 ? (map { "$opts->{tag_package} $_" } keys %tags)
383             : (keys %tags);
384              
385 148 50       391 print STDERR "# overriding ".join(';', @ks)."\n" if $debug;
386 148         477 local(@$tagset{@ks}) = values %tags;
387 148         571 my $start = pos($$textref);
388 148 50 0     310 printf STDERR "# Invoking continue_compile(%s/%s) at %d for %s from %d\n", $onetag || '?', scalar(%$tagset), $start, dstring($$textref, $start), (caller())[2] if $debug;
389 148 100       284 if ($onetag) {
390 76         369 local($dispatch{"/$onetag"}) = HTML::Transmorgify::CloseTag->new($dispatch{"/$onetag"});
391 76         659 my $finaltag = do_compile();
392 76 50 33     761 bomb("Could not find closing ", starting_attr => $starting_attr)
393             unless defined($finaltag) && $finaltag eq "/$onetag";
394             } else {
395 72         158 do_compile();
396             }
397 148 50 0     702 printf STDERR "# Done continue_compile(%s/%s) at %d, now at %d\n", $onetag || '?', scalar(%$tagset), $start, pos($$textref) if $debug;
398             }
399              
400             sub do_compile
401             {
402 148     148 0 378 my $copied = pos($$textref);
403 148 50       339 print STDERR "# starting compile, pos = $copied\n" if $debug;
404 148         419 while (pos($$textref) < length($$textref)) {
405 328 50       1029 printf STDERR "## pos = %d\n", pos($$textref) if $debug;
406 328         1160 $$textref =~ m{ \G [^<]+ }xgc;
407 328         600 my $before = pos($$textref);
408 328 100       1261 unless ($$textref =~ m{ \G < ( /? [^>\s]+ ) }xgc) {
409 22         37 $$textref =~ m{ \G < }xgc;
410 22         75 next;
411             }
412 306         621 my $tag = $1;
413 306         742 $$textref =~ m{ \G \s+ }xgc;
414 306 100       915 if ($dispatch{$tag}) {
    100          
415 294         631 my $boring = substr($$textref, $copied, $before-$copied);
416              
417 294 100       625 if ($before-$copied) {
418 199 50       403 printf STDERR "# pushing pre-tag stuff %d-%d: %s (%s)\n", $copied, $before, dstring($boring), dstring($$textref, $copied, $before) if $debug;
419 199         454 push(@$rbuf, $boring);
420             }
421              
422 294         435 my @atvals;
423 294         1523 while ( $$textref =~ m{
424             \G
425             ([\w\.]+)
426             (?:
427             =
428             (?: ([\w\.]*) | '([^']+)' | "([^"]+)" )
429             )?
430             (?=[\s>])
431             }xgc
432             ) {
433 299         545 my $name = $1;
434 299     849   1858 my $val = (first { defined $_} ($2, $3, $4));
  849         1462  
435 299         1184 push(@atvals, $name => $val);
436 299         1404 $$textref =~ m{ \G \s+ }xgc;
437             }
438 294         843 $$textref =~ m{ \G (/?) > }xgc;
439              
440 294         531 my $closed = $1;
441              
442 294 50       691 printf STDERR "# callback for %s at %d: %s\n", $tag, $copied, dstring($$textref, $copied, pos($$textref)) if $debug;
443 294         1008 my $attr = HTML::Transmorgify::Attributes->new($tag, \@atvals, $closed);
444 294         1207 my $r = $dispatch{$tag}->call($tag, $attr, $closed);
445 294 100 66     947 if ($r && $r == 22) {
446 76 50       174 printf STDERR "# %s indicates - done wih compile() pos is %d\n", $tag, pos($$textref) if $debug;
447 76         465 return $tag;
448             }
449 218 50       414 printf STDERR "# continuing compile at %d: %s\n", pos($$textref), dstring($$textref, $copied, pos($$textref)) if $debug;
450 218         1674 $copied = pos($$textref);
451             } elsif ($dispatch{macro}) {
452 9 100       74 if ($$textref =~ m{ \G (?: [^'">] | '[^'<>]*' | "[^"<>]*" )* > }xgc) {
453             # easy skip
454 6 50       21 printf STDERR "# advancing past tag with no callback & no macros (%s), now at %d: %s\n", $tag, pos($$textref), dstring($$textref, pos($$textref)) if $debug;
455             } else {
456 3 50       8 printf STDERR "# Tag with <> inside quotes found (%s) %s\n", pos($$textref), dstring($$textref, pos($$textref)) if $debug;
457 3         5 my @atvals;
458 3         21 while ( $$textref =~ m{
459             \G
460             ([\w\.]+)
461             (?:
462             =
463             (?: ([\w\.]*) | '([^']+)' | "([^"]+)" )
464             )?
465             (?=[\s>])
466             }xgc
467             ) {
468 3         7 my $name = $1;
469 3     9   23 my $val = (first { defined $_} ($2, $3, $4));
  9         17  
470 3         12 push(@atvals, $name => $val);
471 3         14 $$textref =~ m{ \G \s+ }xgc;
472             }
473 3         10 $$textref =~ m{ \G (/?) > }xgc;
474 3         7 my $closed = $1;
475              
476 3 50       7 if (grep { /
  6         22  
477 3 50       8 printf STDERR "# There are calls in the tag, compiling\n" if $debug;
478 3         7 my $boring = substr($$textref, $copied, $before-$copied);
479              
480 3 50       10 if ($before-$copied) {
481 0 0       0 printf STDERR "# Pushing pre-tag stuff %d-%d: %s (%s)\n", $copied, $before, $boring, dstring($$textref, $copied, $before) if $debug;
482 0         0 push(@$rbuf, $boring);
483             }
484 3         9 my $attr = HTML::Transmorgify::Attributes->new($tag, \@atvals, $closed);
485 3     3   12 push(@$rbuf, sub { $result->[0] .= "$attr" });
  3         51  
486 3         6 $copied = pos($$textref);
487 3 50       10 printf STDERR "# Continuing compile at %d\n", pos($$textref) if $debug;
488             } else {
489 0 0       0 printf STDERR "# advancing past tag with no macros (%s), now at %d: %s\n", $tag, pos($$textref), dstring($$textref, pos($$textref)) if $debug;
490             }
491             }
492             } else {
493             # advance to the end of the tag
494 3         27 $$textref =~ m{ \G (?: [^'">] | '[^']*' | "[^"]*" )* > }xgc;
495 3 50       16 printf STDERR "# Advancing past tag with no callback (%s), now at %d: %s\n", $tag, pos($$textref), dstring($$textref, pos($$textref)) if $debug;
496             }
497 230 100 100     2742 if (@$rbuf > 1 && ! ref($rbuf->[-1]) && ! ref($rbuf->[-2])) {
      100        
498 36         151 $rbuf->[-2] .= pop(@$rbuf);
499             }
500             }
501 72         154 my $boring = substr($$textref, $copied);
502 72 50       163 printf STDERR "# pushing final stuff %d-%d: %s (%s)\n", $copied, length($$textref), $boring, dstring($$textref, $copied) if $debug;
503 72 100       236 push(@$rbuf, $boring) if length($boring);
504 72         277 return;
505             }
506              
507             sub bomb
508             {
509 0     0 0 0 my ($message, %context) = @_;
510 0         0 my $c = '';
511 0 0       0 if ($context{attr}) {
512 0         0 $c .= sprintf(" at <%s> from at %s, line %d",
513             $context{starting_attr}->tag,
514             $context{starting_attr}->location,
515             );
516             }
517 0 0       0 if ($context{starting_attr}) {
518 0         0 $c .= sprintf(" from <%s> starting at %s, line %d",
519             $context{starting_attr}->tag,
520             $context{starting_attr}->location,
521             );
522             }
523 0   0     0 my $clev = $context{caller_level} || 0;
524 0         0 die sprintf("Erorr: %s%s at %s:%d\n", $message, $c, (caller($clev))[1], (caller($clev))[2]);
525             }
526              
527             sub dangling
528             {
529 0     0 0 0 my ($attr, $closed) = @_;
530 0         0 bomb(sprintf("<%s> found without a preceeding start tag in %s:%d", $attr->tag, $input_file, $input_line));
531             }
532              
533             sub tobits
534             {
535 0     0 0 0 join('', unpack("b*", $_[0]));
536             }
537              
538             package HTML::Transmorgify::Attributes;
539              
540 8     8   100 use strict;
  8         19  
  8         315  
541 8     8   43 use warnings;
  8         184  
  8         336  
542 8     8   47 use HTML::Transmorgify::Symbols;
  8         11  
  8         1571  
543              
544             import HTML::Transmorgify qw($tagset $textref $debug run dstring $rbuf $input_file $input_line $xml_quoting module_bits compile rbuf);
545              
546             our @rtmp;
547             our %tagset_hash;
548              
549             my $module_bits = module_bits('tag expand');
550              
551             #
552             # $atvals are pairs representing the attributes.
553             # a value of undef indicates that the attribute
554             # didn't have a value at all. For example:
555             #
556             #
557             #
558             # Boolean values like "selected", "checked", etc
559             # are represented by having an undef value internally.
560             # If you request their value though, they return their
561             # own name. get('selected') will return 'selected'.
562             #
563             # This mans you should not set values to the return value from
564             # get!
565             #
566              
567             sub new
568             {
569 300     300 0 1131 my ($pkg, $tag, $atvals, $closed) = @_;
570              
571 8     8   6718 use integer;
  8         74  
  8         49  
572              
573 300         468 my $dbug = $HTML::Transmorgify::debug;
574              
575 300         437 my $numattr = scalar(@$atvals)/2;
576              
577 300         445 my @callbacks;
578              
579             my $lastpos;
580             {
581 300         321 my $i = 1;
  300         347  
582 300   100     1384 while ($i <= @$atvals && ! defined($atvals->[$i])) {
583 99         754 $i += 2;
584             }
585 300         579 $lastpos = ($i - 3) / 2;
586             }
587              
588 300         373 my %vals;
589 300         741 for (my $j = 0; $j < @$atvals; $j+=2) {
590 311         1309 $vals{lc($atvals->[$j])} = $atvals->[$j+1];
591             }
592              
593 300         919 my %needs_cooking = map { $_ => scalar($vals{$_} =~ /<\w+\s/) } grep { defined($vals{$_}) } keys %vals;
  196         721  
  311         4527  
594              
595 300         424 my %cooked;
596             my @hidden;
597 0         0 my %hidden;
598              
599             my $f_raw = sub {
600 278     278   405 my ($at, $pos) = @_;
601 278 100 100     729 if (defined($pos) && $pos <= $lastpos) {
602 2         8 return $atvals->[$pos*2];
603             }
604 276 100       4050 if (exists $vals{$at}) {
605 46 50       343 return $vals{$at} if defined $vals{$at};
606 0         0 return $at; # boolean
607             }
608 230         955 return;
609 300         1486 };
610              
611             my $f_get = sub {
612 660     660   951 my ($at, $pos) = @_;
613 660 100 100     1639 if (defined($pos) && $pos <= $lastpos) {
614 118         514 return $atvals->[$pos*2];
615             }
616 542 100       1910 return unless exists $vals{$at};
617 329 100       611 unless ($needs_cooking{$at}) {
618 310 100       1252 return $vals{$at} if defined $vals{$at};
619 3         9 return $at; # boolean
620             }
621              
622 19 50       33 printf "# Cooking %s for get attr=%s\n", HTML::Transmorgify::dstring($vals{$at}), $at if $dbug;
623              
624 19 100       39 unless ($cooked{$at}) {
625 9         20 $cooked{$at} = compile($HTML::Transmorgify::modules, \$vals{$at});
626             }
627              
628 19         49 local(@rtmp) = ( '' );
629 19         50 run($cooked{$at}, \@rtmp);
630              
631 8     8   3913 use Data::Dumper;
  8         25  
  8         14450  
632 19 50       42 print STDERR Dumper([ __FILE__, __LINE__, $cooked{$at}]) if $dbug;
633              
634 19 50       37 printf "# get(%s) = '%s'\n", $at, dstring($rtmp[0]) if $dbug;
635              
636 19 50       37 die if @rtmp > 1;
637              
638 19         82 return $rtmp[0];
639 300         1520 };
640              
641             my $f_static = sub {
642 120     120   197 my ($at, $pos) = @_;
643 120 100 100     513 if (defined($pos) && $pos <= $lastpos) {
644 44         197 return $atvals->[$pos*2];
645             }
646 76 100       379 return unless exists $vals{$at};
647 2 50       9 return $at unless defined $vals{$at}; # boolean
648 2 50       17 return $vals{$at} unless $needs_cooking{$at};
649 0         0 return;
650 300         1218 };
651              
652             # stringify
653             my $f_stringify = sub {
654 108     108   142 my ($self) = @_;
655 108         113 my $rv;
656 108         190 for my $cb (@callbacks) {
657 0         0 my $res = $cb->($self);
658 0 0       0 if (defined $res) {
659 0 0       0 die "multiple callbacks providing results for $tag" if defined $rv;
660 0         0 $rv = $res;
661             }
662             }
663 108 50       205 return $rv if defined $rv;
664 108         217 my $text = "<$tag";
665 108         248 for(my $i = 0; $i <= $lastpos; $i++) {
666 0 0       0 next if defined($hidden[$i]);
667 0         0 $text .= " " . _safe($atvals->[$i*2]);
668             }
669 108         247 for(my $j = $lastpos+1; $j < $numattr; $j++) {
670 222         341 my $a = $atvals->[$j*2];
671 222 100       457 next if defined($hidden{$a});
672 200 100       384 if (defined($atvals->[$j*2+1])) {
673 182         438 $text .= " $a=" . _safe($f_get->($atvals->[$j*2]), 1);
674             } else {
675 18         49 $text .= " $a";
676             }
677             }
678             # use Scalar::Util qw(refaddr);
679             # $text .= ' refaddr="' . refaddr($atvals) . '"' if $dbug;
680              
681 108         132 $text .= ">";
682 108 50       201 printf STDERR "# tag text = '%s'\n", dstring($text) if $dbug;
683 108         380 return $text;
684 300         2927 };
685              
686             my $f_hide_position = sub {
687 0     0   0 @hidden[@_] = @_;
688 300         1158 };
689              
690             my $f_set = sub {
691 34     34   128 while (my ($k, $v) = splice(@_, 0, 2)) {
692 34 100       66 unless (exists $vals{$k}) {
693 20         43 push(@$atvals, $k, $v);
694 20         26 $numattr++;
695             }
696 34 50       62 print STDERR "# Setting $tag attribute $k = '$v'\n" if $dbug;
697 34         164 $vals{$k} = $v;
698             }
699 300         1283 };
700              
701 300         408 my $invoking_textref = $textref;
702 300         462 my $invoking_pos = pos($$textref);
703 300         774 my $invoking_file = $input_file;
704 300         363 my $invoking_line = $input_line;
705 300         365 my $lines_in;
706              
707             my $f_location = sub {
708 0   0 0   0 $lines_in ||= (substr($$invoking_textref, 0, $invoking_pos) =~ tr/\n/\n/);
709 0         0 ($invoking_file, $invoking_line + $lines_in)
710 300         990 };
711              
712 300         634 my $eval_at_runtime = grep { $_ } values %needs_cooking;
  196         382  
713              
714             return bless [
715             $f_raw, # 0
716             $f_get, # 1
717             $f_stringify, # 2
718             $closed, # 3
719             $f_static, # 4
720             \%vals, # 5
721 30     30   110 sub { @hidden[@_] = @_ }, # 6
722             $lastpos, # 7
723 101     101   488 sub { @hidden{@_} = @_ }, # 8
724 300         4119 $f_set, # 9
725             $tag, # 10
726             $f_location, # 11
727             $eval_at_runtime, # 12
728             \%needs_cooking, # 13
729             \@callbacks, # 14
730             ], $pkg;
731             }
732              
733             # XXX add runtime pre-stringify callback funcs
734              
735 278     278 1 343 sub raw { my $self = shift; $self->[0]->(@_) };
  278         1013  
736 478     478 1 565 sub get { my $self = shift; $self->[1]->(@_) };
  478         1228  
737 108     108 1 1036 sub as_string { my $self = shift; $self->[2]->($self, @_) };
  108         242  
738 0     0 1 0 sub closed { my $self = shift; $self->[3] };
  0         0  
739 120     120 1 172 sub static { my $self = shift; $self->[4]->(@_) };
  120         410  
740 166     166 1 222 sub vals { my $self = shift; $self->[5] };
  166         499  
741 30     30 1 48 sub hide_position { my $self = shift; $self->[6]->(@_) };
  30         90  
742 37 100   37 1 286 sub last_position { my $self = shift; return @_ ? ($_[0] <= $self->[7]) : $self->[7] };
  37         348  
743 101     101 1 99 sub hide { my $self = shift; $self->[8]->(@_) };
  101         160  
744 34     34 1 40 sub set { my $self = shift; $self->[9]->(@_) };
  34         77  
745 98     98 1 141 sub tag { my $self = shift; $self->[10] };
  98         588  
746 0     0 1 0 sub location { my $self = shift; $self->[11]->(@_) };
  0         0  
747 0     0 1 0 sub needs_cooking { my $self = shift; $self->[13] };
  0         0  
748 0     0 1 0 sub output_callback { my $self = shift; push(@{$self->[14]}, @_); $self->[12] = 2 };
  0         0  
  0         0  
  0         0  
749              
750             sub eval_at_runtime
751             {
752 43     43 1 40 my $self = shift;
753 43         52 my $r = $self->[12];
754 43 50       289 $self->[12] = $_[0] if @_;
755 43         84 return $r;
756             }
757              
758             sub boolean
759             {
760 223     223 1 476 my ($self, $name, $pos, $default, %opts) = @_;
761 223 100       829 my $b = $opts{raw}
762             ? $self->raw($name, $pos, %opts)
763             : $self->get($name, $pos, %opts);
764 223 100       623 $default = 0 unless defined $default;
765 223         594 return HTML::Transmorgify::boolean($b, $default);
766             }
767              
768             sub static_action
769             {
770 0     0 1 0 my ($attr, $tag, $sub) = @_;
771 0 0       0 my @tags = ref($tag) ? @$tag : $tag;
772 0         0 for my $t (@tags) {
773 0 0 0     0 unless ($attr->static($t) || ! defined $attr->raw($t)) {
774 0         0 rbuf($sub);
775 0         0 return;
776             }
777             }
778 0         0 $sub->(1);
779             }
780            
781             sub add_to_result
782             {
783 95     95 1 107 my $self = shift;
784 95 100       234 if ($self->[12]) {
785 43     43   173 rbuf(sub { $HTML::Transmorgify::result->[0] .= $self->as_string });
  43         77  
786             } else {
787 52         123 push(@$HTML::Transmorgify::rbuf, $self->as_string);
788             }
789             }
790              
791             use overload
792 8         84 '""' => \&as_string,
793 8     8   54 ;
  8         18  
794              
795             sub _safe
796             {
797 182     182   236 my ($val, $is_val) = @_;
798              
799 182 50 100     1331 if (! defined($val)) {
    100          
    50          
800 0         0 return '""';
801             } elsif ($val !~ /[^\w.]/ && ! ($is_val && $xml_quoting)) {
802 6         35 return $val;
803             } elsif ($val =~ /'/) {
804 0         0 return qq{'$val'};
805             } else {
806 176         815 return qq{"$val"};
807             }
808            
809             }
810              
811              
812             package HTML::Transmorgify::MutuallyExclusive;
813              
814 8     8   1925 use strict;
  8         17  
  8         271  
815 8     8   36 use warnings;
  8         12  
  8         10547  
816             import HTML::Transmorgify qw($debug);
817              
818             sub call
819             {
820 0     0   0 my $self = shift;
821 0         0 my $tag = shift;
822 0         0 my $attr = shift;
823 0         0 my $i = 0;
824 0 0       0 print STDERR "Callback MUTUALLY EXCLUSIVE for $tag\n" if $debug;
825 0         0 while ($i < @$self) {
826 0         0 my $cb = $HTML::Transmorgify::tagset->{"$self->[$i] $tag"};
827 0         0 $i++;
828 0 0       0 next unless $cb;
829 0         0 my $rv = $cb->($attr, @_);
830 0         0 while ($i < @$self) {
831 0         0 my $cb2 = $HTML::Transmorgify::tagset->{"$self->[$i] $tag"};
832 0         0 $i++;
833 0 0       0 die if $cb2;
834             }
835 0 0       0 if ($rv) {
836 0 0       0 printf STDERR "# Will interpolate $tag later, current value is $attr\n" if $debug;
837 0         0 $attr->add_to_result;
838             }
839 0         0 return 0;
840             }
841 0         0 $attr->add_to_result;
842 0         0 return 0;
843             }
844              
845 0     0   0 sub exclusive { 1 };
846              
847             sub new
848             {
849 0     0   0 my ($pkg, @tags) = @_;
850 0         0 return bless \@tags, $pkg;
851             }
852              
853             sub more
854             {
855 0     0   0 my ($self, @tags) = @_;
856 0         0 push(@$self, @tags);
857             }
858              
859             package HTML::Transmorgify::Exclusive;
860              
861 8     8   57 use strict;
  8         14  
  8         275  
862 8     8   39 use warnings;
  8         15  
  8         3276  
863             import HTML::Transmorgify qw($debug);
864              
865             sub new
866             {
867 68     68   97 my ($pkg, $tag_pkg) = @_;
868 68         274 return bless \$tag_pkg, $pkg;
869             }
870              
871             sub call
872             {
873 143     143   200 my $self = shift;
874 143         305 my $tag = shift;
875 143         184 my $attr = shift;
876 143 50       385 print STDERR "# Callback EXCLUSIVE for $tag\n" if $debug;
877 143         420 my $cb = $HTML::Transmorgify::tagset->{"$$self $tag"};
878 143 50       372 unless ($cb) {
879 0 0       0 print STDERR "# No <$$self $tag> callback\n" if $debug;
880 0         0 push(@$rbuf, "$attr");
881 0         0 return 0;
882             }
883 143         1046 my $rv = $cb->($attr, @_);
884 143 50       438 if ($rv) {
    50          
885 0         0 $attr->add_to_result;
886 0 0       0 printf STDERR "# Including exclusive attribute for $attr\n" if $debug;
887             } elsif ($debug) {
888 0 0       0 printf STDERR "# NOT Including exclusive attribute for $attr\n" if $debug;
889             }
890 143         305 return 0;
891             }
892              
893 0     0   0 sub exclusive { 1 };
894              
895             sub more
896             {
897 0     0   0 my ($self, $tag) = @_;
898 0         0 return HTML::Transmorgify::MutuallyExclusive->new($$self, $tag);
899             }
900              
901             package HTML::Transmorgify::Stack;
902              
903 8     8   43 use strict;
  8         16  
  8         238  
904 8     8   54 use warnings;
  8         14  
  8         5920  
905             import HTML::Transmorgify qw(%priorities $debug continue_compile capture_compile rbuf);
906              
907             #
908             # Tags for shared callbacks are always included in the output stream
909             #
910              
911             sub call
912             {
913 95     95   119 my $self = shift;
914 95         118 my $tag = shift;
915 95         94 my $attr = shift;
916 95         98 my $i = 0;
917 95         135 local(%HTML::Transmorgify::queued_intercepts);
918 95         123 local(@HTML::Transmorgify::queued_captures);
919 95         101 local(@HTML::Transmorgify::post_intercept_push);
920              
921 95 50       840 print STDERR "Callback STACK for $tag\n" if $debug;
922 95         94 my @rt_callback;
923 95         203 while ($i < @$self) {
924 95         316 my $cb = $HTML::Transmorgify::tagset->{"$self->[$i] $tag"};
925 95         97 $i++;
926 95 100       178 unless ($cb) {
927 12 50       25 print STDERR "NO callback for ".$self->[$i-1]." <$tag>\n" if $debug;
928 12         31 next;
929             }
930 83 50       139 print STDERR "Calling ".$self->[$i-1]." <$tag>...\n" if $debug;
931 83         268 my $r = $cb->($attr, @_);
932 83 50 33     320 if (ref($r) && ref($r) eq 'CODE') {
933 0         0 push(@rt_callback, $r);
934             }
935             }
936 95 50       389 if (@rt_callback) {
937 0     0   0 rbuf (sub { $_->($attr) for @rt_callback });
  0         0  
938 0         0 $attr->eval_at_runtime(1);
939             }
940 95         218 $attr->add_to_result;
941 95 50       239 printf STDERR "# Including attribute for $attr\n" if $debug;
942 95 100       403 if (@HTML::Transmorgify::queued_captures) {
    100          
943 8 50       18 print STDERR "# Capturing to /$tag with queued intercepts in play: ".join(';', keys %HTML::Transmorgify::queued_intercepts)."\n" if $debug;
944 8         37 my ($b, $deferred) = capture_compile($tag, $attr, undef, %HTML::Transmorgify::queued_intercepts);
945 8         19 for my $ccb (@HTML::Transmorgify::queued_captures) {
946 8         34 $ccb->($b);
947             }
948 8         18 push(@$HTML::Transmorgify::rbuf, @$b);
949 8         33 $deferred->doit();
950             } elsif (keys %HTML::Transmorgify::queued_intercepts) {
951 11 50       154 print STDERR "# Processing to /$tag with queued intercepts in play: ".join(';', keys %HTML::Transmorgify::queued_intercepts)."\n" if $debug;
952 11         59 continue_compile($tag, $attr, undef, %HTML::Transmorgify::queued_intercepts);
953             }
954 95         134 push(@$HTML::Transmorgify::rbuf, @HTML::Transmorgify::post_intercept_push);
955              
956 95         427 return 0;
957             }
958              
959 0     0   0 sub exclusive { 0 };
960              
961             sub new
962             {
963 29     29   57 my ($pkg, @tag_pkgs) = @_;
964 29         62 my $self = bless \@tag_pkgs, $pkg;
965 29         81 $self->more;
966 29         101 return $self;
967             }
968              
969             sub more
970             {
971 29     29   52 my ($self, @tag_pkgs) = @_;
972 29         109 @$self = sort { $priorities{$a} <=> $priorities{$b} } @$self, @tag_pkgs;
  0         0  
973             }
974              
975             package HTML::Transmorgify::CloseTag;
976              
977 8     8   52 use strict;
  8         15  
  8         449  
978 8     8   44 use warnings;
  8         18  
  8         1633  
979             import HTML::Transmorgify qw($debug);
980              
981             sub new
982             {
983 76     76   214 my ($pkg, $oldval) = @_;
984 76         487 return bless \$oldval, $pkg;
985             }
986              
987             sub call
988             {
989 76     76   127 my $self = shift;
990 76 50       351 if ($$self) {
991 76 50       1644 print STDERR "# CLOSE TAG WILL CALL CALLBACK\n" if $debug;
992 76         241 $$self->call(@_);
993             } else {
994 0 0       0 print STDERR "# CLOSE TAG NO CALLBACK TO CALL\n" if $debug;
995 0         0 my $attr = shift;
996 0         0 $attr->add_to_result;
997             }
998 76         142 return 22;
999             }
1000              
1001             package HTML::Transmorgify::Deferred;
1002              
1003 8     8   64 use strict;
  8         13  
  8         264  
1004 8     8   64 use warnings;
  8         12  
  8         1788  
1005              
1006             import HTML::Transmorgify qw($debug);
1007              
1008             sub new
1009             {
1010 65     65   189 my ($pkg, $oldval) = @_;
1011 65         396 return bless [$oldval], $pkg;
1012             }
1013              
1014             sub call
1015             {
1016 65     65   500 my $self = shift;
1017 65         206 push(@$self, @_);
1018 65         123 return 0;
1019             }
1020              
1021             sub doit
1022             {
1023 9     9   52 my $self = shift;
1024 9 50       26 if ($self->[0]) {
1025 9         13 my $cb = shift(@$self);
1026 9         38 $cb->call(@$self);
1027             } else {
1028 0         0 my $attr = shift(@$self);
1029 0         0 $attr->add_to_result;
1030             }
1031 9         251 return 0;
1032             }
1033              
1034             1;