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   1123293 use 5.008001;
  11         167  
5 11     11   80 use strict;
  11         23  
  11         268  
6 11     11   55 use warnings;
  11         17  
  11         701  
7              
8             our $AUTOLOAD;
9             our $VERSION = '0.992';
10              
11 11     11   71 use Carp;
  11         30  
  11         774  
12 11     11   5194 use HTML::HTML5::Parser::Error;
  11         101  
  11         375  
13 11     11   15121 use HTML::HTML5::Parser::TagSoupParser;
  11         49  
  11         653  
14 11     11   118 use Scalar::Util qw(blessed);
  11         26  
  11         811  
15 11     11   6366 use URI::file;
  11         102431  
  11         428  
16 11     11   90 use Encode qw(encode_utf8);
  11         25  
  11         943  
17 11     11   76 use XML::LibXML;
  11         27  
  11         103  
18              
19             BEGIN {
20 11 50   11   2452 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 926755 my $class = shift;
27 705         1604 my %p = @_;
28 705         5052 my $self = bless {
29             errors => [],
30             parser => HTML::HTML5::Parser::TagSoupParser->new(%p),
31             }, $class;
32 705         3615 return $self;
33             }
34              
35             sub parse_file
36             {
37 4     4 1 535 require HTML::HTML5::Parser::UA;
38            
39 4         13 my $self = shift;
40 4         9 my $file = shift;
41 4   50     48 my $opts = shift || {};
42            
43 4 50 33     30 unless (blessed($file) and $file->isa('URI'))
44             {
45 4 100       57 if ($file =~ /^[a-z][a-z0-9_\.\+-]+:\S+$/i)
46 2         19 { $file = URI->new($file); }
47             else
48 2         13 { $file = URI::file->new_abs($file); }
49             }
50            
51 4         22104 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     7356 unless ($response->{success} || $opts->{ignore_http_response_code});
54            
55 4         28 my $content = $response->{decoded_content};
56 4         25 my $c_type = $response->{headers}{'content-type'};
57            
58 4         20 $opts->{'response'} = $response;
59            
60 4 50 33     35 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         33 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     6 my $opts = shift || {};
81            
82 1         3 my $string = '';
83 1         6 while (<$handle>)
84             {
85 3         20 $string .= $_;
86             }
87            
88 1         7 return $self->parse_string($string, $opts);
89             }
90             *parse_html_fh = \&parse_fh;
91              
92             sub parse_string
93             {
94 708     708 1 1382 my $self = shift;
95 708         1702 my $text = shift;
96 708   100     2939 my $opts = shift || {};
97              
98 708         1696 $self->{'errors'} = [];
99 708         1570 $opts->{'parser_used'} = 'HTML::HTML5::Parser';
100 708         5545 my $dom = XML::LibXML::Document->createDocument;
101              
102 708 50 50     3149 if (defined $opts->{'encoding'}||1)
103             {
104             # XXX AGAIN DO THIS TO STOP ENORMOUS MEMORY LEAKS
105 708 100       2244 if (utf8::is_utf8($text)) {
106 2         10 $text = encode_utf8($text);
107             }
108 708         1242 my ($errh, $errors) = @{$self}{qw(error_handler errors)};
  708         1926  
109             $self->{parser}->parse_byte_string(
110             $opts->{'encoding'}, $text, $dom,
111             sub {
112 2147     2147   8958 my $err = HTML::HTML5::Parser::Error->new(@_);
113 2147 50       5260 $errh->($err) if $errh;
114 2147         5266 push @$errors, $err;
115 708         5363 });
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         3627 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 4386 my ($self, $chunk, $o) = @_;
175 12 50       30 my %options = %{ $o || {} };
  12         72  
176            
177 12 100       55 $options{as} = 'default' unless defined $options{as};
178            
179 12   50     75 my $w = $options{force_within} || $options{within} || 'div';
180 12         50 my $ancestors = $within{ lc $w };
181 12 50       34 croak "Cannot parse chunk as if within $w."
182             if !defined $ancestors;
183            
184 12         34 my $parent = $ancestors->[-1];
185 12         28 my $n = scalar(@$ancestors) - 2;
186 12 100       64 my @a = $n ? @$ancestors[0 .. $n] : ();
187            
188 12         184 my $uniq = sprintf('rand_id_%09d', int rand 1_000_000_000);
189             my $document =
190             "\n".
191 12         52 (join q{}, map { "<$_>" } @a).
  32         119  
192             "<$parent id='$uniq'>".
193             $chunk.
194             ''.# "".
195             '';# (join q{}, map { "" } reverse @a);
196            
197 12         50 my $dom = $self->parse_html_string($document);
198 12         112 $parent = $dom->findnodes("//*[\@id='$uniq']")->get_node(1);
199            
200 12 50       1552 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         56 my @results = $parent->childNodes;
215            
216 12 100       151 unless ($options{force_within})
217             {
218 11         44 while ($parent)
219             {
220 50         712 my $sibling = $parent->nextSibling;
221 50         154 while ($sibling)
222             {
223 4 100       60 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     33 && $sibling->can('setAttribute');
228 2         8 push @results, $sibling;
229             }
230 4         25 $sibling = $sibling->nextSibling;
231             }
232            
233 50         145 $sibling = $parent->previousSibling;
234 50         102 while ($sibling)
235             {
236 9 100       120 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         24 unshift @results, $sibling;
242             }
243 9         58 $sibling = $sibling->previousSibling;
244             }
245            
246 50         275 $parent = $parent->parentNode;
247             }
248             }
249            
250 12         88 my $frag = XML::LibXML::DocumentFragment->new;
251 12         104 $frag->appendChild($_) foreach @results;
252            
253 12 100       184 if (lc $options{as} eq 'list')
254             {
255 1 50       10 return wantarray ? @results : XML::LibXML::NodeList->new(@results);
256             }
257            
258 11 50       190 return wantarray ? @results : $frag;
259             }
260              
261             sub load_html
262             {
263 4     4 1 6385 my $class_or_self = shift;
264            
265 4 50       21 my %args = map { ref($_) eq 'HASH' ? (%$_) : $_ } @_;
  8         94  
266 4         17 my $URI = delete($args{URI});
267 4 50       22 $URI = "$URI" if defined $URI; # stringify in case it is an URI object
268 4 50       130 my $parser = ref($class_or_self)
269             ? $class_or_self
270             : $class_or_self->new;
271            
272 4         10 my $dom;
273 4 100       29 if ( defined $args{location} )
    100          
    50          
274 2         20 { $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         3 { $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         177 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         9 my $node = shift;
368            
369 2         9 return $self->{parser}->_data($node)->{'charset'};
370             }
371              
372             sub dtd_public_id
373             {
374 688     688 1 2537 my $self = shift;
375 688         986 my $node = shift;
376            
377 688         1479 return $self->{parser}->_data($node)->{'DTD_PUBLIC_ID'};
378             }
379              
380             sub dtd_system_id
381             {
382 687     687 1 2735 my $self = shift;
383 687         1102 my $node = shift;
384            
385 687         1396 return $self->{parser}->_data($node)->{'DTD_SYSTEM_ID'};
386             }
387              
388             sub dtd_element
389             {
390 687     687 1 34484 my $self = shift;
391 687         1096 my $node = shift;
392            
393 687         2034 return $self->{parser}->_data($node)->{'DTD_ELEMENT'};
394             }
395              
396             sub source_line
397             {
398 10     10 1 18256 my $self = shift;
399 10         20 my $node = shift;
400              
401 10 50       43 my $data = ref $self ? $self->{parser}->_data($node) :
402             HTML::HTML5::Parser::TagSoupParser::DATA($node);
403 10         21 my $line = $data->{'manakai_source_line'};
404              
405 10 100       22 if (wantarray)
406             {
407             return (
408             $line,
409             $data->{'manakai_source_column'},
410 9   100     59 ($data->{'implied'} || 0),
411             );
412             }
413             else
414             {
415 1         4 return $line;
416             }
417             }
418              
419       0     sub DESTROY {}
420              
421             __END__