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   2622458 use HTML::Blitz::pragma;
  11         33  
  11         71  
3 11     11   9439 use HTML::Blitz::Template ();
  11         31  
  11         279  
4 11     11   5857 use HTML::Blitz::RuleSet ();
  11         41  
  11         505  
5 11     11   4775 use HTML::Blitz::SSSelector ();
  11         36  
  11         472  
6 11         967 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   81 );
  11         23  
24 11         778 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   73 );
  11         26  
43 11     11   73 use Carp qw(croak);
  11         23  
  11         565  
44 11     11   69 use Scalar::Util qw(blessed);
  11         22  
  11         496  
45 11     11   65 use overload ();
  11         23  
  11         3086  
46              
47             our $VERSION = '0.09';
48              
49 251 50   251 1 140051 method new($class: @rules) {
  251         514  
  251         489  
  251         346  
50 251         862 my $self = bless {
51             ruleset => HTML::Blitz::RuleSet->new,
52             }, $class;
53 251 100 100     1349 if (@rules && ref($rules[0]) eq 'HASH') {
54 7         14 my %opt = %{shift @rules};
  7         26  
55 7 50       24 $self->set_keep_doctype(delete $opt{keep_doctype}) if exists $opt{keep_doctype};
56 7 100       28 $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       24 croak "Invalid HTML::Blitz option name(s): " . join(", ", sort keys %opt)
59             if keys %opt;
60             }
61 251         828 $self->add_rules(@rules);
62 251         813 $self
63             }
64              
65 1 50   1 1 321 method set_keep_doctype($val) {
  1 50       4  
  1         2  
  1         3  
  1         2  
66 1         6 $self->{ruleset}->set_keep_doctype($val);
67             }
68              
69 4 50   4 1 18 method set_keep_comments_re($keep_comments_re) {
  4 50       10  
  4         7  
  4         7  
  4         8  
70 4         20 $self->{ruleset}->set_keep_comments_re($keep_comments_re);
71             }
72              
73 5 50   5 1 19 method set_dummy_marker_re($dummy_marker_re) {
  5 50       12  
  5         8  
  5         10  
  5         6  
74 5         18 $self->{ruleset}->set_dummy_marker_re($dummy_marker_re);
75             }
76              
77 306 50   306   623 fun _css_unescape($str) {
  306 50       593  
  306         793  
  306         411  
78 306         1835 $str =~ s{
79             \\ (?:
80             ( [[:xdigit:]]{1,6} ) (?: \r\n | [ \t\r\n\f] )?+
81             |
82             ( [^\n\r\f[:xdigit:]] )
83             )
84             }{
85 30   66     122 $2 // do {
86 20         47 my $n = hex $1;
87 20 50       151 $n > 0x10_ffff ? "\x{fffd}" : chr $n
88             }
89             }xegr
90             }
91              
92 20 50   20   42 fun _css_unescape_string($str) {
  20 50       36  
  20         37  
  20         24  
93 20 100       75 if ($str =~ s/\A"//) {
94 15 50       62 $str =~ s/"\z// or die "Internal error: unterminated \" string";
95             } else {
96 5 50       32 $str =~ s/\A'// or die "Internal error: malformed (unquoted) string: $str";
97 5 50       23 $str =~ s/'\z// or die "Internal error: unterminated ' string";
98             }
99 20         68 $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     33 $2 // do {
110 4         13 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 606 50 33 606   1175 fun _try_parse_simple_selector($src_ref, :$allow_tag_name) {
  606 50       2153  
  606 50       1561  
  606 50       1536  
  606         1326  
  606         726  
152 606 100 100     3787 if ($allow_tag_name && $$src_ref =~ /\G(\*|$ident)/gc) {
153 160         429 return { type => ST_TAG_NAME, name => _css_unescape($1) };
154             }
155              
156 446 100       2427 if ($$src_ref =~ /\G#($nmchar++)/gc) {
157 60         154 return { type => ST_ATTR_EQ, attr => 'id', value => _css_unescape($1) };
158             }
159              
160 386 100       1920 if ($$src_ref =~ /\G\.($ident)/gc) {
161 35         101 return { type => ST_ATTR_LIST_HAS, attr => 'class', value => _css_unescape($1) };
162             }
163              
164 351 100       4261 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         179 my ($attr, $op, $val_ident, $val_string) = ($1, $2, $3, $4);
176 40         88 $attr = _css_unescape $attr;
177 40         88 $attr =~ tr/A-Z/a-z/;
178 40 100       88 if (!defined $op) {
179 9         53 return { type => ST_ATTR_HAS, attr => $attr };
180             }
181              
182 31 100       75 my $value = defined($val_ident) ? _css_unescape($val_ident) : _css_unescape_string($val_string);
183 31 100 66     222 if (
      100        
      100        
      100        
184             ($op eq '~' && ($value eq '' || $value =~ /$ws/)) ||
185             ($op =~ /\A[\^\$*]\z/ && $value eq '')
186             ) {
187 4         34 return { type => ST_FALSE };
188             }
189 27         192 return { type => $attr_op_type{$op}, attr => $attr, value => $value };
190             }
191              
192 311 100       754 if ($$src_ref =~ /\G:(nth-child|nth-of-type())\(/iaagc) {
193 17         53 my $pos = $-[0];
194 17         48 my $name = $1;
195 17 100       48 my $type = defined $2 ? ST_NTH_CHILD_OF_TYPE : ST_NTH_CHILD;
196 17         96 $$src_ref =~ /\G$ws++/gc;
197 17 50       189 $$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         68 my ($arg, $num1, $sign, $num2) = ($1, $2, $3, $4);
211 17         67 $$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       46 if (defined $num1) {
    100          
    100          
216 11 100 66     51 if ($num1 eq '+' || $num1 eq '') {
    100          
217 2         11 $num1 = 1;
218             } elsif ($num1 eq '-') {
219 3         8 $num1 = -1;
220             } else {
221 6         12 $num1 = 0 + $num1;
222             }
223 11 50       19 if (defined $sign) {
224 11         18 $num2 = 0 + $num2;
225 11 100       24 $num2 = -$num2 if $sign eq '-';
226             } else {
227 0         0 $num2 = 0;
228             }
229             } elsif (lc($arg) eq 'odd') {
230 2         5 $num1 = 2;
231 2         3 $num2 = 1;
232             } elsif (lc($arg) eq 'even') {
233 2         5 $num1 = 2;
234 2         3 $num2 = 0;
235             } else {
236 2         5 $num1 = 0;
237 2         4 $num2 = 0 + $arg;
238             }
239 17         99 return { type => $type, a => $num1, b => $num2 };
240             }
241              
242 294 100       613 if ($$src_ref =~ /\G:first-child(?![^.#:\[\]),>~ \t\r\n\f])/iaagc) {
243 2         16 return { type => ST_NTH_CHILD, a => 0, b => 1 };
244             }
245              
246 292 100       510 if ($$src_ref =~ /\G:first-of-type(?![^.#:\[\]),>~ \t\r\n\f])/iaagc) {
247 2         20 return { type => ST_NTH_CHILD_OF_TYPE, a => 0, b => 1 };
248             }
249              
250 290 50       1340 if ($$src_ref =~ /\G:($ident)/gc) {
251 0         0 croak "Unsupported pseudo-class :$1";
252             }
253              
254             undef
255 290         2496 }
256              
257 256 50   256   546 fun _parse_selector($src) {
  256 50       478  
  256         393  
  256         337  
258 256 50       452 croak "Invalid selector: $src" if ref $src;
259              
260 256         343 my @alternatives;
261 256         409 my $sequences = [];
262 256         380 my $simples = [];
263              
264 256         709 pos($src) = 0;
265 256         1927 $src =~ /\G$ws++/gc;
266              
267 256         832 while () {
268 606 100       1871 if ($src =~ /\G:not\(/iaagc) {
    100          
    100          
    100          
269 5         16 my $pos = $-[0];
270 5         98 $src =~ /\G$ws++/gc;
271 5 50       19 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         53 $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         85 push @$simples, $simple;
278             } elsif (defined(my $simple = _try_parse_simple_selector \$src, allow_tag_name => !@$simples)) {
279 311         790 push @$simples, $simple;
280             } elsif ($src =~ /\G(?>$ws*([>~+])|$ws)$ws*+/gc) {
281 33   100     125 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         179 link_type => $comb_type{$comb},
287             );
288 33         405 $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         5 push @$sequences, HTML::Blitz::SSSelector->new(
293             simple_selectors => $simples,
294             link_type => undef,
295             );
296 1         3 $simples = [];
297 1         3 push @alternatives, $sequences;
298 1         3 $sequences = [];
299             } else {
300 256         597 last;
301             }
302             }
303              
304 256 50       1487 $src =~ /\G$ws*+\z/
305             or croak "Unparsable selector: " . substr($src, pos($src), 100);
306 256 0       594 @$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 256         1058 push @$sequences, HTML::Blitz::SSSelector->new(
313             simple_selectors => $simples,
314             link_type => undef,
315             );
316 256         490 push @alternatives, $sequences;
317             \@alternatives
318 256         699 }
319              
320 132 50   132   333 fun _text($str) {
  132 50       241  
  132         235  
  132         192  
321 132         1689 +{ type => AT_P_IMMEDIATE, value => '' . $str }
322             }
323              
324 82 50   82   170 fun _varify($throw, $var) {
  82 50       159  
  82         135  
  82         104  
325 82 50       366 $var =~ /\A[^\W\d][\w\-.]*\z/ or $throw->("Invalid variable name '$var'");
326 82         698 [undef, $var]
327             }
328              
329 41 50   41   161 fun _var($throw, $var) {
  41 50       100  
  41         82  
  41         87  
330 41         106 +{ type => AT_P_VARIABLE, value => _varify($throw, $var) }
331             }
332              
333 30 50   30   72 fun _is_callable($val) {
  30 50       68  
  30         56  
  30         37  
334 30 50 0     122 ref($val) eq 'CODE' ||
335             (blessed($val) && overload::Method($val, '&{}'))
336             }
337              
338 7 50   7   23 fun _template($throw, $val) {
  7 50       20  
  7         17  
  7         10  
339 7 50 33     77 blessed($val) && $val->isa('HTML::Blitz::Template')
340             or $throw->("Argument must be an instance of HTML::Blitz::Template: '$val'");
341 7         38 +{ 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         6  
345 5         15 +{ 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   84 fun _id($x) { $x }
  33 50       72  
  33         72  
  33         57  
  33         83  
359              
360 17 50   17   48 fun _mk_transform_attr($attr, $fn) {
  17 50       38  
  17         35  
  17         23  
361             +{
362 17         395 %_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         21  
  6         8  
376 6         17 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   69 _mk_transform_attr $attr, fun ($value) {
  30 50       80  
  30         71  
  30         38  
381 30         49 my (@list, %seen);
382 30   100     119 for my $word (($value // '') =~ /[^ \t\r\n\f]+/g, @words) {
383 41 100       148 push @list, $word if !$seen{$word}++;
384             }
385 30         113 join ' ', @list
386 6         33 }
387             }
388              
389 6 50   6   16 fun _attr_remove_word($throw, $attr, @words) {
  6         17  
  6         7  
390 6         8 my %banned;
391 6         13 for my $word (@words) {
392 22 50       46 $throw->("Argument cannot contain whitespace: '$word'")
393             if $word =~ /[ \t\r\n\f]/;
394 22         42 $banned{$word} = 1;
395             }
396 6 50   6   18 _mk_transform_attr $attr, fun ($value) {
  6 50       13  
  6         13  
  6         10  
397 6         9 my @list;
398 6   100     69 my $new_value = join ' ', grep !$banned{$_}, ($value // '') =~ /[^ \t\r\n\f]+/g;
399 6 100       27 length $new_value ? $new_value : undef
400 6         27 }
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 259 50   259   660 fun _parse_action($throw, $action_proto, $custom_action = {}) {
  259 50       530  
  259 100       696  
  259         357  
691 259 50       652 ref($action_proto) eq 'ARRAY'
692             or $throw->("Not an ARRAY reference: '$action_proto'");
693 259 50       722 @$action_proto
694             or $throw->("Action cannot be empty");
695 259         612 my ($type, @args) = @$action_proto;
696 259 0 66     1246 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 259 0   0   1292 $action_fn->(fun ($err) { $throw->("'$type': $err"); }, @args)
  0 0       0  
  0         0  
  0         0  
  0         0  
  0         0  
698             }
699              
700 256 50 66 256   502 fun _parse_rule($proto, :$custom_action = {}) {
  256 50       643  
  256 100       484  
  256 50       670  
  256         612  
  256         355  
701 256         604 my ($sel_str, @action_protos) = @$proto;
702 256         547 my $selector = _parse_selector $sel_str;
703 256 0   0   1606 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 256         1067 $selector, \@actions
705             }
706              
707 253 50   253 1 1151 method add_rules(@rules) {
  253         387  
  253         438  
  253         340  
708 253         513 my $ruleset = $self->{ruleset};
709 253         509 for my $rule (@rules) {
710 249         507 my ($selector, $actions) = _parse_rule $rule;
711 249 50       1075 $ruleset->add_rule($selector, @$actions)
712             if @$actions;
713             }
714             }
715              
716 276 50   276 1 26123 method apply_to_html($name, $html) {
  276 50       608  
  276         432  
  276         536  
  276         374  
717 276         891 HTML::Blitz::Template->new(_codegen => $self->{ruleset}->compile($name, $html))
718             }
719              
720 10 50   10 1 6568 method apply_to_file($file) {
  10 50       24  
  10         17  
  10         16  
  10         43  
721 10         14 my $html = do {
722 10 50       447 open my $fh, '<:encoding(UTF-8)', $file
723             or croak "Can't open $file: $!";
724 10         1962 local $/;
725 10         409 readline $fh
726             };
727 10         401 $self->apply_to_html($file, $html)
728             }
729              
730             1
731             __END__