File Coverage

blib/lib/Mojo/DOM58/_HTML.pm
Criterion Covered Total %
statement 117 119 98.3
branch 101 104 97.1
condition 45 52 86.5
subroutine 18 18 100.0
pod 0 7 0.0
total 281 300 93.6


line stmt bran cond sub pod time code
1             package Mojo::DOM58::_HTML;
2              
3             # This file is part of Mojo::DOM58 which is released under:
4             # The Artistic License 2.0 (GPL Compatible)
5             # See the documentation for Mojo::DOM58 for full license details.
6              
7 2     2   16 use strict;
  2         5  
  2         83  
8 2     2   9 use warnings;
  2         5  
  2         134  
9 2     2   10 use Exporter 'import';
  2         6  
  2         83  
10 2     2   1162 use Mojo::DOM58::Entities qw(html_attr_unescape html_escape html_unescape);
  2         9  
  2         491  
11 2     2   25 use Scalar::Util 'weaken';
  2         5  
  2         3925  
12              
13             our $VERSION = '3.002';
14              
15             our @EXPORT_OK = 'tag_to_html';
16              
17             my $ATTR_RE = qr/
18             ([^<>=\s\/0-9.\-][^<>=\s\/]*|\/) # Key
19             (?:
20             \s*=\s*
21             (?s:(?:"(.*?)")|(?:'(.*?)')|([^>\s]*)) # Value
22             )?
23             \s*
24             /x;
25             my $TOKEN_RE = qr/
26             ([^<]+)? # Text
27             (?:
28             <(?:
29             !(?:
30             DOCTYPE(
31             \s+\w+ # Doctype
32             (?:(?:\s+\w+)?(?:\s+(?:"[^"]*"|'[^']*'))+)? # External ID
33             (?:\s+\[.+?\])? # Int Subset
34             \s*)
35             |
36             --(.*?)--\s* # Comment
37             |
38             \[CDATA\[(.*?)\]\] # CDATA
39             )
40             |
41             \?(.*?)\? # Processing Instruction
42             |
43             \s*((?:\/\s*)?[^<>\s\/0-9.\-][^<>\s\/]*\s*(?>(?:$ATTR_RE){0,32766})*) # Tag
44             # Workaround for perl's limit of * to {0,32767}
45             )>
46             |
47             (<) # Runaway "<"
48             )??
49             /xis;
50              
51             # HTML elements that only contain raw text
52             my %RAW = map { $_ => 1 } qw(script style);
53              
54             # HTML elements that only contain raw text and entities
55             my %RCDATA = map { $_ => 1 } qw(title textarea);
56              
57             # HTML elements with optional end tags
58             my %END = (body => 'head', optgroup => 'optgroup', option => 'option');
59              
60             # HTML elements that break paragraphs
61             $END{$_} = 'p' for
62             qw(address article aside blockquote details dialog div dl fieldset),
63             qw(figcaption figure footer form h1 h2 h3 h4 h5 h6 header hgroup hr main),
64             qw(menu nav ol p pre section table ul);
65              
66             # Container HTML elements that create their own scope
67             my %SCOPE = map { $_ => 1 } qw(math svg);
68              
69             # HTML table elements with optional end tags
70             my %TABLE = map { $_ => 1 } qw(colgroup tbody td tfoot th thead tr);
71              
72             # HTML elements with optional end tags and scoping rules
73             my %CLOSE
74             = (li => [{li => 1}, {ul => 1, ol => 1}], tr => [{tr => 1}, {table => 1}]);
75             $CLOSE{$_} = [\%TABLE, {table => 1}] for qw(colgroup tbody tfoot thead);
76             $CLOSE{$_} = [{dd => 1, dt => 1}, {dl => 1}] for qw(dd dt);
77             $CLOSE{$_} = [{rp => 1, rt => 1}, {ruby => 1}] for qw(rp rt);
78             $CLOSE{$_} = [{th => 1, td => 1}, {table => 1}] for qw(td th);
79              
80             # HTML parent elements that signal no more content when closed, but that are also phrasing content
81             my %NO_MORE_CONTENT = (ruby => [qw(rt rp)], select => [qw(option optgroup)]);
82              
83             # HTML elements without end tags
84             my %EMPTY = map { $_ => 1 } (
85             qw(area base br col embed hr img input keygen link menuitem meta param),
86             qw(source track wbr)
87             );
88              
89             # HTML elements categorized as phrasing content (and obsolete inline elements)
90             my @PHRASING = (
91             qw(a abbr area audio b bdi bdo br button canvas cite code data datalist),
92             qw(del dfn em embed i iframe img input ins kbd keygen label link map mark),
93             qw(math meta meter noscript object output picture progress q ruby s samp),
94             qw(script select slot small span strong sub sup svg template textarea time u),
95             qw(var video wbr)
96             );
97             my @OBSOLETE = qw(acronym applet basefont big font strike tt);
98             my %PHRASING = map { $_ => 1 } @OBSOLETE, @PHRASING;
99              
100             # HTML elements that don't get their self-closing flag acknowledged
101             my %BLOCK = map { $_ => 1 } (
102             qw(a address applet article aside b big blockquote body button caption),
103             qw(center code col colgroup dd details dialog dir div dl dt em fieldset),
104             qw(figcaption figure font footer form frameset h1 h2 h3 h4 h5 h6 head),
105             qw(header hgroup html i iframe li listing main marquee menu nav nobr),
106             qw(noembed noframes noscript object ol optgroup option p plaintext pre rp),
107             qw(rt s script section select small strike strong style summary table),
108             qw(tbody td template textarea tfoot th thead title tr tt u ul xmp)
109             );
110              
111             sub new {
112 2250     2250 0 7229 my $class = shift;
113 2250 50 33     17942 bless @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {}, ref $class || $class;
  0 100       0  
114             }
115              
116 11     11 0 26 sub tag { shift->tree(['root', _tag(@_)]) }
117              
118 1     1 0 5 sub tag_to_html { _render(_tag(@_), undef) }
119              
120             sub tree {
121 5197     5197 0 8544 my $self = shift;
122 5197 100       22752 return exists $self->{tree} ? $self->{tree} : ($self->{tree} = ['root']) unless @_;
    100          
123 2259         5214 $self->{tree} = shift;
124 2259         9932 return $self;
125             }
126              
127             sub xml {
128 3777     3777 0 5825 my $self = shift;
129 3777 100       11329 return $self->{xml} unless @_;
130 2026         5214 $self->{xml} = shift;
131 2026         17948 return $self;
132             }
133              
134             sub parse {
135 243     243 0 1058 my ($self, $html) = (shift, "$_[0]");
136              
137 243         622 my $xml = $self->xml;
138 243         744 my $current = my $tree = ['root'];
139 243         2495 while ($html =~ /\G$TOKEN_RE/gcso) {
140 4623         17886 my ($text, $doctype, $comment, $cdata, $pi, $tag, $runaway)
141             = ($1, $2, $3, $4, $5, $6, $11);
142              
143             # Text (and runaway "<")
144 4623 100       8285 $text .= '<' if defined $runaway;
145 4623 100       9692 _node($current, 'text', html_unescape $text) if defined $text;
146              
147             # Tag
148 4623 100       166609 if (defined $tag) {
    100          
    100          
    100          
    100          
149              
150             # End
151 1541 100       6000 if ($tag =~ /^\/\s*(\S+)/) {
    50          
152 633 100       1650 my $end = $xml ? $1 : lc $1;
153              
154             # No more content
155 633 100 100     2540 if (!$xml && (my $tags = $NO_MORE_CONTENT{$end})) { _end($_, $xml, \$current) for @$tags }
  11         47  
156              
157 633         1438 _end($end, $xml, \$current);
158             }
159              
160             # Start
161             elsif ($tag =~ m!^([^\s/]+)([\s\S]*)!) {
162 908 100       3168 my ($start, $attr) = ($xml ? $1 : lc $1, $2);
163              
164             # Attributes
165 908         1802 my (%attrs, $closing);
166 908         3185 while ($attr =~ /$ATTR_RE/go) {
167 33117 100       83698 my $key = $xml ? $1 : lc $1;
168 33117 100       94048 my $value = defined($2) ? $2 : defined($3) ? $3 : $4;
    100          
169              
170             # Empty tag
171 33117 100 50     78312 ++$closing and next if $key eq '/';
172              
173 33065 100       80507 $attrs{$key} = defined $value ? html_attr_unescape $value : $value;
174             }
175              
176             # "image" is an alias for "img"
177 908 100 100     3184 $start = 'img' if !$xml && $start eq 'image';
178 908         2940 _start($start, \%attrs, $xml, \$current);
179              
180             # Element without end tag (self-closing)
181             _end($start, $xml, \$current)
182 908 100 100     7501 if !$xml && $EMPTY{$start} || ($xml || !$BLOCK{$start}) && $closing;
      100        
      100        
      100        
183              
184             # Raw text elements
185 908 100 100     7372 next if $xml || !$RAW{$start} && !$RCDATA{$start};
      100        
186 43 100       1825 next unless $html =~ m!\G(.*?))!gcsi;
187 42 100       231 _node($current, 'raw', $RCDATA{$start} ? html_unescape $1 : $1);
188 42         154 _end($start, 0, \$current);
189             }
190             }
191              
192             # DOCTYPE
193 15         61 elsif (defined $doctype) { _node($current, 'doctype', $doctype) }
194              
195             # Comment
196 11         34 elsif (defined $comment) { _node($current, 'comment', $comment) }
197              
198             # CDATA
199 7         20 elsif (defined $cdata) { _node($current, 'cdata', $cdata) }
200              
201             # Processing instruction (try to detect XML)
202             elsif (defined $pi) {
203 18 100 100     183 $self->xml($xml = 1) if !exists $self->{xml} && $pi =~ /xml/i;
204 18         58 _node($current, 'pi', $pi);
205             }
206             }
207              
208 243         716 return $self->tree($tree);
209             }
210              
211 165     165 0 438 sub render { _render($_[0]->tree, $_[0]->xml) }
212              
213             sub _end {
214 981     981   1868 my ($end, $xml, $current) = @_;
215              
216             # Search stack for start tag
217 981         1558 my $next = $$current;
218 981         1452 do {
219              
220             # Ignore useless end tag
221 1352 100       3222 return if $next->[0] eq 'root';
222              
223             # Don't traverse a container tag
224 1190 100 100     3435 return if $SCOPE{$next->[1]} && $next->[1] ne $end;
225              
226             # Right tag
227 1187 100       5082 return $$current = $next->[3] if $next->[1] eq $end;
228              
229             # Phrasing content can only cross phrasing content
230 377 100 100     1780 return if !$xml && $PHRASING{$end} && !$PHRASING{$next->[1]};
      66        
231              
232             } while $next = $next->[3];
233             }
234              
235             sub _node {
236 1268     1268   6566 my ($current, $type, $content) = @_;
237 1268         4398 push @$current, my $new = [$type, $content, $current];
238 1268         4445 weaken $new->[2];
239             }
240              
241             sub _render {
242 1036     1036   1914 my ($tree, $xml) = @_;
243              
244             # Tag
245 1036         1739 my $type = $tree->[0];
246 1036 100       2489 if ($type eq 'tag') {
247              
248             # Start tag
249 455         788 my $tag = $tree->[1];
250 455         707 my $result = "<$tag";
251              
252             # Attributes
253 455         708 for my $key (sort keys %{$tree->[2]}) {
  455         1325  
254 66         134 my $value = $tree->[2]{$key};
255 66 100 50     242 $result .= $xml ? qq{ $key="$key"} : " $key" and next
    100          
256             unless defined $value;
257 56         227 $result .= qq{ $key="} . html_escape($value) . '"';
258             }
259              
260             # No children
261 455 100       1357 return $xml ? "$result />" : $EMPTY{$tag} ? "$result>" : "$result>"
    100          
    100          
262             unless $tree->[4];
263              
264             # Children
265 2     2   17 no warnings 'recursion';
  2         4  
  2         1422  
266 409         868 $result .= '>' . join '', map { _render($_, $xml) } @$tree[4 .. $#$tree];
  681         1643  
267              
268             # End tag
269 409         2115 return "$result";
270             }
271              
272             # Text (escaped)
273 581 100       1338 return html_escape($tree->[1]) if $type eq 'text';
274              
275             # Raw text
276 157 100       405 return $tree->[1] if $type eq 'raw';
277              
278             # Root
279 151 100       690 return join '', map { _render($_, $xml) } @$tree[1 .. $#$tree]
  189         395  
280             if $type eq 'root';
281              
282             # DOCTYPE
283 14 100       57 return '[1] . '>' if $type eq 'doctype';
284              
285             # Comment
286 10 100       46 return '' if $type eq 'comment';
287              
288             # CDATA
289 6 100       31 return '[1] . ']]>' if $type eq 'cdata';
290              
291             # Processing instruction
292 3 50       25 return '[1] . '?>' if $type eq 'pi';
293              
294             # Everything else
295 0         0 return '';
296             }
297              
298             sub _start {
299 908     908   2087 my ($start, $attrs, $xml, $current) = @_;
300              
301             # Autoclose optional HTML elements
302 908 100 100     3707 if (!$xml && $$current->[0] ne 'root') {
303 578 100       2319 if (my $end = $END{$start}) { _end($end, 0, $current) }
  155 100       418  
304              
305             elsif (my $close = $CLOSE{$start}) {
306 148         372 my ($allowed, $scope) = @$close;
307              
308             # Close allowed parent elements in scope
309 148         248 my $parent = $$current;
310 148   66     819 while ($parent->[0] ne 'root' && !$scope->{$parent->[1]}) {
311 148 100       461 _end($parent->[1], 0, $current) if $allowed->{$parent->[1]};
312 148         674 $parent = $parent->[3];
313             }
314             }
315             }
316              
317             # New tag
318 908         3414 push @$$current, my $new = ['tag', $start, $attrs, $$current];
319 908         1929 weaken $new->[3];
320 908         7437 $$current = $new;
321             }
322              
323             sub _tag {
324 12     12   25 my $tree = ['tag', shift, undef, undef];
325              
326             # Content
327 12 100       44 push @$tree, ref $_[-1] eq 'CODE' ? ['raw', pop->()] : ['text', pop]
    100          
328             if @_ % 2;
329              
330             # Attributes
331 12         33 my $attrs = $tree->[2] = {@_};
332 12 100 66     47 return $tree unless exists $attrs->{data} && ref $attrs->{data} eq 'HASH';
333 1         3 my $data = delete $attrs->{data};
334 1         4 @$attrs{map { y/_/-/; lc "data-$_" } keys %$data} = values %$data;
  2         5  
  2         6  
335              
336 1         5 return $tree;
337             }
338              
339             1;
340              
341             =for Pod::Coverage *EVERYTHING*
342              
343             =cut