File Coverage

blib/lib/Mojo/DOM58/_HTML.pm
Criterion Covered Total %
statement 116 118 98.3
branch 101 104 97.1
condition 42 49 85.7
subroutine 18 18 100.0
pod 0 7 0.0
total 277 296 93.5


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   15 use strict;
  2         4  
  2         67  
8 2     2   10 use warnings;
  2         4  
  2         67  
9 2     2   12 use Exporter 'import';
  2         3  
  2         80  
10 2     2   1154 use Mojo::DOM58::Entities qw(html_attr_unescape html_escape html_unescape);
  2         5  
  2         425  
11 2     2   24 use Scalar::Util 'weaken';
  2         4  
  2         3442  
12              
13             our $VERSION = '3.000';
14              
15             our @EXPORT_OK = 'tag_to_html';
16              
17             my $ATTR_RE = qr/
18             ([^<>=\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*(?>(?:$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             # HTML table elements with optional end tags
67             my %TABLE = map { $_ => 1 } qw(colgroup tbody td tfoot th thead tr);
68              
69             # HTML elements with optional end tags and scoping rules
70             my %CLOSE
71             = (li => [{li => 1}, {ul => 1, ol => 1}], tr => [{tr => 1}, {table => 1}]);
72             $CLOSE{$_} = [\%TABLE, {table => 1}] for qw(colgroup tbody tfoot thead);
73             $CLOSE{$_} = [{dd => 1, dt => 1}, {dl => 1}] for qw(dd dt);
74             $CLOSE{$_} = [{rp => 1, rt => 1}, {ruby => 1}] for qw(rp rt);
75             $CLOSE{$_} = [{th => 1, td => 1}, {table => 1}] for qw(td th);
76              
77             # HTML parent elements that signal no more content when closed, but that are also phrasing content
78             my %NO_MORE_CONTENT = (ruby => [qw(rt rp)], select => [qw(option optgroup)]);
79              
80             # HTML elements without end tags
81             my %EMPTY = map { $_ => 1 } (
82             qw(area base br col embed hr img input keygen link menuitem meta param),
83             qw(source track wbr)
84             );
85              
86             # HTML elements categorized as phrasing content (and obsolete inline elements)
87             my @PHRASING = (
88             qw(a abbr area audio b bdi bdo br button canvas cite code data datalist),
89             qw(del dfn em embed i iframe img input ins kbd keygen label link map mark),
90             qw(math meta meter noscript object output picture progress q ruby s samp),
91             qw(script select slot small span strong sub sup svg template textarea time u),
92             qw(var video wbr)
93             );
94             my @OBSOLETE = qw(acronym applet basefont big font strike tt);
95             my %PHRASING = map { $_ => 1 } @OBSOLETE, @PHRASING;
96              
97             # HTML elements that don't get their self-closing flag acknowledged
98             my %BLOCK = map { $_ => 1 } (
99             qw(a address applet article aside b big blockquote body button caption),
100             qw(center code col colgroup dd details dialog dir div dl dt em fieldset),
101             qw(figcaption figure font footer form frameset h1 h2 h3 h4 h5 h6 head),
102             qw(header hgroup html i iframe li listing main marquee menu nav nobr),
103             qw(noembed noframes noscript object ol optgroup option p plaintext pre rp),
104             qw(rt s script section select small strike strong style summary table),
105             qw(tbody td template textarea tfoot th thead title tr tt u ul xmp)
106             );
107              
108             sub new {
109 2194     2194 0 3372 my $class = shift;
110 2194 50 33     12668 bless @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {}, ref $class || $class;
  0 100       0  
111             }
112              
113 11     11 0 42 sub tag { shift->tree(['root', _tag(@_)]) }
114              
115 1     1 0 5 sub tag_to_html { _render(_tag(@_), undef) }
116              
117             sub tree {
118 5055     5055 0 8119 my $self = shift;
119 5055 100       18265 return exists $self->{tree} ? $self->{tree} : ($self->{tree} = ['root']) unless @_;
    100          
120 2203         4587 $self->{tree} = shift;
121 2203         8169 return $self;
122             }
123              
124             sub xml {
125 3669     3669 0 5391 my $self = shift;
126 3669 100       9013 return $self->{xml} unless @_;
127 1980         3239 $self->{xml} = shift;
128 1980         8878 return $self;
129             }
130              
131             sub parse {
132 225     225 0 980 my ($self, $html) = (shift, "$_[0]");
133              
134 225         602 my $xml = $self->xml;
135 225         596 my $current = my $tree = ['root'];
136 225         2003 while ($html =~ /\G$TOKEN_RE/gcso) {
137 4359         14014 my ($text, $doctype, $comment, $cdata, $pi, $tag, $runaway)
138             = ($1, $2, $3, $4, $5, $6, $11);
139              
140             # Text (and runaway "<")
141 4359 100       8313 $text .= '<' if defined $runaway;
142 4359 100       8236 _node($current, 'text', html_unescape $text) if defined $text;
143              
144             # Tag
145 4359 100       160680 if (defined $tag) {
    100          
    100          
    100          
    100          
146              
147             # End
148 1468 100       5481 if ($tag =~ /^\/\s*(\S+)/) {
    50          
149 605 100       1415 my $end = $xml ? $1 : lc $1;
150              
151             # No more content
152 605 100 100     2218 if (!$xml && (my $tags = $NO_MORE_CONTENT{$end})) { _end($_, $xml, \$current) for @$tags }
  11         50  
153              
154 605 100       1772 _end($xml ? $1 : lc $1, $xml, \$current);
155             }
156              
157             # Start
158             elsif ($tag =~ m!^([^\s/]+)([\s\S]*)!) {
159 863 100       3110 my ($start, $attr) = ($xml ? $1 : lc $1, $2);
160              
161             # Attributes
162 863         1406 my (%attrs, $closing);
163 863         2652 while ($attr =~ /$ATTR_RE/go) {
164 33102 100       70177 my $key = $xml ? $1 : lc $1;
165 33102 100       68674 my $value = defined($2) ? $2 : defined($3) ? $3 : $4;
    100          
166              
167             # Empty tag
168 33102 100 50     56236 ++$closing and next if $key eq '/';
169              
170 33058 100       68541 $attrs{$key} = defined $value ? html_attr_unescape $value : $value;
171             }
172              
173             # "image" is an alias for "img"
174 863 100 100     3028 $start = 'img' if !$xml && $start eq 'image';
175 863         2452 _start($start, \%attrs, $xml, \$current);
176              
177             # Element without end tag (self-closing)
178             _end($start, $xml, \$current)
179 863 100 100     5595 if !$xml && $EMPTY{$start} || ($xml || !$BLOCK{$start}) && $closing;
      100        
      100        
      100        
180              
181             # Raw text elements
182 863 100 100     6436 next if $xml || !$RAW{$start} && !$RCDATA{$start};
      100        
183 40 100       970 next unless $html =~ m!\G(.*?)<\s*/\s*\Q$start\E\s*>!gcsi;
184 39 100       226 _node($current, 'raw', $RCDATA{$start} ? html_unescape $1 : $1);
185 39         123 _end($start, 0, \$current);
186             }
187             }
188              
189             # DOCTYPE
190 12         45 elsif (defined $doctype) { _node($current, 'doctype', $doctype) }
191              
192             # Comment
193 10         30 elsif (defined $comment) { _node($current, 'comment', $comment) }
194              
195             # CDATA
196 7         22 elsif (defined $cdata) { _node($current, 'cdata', $cdata) }
197              
198             # Processing instruction (try to detect XML)
199             elsif (defined $pi) {
200 18 100 100     171 $self->xml($xml = 1) if !exists $self->{xml} && $pi =~ /xml/i;
201 18         55 _node($current, 'pi', $pi);
202             }
203             }
204              
205 225         676 return $self->tree($tree);
206             }
207              
208 152     152 0 374 sub render { _render($_[0]->tree, $_[0]->xml) }
209              
210             sub _end {
211 938     938   1904 my ($end, $xml, $current) = @_;
212              
213             # Search stack for start tag
214 938         1436 my $next = $$current;
215 938         1224 do {
216              
217             # Ignore useless end tag
218 1302 100       2717 return if $next->[0] eq 'root';
219              
220             # Right tag
221 1145 100       4553 return $$current = $next->[3] if $next->[1] eq $end;
222              
223             # Phrasing content can only cross phrasing content
224 368 100 100     1576 return if !$xml && $PHRASING{$end} && !$PHRASING{$next->[1]};
      66        
225              
226             } while $next = $next->[3];
227             }
228              
229             sub _node {
230 1183     1183   2338 my ($current, $type, $content) = @_;
231 1183         3492 push @$current, my $new = [$type, $content, $current];
232 1183         3252 weaken $new->[2];
233             }
234              
235             sub _render {
236 981     981   1639 my ($tree, $xml) = @_;
237              
238             # Tag
239 981         1589 my $type = $tree->[0];
240 981 100       1852 if ($type eq 'tag') {
241              
242             # Start tag
243 434         731 my $tag = $tree->[1];
244 434         809 my $result = "<$tag";
245              
246             # Attributes
247 434         549 for my $key (sort keys %{$tree->[2]}) {
  434         1252  
248 64         141 my $value = $tree->[2]{$key};
249 64 100 50     168 $result .= $xml ? qq{ $key="$key"} : " $key" and next
    100          
250             unless defined $value;
251 54         175 $result .= qq{ $key="} . html_escape($value) . '"';
252             }
253              
254             # No children
255 434 100       1189 return $xml ? "$result />" : $EMPTY{$tag} ? "$result>" : "$result>"
    100          
    100          
256             unless $tree->[4];
257              
258             # Children
259 2     2   19 no warnings 'recursion';
  2         4  
  2         1202  
260 394         821 $result .= '>' . join '', map { _render($_, $xml) } @$tree[4 .. $#$tree];
  653         1533  
261              
262             # End tag
263 394         2515 return "$result";
264             }
265              
266             # Text (escaped)
267 547 100       1346 return html_escape($tree->[1]) if $type eq 'text';
268              
269             # Raw text
270 144 100       344 return $tree->[1] if $type eq 'raw';
271              
272             # Root
273 138 100       655 return join '', map { _render($_, $xml) } @$tree[1 .. $#$tree]
  175         361  
274             if $type eq 'root';
275              
276             # DOCTYPE
277 14 100       71 return '[1] . '>' if $type eq 'doctype';
278              
279             # Comment
280 10 100       55 return '' if $type eq 'comment';
281              
282             # CDATA
283 6 100       37 return '[1] . ']]>' if $type eq 'cdata';
284              
285             # Processing instruction
286 3 50       31 return '[1] . '?>' if $type eq 'pi';
287              
288             # Everything else
289 0         0 return '';
290             }
291              
292             sub _start {
293 863     863   1720 my ($start, $attrs, $xml, $current) = @_;
294              
295             # Autoclose optional HTML elements
296 863 100 100     3159 if (!$xml && $$current->[0] ne 'root') {
297 557 100       1735 if (my $end = $END{$start}) { _end($end, 0, $current) }
  148 100       320  
298              
299             elsif (my $close = $CLOSE{$start}) {
300 146         297 my ($allowed, $scope) = @$close;
301              
302             # Close allowed parent elements in scope
303 146         257 my $parent = $$current;
304 146   66     657 while ($parent->[0] ne 'root' && !$scope->{$parent->[1]}) {
305 147 100       416 _end($parent->[1], 0, $current) if $allowed->{$parent->[1]};
306 147         523 $parent = $parent->[3];
307             }
308             }
309             }
310              
311             # New tag
312 863         2842 push @$$current, my $new = ['tag', $start, $attrs, $$current];
313 863         2637 weaken $new->[3];
314 863         1578 $$current = $new;
315             }
316              
317             sub _tag {
318 12     12   34 my $tree = ['tag', shift, undef, undef];
319              
320             # Content
321 12 100       60 push @$tree, ref $_[-1] eq 'CODE' ? ['raw', pop->()] : ['text', pop]
    100          
322             if @_ % 2;
323              
324             # Attributes
325 12         45 my $attrs = $tree->[2] = {@_};
326 12 100 66     62 return $tree unless exists $attrs->{data} && ref $attrs->{data} eq 'HASH';
327 1         2 my $data = delete $attrs->{data};
328 1         6 @$attrs{map { y/_/-/; lc "data-$_" } keys %$data} = values %$data;
  2         7  
  2         9  
329              
330 1         5 return $tree;
331             }
332              
333             1;
334              
335             =for Pod::Coverage *EVERYTHING*
336              
337             =cut