File Coverage

blib/lib/HTML/Blitz/Parser.pm
Criterion Covered Total %
statement 191 195 98.4
branch 123 158 77.8
condition 19 24 79.1
subroutine 10 10 100.0
pod 0 4 0.0
total 343 391 87.9


line stmt bran cond sub pod time code
1             # This code can be redistributed and modified under the terms of the GNU
2             # General Public License as published by the Free Software Foundation, either
3             # version 3 of the License, or (at your option) any later version.
4             # See the "COPYING" file for details.
5             package HTML::Blitz::Parser;
6 11     11   82 use HTML::Blitz::pragma;
  11         22  
  11         74  
7 11     11   8984 use HTML::Blitz::ParseError ();
  11         29  
  11         439  
8 11         2714 use HTML::Blitz::TokenType qw(
9             TT_TAG_OPEN
10             TT_TAG_CLOSE
11             TT_TEXT
12             TT_COMMENT
13             TT_DOCTYPE
14 11     11   4572 );
  11         30  
15              
16             our $VERSION = '0.08';
17              
18             method _fail(
19             $msg,
20 8         22 :$pos = pos(${$self->{src_ref}}),
21             :$width = 1,
22             :$alt_msg = undef,
23             :$alt_pos = undef,
24             :$alt_width = 1,
25 28 50 66 28   82 ) {
  28 50       136  
  28 100       57  
  28 100       102  
  28 100       100  
  28 50       102  
  28         52  
  28         52  
  28         58  
  28         85  
  28         39  
26             die HTML::Blitz::ParseError->new(
27             src_name => $self->{src_name},
28             src_ref => $self->{src_ref},
29 28         165 msg => $msg,
30             pos => $pos,
31             width => $width,
32             alt_msg => $alt_msg,
33             alt_pos => $alt_pos,
34             alt_width => $alt_width,
35             )
36             }
37              
38 11 50   11 0 31 method throw_for($token, $msg) {
  11 50       27  
  11         19  
  11         22  
  11         16  
39 11         21 my $type = $token->{type};
40             $self->_fail(
41             $msg,
42             pos => $token->{pos},
43             $type eq TT_TAG_OPEN || $type eq TT_TAG_CLOSE
44             ? (width => 1 + ($type eq TT_TAG_CLOSE) + length $token->{name})
45 11 100 66     66 : (),
46             );
47             }
48              
49 274 50   274 0 634 method new($class: $src_name, $src) {
  274 50       551  
  274         416  
  274         532  
  274         371  
50 274         1038 my $self = bless {
51             src_name => $src_name,
52             src_ref => \$src,
53             tag_stack => [],
54             in_foreign_elem => 0,
55             }, $class;
56              
57 274         870 $src =~ s/\r\n?/\n/g; # normalize newlines
58 274 50       1363 $src =~ /([\x{d800}-\x{dfff}])/
59             and $self->_fail(sprintf("surrogate codepoint U+%04X in input", ord $1), pos => $-[1]);
60 274 50       21710 $src =~ /(
61             [\x{fdd0}-\x{fdef}]
62             | [\x{fffe}\x{ffff}]
63             | [\x{1fffe}\x{1ffff}]
64             | [\x{2fffe}\x{2ffff}]
65             | [\x{3fffe}\x{3ffff}]
66             | [\x{4fffe}\x{4ffff}]
67             | [\x{5fffe}\x{5ffff}]
68             | [\x{6fffe}\x{6ffff}]
69             | [\x{7fffe}\x{7ffff}]
70             | [\x{8fffe}\x{8ffff}]
71             | [\x{9fffe}\x{9ffff}]
72             | [\x{afffe}\x{affff}]
73             | [\x{bfffe}\x{bffff}]
74             | [\x{cfffe}\x{cffff}]
75             | [\x{dfffe}\x{dffff}]
76             | [\x{efffe}\x{effff}]
77             | [\x{ffffe}\x{fffff}]
78             | [\x{10fffe}\x{10ffff}]
79             )/x and $self->_fail(sprintf("non-character codepoint U+%04X in input", ord $1), pos => $-[1]);
80 274 50       1017 $src =~ /((?![ \t\n\f])[\x00-\x1f\x7f-\x9f])/
81             and $self->_fail(sprintf("control character U+%04X in input", ord $1), pos => $-[1]);
82              
83 274         941 pos($src) = 0;
84              
85 274         843 $self
86             }
87              
88             my %entities;
89             {
90             while (my $line = readline DATA) {
91             chomp $line;
92             my ($name, $value) = $line =~ /^(\w+) (\d+(?:,\d+)*)\z/a
93             or die "Internal error: malformed entitiy definition '$line'";
94             $value =~ s/(\d+),?/chr $1/aeg;
95             $entities{$name} = $value;
96             }
97             close DATA;
98             }
99              
100             my %void_tags = map +($_ => 1), qw(
101             area
102             base br
103             col
104             embed
105             hr
106             img input
107             link
108             meta
109             source
110             track
111             wbr
112              
113             basefont bgsound
114             frame
115             keygen
116             param
117             );
118              
119             my %foreign_tags = map +($_ => 1), qw(
120             math
121             svg
122             );
123              
124 3625 50   3625   6664 method _consume_entity_maybe($chunk) {
  3625 50       6058  
  3625         4833  
  3625         8010  
  3625         4426  
125 3625 100       17498 $chunk eq '&' or return $chunk;
126              
127 162         249 my $src_ref = $self->{src_ref};
128 162         210 my $char;
129              
130 162 100       390 if ($$src_ref =~ /\G#/gc) {
131 30 100       61 if ($$src_ref =~ /\G[xX]/gc) {
132 18 50       48 $$src_ref =~ /\G([[:xdigit:]]+)/gc
133             or $self->_fail("missing hex digits after '&#x'");
134 18         54 $char = chr hex $1;
135             } else {
136 12 50       36 $$src_ref =~ /\G(\d+)/agc
137             or $self->_fail("missing digits after '&#'");
138 12         44 $char = chr $1;
139             }
140             } else {
141 132 50       389 $$src_ref =~ /\G(\w+)/agc
142             or $self->_fail("missing character name after '&'");
143 132   33     457 $char = $entities{$1}
144             // $self->_fail("invalid character reference '$1' after '&'", pos => $-[1], width => length $1 );
145             }
146 162 50       432 $$src_ref =~ /\G;/gc
147             or $self->_fail("missing ';' after character reference");
148              
149 162         610 $char
150             }
151              
152 2167 50   2167 0 4099 method current_tag() {
  2167 50       3727  
  2167         3008  
  2167         2558  
153 2167         3260 my $tag_stack = $self->{tag_stack};
154 2167 100       6098 @$tag_stack ? $tag_stack->[-1][0] : ''
155             }
156              
157 5628 50   5628 0 10949 method parse() {
  5628 50       9453  
  5628         7981  
  5628         7067  
158 5628         8346 my $src_ref = $self->{src_ref};
159 5628         7677 my $tag_stack = $self->{tag_stack};
160              
161 5628 100       11068 my $cur_tag = @$tag_stack ? $tag_stack->[-1][0] : '';
162              
163 5628 100       14192 if ($$src_ref =~ /\G\z/) {
164 236 50       534 length $cur_tag
165             and $self->_fail("unclosed '<$cur_tag>' tag", pos => $tag_stack->[-1][1], width => 1 + length($cur_tag));
166 236         790 return undef;
167             }
168              
169 5392         8812 my $pos = pos $$src_ref;
170              
171             {
172 5392         6919 my $text = '';
  5392         7272  
173              
174 5392 100       13646 if ($cur_tag eq 'script') {
    100          
    100          
    50          
175 202 50   1   760 my $err = fun () { $self->_fail("unclosed '<$cur_tag>' tag", pos => $tag_stack->[-1][1], width => 1 + length($cur_tag)) };
  1         6  
  1         2  
  1         12  
176             SCRIPT_DATA: {
177 202 50       302 $$src_ref =~ m{ ( ) | < (/?) script [ \t\n\f/>] }xaaigc or $err->();
  12         70  
182 11         26 $match_start = $-[0];
183 11 100       38 if ($1) {
184 2         6 redo SCRIPT_DATA;
185             }
186 9 100       28 if (!$2) {
187 6 50       28 $$src_ref =~ m{ (-->) | ] }xaaigc or $err->();
188 6 100       21 if ($1) {
189 3         10 redo SCRIPT_DATA;
190             }
191 3         6 redo SCRIPT_DATA_ESCAPED;
192             }
193             }
194             }
195 201         443 pos($$src_ref) = $match_start;
196             }
197 201         1039 $text = substr $$src_ref, $pos, pos($$src_ref) - $pos;
198             } elsif ($cur_tag eq 'style') {
199 140 100       575 if ($$src_ref =~ m{\G ( (?: (?! ] ) . )+ ) }xsgc) {
200 70         169 $text = $1;
201             }
202             } elsif ($cur_tag eq 'title') {
203 188         820 while ($$src_ref =~ m{\G ( (?: (?! ] ) [^&] )+ | & ) }xgc) {
204 94         222 $text .= $self->_consume_entity_maybe($1);
205             }
206             } elsif ($cur_tag eq 'textarea') {
207 0         0 while ($$src_ref =~ m{\G ( (?: (?! ] ) [^&] )+ | & ) }xgc) {
208 0         0 $text .= $self->_consume_entity_maybe($1);
209             }
210             } else {
211 4862         14646 while ($$src_ref =~ /\G ( [^<&]+ | & )/xgc) {
212 2432         4979 $text .= $self->_consume_entity_maybe($1);
213             }
214             }
215              
216 5391 100       11224 if (length $text) {
217             return {
218 2408         11608 type => TT_TEXT,
219             pos => $pos,
220             content => $text,
221             };
222             }
223             }
224              
225 2983 50       8222 if ($$src_ref =~ /\G
226 2983 100       6119 if ($$src_ref =~ /\G!/gc) {
227 59 100       149 if ($$src_ref =~ /\G--/gc) {
228 47 100       137 if ($$src_ref =~ /\G(-?>)/) {
229 2         10 $self->_fail("improperly closed comment", width => length($1));
230             }
231 45 100       214 $$src_ref =~ /\G(.*?)(?|--!?>)/sgc
232             or $self->_fail("unterminated comment", pos => $pos, width => 4);
233 44         152 my ($text, $closer) = ($1, $2);
234 44 100       89 if ($closer eq '') {
245 1         7 $self->_fail(
246             "improperly closed comment (should be '-->')",
247             pos => $-[2],
248             width => length($closer),
249             alt_msg => "comment starting here",
250             alt_pos => $pos,
251             alt_width => 4,
252             );
253             }
254 42 100       69 if ($closer eq '') {
255 2         5 $text .= '
256 2         4 $closer = '-->';
257             }
258             return {
259 42         210 type => TT_COMMENT,
260             pos => $pos,
261             content => $text,
262             };
263             }
264              
265 12 100       42 if ($$src_ref =~ /\Gdoctype/aaigc) {
266 10 50       35 $$src_ref =~ /\G[ \t\n\f]+/gc
267             or $self->_fail("missing whitespace after '
268 10 50       25 $$src_ref =~ /\Ghtml/aaigc
269             or $self->_fail("invalid non-html doctype");
270 10 50       41 $$src_ref =~ /\G[ \t\n\f]*>/gc
271             or $self->_fail("missing '>' after '
272             return {
273 10         66 type => TT_DOCTYPE,
274             pos => $pos,
275             };
276             }
277              
278 2 100 66     12 if ($self->{in_foreign_elem} && $$src_ref =~ /\G\[CDATA\[/gc) {
279 1         5 my $text_start = $+[0];
280 1 50       6 $$src_ref =~ /\]\]>/gc or $self->_fail("missing ']]>' after '
281 1         4 my $text_end = $-[0];
282             return {
283 1         9 type => TT_TEXT,
284             pos => $text_start,
285             content => substr($$src_ref, $text_start, $text_end - $text_start),
286             };
287             }
288              
289 1         4 $self->_fail("invalid declaration (should be '--' or 'DOCTYPE')");
290             }
291              
292 2924         5435 my $closing = $$src_ref =~ m{\G/}gc;
293              
294 2924 50       7877 $$src_ref =~ m{\G([a-zA-Z][^\s/>[:cntrl:]]*)}gc
295             or $self->_fail("invalid tag name");
296 2924         7579 (my $name = $1) =~ tr/A-Z/a-z/;
297              
298 2924         5729 $$src_ref =~ /\G[ \t\n\f]+/gc;
299              
300 2924         4457 my (%attrs, %attr_pos);
301 2924         6824 while ($$src_ref =~ m{\G([^\s/>="'<[:cntrl:]]+)}gc) {
302 1104         3101 my $apos = $-[1];
303 1104 100       2402 if ($closing) {
304 1         10 $self->_fail("invalid attribute in end tag ''", pos => $apos, width => length $1);
305             }
306 1103         2866 (my $attr_name = $1) =~ tr/A-Z/a-z/;
307              
308 1103 100       2357 if (exists $attrs{$attr_name}) {
309 1         19 $self->_fail("duplicate attribute '$attr_name' in '<$name>' tag", pos => $apos, width => length($attr_name), alt_msg => "first defined here", alt_pos => $attr_pos{$attr_name}, alt_width => length($attr_name));
310             }
311              
312 1102         2289 $$src_ref =~ /\G[ \t\n\f]+/gc;
313              
314 1102         1589 my $attr_value = '';
315 1102 100       2808 if ($$src_ref =~ /\G=[ \t\n\f]*/gc) {
316 1095 100       3470 if ($$src_ref =~ /\G"/gc) {
    100          
    100          
317 339         854 my $qpos = $-[0];
318 339         584 my $text = '';
319 339         966 while ($$src_ref =~ /\G ( [^"&]+ | & ) /xgc) {
320 338         691 $text .= $self->_consume_entity_maybe($1);
321             }
322 339 50       927 $$src_ref =~ /\G"/gc
323             or $self->_fail(q{missing '"' after attribute value}, alt_msg => 'starting here', alt_pos => $qpos);
324 339 50       763 $$src_ref =~ m{\G[^ \t\n\f/>]}
325             and $self->_fail('missing whitespace after attribute value');
326 339         638 $attr_value = $text;
327             } elsif ($$src_ref =~ /\G'/gc) {
328 8         43 my $qpos = $-[0];
329 8         19 my $text = '';
330 8         30 while ($$src_ref =~ /\G ( [^'&]+ | & ) /xgc) {
331 14         53 $text .= $self->_consume_entity_maybe($1);
332             }
333 8 100       29 $$src_ref =~ /\G'/gc
334             or $self->_fail(q{missing "'" after attribute value}, alt_msg => 'starting here', alt_pos => $qpos);
335 7 100       24 $$src_ref =~ m{\G[^ \t\n\f/>]}
336             and $self->_fail('missing whitespace after attribute value');
337 6         11 $attr_value = $text;
338             } elsif ($$src_ref =~ /\G ( [^ \t\n\f&>"'<=`]+ | & )/xgc) {
339 747         1102 my $text = '';
340 747         952 do {
341 747         1580 $text .= $self->_consume_entity_maybe($1);
342             } while $$src_ref =~ /\G ( [^ \t\n\f&>"'<=`]+ | & )/xgc;
343 747         1359 $attr_value = $text;
344             } else {
345 1         5 $self->_fail("missing attribute value after '='");
346             }
347             }
348              
349 1099         2557 $attrs{$attr_name} = $attr_value;
350 1099         1802 $attr_pos{$attr_name} = $apos;
351              
352 1099         3329 $$src_ref =~ /\G[ \t\n\f]+/gc;
353             }
354              
355 2919 100       8828 $$src_ref =~ m{\G(/?)>}gc
356             or $self->_fail("missing '>' at end of tag", alt_msg => 'starting here', alt_pos => $pos, alt_width => 1 + $closing + length($name));
357 2917         5381 my $is_self_closing = length $1;
358              
359 2917 100       5175 if ($closing) {
360 1347 50       2273 $is_self_closing and $self->_fail("invalid '/' at end of closing tag ''", pos => $-[1]);
361 1347 100       2789 @$tag_stack
362             or $self->_fail("closing tag '' has no corresponding open tag", pos => $pos, width => 1 + 1 + length($name));
363              
364 1345 50       2469 $cur_tag eq $name
365             or $self->_fail("closing tag '' does not match current open tag '<$cur_tag>'", pos => $pos, width => 1 + 1 + length($name), alt_msg => 'starting here', alt_pos => $tag_stack->[-1][1], alt_width => 1 + length($cur_tag));
366              
367 1345 100       2901 if ($foreign_tags{$cur_tag}) {
368 2         7 $self->{in_foreign_elem}--;
369             }
370 1345         1954 pop @$tag_stack;
371             return {
372 1345         7075 type => TT_TAG_CLOSE,
373             pos => $pos,
374             name => $name,
375             };
376             }
377              
378 1570         2732 my $is_void = $void_tags{$name};
379 1570 100 100     3185 if ($is_self_closing && !$is_void && !$foreign_tags{$name} && !$self->{in_foreign_elem}) {
      100        
      100        
380 1         10 $self->_fail("invalid '/' at end of non-void tag '<$name>'", pos => $-[1], alt_msg => 'starting here', alt_pos => $pos, alt_width => 1 + length($name));
381             }
382 1569   100     5796 $is_self_closing ||= $is_void;
383              
384 1569 100       2764 if (!$is_self_closing) {
385 1376         1755 push @{$self->{tag_stack}}, [$name, $pos];
  1376         3822  
386 1376 100       3213 if ($foreign_tags{$name}) {
387 2         5 $self->{in_foreign_elem}++;
388             }
389             }
390              
391             return {
392 1569         10036 type => TT_TAG_OPEN,
393             pos => $pos,
394             name => $name,
395             attrs => \%attrs,
396             is_void => $is_void,
397             is_self_closing => $is_self_closing,
398             };
399             }
400              
401             # uncoverable statement
402 0           die "Internal error: unparsable input '${\substr $$src_ref, pos($$src_ref), 10}'...";
  0            
403             }
404              
405             1
406             __DATA__