File Coverage

blib/lib/HTML/Blitz.pm
Criterion Covered Total %
statement 283 295 95.9
branch 135 226 59.7
condition 36 54 66.6
subroutine 35 37 94.5
pod 7 7 100.0
total 496 619 80.1


line stmt bran cond sub pod time code
1             package HTML::Blitz;
2 11     11   2548121 use HTML::Blitz::pragma;
  11         38  
  11         66  
3 11     11   9342 use HTML::Blitz::Template ();
  11         26  
  11         260  
4 11     11   6030 use HTML::Blitz::RuleSet ();
  11         46  
  11         390  
5 11     11   4855 use HTML::Blitz::SSSelector ();
  11         32  
  11         470  
6 11         966 use HTML::Blitz::SelectorType qw(
7             ST_FALSE
8             ST_TAG_NAME
9             ST_ATTR_HAS
10             ST_ATTR_EQ
11             ST_ATTR_PREFIX
12             ST_ATTR_SUFFIX
13             ST_ATTR_INFIX
14             ST_ATTR_LIST_HAS
15             ST_ATTR_LANG_PREFIX
16             ST_NTH_CHILD
17             ST_NTH_CHILD_OF_TYPE
18              
19             LT_DESCENDANT
20             LT_CHILD
21             LT_SIBLING
22             LT_ADJACENT_SIBLING
23 11     11   83 );
  11         25  
24 11         751 use HTML::Blitz::ActionType qw(
25             AT_REMOVE_IF
26             AT_REPLACE_INNER
27             AT_REPLACE_OUTER
28             AT_REPEAT_OUTER
29              
30             AT_AS_MODIFY_ATTRS
31             AT_AS_REPLACE_ATTRS
32              
33             AT_A_REMOVE_ATTR
34             AT_A_SET_ATTR
35             AT_A_MODIFY_ATTR
36              
37             AT_P_VARIABLE
38             AT_P_IMMEDIATE
39             AT_P_TRANSFORM
40             AT_P_FRAGMENT
41             AT_P_VARHTML
42 11     11   70 );
  11         25  
43 11     11   73 use Carp qw(croak);
  11         21  
  11         502  
44 11     11   71 use Scalar::Util qw(blessed);
  11         23  
  11         495  
45 11     11   76 use overload ();
  11         21  
  11         3102  
