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 0.1001;
6 11     11   182 use HTML::Blitz::pragma;
  11         80  
  11         84  
7 11     11   12444 use HTML::Blitz::ParseError ();
  11         36  
  11         553  
8 11         4151 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   5869 );
  11         39  
15              
16             method _fail(
17             $msg,
18 8         29 :$pos = pos(${$self->{src_ref}}),
19             :$width = 1,
20             :$alt_msg = undef,
21             :$alt_pos = undef,
22             :$alt_width = 1,
23 28 50 66 28   99 ) {
  28 50       176  
  28 100       58  
  28 100       173  
  28 100       121  
  28 50       107  
  28         77  
  28         72  
  28         82  
  28         88  
  28         47  
24             die HTML::Blitz::ParseError->new(
25             src_name => $self->{src_name},
26             src_ref => $self->{src_ref},
27 28         352 msg => $msg,
28             pos => $pos,
29             width => $width,
30             alt_msg => $alt_msg,
31             alt_pos => $alt_pos,
32             alt_width => $alt_width,
33             )
34             }
35              
36 11 50   11 0 143 method throw_for($token, $msg) {
  11 50       33  
  11         22  
  11         29  
  11         18  
37 11         24 my $type = $token->{type};
38             $self->_fail(
39             $msg,
40             pos => $token->{pos},
41             $type eq TT_TAG_OPEN || $type eq TT_TAG_CLOSE
42             ? (width => 1 + ($type eq TT_TAG_CLOSE) + length $token->{name})
43 11 100 66     88 : (),
44             );
45             }
46              
47 276 50   276 0 803 method new($class: $src_name, $src) {
  276 50       3056  
  276         489  
  276         770  
  276         468  
48 276         1672 my $self = bless {
49             src_name => $src_name,
50             src_ref => \$src,
51             tag_stack => [],
52             in_foreign_elem => 0,
53             }, $class;
54              
55 276         949 $src =~ s/\r\n?/\n/g; # normalize newlines
56 276 50       1347 $src =~ /([\x{d800}-\x{dfff}])/
57             and $self->_fail(sprintf("surrogate codepoint U+%04X in input", ord $1), pos => $-[1]);
58 276 50       32791 $src =~ /(
59             [\x{fdd0}-\x{fdef}]
60             | [\x{fffe}\x{ffff}]
61             | [\x{1fffe}\x{1ffff}]
62             | [\x{2fffe}\x{2ffff}]
63             | [\x{3fffe}\x{3ffff}]
64             | [\x{4fffe}\x{4ffff}]
65             | [\x{5fffe}\x{5ffff}]
66             | [\x{6fffe}\x{6ffff}]
67             | [\x{7fffe}\x{7ffff}]
68             | [\x{8fffe}\x{8ffff}]
69             | [\x{9fffe}\x{9ffff}]
70             | [\x{afffe}\x{affff}]
71             | [\x{bfffe}\x{bffff}]
72             | [\x{cfffe}\x{cffff}]
73             | [\x{dfffe}\x{dffff}]
74             | [\x{efffe}\x{effff}]
75             | [\x{ffffe}\x{fffff}]
76             | [\x{10fffe}\x{10ffff}]
77             )/x and $self->_fail(sprintf("non-character codepoint U+%04X in input", ord $1), pos => $-[1]);
78 276 50       1654 $src =~ /((?![ \t\n\f])[\x00-\x1f\x7f-\x9f])/
79             and $self->_fail(sprintf("control character U+%04X in input", ord $1), pos => $-[1]);
80              
81 276         1359 pos($src) = 0;
82              
83 276         1185 $self
84             }
85              
86             my %entities;
87             {
88             while (my $line = readline DATA) {
89             chomp $line;
90             my ($name, $value) = $line =~ /^(\w+) (\d+(?:,\d+)*)\z/a
91             or die "Internal error: malformed entitiy definition '$line'";
92             $value =~ s/(\d+),?/chr $1/aeg;
93             $entities{$name} = $value;
94             }
95             close DATA;
96             }
97              
98             my %void_tags = map +($_ => 1), qw(
99             area
100             base br
101             col
102             embed
103             hr
104             img input
105             link
106             meta
107             source
108             track
109             wbr
110              
111             basefont bgsound
112             frame
113             keygen
114             param
115             );
116              
117             my %foreign_tags = map +($_ => 1), qw(
118             math
119             svg
120             );
121              
122 3640 50   3640   7780 method _consume_entity_maybe($chunk) {
  3640 50       8971  
  3640         5803  
  3640         27466  
  3640         7038  
123 3640 100       36249 $chunk eq '&' or return $chunk;
124              
125 163         316 my $src_ref = $self->{src_ref};
126 163         284 my $char;
127              
128 163 100       508 if ($$src_ref =~ /\G#/gc) {
129 30 100       97 if ($$src_ref =~ /\G[xX]/gc) {
130 18 50       60 $$src_ref =~ /\G([[:xdigit:]]+)/gc
131             or $self->_fail("missing hex digits after '&#x'");
132 18         69 $char = chr hex $1;
133             } else {
134 12 50       45 $$src_ref =~ /\G(\d+)/agc
135             or $self->_fail("missing digits after '&#'");
136 12         67 $char = chr $1;
137             }
138             } else {
139 133 50       567 $$src_ref =~ /\G(\w+)/agc
140             or $self->_fail("missing character name after '&'");
141 133   33     696 $char = $entities{$1}
142             // $self->_fail("invalid character reference '$1' after '&'", pos => $-[1], width => length $1 );
143             }
144 163 50       616 $$src_ref =~ /\G;/gc
145             or $self->_fail("missing ';' after character reference");
146              
147 163         805 $char
148             }
149              
150 2172 50   2172 0 4228 method current_tag() {
  2172 50       4267  
  2172         3194  
  2172         3645  
151 2172         6580 my $tag_stack = $self->{tag_stack};
152 2172 100       6779 @$tag_stack ? $tag_stack->[-1][0] : ''
153             }
154              
155 5653 50   5653 0 15410 method parse() {
  5653 50       10770  
  5653         9271  
  5653         7315  
156 5653         14501 my $src_ref = $self->{src_ref};
157 5653         21644 my $tag_stack = $self->{tag_stack};
158              
159 5653 100       12739 my $cur_tag = @$tag_stack ? $tag_stack->[-1][0] : '';
160              
161 5653 100       20150 if ($$src_ref =~ /\G\z/) {
162 238 50       4685 length $cur_tag
163             and $self->_fail("unclosed '<$cur_tag>' tag", pos => $tag_stack->[-1][1], width => 1 + length($cur_tag));
164 238         920 return undef;
165             }
166              
167 5415         16752 my $pos = pos $$src_ref;
168              
169             {
170 5415         8225 my $text = '';
  5415         26647  
171              
172 5415 100       16358 if ($cur_tag eq 'script') {
    100          
    100          
    50          
173 202 50   1   970 my $err = fun () { $self->_fail("unclosed '<$cur_tag>' tag", pos => $tag_stack->[-1][1], width => 1 + length($cur_tag)) };
  1         3  
  1         2  
  1         8  
174             SCRIPT_DATA: {
175 202 50       359 $$src_ref =~ m{ ( ) | < (/?) script [ \t\n\f/>] }xaaigc or $err->();
  12         80  
180 11         40 $match_start = $-[0];
181 11 100       42 if ($1) {
182 2         6 redo SCRIPT_DATA;
183             }
184 9 100       63 if (!$2) {
185 6 50       27 $$src_ref =~ m{ (-->) | ] }xaaigc or $err->();
186 6 100       18 if ($1) {
187 3         10 redo SCRIPT_DATA;
188             }
189 3         9 redo SCRIPT_DATA_ESCAPED;
190             }
191             }
192             }
193 201         640 pos($$src_ref) = $match_start;
194             }
195 201         1333 $text = substr $$src_ref, $pos, pos($$src_ref) - $pos;
196             } elsif ($cur_tag eq 'style') {
197 140 100       753 if ($$src_ref =~ m{\G ( (?: (?! ] ) . )+ ) }xsgc) {
198 70         220 $text = $1;
199             }
200             } elsif ($cur_tag eq 'title') {
201 188         1083 while ($$src_ref =~ m{\G ( (?: (?! ] ) [^&] )+ | & ) }xgc) {
202 94         273 $text .= $self->_consume_entity_maybe($1);
203             }
204             } elsif ($cur_tag eq 'textarea') {
205 0         0 while ($$src_ref =~ m{\G ( (?: (?! ] ) [^&] )+ | & ) }xgc) {
206 0         0 $text .= $self->_consume_entity_maybe($1);
207             }
208             } else {
209 4885         48107 while ($$src_ref =~ /\G ( [^<&]+ | & )/xgc) {
210 2441         14268 $text .= $self->_consume_entity_maybe($1);
211             }
212             }
213              
214 5414 100       21851 if (length $text) {
215             return {
216 2415         31737 type => TT_TEXT,
217             pos => $pos,
218             content => $text,
219             };
220             }
221             }
222              
223 2999 50       12395 if ($$src_ref =~ /\G
224 2999 100       10820 if ($$src_ref =~ /\G!/gc) {
225 59 100       193 if ($$src_ref =~ /\G--/gc) {
226 47 100       187 if ($$src_ref =~ /\G(-?>)/) {
227 2         14 $self->_fail("improperly closed comment", width => length($1));
228             }
229 45 100       310 $$src_ref =~ /\G(.*?)(?|--!?>)/sgc
230             or $self->_fail("unterminated comment", pos => $pos, width => 4);
231 44         214 my ($text, $closer) = ($1, $2);
232 44 100       106 if ($closer eq '') {
243 1         9 $self->_fail(
244             "improperly closed comment (should be '-->')",
245             pos => $-[2],
246             width => length($closer),
247             alt_msg => "comment starting here",
248             alt_pos => $pos,
249             alt_width => 4,
250             );
251             }
252 42 100       88 if ($closer eq '') {
253 2         6 $text .= '
254 2         3 $closer = '-->';
255             }
256             return {
257 42         309 type => TT_COMMENT,
258             pos => $pos,
259             content => $text,
260             };
261             }
262              
263 12 100       87 if ($$src_ref =~ /\Gdoctype/aaigc) {
264 10 50       47 $$src_ref =~ /\G[ \t\n\f]+/gc
265             or $self->_fail("missing whitespace after '
266 10 50       44 $$src_ref =~ /\Ghtml/aaigc
267             or $self->_fail("invalid non-html doctype");
268 10 50       61 $$src_ref =~ /\G[ \t\n\f]*>/gc
269             or $self->_fail("missing '>' after '
270             return {
271 10         85 type => TT_DOCTYPE,
272             pos => $pos,
273             };
274             }
275              
276 2 100 66     16 if ($self->{in_foreign_elem} && $$src_ref =~ /\G\[CDATA\[/gc) {
277 1         5 my $text_start = $+[0];
278 1 50       7 $$src_ref =~ /\]\]>/gc or $self->_fail("missing ']]>' after '
279 1         3 my $text_end = $-[0];
280             return {
281 1         12 type => TT_TEXT,
282             pos => $text_start,
283             content => substr($$src_ref, $text_start, $text_end - $text_start),
284             };
285             }
286              
287 1         5 $self->_fail("invalid declaration (should be '--' or 'DOCTYPE')");
288             }
289              
290 2940         9948 my $closing = $$src_ref =~ m{\G/}gc;
291              
292 2940 50       10348 $$src_ref =~ m{\G([a-zA-Z][^\s/>[:cntrl:]]*)}gc
293             or $self->_fail("invalid tag name");
294 2940         8831 (my $name = $1) =~ tr/A-Z/a-z/;
295              
296 2940         9524 $$src_ref =~ /\G[ \t\n\f]+/gc;
297              
298 2940         5201 my (%attrs, %attr_pos);
299 2940         21346 while ($$src_ref =~ m{\G([^\s/>="'<[:cntrl:]]+)}gc) {
300 1110         3966 my $apos = $-[1];
301 1110 100       3024 if ($closing) {
302 1         8 $self->_fail("invalid attribute in end tag ''", pos => $apos, width => length $1);
303             }
304 1109         3448 (my $attr_name = $1) =~ tr/A-Z/a-z/;
305              
306 1109 100       2703 if (exists $attrs{$attr_name}) {
307 1         10 $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));
308             }
309              
310 1108         2538 $$src_ref =~ /\G[ \t\n\f]+/gc;
311              
312 1108         1974 my $attr_value = '';
313 1108 100       4012 if ($$src_ref =~ /\G=[ \t\n\f]*/gc) {
314 1101 100       5865 if ($$src_ref =~ /\G"/gc) {
    100          
    100          
315 341         943 my $qpos = $-[0];
316 341         708 my $text = '';
317 341         1170 while ($$src_ref =~ /\G ( [^"&]+ | & ) /xgc) {
318 340         907 $text .= $self->_consume_entity_maybe($1);
319             }
320 341 50       1172 $$src_ref =~ /\G"/gc
321             or $self->_fail(q{missing '"' after attribute value}, alt_msg => 'starting here', alt_pos => $qpos);
322 341 50       2521 $$src_ref =~ m{\G[^ \t\n\f/>]}
323             and $self->_fail('missing whitespace after attribute value');
324 341         2083 $attr_value = $text;
325             } elsif ($$src_ref =~ /\G'/gc) {
326 8         31 my $qpos = $-[0];
327 8         22 my $text = '';
328 8         40 while ($$src_ref =~ /\G ( [^'&]+ | & ) /xgc) {
329 14         41 $text .= $self->_consume_entity_maybe($1);
330             }
331 8 100       37 $$src_ref =~ /\G'/gc
332             or $self->_fail(q{missing "'" after attribute value}, alt_msg => 'starting here', alt_pos => $qpos);
333 7 100       30 $$src_ref =~ m{\G[^ \t\n\f/>]}
334             and $self->_fail('missing whitespace after attribute value');
335 6         16 $attr_value = $text;
336             } elsif ($$src_ref =~ /\G ( [^ \t\n\f&>"'<=`]+ | & )/xgc) {
337 751         2455 my $text = '';
338 751         1144 do {
339 751         2156 $text .= $self->_consume_entity_maybe($1);
340             } while $$src_ref =~ /\G ( [^ \t\n\f&>"'<=`]+ | & )/xgc;
341 751         1496 $attr_value = $text;
342             } else {
343 1         5 $self->_fail("missing attribute value after '='");
344             }
345             }
346              
347 1105         3457 $attrs{$attr_name} = $attr_value;
348 1105         2333 $attr_pos{$attr_name} = $apos;
349              
350 1105         4144 $$src_ref =~ /\G[ \t\n\f]+/gc;
351             }
352              
353 2935 100       14457 $$src_ref =~ m{\G(/?)>}gc
354             or $self->_fail("missing '>' at end of tag", alt_msg => 'starting here', alt_pos => $pos, alt_width => 1 + $closing + length($name));
355 2933         7214 my $is_self_closing = length $1;
356              
357 2933 100       6015 if ($closing) {
358 1354 50       2893 $is_self_closing and $self->_fail("invalid '/' at end of closing tag ''", pos => $-[1]);
359 1354 100       3205 @$tag_stack
360             or $self->_fail("closing tag '' has no corresponding open tag", pos => $pos, width => 1 + 1 + length($name));
361              
362 1352 50       3717 $cur_tag eq $name
363             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));
364              
365 1352 100       3371 if ($foreign_tags{$cur_tag}) {
366 2         5 $self->{in_foreign_elem}--;
367             }
368 1352         2375 pop @$tag_stack;
369             return {
370 1352         14155 type => TT_TAG_CLOSE,
371             pos => $pos,
372             name => $name,
373             };
374             }
375              
376 1579         3252 my $is_void = $void_tags{$name};
377 1579 100 100     4475 if ($is_self_closing && !$is_void && !$foreign_tags{$name} && !$self->{in_foreign_elem}) {
      100        
      100        
378 1         9 $self->_fail("invalid '/' at end of non-void tag '<$name>'", pos => $-[1], alt_msg => 'starting here', alt_pos => $pos, alt_width => 1 + length($name));
379             }
380 1578   100     8087 $is_self_closing ||= $is_void;
381              
382 1578 100       4704 if (!$is_self_closing) {
383 1383         1985 push @{$self->{tag_stack}}, [$name, $pos];
  1383         4754  
384 1383 100       10084 if ($foreign_tags{$name}) {
385 2         6 $self->{in_foreign_elem}++;
386             }
387             }
388              
389             return {
390 1578         19869 type => TT_TAG_OPEN,
391             pos => $pos,
392             name => $name,
393             attrs => \%attrs,
394             is_void => $is_void,
395             is_self_closing => $is_self_closing,
396             };
397             }
398              
399             # uncoverable statement
400 0           die "Internal error: unparsable input '${\substr $$src_ref, pos($$src_ref), 10}'...";
  0            
401             }
402              
403             1
404             __DATA__