File Coverage

blib/lib/HTML/Blitz.pm
Criterion Covered Total %
statement 280 292 95.8
branch 135 226 59.7
condition 36 54 66.6
subroutine 34 36 94.4
pod 7 7 100.0
total 492 615 80.0


line stmt bran cond sub pod time code
1             package HTML::Blitz;
2 11     11   2646774 use HTML::Blitz::pragma;
  11         31  
  11         66  
3 11     11   9393 use HTML::Blitz::Template ();
  11         33  
  11         278  
4 11     11   5818 use HTML::Blitz::RuleSet ();
  11         36  
  11         394  
5 11     11   4793 use HTML::Blitz::SSSelector ();
  11         27  
  11         476  
6 11         996 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   79 );
  11         25  
24 11         780 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   83 );
  11         22  
43 11     11   67 use Carp qw(croak);
  11         27  
  11         549  
44 11     11   77 use Scalar::Util qw(blessed);
  11         26  
  11         510  
45 11     11   62 use overload ();
  11         25  
  11         3164  
46              
47             our $VERSION = '0.08';
48              
49 249 50   249 1 146220 method new($class: @rules) {
  249         505  
  249         515  
  249         356  
50 249         860 my $self = bless {
51             ruleset => HTML::Blitz::RuleSet->new,
52             }, $class;
53 249 100 100     1327 if (@rules && ref($rules[0]) eq 'HASH') {
54 7         12 my %opt = %{shift @rules};
  7         28  
55 7 50       21 $self->set_keep_doctype(delete $opt{keep_doctype}) if exists $opt{keep_doctype};
56 7 100       27 $self->set_keep_comments_re(delete $opt{keep_comments_re}) if exists $opt{keep_comments_re};
57 7 100       29 $self->set_dummy_marker_re(delete $opt{dummy_marker_re}) if exists $opt{dummy_marker_re};
58 7 50       22 croak "Invalid HTML::Blitz option name(s): " . join(", ", sort keys %opt)
59             if keys %opt;
60             }
61 249         808 $self->add_rules(@rules);
62 249         765 $self
63             }
64              
65 1 50   1 1 332 method set_keep_doctype($val) {
  1 50       4  
  1         3  
  1         4  
  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       11  
  4         7  
  4         8  
  4         7  
70 4         21 $self->{ruleset}->set_keep_comments_re($keep_comments_re);
71             }
72              
73 5 50   5 1 55 method set_dummy_marker_re($dummy_marker_re) {
  5 50       11  
  5         8  
  5         9  
  5         6  
74 5         17 $self->{ruleset}->set_dummy_marker_re($dummy_marker_re);
75             }
76              
77 303 50   303   614 fun _css_unescape($str) {
  303 50       928  
  303         785  
  303         403  
78 303         1733 $str =~ s{
79             \\ (?:
80             ( [[:xdigit:]]{1,6} ) (?: \r\n | [ \t\r\n\f] )?+
81             |
82             ( [^\n\r\f[:xdigit:]] )
83             )
84             }{
85 30   66     110 $2 // do {
86 20         49 my $n = hex $1;
87 20 50       139 $n > 0x10_ffff ? "\x{fffd}" : chr $n
88             }
89             }xegr
90             }
91              
92 20 50   20   39 fun _css_unescape_string($str) {
  20 50       43  
  20         32  
  20         27  
93 20 100       68 if ($str =~ s/\A"//) {
94 15 50       92 $str =~ s/"\z// or die "Internal error: unterminated \" string";
95             } else {
96 5 50       21 $str =~ s/\A'// or die "Internal error: malformed (unquoted) string: $str";
97 5 50       24 $str =~ s/'\z// or die "Internal error: unterminated ' string";
98             }
99 20         66 $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     29 $2 // do {
110 4         12 my $n = hex $1;
111 4 50       22 $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 600 50 33 600   1241 fun _try_parse_simple_selector($src_ref, :$allow_tag_name) {
  600 50       2039  
  600 50       1570  
  600 50       1459  
  600         1223  
  600         718  
152 600 100 100     3735 if ($allow_tag_name && $$src_ref =~ /\G(\*|$ident)/gc) {
153 158         412 return { type => ST_TAG_NAME, name => _css_unescape($1) };
154             }
155              
156 442 100       2372 if ($$src_ref =~ /\G#($nmchar++)/gc) {
157 59         182 return { type => ST_ATTR_EQ, attr => 'id', value => _css_unescape($1) };
158             }
159              
160 383 100       1848 if ($$src_ref =~ /\G\.($ident)/gc) {
161 35         97 return { type => ST_ATTR_LIST_HAS, attr => 'class', value => _css_unescape($1) };
162             }
163              
164 348 100       4206 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         262 my ($attr, $op, $val_ident, $val_string) = ($1, $2, $3, $4);
176 40         86 $attr = _css_unescape $attr;
177 40         91 $attr =~ tr/A-Z/a-z/;
178 40 100       96 if (!defined $op) {
179 9         48 return { type => ST_ATTR_HAS, attr => $attr };
180             }
181              
182 31 100       86 my $value = defined($val_ident) ? _css_unescape($val_ident) : _css_unescape_string($val_string);
183 31 100 66     228 if (
      100        
      100        
      100        
184             ($op eq '~' && ($value eq '' || $value =~ /$ws/)) ||
185             ($op =~ /\A[\^\$*]\z/ && $value eq '')
186             ) {
187 4         23 return { type => ST_FALSE };
188             }
189 27         191 return { type => $attr_op_type{$op}, attr => $attr, value => $value };
190             }
191              
192 308 100       766 if ($$src_ref =~ /\G:(nth-child|nth-of-type())\(/iaagc) {
193 17         46 my $pos = $-[0];
194 17         49 my $name = $1;
195 17 100       44 my $type = defined $2 ? ST_NTH_CHILD_OF_TYPE : ST_NTH_CHILD;
196 17         85 $$src_ref =~ /\G$ws++/gc;
197 17 50       187 $$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         69 my ($arg, $num1, $sign, $num2) = ($1, $2, $3, $4);
211 17         64 $$src_ref =~ /\G$ws++/gc;
212 17 50       52 $$src_ref =~ /\G\)/gc
213             or croak "Missing ')' after argument to :$name(): " . substr($$src_ref, $pos, 100);
214              
215 17 100       54 if (defined $num1) {
    100          
    100          
216 11 100 66     55 if ($num1 eq '+' || $num1 eq '') {
    100          
217 2         5 $num1 = 1;
218             } elsif ($num1 eq '-') {
219 3         6 $num1 = -1;
220             } else {
221 6         15 $num1 = 0 + $num1;
222             }
223 11 50       22 if (defined $sign) {
224 11         19 $num2 = 0 + $num2;
225 11 100       25 $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         5 $num2 = 1;
232             } elsif (lc($arg) eq 'even') {
233 2         6 $num1 = 2;
234 2         9 $num2 = 0;
235             } else {
236 2         3 $num1 = 0;
237 2         4 $num2 = 0 + $arg;
238             }
239 17         95 return { type => $type, a => $num1, b => $num2 };
240             }
241              
242 291 100       601 if ($$src_ref =~ /\G:first-child(?![^.#:\[\]),>~ \t\r\n\f])/iaagc) {
243 2         17 return { type => ST_NTH_CHILD, a => 0, b => 1 };
244             }
245              
246 289 100       531 if ($$src_ref =~ /\G:first-of-type(?![^.#:\[\]),>~ \t\r\n\f])/iaagc) {
247 2         17 return { type => ST_NTH_CHILD_OF_TYPE, a => 0, b => 1 };
248             }
249              
250 287 50       1349 if ($$src_ref =~ /\G:($ident)/gc) {
251 0         0 croak "Unsupported pseudo-class :$1";
252             }
253              
254             undef
255 287         2403 }
256              
257 253 50   253   495 fun _parse_selector($src) {
  253 50       487  
  253         444  
  253         327  
258 253 50       462 croak "Invalid selector: $src" if ref $src;
259              
260 253         336 my @alternatives;
261 253         394 my $sequences = [];
262 253         379 my $simples = [];
263              
264 253         672 pos($src) = 0;
265 253         1965 $src =~ /\G$ws++/gc;
266              
267 253         500 while () {
268 600 100       1802 if ($src =~ /\G:not\(/iaagc) {
    100          
    100          
    100          
269 5         24 my $pos = $-[0];
270 5         80 $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         50 $src =~ /\G$ws++/gc;
274 5 50       21 $src =~ /\G\)/gc
275             or croak "Missing ')' after argument to ':not(': " . substr($src, pos($src), 100);
276 5         12 $simple->{negated} = 1;
277 5         15 push @$simples, $simple;
278             } elsif (defined(my $simple = _try_parse_simple_selector \$src, allow_tag_name => !@$simples)) {
279 308         756 push @$simples, $simple;
280             } elsif ($src =~ /\G(?>$ws*([>~+])|$ws)$ws*+/gc) {
281 33   100     120 my $comb = $1 // ' ';
282 33 50       64 @$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         130 link_type => $comb_type{$comb},
287             );
288 33         69 $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         5 $simples = [];
297 1         3 push @alternatives, $sequences;
298 1         3 $sequences = [];
299             } else {
300 253         548 last;
301             }
302             }
303              
304 253 50       1477 $src =~ /\G$ws*+\z/
305             or croak "Unparsable selector: " . substr($src, pos($src), 100);
306 253 0       651 @$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 253         1112 push @$sequences, HTML::Blitz::SSSelector->new(
313             simple_selectors => $simples,
314             link_type => undef,
315             );
316 253         469 push @alternatives, $sequences;
317             \@alternatives
318 253         682 }
319              
320 132 50   132   316 fun _text($str) {
  132 50       278  
  132         235  
  132         202  
321 132         1713 +{ type => AT_P_IMMEDIATE, value => '' . $str }
322             }
323              
324 80 50   80   187 fun _varify($throw, $var) {
  80 50       156  
  80         133  
  80         114  
325 80 50       363 $var =~ /\A[^\W\d][\w\-.]*\z/ or $throw->("Invalid variable name '$var'");
326 80         675 [undef, $var]
327             }
328              
329 40 50   40   239 fun _var($throw, $var) {
  40 50       105  
  40         84  
  40         87  
330 40         105 +{ type => AT_P_VARIABLE, value => _varify($throw, $var) }
331             }
332              
333 30 50   30   72 fun _is_callable($val) {
  30 50       61  
  30         54  
  30         40  
334 30 50 0     129 ref($val) eq 'CODE' ||
335             (blessed($val) && overload::Method($val, '&{}'))
336             }
337              
338 6 50   6   21 fun _template($throw, $val) {
  6 50       17  
  6         14  
  6         9  
339 6 50 33     68 blessed($val) && $val->isa('HTML::Blitz::Template')
340             or $throw->("Argument must be an instance of HTML::Blitz::Template: '$val'");
341 6         30 +{ type => AT_P_FRAGMENT, value => $val->_codegen }
342             }
343              
344 5 50   5   14 fun _dyn_builder($throw, $var) {
  5 50       14  
  5         14  
  5         7  
345 5         16 +{ 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   102 fun _id($x) { $x }
  33 50       84  
  33         67  
  33         47  
  33         77  
359              
360 17 50   17   40 fun _mk_transform_attr($attr, $fn) {
  17 50       41  
  17         33  
  17         23  
361             +{
362 17         269 %_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   20 fun _attr_add_word($throw, $attr, @words) {
  6         18  
  6         8  
376 6         13 for my $word (@words) {
377 14 50       37 $throw->("Argument cannot contain whitespace: '$word'")
378             if $word =~ /[ \t\r\n\f]/;
379             }
380 30 50   30   74 _mk_transform_attr $attr, fun ($value) {
  30 50       58  
  30         69  
  30         55  
381 30         46 my (@list, %seen);
382 30   100     106 for my $word (($value // '') =~ /[^ \t\r\n\f]+/g, @words) {
383 41 100       166 push @list, $word if !$seen{$word}++;
384             }
385 30         114 join ' ', @list
386 6         33 }
387             }
388              
389 6 50   6   14 fun _attr_remove_word($throw, $attr, @words) {
  6         15  
  6         7  
390 6         11 my %banned;
391 6         11 for my $word (@words) {
392 22 50       50 $throw->("Argument cannot contain whitespace: '$word'")
393             if $word =~ /[ \t\r\n\f]/;
394 22         37 $banned{$word} = 1;
395             }
396 6 50   6   19 _mk_transform_attr $attr, fun ($value) {
  6 50       14  
  6         14  
  6         9  
397 6         12 my @list;
398 6   100     57 my $new_value = join ' ', grep !$banned{$_}, ($value // '') =~ /[^ \t\r\n\f]+/g;
399 6 100       26 length $new_value ? $new_value : undef
400 6         26 }
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 256 50   256   647 fun _parse_action($throw, $action_proto, $custom_action = {}) {
  256 50       486  
  256 100       656  
  256         385  
691 256 50       686 ref($action_proto) eq 'ARRAY'
692             or $throw->("Not an ARRAY reference: '$action_proto'");
693 256 50       758 @$action_proto
694             or $throw->("Action cannot be empty");
695 256         583 my ($type, @args) = @$action_proto;
696 256 0 66     1209 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 256 0   0   1346 $action_fn->(fun ($err) { $throw->("'$type': $err"); }, @args)
  0 0       0  
  0         0  
  0         0  
  0         0  
  0         0  
698             }
699              
700 253 50 66 253   524 fun _parse_rule($proto, :$custom_action = {}) {
  253 50       634  
  253 100       481  
  253 50       621  
  253         625  
  253         321  
701 253         537 my ($sel_str, @action_protos) = @$proto;
702 253         527 my $selector = _parse_selector $sel_str;
703 253 0   0   1530 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 253         1030 $selector, \@actions
705             }
706              
707 251 50   251 1 1097 method add_rules(@rules) {
  251         380  
  251         521  
  251         316  
708 251         508 my $ruleset = $self->{ruleset};
709 251         521 for my $rule (@rules) {
710 247         496 my ($selector, $actions) = _parse_rule $rule;
711 247 50       1124 $ruleset->add_rule($selector, @$actions)
712             if @$actions;
713             }
714             }
715              
716 274 50   274 1 25594 method apply_to_html($name, $html) {
  274 50       555  
  274         416  
  274         587  
  274         332  
717 274         887 HTML::Blitz::Template->new(_codegen => $self->{ruleset}->compile($name, $html))
718             }
719              
720 10 50   10 1 6431 method apply_to_file($file) {
  10 50       27  
  10         14  
  10         20  
  10         11  
721 10         14 my $html = do {
722 10 50       435 open my $fh, '<:encoding(UTF-8)', $file
723             or croak "Can't open $file: $!";
724 10         1903 local $/;
725 10         436 readline $fh
726             };
727 10         373 $self->apply_to_html($file, $html)
728             }
729              
730             1
731             __END__