46              
47             our $VERSION = '0.07';
48              
49 248 50   248 1 149919 method new($class: @rules) {
  248         503  
  248         488  
  248         342  
50 248         882 my $self = bless {
51             ruleset => HTML::Blitz::RuleSet->new,
52             }, $class;
53 248 100 100     1300 if (@rules && ref($rules[0]) eq 'HASH') {
54 6         16 my %opt = %{shift @rules};
  6         25  
55 6 50       19 $self->set_keep_doctype(delete $opt{keep_doctype}) if exists $opt{keep_doctype};
56 6 100       23 $self->set_keep_comments_re(delete $opt{keep_comments_re}) if exists $opt{keep_comments_re};
57 6 100       27 $self->set_dummy_marker_re(delete $opt{dummy_marker_re}) if exists $opt{dummy_marker_re};
58 6 50       21 croak "Invalid HTML::Blitz option name(s): " . join(", ", sort keys %opt)
59             if keys %opt;
60             }
61 248         819 $self->add_rules(@rules);
62 248         858 $self
63             }
64              
65 1 50   1 1 462 method set_keep_doctype($val) {
  1 50       4  
  1         3  
  1         2  
  1         2  
66 1         6 $self->{ruleset}->set_keep_doctype($val);
67             }
68              
69 4 50   4 1 15 method set_keep_comments_re($keep_comments_re) {
  4 50       10  
  4         6  
  4         7  
  4         8  
70 4         19 $self->{ruleset}->set_keep_comments_re($keep_comments_re);
71             }
72              
73 4 50   4 1 16 method set_dummy_marker_re($dummy_marker_re) {
  4 50       10  
  4         5  
  4         8  
  4         18  
74 4         13 $self->{ruleset}->set_dummy_marker_re($dummy_marker_re);
75             }
76              
77 298 50   298   591 fun _css_unescape($str) {
  298 50       557  
  298         716  
  298         426  
78 298         1668 $str =~ s{
79             \\ (?:
80             ( [[:xdigit:]]{1,6} ) (?: \r\n | [ \t\r\n\f] )?+
81             |
82             ( [^\n\r\f[:xdigit:]] )
83             )
84             }{
85 30   66     113 $2 // do {
86 20         48 my $n = hex $1;
87 20 50       139 $n > 0x10_ffff ? "\x{fffd}" : chr $n
88             }
89             }xegr
90             }
91              
92 20 50   20   58 fun _css_unescape_string($str) {
  20 50       59  
  20         31  
  20         29  
93 20 100       76 if ($str =~ s/\A"//) {
94 15 50       79 $str =~ s/"\z// or die "Internal error: unterminated \" string";
95             } else {
96 5 50       33 $str =~ s/\A'// or die "Internal error: malformed (unquoted) string: $str";
97 5 50       25 $str =~ s/'\z// or die "Internal error: unterminated ' string";
98             }
99 20         71 $str =~ s{
100             \\ (?:
101             ( [[:xdigit:]]{1,6} ) (?: \r\n | [ \t\r\n\f] )?+
102             |
103             ( [^\n\r\f[:xdigit:]] )
104             |
105             ( \r \n?+ | [\n\f] )
106             )
107             }{
108             defined $3 ? '' :
109 6 50 66     32 $2 // do {
110 4         9 my $n = hex $1;
111 4 50       24 $n > 0x10_ffff ? "\x{fffd}" : chr $n
112             }
113             }xegr
114             }
115              
116             my $ws = qr/[ \t\r\n\f]/;
117             my $nmchar = qr{
118             (?:
119             [a-zA-Z0-9_\-]
120             |
121             [^\x00-\x7f]
122             |
123             \\ [[:xdigit:]]{1,6} (?: \r\n | $ws )?+
124             |
125             \\ [^\n\r\f[:xdigit:]]
126             )
127             }x;
128             my $ident = qr{ -? (?! [0-9\-] ) $nmchar++ }x;
129             my $string = qr{
130             " (?: [^\n\r\f\\"] | \\ (?: \r \n?+ | [^\r[:xdigit:]] | [[:xdigit:]]{1,6} (?: \r\n | $ws )?+ ) | [^\x00-\x7f] )*+ "
131             |
132             ' (?: [^\n\r\f\\'] | \\ (?: \r \n?+ | [^\r[:xdigit:]] | [[:xdigit:]]{1,6} (?: \r\n | $ws )?+ ) | [^\x00-\x7f] )*+ '
133             }x;
134              
135             my %attr_op_type = (
136             '' => ST_ATTR_EQ,
137             '^' => ST_ATTR_PREFIX,
138             '$' => ST_ATTR_SUFFIX,
139             '*' => ST_ATTR_INFIX,
140             '~' => ST_ATTR_LIST_HAS,
141             '|' => ST_ATTR_LANG_PREFIX,
142             );
143              
144             my %comb_type = (
145             ' ' => LT_DESCENDANT,
146             '>' => LT_CHILD,
147             '~' => LT_SIBLING,
148             '+' => LT_ADJACENT_SIBLING,
149             );
150              
151 592 50 33 592   1234 fun _try_parse_simple_selector($src_ref, :$allow_tag_name) {
  592 50       2044  
  592 50       1606  
  592 50       1431  
  592         1229  
  592         799  
152 592 100 100     3750 if ($allow_tag_name && $$src_ref =~ /\G(\*|$ident)/gc) {
153 156         411 return { type => ST_TAG_NAME, name => _css_unescape($1) };
154             }
155              
156 436 100       2396 if ($$src_ref =~ /\G#($nmchar++)/gc) {
157 59         184 return { type => ST_ATTR_EQ, attr => 'id', value => _css_unescape($1) };
158             }
159              
160 377 100       1861 if ($$src_ref =~ /\G\.($ident)/gc) {
161 32         101 return { type => ST_ATTR_LIST_HAS, attr => 'class', value => _css_unescape($1) };
162             }
163              
164 345 100       4223 if (
165             $$src_ref =~ m{
166             \G
167             \[ $ws*+
168             ($ident) $ws*+
169             (?:
170             ( [\^\$\*~\|]?+ ) = $ws*+ (?: ($ident) | ($string) ) $ws*+
171             )?+
172             \]
173             }xgc
174             ) {
175 40         264 my ($attr, $op, $val_ident, $val_string) = ($1, $2, $3, $4);
176 40         88 $attr = _css_unescape $attr;
177 40         89 $attr =~ tr/A-Z/a-z/;
178 40 100       98 if (!defined $op) {
179 9         52 return { type => ST_ATTR_HAS, attr => $attr };
180             }
181              
182 31 100       87 my $value = defined($val_ident) ? _css_unescape($val_ident) : _css_unescape_string($val_string);
183 31 100 66     258 if (
      100        
      100        
      100        
184             ($op eq '~' && ($value eq '' || $value =~ /$ws/)) ||
185             ($op =~ /\A[\^\$*]\z/ && $value eq '')
186             ) {
187 4         25 return { type => ST_FALSE };
188             }
189 27         231 return { type => $attr_op_type{$op}, attr => $attr, value => $value };
190             }
191              
192 305 100       740 if ($$src_ref =~ /\G:(nth-child|nth-of-type())\(/iaagc) {
193 17         51 my $pos = $-[0];
194 17         39 my $name = $1;
195 17 100       54 my $type = defined $2 ? ST_NTH_CHILD_OF_TYPE : ST_NTH_CHILD;
196 17         124 $$src_ref =~ /\G$ws++/gc;
197 17 50       185 $$src_ref =~ m{
198             \G
199             (
200             ( [\-+]? [0-9]* ) [Nn] (?: $ws*+ ([\-+]) $ws*+ ([0-9]+) )?+
201             |
202             [\-+]? [0-9]+
203             |
204             [Oo][Dd][Dd]
205             |
206             [Ee][Vv][Ee][Nn]
207             )
208             }xgc
209             or croak "Bad argument to :$name(): " . substr($$src_ref, $pos, 100);
210 17         67 my ($arg, $num1, $sign, $num2) = ($1, $2, $3, $4);
211 17         59 $$src_ref =~ /\G$ws++/gc;
212 17 50       48 $$src_ref =~ /\G\)/gc
213             or croak "Missing ')' after argument to :$name(): " . substr($$src_ref, $pos, 100);
214              
215 17 100       59 if (defined $num1) {
    100          
    100          
216 11 100 66     57 if ($num1 eq '+' || $num1 eq '') {
    100          
217 2         5 $num1 = 1;
218             } elsif ($num1 eq '-') {
219 3         8 $num1 = -1;
220             } else {
221 6         13 $num1 = 0 + $num1;
222             }
223 11 50       25 if (defined $sign) {
224 11         20 $num2 = 0 + $num2;
225 11 100       26 $num2 = -$num2 if $sign eq '-';
226             } else {
227 0         0 $num2 = 0;
228             }
229             } elsif (lc($arg) eq 'odd') {
230 2         4 $num1 = 2;
231 2         7 $num2 = 1;
232             } elsif (lc($arg) eq 'even') {
233 2         5 $num1 = 2;
234 2         9 $num2 = 0;
235             } else {
236 2         4 $num1 = 0;
237 2         5 $num2 = 0 + $arg;
238             }
239 17         98 return { type => $type, a => $num1, b => $num2 };
240             }
241              
242 288 100       593 if ($$src_ref =~ /\G:first-child(?![^.#:\[\]),>~ \t\r\n\f])/iaagc) {
243 2         15 return { type => ST_NTH_CHILD, a => 0, b => 1 };
244             }
245              
246 286 100       504 if ($$src_ref =~ /\G:first-of-type(?![^.#:\[\]),>~ \t\r\n\f])/iaagc) {
247 2         18 return { type => ST_NTH_CHILD_OF_TYPE, a => 0, b => 1 };
248             }
249              
250 284 50       1325 if ($$src_ref =~ /\G:($ident)/gc) {
251 0         0 croak "Unsupported pseudo-class :$1";
252             }
253              
254             undef
255 284         2450 }
256              
257 250 50   250   510 fun _parse_selector($src) {
  250 50       443  
  250         433  
  250         341  
258 250 50       457 croak "Invalid selector: $src" if ref $src;
259              
260 250         346 my @alternatives;
261 250         388 my $sequences = [];
262 250         412 my $simples = [];
263              
264 250         678 pos($src) = 0;
265 250         1832 $src =~ /\G$ws++/gc;
266              
267 250         443 while () {
268 592 100       1850 if ($src =~ /\G:not\(/iaagc) {
    100          
    100          
    100          
269 5         16 my $pos = $-[0];
270 5         97 $src =~ /\G$ws++/gc;
271 5 50       18 my $simple = _try_parse_simple_selector \$src, allow_tag_name => 1
272             or croak "Unparsable selector in argument to ':not()': " . substr($src, $pos, 100);
273 5         46 $src =~ /\G$ws++/gc;
274 5 50       22 $src =~ /\G\)/gc
275             or croak "Missing ')' after argument to ':not(': " . substr($src, pos($src), 100);
276 5         12 $simple->{negated} = 1;
277 5         14 push @$simples, $simple;
278             } elsif (defined(my $simple = _try_parse_simple_selector \$src, allow_tag_name => !@$simples)) {
279 303         755 push @$simples, $simple;
280             } elsif ($src =~ /\G(?>$ws*([>~+])|$ws)$ws*+/gc) {
281 33   100     119 my $comb = $1 // ' ';
282 33 50       72 @$simples
283             or croak "Selector list before '$comb' cannot be empty: " . substr($src, $-[0], 100);
284             push @$sequences, HTML::Blitz::SSSelector->new(
285             simple_selectors => $simples,
286 33         144 link_type => $comb_type{$comb},
287             );
288 33         76 $simples = [];
289             } elsif ($src =~ /\G,$ws*+/gc) {
290 1 50       6 @$simples
291             or croak "Selector list before ',' cannot be empty: " . substr($src, $-[0], 100);
292 1         7 push @$sequences, HTML::Blitz::SSSelector->new(
293             simple_selectors => $simples,
294             link_type => undef,
295             );
296 1         3 $simples = [];
297 1         5 push @alternatives, $sequences;
298 1         3 $sequences = [];
299             } else {
300 250         586 last;
301             }
302             }
303              
304 250 50       1436 $src =~ /\G$ws*+\z/
305             or croak "Unparsable selector: " . substr($src, pos($src), 100);
306 250 0       623 @$simples
    0          
    50          
307             or croak
308             @$sequences ? "Trailing combinator after last selector list" :
309             @alternatives ? "trailing comma after last selector list" :
310             "Selector cannot be empty";
311              
312 250         1030 push @$sequences, HTML::Blitz::SSSelector->new(
313             simple_selectors => $simples,
314             link_type => undef,
315             );
316 250         502 push @alternatives, $sequences;
317             \@alternatives
318 250         682 }
319              
320 129 50   129   254 fun _text($str) {
  129 50       267  
  129         224  
  129         174  
321 129         1662 +{ type => AT_P_IMMEDIATE, value => '' . $str }
322             }
323              
324 80 50   80   164 fun _varify($throw, $var) {
  80 50       161  
  80         145  
  80         94  
325 80 50       399 $var =~ /\A[^\W\d][\w\-.]*\z/ or $throw->("Invalid variable name '$var'");
326 80         660 [undef, $var]
327             }
328              
329 40 50   40   98 fun _var($throw, $var) {
  40 50       107  
  40         80  
  40         56  
330 40         91 +{ type => AT_P_VARIABLE, value => _varify($throw, $var) }
331             }
332              
333 30 50   30   73 fun _is_callable($val) {
  30 50       62  
  30         53  
  30         36  
334 30 50 0     151 ref($val) eq 'CODE' ||
335             (blessed($val) && overload::Method($val, '&{}'))
336             }
337              
338 6 50   6   48 fun _template($throw, $val) {
  6 50       18  
  6         14  
  6         12  
339 6 50 33     115 blessed($val) && $val->isa('HTML::Blitz::Template')
340             or $throw->("Argument must be an instance of HTML::Blitz::Template: '$val'");
341 6         37 +{ type => AT_P_FRAGMENT, value => $val->_codegen }
342             }
343              
344 5 50   5   14 fun _dyn_builder($throw, $var) {
  5 50       13  
  5         10  
  5         8  
345 5         17 +{ type => AT_P_VARHTML, value => _varify($throw, $var) }
346             }
347              
348             my %_nop = (
349             type => AT_REPLACE_INNER,
350             attrset => {
351             type => AT_AS_MODIFY_ATTRS,
352             content => {},
353             },
354             content => undef,
355             repeat => [],
356             );
357              
358 33 50   33   92 fun _id($x) { $x }
  33 50       70  
  33         69  
  33         41  
  33         80  
359              
360 17 50   17   44 fun _mk_transform_attr($attr, $fn) {
  17 50       40  
  17         33  
  17         23  
361             +{
362 17         270 %_nop,
363             attrset => {
364             type => AT_AS_MODIFY_ATTRS,
365             content => {
366             $attr => {
367             type => AT_A_MODIFY_ATTR,
368             param => { type => AT_P_TRANSFORM, static => $fn, dynamic => [] },
369             },
370             },
371             },
372             }
373             }
374              
375 6 50   6   16 fun _attr_add_word($throw, $attr, @words) {
  6         18  
  6         9  
376 6         13 for my $word (@words) {
377 14 50       39 $throw->("Argument cannot contain whitespace: '$word'")
378             if $word =~ /[ \t\r\n\f]/;
379             }
380 30 50   30   70 _mk_transform_attr $attr, fun ($value) {
  30 50       57  
  30         69  
  30         40  
381 30         44 my (@list, %seen);
382 30   100     114 for my $word (($value // '') =~ /[^ \t\r\n\f]+/g, @words) {
383 41 100       188 push @list, $word if !$seen{$word}++;
384             }
385 30         117 join ' ', @list
386 6         30 }
387             }
388              
389 6 50   6   15 fun _attr_remove_word($throw, $attr, @words) {
  6         16  
  6         6  
390 6         10 my %banned;
391 6         13 for my $word (@words) {
392 22 50       45 $throw->("Argument cannot contain whitespace: '$word'")
393             if $word =~ /[ \t\r\n\f]/;
394 22         54 $banned{$word} = 1;
395             }
396 6 50   6   24 _mk_transform_attr $attr, fun ($value) {
  6 50       44  
  6         15  
  6         7  
397 6         8 my @list;
398 6   100     55 my $new_value = join ' ', grep !$banned{$_}, ($value // '') =~ /[^ \t\r\n\f]+/g;
399 6 100       24 length $new_value ? $new_value : undef
400 6         28 }
401             }
402              
403             my %actions = (
404             remove => fun ($throw, @args) {
405             $throw->("Expected 0 arguments, got " . @args)
406             if @args != 0;
407             +{ type => AT_REPLACE_OUTER, param => _text('') }
408             },
409              
410             remove_inner => fun ($throw, @args) {
411             $throw->("Expected 0 arguments, got " . @args)
412             if @args != 0;
413             +{ %_nop, content => _text('') }
414             },
415              
416             remove_if => fun ($throw, @args) {
417             $throw->("Expected 1 argument, got " . @args)
418             if @args != 1;
419             my $var = _varify $throw, $args[0];
420             +{ type => AT_REMOVE_IF, cond => [$var], else => undef }
421             },
422              
423             replace_inner_text => fun ($throw, @args) {
424             $throw->("Expected 1 argument, got " . @args)
425             if @args != 1;
426             +{ %_nop, content => _text($args[0]) }
427             },
428              
429             replace_inner_var => fun ($throw, @args) {
430             $throw->("Expected 1 argument, got " . @args)
431             if @args != 1;
432             +{ %_nop, content => _var($throw, $args[0]) }
433             },
434              
435             replace_inner_template => fun ($throw, @args) {
436             $throw->("Expected 1 argument, got " . @args)
437             if @args != 1;
438             +{ %_nop, content => _template($throw, $args[0]) }
439             },
440              
441             #replace_inner_builder => fun ($throw, @args) {
442             # $throw->("Expected 1 argument, got " . @args)
443             # if @args != 1;
444             # +{ %_nop, content => _builder($throw, $args[0]) }
445             #},
446              
447             replace_inner_dyn_builder => fun ($throw, @args) {
448             $throw->("Expected 1 argument, got " . @args)
449             if @args != 1;
450             +{ %_nop, content => _dyn_builder($throw, $args[0]) }
451             },
452              
453             replace_outer_text => fun ($throw, @args) {
454             $throw->("Expected 1 argument, got " . @args)
455             if @args != 1;
456             +{ type => AT_REPLACE_OUTER, param => _text($args[0]) }
457             },
458              
459             replace_outer_var => fun ($throw, @args) {
460             $throw->("Expected 1 argument, got " . @args)
461             if @args != 1;
462             +{ type => AT_REPLACE_OUTER, param => _var($throw, $args[0]) }
463             },
464              
465             replace_outer_template => fun ($throw, @args) {
466             $throw->("Expected 1 argument, got " . @args)
467             if @args != 1;
468             +{ type => AT_REPLACE_OUTER, param => _template($throw, $args[0]) }
469             },
470              
471             #replace_outer_builder => fun ($throw, @args) {
472             # $throw->("Expected 1 argument, got " . @args)
473             # if @args != 1;
474             # +{ type => AT_REPLACE_OUTER, param => _builder($throw, $args[0]) }
475             #},
476              
477             replace_outer_dyn_builder => fun ($throw, @args) {
478             $throw->("Expected 1 argument, got " . @args)
479             if @args != 1;
480             +{ type => AT_REPLACE_OUTER, param => _dyn_builder($throw, $args[0]) }
481             },
482              
483             transform_inner_sub => fun ($throw, @args) {
484             $throw->("Expected 1 argument, got " . @args)
485             if @args != 1;
486             my $fn = $args[0];
487             _is_callable $fn
488             or $throw->("Argument must be a function");
489             +{ %_nop, content => { type => AT_P_TRANSFORM, static => $fn, dynamic => [] } }
490             },
491              
492             transform_inner_var => fun ($throw, @args) {
493             $throw->("Expected 1 argument, got " . @args)
494             if @args != 1;
495             my $var = _varify $throw, $args[0];
496             +{ %_nop, content => { type => AT_P_TRANSFORM, static => \&_id, dynamic => [$var] } }
497             },
498              
499             transform_outer_sub => fun ($throw, @args) {
500             $throw->("Expected 1 argument, got " . @args)
501             if @args != 1;
502             my $fn = $args[0];
503             _is_callable $fn
504             or $throw->("Argument must be a function");
505             +{ type => AT_REPLACE_OUTER, param => { type => AT_P_TRANSFORM, static => $fn, dynamic => [] } }
506             },
507              
508             transform_outer_var => fun ($throw, @args) {
509             $throw->("Expected 1 argument, got " . @args)
510             if @args != 1;
511             my $var = _varify $throw, $args[0];
512             +{ type => AT_REPLACE_OUTER, param => { type => AT_P_TRANSFORM, static => \&_id, dynamic => [$var] } }
513             },
514              
515             remove_attribute => fun ($throw, @args) {
516             +{ %_nop, attrset => { type => AT_AS_MODIFY_ATTRS, content => { map +($_ => { type => AT_A_REMOVE_ATTR }), @args } } }
517             },
518              
519             replace_all_attributes => fun ($throw, @args) {
520             $throw->("Expected 1 argument, got " . @args)
521             if @args != 1;
522             my $attr = $args[0];
523             +{
524             %_nop,
525             attrset => {
526             type => AT_AS_REPLACE_ATTRS,
527             content => {
528             map {
529             my $v = $attr->{$_};
530             ref($v) eq 'ARRAY'
531             or $throw->("Attribute replacement value must be an array reference, not '$v'");
532             @$v == 2
533             or $throw->("Attribute replacement value must have 2 elements, not " . @$v);
534             $_ =>
535             $v->[0] eq 'text' ? _text($v->[1]) :
536             $v->[0] eq 'var' ? _var($throw, $v->[1]) :
537             $throw->("Invalid attribute replacement type (must be 'text' or 'var'): '$v->[0]'")
538             } keys %$attr
539             },
540             },
541             }
542             },
543              
544             remove_all_attributes => fun ($throw, @args) {
545             $throw->("Expected 0 arguments, got " . @args)
546             if @args != 0;
547             +{ %_nop, attrset => { type => AT_AS_REPLACE_ATTRS, content => {} } }
548             },
549              
550             set_attribute_text => fun ($throw, @args) {
551             $throw->("Expected 1 or 2 arguments, got " . @args)
552             if @args < 1 || @args > 2;
553             if (@args == 1) {
554             ref(my $attr = $args[0]) eq 'HASH'
555             or $throw->(ref $args[0] ? "Invalid reference type (must be HASH): $args[0]" : "Missing value for attribute '$args[0]'");
556             return +{ %_nop, attrset => { type => AT_AS_MODIFY_ATTRS, content => { map +($_ => { type => AT_A_SET_ATTR, param => _text($attr->{$_}) }), keys %$attr } } };
557             }
558             +{ %_nop, attrset => { type => AT_AS_MODIFY_ATTRS, content => { $args[0] => { type => AT_A_SET_ATTR, param => _text($args[1]) } } } }
559             },
560              
561             set_attribute_var => fun ($throw, @args) {
562             $throw->("Expected 1 or 2 arguments, got " . @args)
563             if @args < 1 || @args > 2;
564             if (@args == 1) {
565             ref(my $attr = $args[0]) eq 'HASH'
566             or $throw->(ref $args[0] ? "Invalid reference type (must be HASH): $args[0]" : "Missing value for attribute '$args[0]'");
567             return +{ %_nop, attrset => { type => AT_AS_MODIFY_ATTRS, content => { map +($_ => { type => AT_A_SET_ATTR, param => _var($throw, $attr->{$_}) }), keys %$attr } } };
568             }
569             +{ %_nop, attrset => { type => AT_AS_MODIFY_ATTRS, content => { $args[0] => { type => AT_A_SET_ATTR, param => _var($throw, $args[1]) } } } }
570             },
571              
572             set_attributes => fun ($throw, @args) {
573             $throw->("Expected 1 argument, got " . @args)
574             if @args != 1;
575             my $attr = $args[0];
576             +{
577             %_nop,
578             attrset => {
579             type => AT_AS_MODIFY_ATTRS,
580             content => {
581             map {
582             my $v = $attr->{$_};
583             ref($v) eq 'ARRAY'
584             or $throw->("Attribute replacement value must be an array reference, not '$v'");
585             @$v == 2
586             or $throw->("Attribute replacement value must have 2 elements, not " . @$v);
587             $_ => {
588             type => AT_A_SET_ATTR,
589             param =>
590             $v->[0] eq 'text' ? _text($v->[1]) :
591             $v->[0] eq 'var' ? _var($throw, $v->[1]) :
592             $throw->("Invalid attribute replacement type (must be 'text' or 'var'): '$v->[0]'")
593             }
594             } keys %$attr
595             },
596             },
597             }
598             },
599              
600             transform_attribute_sub => fun ($throw, @args) {
601             $throw->("Expected 2 arguments, got " . @args)
602             if @args != 2;
603             my ($attr, $fn) = @args;
604             _is_callable $fn
605             or $throw->("Argument must be a function");
606             _mk_transform_attr $attr, $fn
607             },
608              
609             transform_attribute_var => fun ($throw, @args) {
610             $throw->("Expected 2 arguments, got " . @args)
611             if @args != 2;
612             my ($attr, $var) = @args;
613             $var = _varify $throw, $var;
614             +{
615             %_nop,
616             attrset => {
617             type => AT_AS_MODIFY_ATTRS,
618             content => {
619             $attr => {
620             type => AT_A_MODIFY_ATTR,
621             param => { type => AT_P_TRANSFORM, static => \&_id, dynamic => [$var] },
622             },
623             },
624             },
625             }
626             },
627              
628             add_attribute_word => fun ($throw, @args) {
629             $throw->("Expected 2 or more arguments, not " . @args)
630             if @args < 2;
631             _attr_add_word $throw, @args
632             },
633              
634             remove_attribute_word => fun ($throw, @args) {
635             $throw->("Expected 2 or more arguments, not " . @args)
636             if @args < 2;
637             _attr_remove_word $throw, @args
638             },
639              
640             add_class => fun ($throw, @args) {
641             $throw->("Expected 1 or more arguments, not " . @args)
642             if @args < 1;
643             _attr_add_word $throw, 'class', @args
644             },
645              
646             remove_class => fun ($throw, @args) {
647             $throw->("Expected 1 or more arguments, not " . @args)
648             if @args < 1;
649             _attr_remove_word $throw, 'class', @args
650             },
651              
652             repeat_outer => fun ($throw, @args) {
653             $throw->("Expected 1 or more arguments, not " . @args)
654             if @args < 1;
655             my $var = _varify $throw, shift @args;
656             my @inplace;
657             if (@args && ref($args[0]) eq 'REF' && ref(${$args[0]}) eq 'ARRAY') {
658             my $actions = ${shift @args};
659             @inplace = map _parse_action(fun ($err) { $throw->("Root action: $err") }, $_), ref($actions->[0]) ? @$actions : $actions;
660             }
661             my @rules;
662             for my $proto (@args) {
663             my ($selector, $actions) = _parse_rule($proto);
664             push @rules, [$selector, @$actions]
665             if @$actions;
666             }
667             +{ type => AT_REPEAT_OUTER, var => $var, inplace => \@inplace, nested => \%_nop, rules => \@rules }
668             },
669              
670             repeat_inner => fun ($throw, @args) {
671             $throw->("Expected 1 or more arguments, not " . @args)
672             if @args < 1;
673             my $var = _varify $throw, shift @args;
674             my @rules;
675             for my $proto (@args) {
676             my ($selector, $actions) = _parse_rule($proto, custom_action => {
677             separator => fun ($throw, @args) {
678             $throw->("Expected 0 arguments, got " . @args)
679             if @args != 0;
680             +{ type => AT_REMOVE_IF, else => undef, cond => [[undef, \'iter0']] }
681             },
682             });
683             push @rules, [$selector, @$actions]
684             if @$actions;
685             }
686             +{ %_nop, repeat => [{ var => $var, rules => \@rules }] }
687             },
688             );
689              
690 253 50   253   627 fun _parse_action($throw, $action_proto, $custom_action = {}) {
  253 50       539  
  253 100       655  
  253         393  
691 253 50       631 ref($action_proto) eq 'ARRAY'
692             or $throw->("Not an ARRAY reference: '$action_proto'");
693 253 50       497 @$action_proto
694             or $throw->("Action cannot be empty");
695 253         952 my ($type, @args) = @$action_proto;
696 253 0 66     1279 my $action_fn = $custom_action->{$type} // $actions{$type} // $throw->("Unknown action type '$type'" . ($type eq 'seperator' && $custom_action->{separator} ? " (did you mean 'separator'?)" : ""));
      0        
      33        
697 253 0   0   1496 $action_fn->(fun ($err) { $throw->("'$type': $err"); }, @args)
  0 0       0  
  0         0  
  0         0  
  0         0  
  0         0  
698             }
699              
700 250 50 66 250   512 fun _parse_rule($proto, :$custom_action = {}) {
  250 50       675  
  250 100       474  
  250 50       708  
  250         600  
  250         335  
701 250         547 my ($sel_str, @action_protos) = @$proto;
702 250         510 my $selector = _parse_selector $sel_str;
703 250 0   0   1514 my @actions = map _parse_action(fun ($err) { croak "Invalid action for '$sel_str': $err" }, $_, $custom_action), @action_protos;
  0 0       0  
  0         0  
  0         0  
  0         0  
  0         0  
704 250         1012 $selector, \@actions
705             }
706              
707 250 50   250 1 1036 method add_rules(@rules) {
  250         385  
  250         438  
  250         334  
708 250         525 my $ruleset = $self->{ruleset};
709 250         494 for my $rule (@rules) {
710 244         520 my ($selector, $actions) = _parse_rule $rule;
711 244 50       1095 $ruleset->add_rule($selector, @$actions)
712             if @$actions;
713             }
714             }
715              
716 273 50   273 1 25821 method apply_to_html($name, $html) {
  273 50       567  
  273         413  
  273         529  
  273         332  
717 273         876 HTML::Blitz::Template->new(_codegen => $self->{ruleset}->compile($name, $html))
718             }
719              
720 9 50   9 1 6240 method apply_to_file($file) {
  9 50       21  
  9         11  
  9         19  
  9         11  
721 9         11 my $html = do {
722 9 50   1   446 open my $fh, '<:encoding(UTF-8)', $file
  1         8  
  1         2  
  1         6  
723             or croak "Can't open $file: $!";
724 9         1794 local $/;
725 9         402 readline $fh
726             };
727 9         350 $self->apply_to_html($file, $html)
728             }
729              
730             1
731             __END__