File Coverage

blib/lib/HTML/HTML5/Parser.pm
Criterion Covered Total %
statement 140 191 73.3
branch 40 82 48.7
condition 15 33 45.4
subroutine 23 30 76.6
pod 15 15 100.0
total 233 351 66.3


line stmt bran cond sub pod time code
1             package HTML::HTML5::Parser;
2              
3             ## skip Test::Tabs
4 11     11   1136069 use 5.008001;
  11         120  
5 11     11   64 use strict;
  11         22  
  11         257  
6 11     11   51 use warnings;
  11         20  
  11         649  
7              
8             our $AUTOLOAD;
9             our $VERSION = '0.991';
10              
11 11     11   77 use Carp;
  11         23  
  11         708  
12 11     11   5070 use HTML::HTML5::Parser::Error;
  11         39  
  11         409  
13 11     11   15687 use HTML::HTML5::Parser::TagSoupParser;
  11         115  
  11         623  
14 11     11   107 use Scalar::Util qw(blessed);
  11         25  
  11         744  
15 11     11   6489 use URI::file;
  11         102178  
  11         405  
16 11     11   92 use Encode qw(encode_utf8);
  11         27  
  11         985  
17 11     11   75 use XML::LibXML;
  11         28  
  11         112  
18              
19             BEGIN {
20 11 50   11   2515 croak "Please upgrade to XML::LibXML 1.94"
21             if XML::LibXML->VERSION =~ /^1\.9[12]/;
22             }
23              
24             sub new
25             {
26 705     705 1 835499 my $class = shift;
27 705         1520 my %p = @_;
28 705         4698 my $self = bless {
29             errors => [],
30             parser => HTML::HTML5::Parser::TagSoupParser->new(%p),
31             }, $class;
32 705         3479 return $self;
33             }
34              
35             sub parse_file
36             {
37 4     4 1 514 require HTML::HTML5::Parser::UA;
38            
39 4         14 my $self = shift;
40 4         15 my $file = shift;
41 4   50     49 my $opts = shift || {};
42            
43 4 50 33     29 unless (blessed($file) and $file->isa('URI'))
44             {
45 4 100       65 if ($file =~ /^[a-z][a-z0-9_\.\+-]+:\S+$/i)
46 2         20 { $file = URI->new($file); }
47             else
48 2         18 { $file = URI::file->new_abs($file); }
49             }
50            
51 4         19523 my $response = HTML::HTML5::Parser::UA->get($file, $opts->{user_agent});
52             croak "HTTP response code was not 200 OK. (Set \$opts{ignore_http_response_code} to ignore this error.)"
53 4 0 33     7516 unless ($response->{success} || $opts->{ignore_http_response_code});
54            
55 4         12 my $content = $response->{decoded_content};
56 4         17 my $c_type = $response->{headers}{'content-type'};
57            
58 4         18 $opts->{'response'} = $response;
59            
60 4 50 33     61 if ($c_type =~ /xml/i and not $opts->{'force_html'})
61             {
62 0         0 $opts->{'parser_used'} = 'XML::LibXML::Parser';
63 0         0 my $xml_parser = XML::LibXML->new;
64 0         0 $xml_parser->validation(0);
65 0         0 $xml_parser->recover(2);
66 0         0 $xml_parser->base_uri($response->base);
67             $xml_parser->load_catalog($opts->{'xml_catalogue'})
68 0 0       0 if -r $opts->{'xml_catalogue'};
69 0         0 return $xml_parser->parse_string($content);
70             }
71            
72 4         44 return $self->parse_string($content, $opts);
73             }
74             *parse_html_file = \&parse_file;
75              
76             sub parse_fh
77             {
78 1     1 1 3 my $self = shift;
79 1         2 my $handle = shift;
80 1   50     7 my $opts = shift || {};
81            
82 1         2 my $string = '';
83 1         6 while (<$handle>)
84             {
85 3         20 $string .= $_;
86             }
87            
88 1         6 return $self->parse_string($string, $opts);
89             }
90             *parse_html_fh = \&parse_fh;
91              
92             sub parse_string
93             {
94 708     708 1 1525 my $self = shift;
95 708         1441 my $text = shift;
96 708   100     2848 my $opts = shift || {};
97              
98 708         1646 $self->{'errors'} = [];
99 708         1789 $opts->{'parser_used'} = 'HTML::HTML5::Parser';
100 708         5522 my $dom = XML::LibXML::Document->createDocument;
101              
102 708 50 50     2965 if (defined $opts->{'encoding'}||1)
103             {
104             # XXX AGAIN DO THIS TO STOP ENORMOUS MEMORY LEAKS
105 708 100       1930 if (utf8::is_utf8($text)) {
106 2         12 $text = encode_utf8($text);
107             }
108 708         1017 my ($errh, $errors) = @{$self}{qw(error_handler errors)};
  708         1900  
109             $self->{parser}->parse_byte_string(
110             $opts->{'encoding'}, $text, $dom,
111             sub {
112 2147     2147   10369 my $err = HTML::HTML5::Parser::Error->new(@_);
113 2147 50       5011 $errh->($err) if $errh;
114 2147         5458 push @$errors, $err;
115 708         5117 });
116             }
117             else
118             {
119             $self->{parser}->parse_char_string($text, $dom, sub{
120 0     0   0 my $err = HTML::HTML5::Parser::Error->new(@_);
121 0 0       0 $self->{error_handler}->($err) if $self->{error_handler};
122 0         0 push @{$self->{'errors'}}, $err;
  0         0  
123 0         0 });
124             }
125            
126 708         3186 return $dom;
127             }
128             *parse_html_string = \&parse_string;
129              
130             # TODO: noembed, noframes, noscript
131             my %within = (
132             html => [qw/html/],
133             frameset => [qw/html frameset/],
134             frame => [qw/html frameset frame/],
135             head => [qw/html head/],
136             title => [qw/html head title/],
137             style => [qw/html head style/],
138             (map { $_ => undef }
139             qw/base link meta basefont bgsound/),
140             body => [qw/html body/],
141             script => [qw/html body script/],
142             div => [qw/html body div/],
143             (map { $_ => [qw/html body div/, $_] }
144             qw/a abbr acronym address applet area article aside big blockquote
145             button center code details dir dl em fieldset figure font
146             footer form h1 h2 h3 h4 h5 h6 header hgroup i iframe
147             listing marquee menu nav nobr object ol p plaintext pre
148             ruby s section small strike strong tt u ul xmp/),
149             (map { $_ => undef }
150             qw/br col command datagrid embed hr img input keygen
151             param wbr/),
152             dd => [qw/html body dl dd/],
153             dd => [qw/html body dl dt/],
154             figcaption => [qw/html body figure/],
155             li => [qw/html body ul li/],
156             ul__li => [qw/html body ul li/],
157             ol__li => [qw/html body ol li/],
158             optgroup => [qw/html body form div select/],
159             option => [qw/html body form div select/],
160             rp => [qw/html body div ruby/],
161             rt => [qw/html body div ruby/],
162             select => [qw/html body form div select/],
163             summary => [qw/html body div details/],
164             table => [qw/html body table/],
165             (map { $_ => [qw/html body table/, $_] }
166             qw/thead tfoot tbody tr caption colgroup/),
167             (map { $_ => [qw/html body table tbody tr/, $_] }
168             qw/td th/),
169             textarea => [qw/html body form div textarea/],
170             );
171              
172             sub parse_balanced_chunk
173             {
174 12     12 1 3322 my ($self, $chunk, $o) = @_;
175 12 50       21 my %options = %{ $o || {} };
  12         69  
176            
177 12 100       50 $options{as} = 'default' unless defined $options{as};
178            
179 12   50     67 my $w = $options{force_within} || $options{within} || 'div';
180 12         49 my $ancestors = $within{ lc $w };
181 12 50       88 croak "Cannot parse chunk as if within $w."
182             if !defined $ancestors;
183            
184 12         40 my $parent = $ancestors->[-1];
185 12         29 my $n = scalar(@$ancestors) - 2;
186 12 100       66 my @a = $n ? @$ancestors[0 .. $n] : ();
187            
188 12         156 my $uniq = sprintf('rand_id_%09d', int rand 1_000_000_000);
189             my $document =
190             "\n".
191 12         44 (join q{}, map { "<$_>" } @a).
  32         107  
192             "<$parent id='$uniq'>".
193             $chunk.
194             ''.# "".
195             '';# (join q{}, map { "" } reverse @a);
196            
197 12         46 my $dom = $self->parse_html_string($document);
198 12         106 $parent = $dom->findnodes("//*[\@id='$uniq']")->get_node(1);
199            
200 12 50       1351 if ($options{debug})
201             {
202 0 0       0 if (exists &Test::More::diag)
203             {
204 0         0 Test::More::diag($document);
205 0         0 Test::More::diag($dom->toString);
206             }
207             else
208             {
209 0         0 warn $document."\n";
210 0         0 warn $dom->toString."\n";
211             }
212             }
213            
214 12         53 my @results = $parent->childNodes;
215            
216 12 100       154 unless ($options{force_within})
217             {
218 11         44 while ($parent)
219             {
220 50         737 my $sibling = $parent->nextSibling;
221 50         153 while ($sibling)
222             {
223 4 100       69 unless ($sibling->nodeName =~ /^(head|body)$/)
224             {
225             $sibling->setAttribute('data-perl-html-html5-parser-outlier', 1)
226             if $options{mark_outliers}
227 2 50 66     29 && $sibling->can('setAttribute');
228 2         6 push @results, $sibling;
229             }
230 4         20 $sibling = $sibling->nextSibling;
231             }
232            
233 50         148 $sibling = $parent->previousSibling;
234 50         101 while ($sibling)
235             {
236 9 100       119 unless ($sibling->nodeName =~ /^(head|body)$/)
237             {
238             $sibling->setAttribute('data-perl-html-html5-parser-outlier', 1)
239             if $options{mark_outliers}
240 2 100 66     24 && $sibling->can('setAttribute');
241 2         22 unshift @results, $sibling;
242             }
243 9         38 $sibling = $sibling->previousSibling;
244             }
245            
246 50         282 $parent = $parent->parentNode;
247             }
248             }
249            
250 12         92 my $frag = XML::LibXML::DocumentFragment->new;
251 12         99 $frag->appendChild($_) foreach @results;
252            
253 12 100       185 if (lc $options{as} eq 'list')
254             {
255 1 50       18 return wantarray ? @results : XML::LibXML::NodeList->new(@results);
256             }
257            
258 11 50       195 return wantarray ? @results : $frag;
259             }
260              
261             sub load_html
262             {
263 4     4 1 5940 my $class_or_self = shift;
264            
265 4 50       18 my %args = map { ref($_) eq 'HASH' ? (%$_) : $_ } @_;
  8         73  
266 4         18 my $URI = delete($args{URI});
267 4 50       18 $URI = "$URI" if defined $URI; # stringify in case it is an URI object
268 4 50       99 my $parser = ref($class_or_self)
269             ? $class_or_self
270             : $class_or_self->new;
271            
272 4         9 my $dom;
273 4 100       21 if ( defined $args{location} )
    100          
    50          
274 2         15 { $dom = $parser->parse_file( "$args{location}" ) }
275             elsif ( defined $args{string} )
276 1         6 { $dom = $parser->parse_string( $args{string}, $URI ) }
277             elsif ( defined $args{IO} )
278 1         5 { $dom = $parser->parse_fh( $args{IO}, $URI ) }
279             else
280 0         0 { croak("HTML::HTML5::Parser->load_html: specify location, string, or IO"); }
281            
282 4         172 return $dom;
283             }
284              
285             sub load_xml
286             {
287 0     0 1 0 my $self = shift;
288 0         0 my $dom;
289 0         0 eval {
290 0         0 $dom = XML::LibXML->load_xml(@_);
291             };
292 0 0       0 return $dom if blessed($dom);
293 0         0 return $self->load_html(@_);
294             }
295              
296             sub AUTOLOAD
297             {
298 0     0   0 my $self = shift;
299 0         0 my $func = $AUTOLOAD;
300 0         0 $func =~ s/.*://;
301            
302             # LibXML Push Parser.
303 0 0       0 if ($func =~ /^( parse_chunk | start_push | push | finish_push )$/xi)
304             {
305 0         0 croak "Push parser ($func) not implemented by HTML::HTML5::Parser.";
306             }
307            
308             # Misc LibXML functions with no compatible interface provided.
309 0 0       0 if ($func =~ /^( parse_balanced_chunk | parse_xml_chunk |
310             process_?xincludes | get_last_error )$/xi)
311             {
312 0         0 croak "$func not implemented by HTML::HTML5::Parser.";
313             }
314            
315             # Fixed options which are true.
316 0 0       0 if ($func =~ /^( recover | recover_silently | expand_entities |
317             keep_blanks | no_network )$/xi)
318             {
319 0         0 my $set = shift;
320 0 0 0     0 if ((!$set) && defined $set)
321             {
322 0         0 carp "Option $func cannot be switched off.";
323             }
324 0         0 return 1;
325             }
326              
327             # Fixed options which are false.
328 0 0       0 if ($func =~ /^( validation | pedantic_parser | line_numbers
329             load_ext_dtd | complete_attributes | expand_xinclude |
330             load_catalog | base_uri | gdome_dom | clean_namespaces )$/xi)
331             {
332 0         0 my $set = shift;
333 0 0 0     0 if (($set) && defined $set)
334             {
335 0         0 carp "Option $func cannot be switched on.";
336             }
337 0         0 return 0;
338             }
339              
340 0 0       0 carp "HTML::HTML5::Parser doesn't understand '$func'." if length $func;
341             }
342              
343             sub error_handler
344             {
345 0     0 1 0 my $self = shift;
346 0 0       0 $self->{error_handler} = shift if @_;
347 0         0 return $self->{error_handler};
348             }
349              
350             sub errors
351             {
352 0     0 1 0 my $self = shift;
353 0         0 return @{ $self->{errors} };
  0         0  
354             }
355              
356             sub compat_mode
357             {
358 0     0 1 0 my $self = shift;
359 0         0 my $node = shift;
360            
361 0         0 return $self->{parser}->_data($node)->{'manakai_compat_mode'};
362             }
363              
364             sub charset
365             {
366 2     2 1 30 my $self = shift;
367 2         4 my $node = shift;
368            
369 2         10 return $self->{parser}->_data($node)->{'charset'};
370             }
371              
372             sub dtd_public_id
373             {
374 688     688 1 2330 my $self = shift;
375 688         1036 my $node = shift;
376            
377 688         1602 return $self->{parser}->_data($node)->{'DTD_PUBLIC_ID'};
378             }
379              
380             sub dtd_system_id
381             {
382 687     687 1 2970 my $self = shift;
383 687         1004 my $node = shift;
384            
385 687         1598 return $self->{parser}->_data($node)->{'DTD_SYSTEM_ID'};
386             }
387              
388             sub dtd_element
389             {
390 687     687 1 31675 my $self = shift;
391 687         1077 my $node = shift;
392            
393 687         2092 return $self->{parser}->_data($node)->{'DTD_ELEMENT'};
394             }
395              
396             sub source_line
397             {
398 10     10 1 14409 my $self = shift;
399 10         19 my $node = shift;
400              
401 10 50       44 my $data = ref $self ? $self->{parser}->_data($node) :
402             HTML::HTML5::Parser::TagSoupParser::DATA($node);
403 10         24 my $line = $data->{'manakai_source_line'};
404              
405 10 100       23 if (wantarray)
406             {
407             return (
408             $line,
409             $data->{'manakai_source_column'},
410 9   100     57 ($data->{'implied'} || 0),
411             );
412             }
413             else
414             {
415 1         6 return $line;
416             }
417             }
418              
419       0     sub DESTROY {}
420              
421             __END__