File Coverage

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


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