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         5  
  2         65  
8 2     2   10 use warnings;
  2         4  
  2         62  
9 2     2   9 use Exporter 'import';
  2         3  
  2         81  
10 2     2   1170 use Mojo::DOM58::Entities qw(html_attr_unescape html_escape html_unescape);
  2         6  
  2         381  
11 2     2   20 use Scalar::Util 'weaken';
  2         4  
  2         3028  
12              
13             our $VERSION = '3.001';
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 3610 my $class = shift;
110 2194 50 33     12353 bless @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {}, ref $class || $class;
  0 100       0  
111             }
112              
113 11     11 0 28 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 8020 my $self = shift;
119 5055 100       18227 return exists $self->{tree} ? $self->{tree} : ($self->{tree} = ['root']) unless @_;
    100          
120 2203         4289 $self->{tree} = shift;
121 2203         7856 return $self;
122             }
123              
124             sub xml {
125 3669     3669 0 5532 my $self = shift;
126 3669 100       9060 return $self->{xml} unless @_;
127 1980         3256 $self->{xml} = shift;
128 1980         8651 return $self;
129             }
130              
131             sub parse {
132 225     225 0 940 my ($self, $html) = (shift, "$_[0]");
133              
134 225         435 my $xml = $self->xml;
135 225         563 my $current = my $tree = ['root'];
136 225         1739 while ($html =~ /\G$TOKEN_RE/gcso) {
137 4359         13989 my ($text, $doctype, $comment, $cdata, $pi, $tag, $runaway)
138             = ($1, $2, $3, $4, $5, $6, $11);
139              
140             # Text (and runaway "<")
141 4359 100       7353 $text .= '<' if defined $runaway;
142 4359 100       8334 _node($current, 'text', html_unescape $text) if defined $text;
143              
144             # Tag
145 4359 100       161769 if (defined $tag) {
    100          
    100          
    100          
    100          
146              
147             # End
148 1468 100       5042 if ($tag =~ /^\/\s*(\S+)/) {
    50          
149 605 100       1372 my $end = $xml ? $1 : lc $1;
150              
151             # No more content
152 605 100 100     2165 if (!$xml && (my $tags = $NO_MORE_CONTENT{$end})) { _end($_, $xml, \$current) for @$tags }
  11         45  
153              
154 605 100       1877 _end($xml ? $1 : lc $1, $xml, \$current);
155             }
156              
157             # Start
158             elsif ($tag =~ m!^([^\s/]+)([\s\S]*)!) {
159 863 100       2838 my ($start, $attr) = ($xml ? $1 : lc $1, $2);
160              
161             # Attributes
162 863         1287 my (%attrs, $closing);
163 863         2493 while ($attr =~ /$ATTR_RE/go) {
164 33102 100       69176 my $key = $xml ? $1 : lc $1;
165 33102 100       67150 my $value = defined($2) ? $2 : defined($3) ? $3 : $4;
    100          
166              
167             # Empty tag
168 33102 100 50     54298 ++$closing and next if $key eq '/';
169              
170 33058 100       66066 $attrs{$key} = defined $value ? html_attr_unescape $value : $value;
171             }
172              
173             # "image" is an alias for "img"
174 863 100 100     3103 $start = 'img' if !$xml && $start eq 'image';
175 863         2536 _start($start, \%attrs, $xml, \$current);
176              
177             # Element without end tag (self-closing)
178             _end($start, $xml, \$current)
179 863 100 100     5253 if !$xml && $EMPTY{$start} || ($xml || !$BLOCK{$start}) && $closing;
      100        
      100        
      100        
180              
181             # Raw text elements
182 863 100 100     6073 next if $xml || !$RAW{$start} && !$RCDATA{$start};
      100        
183 40 100       785 next unless $html =~ m!\G(.*?)<\s*/\s*\Q$start\E\s*>!gcsi;
184 39 100       199 _node($current, 'raw', $RCDATA{$start} ? html_unescape $1 : $1);
185 39         109 _end($start, 0, \$current);
186             }
187             }
188              
189             # DOCTYPE
190 12         37 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         19 elsif (defined $cdata) { _node($current, 'cdata', $cdata) }
197              
198             # Processing instruction (try to detect XML)
199             elsif (defined $pi) {
200 18 100 100     150 $self->xml($xml = 1) if !exists $self->{xml} && $pi =~ /xml/i;
201 18         52 _node($current, 'pi', $pi);
202             }
203             }
204              
205 225         616 return $self->tree($tree);
206             }
207              
208 152     152 0 384 sub render { _render($_[0]->tree, $_[0]->xml) }
209              
210             sub _end {
211 938     938   1754 my ($end, $xml, $current) = @_;
212              
213             # Search stack for start tag
214 938         1316 my $next = $$current;
215 938         1307 do {
216              
217             # Ignore useless end tag
218 1302 100       2521 return if $next->[0] eq 'root';
219              
220             # Right tag
221 1145 100       4549 return $$current = $next->[3] if $next->[1] eq $end;
222              
223             # Phrasing content can only cross phrasing content
224 368 100 100     1464 return if !$xml && $PHRASING{$end} && !$PHRASING{$next->[1]};
      66        
225              
226             } while $next = $next->[3];
227             }
228              
229             sub _node {
230 1183     1183   2206 my ($current, $type, $content) = @_;
231 1183         3267 push @$current, my $new = [$type, $content, $current];
232 1183         3287 weaken $new->[2];
233             }
234              
235             sub _render {
236 981     981   1577 my ($tree, $xml) = @_;
237              
238             # Tag
239 981         1496 my $type = $tree->[0];
240 981 100       1764 if ($type eq 'tag') {
241              
242             # Start tag
243 434         658 my $tag = $tree->[1];
244 434         828 my $result = "<$tag";
245              
246             # Attributes
247 434         566 for my $key (sort keys %{$tree->[2]}) {
  434         1207  
248 64         137 my $value = $tree->[2]{$key};
249 64 100 50     166 $result .= $xml ? qq{ $key="$key"} : " $key" and next
    100          
250             unless defined $value;
251 54         185 $result .= qq{ $key="} . html_escape($value) . '"';
252             }
253              
254             # No children
255 434 100       1142 return $xml ? "$result />" : $EMPTY{$tag} ? "$result>" : "$result>"
    100          
    100          
256             unless $tree->[4];
257              
258             # Children
259 2     2   19 no warnings 'recursion';
  2         7  
  2         1198  
260 394         833 $result .= '>' . join '', map { _render($_, $xml) } @$tree[4 .. $#$tree];
  653         1475  
261              
262             # End tag
263 394         2345 return "$result";
264             }
265              
266             # Text (escaped)
267 547 100       1408 return html_escape($tree->[1]) if $type eq 'text';
268              
269             # Raw text
270 144 100       413 return $tree->[1] if $type eq 'raw';
271              
272             # Root
273 138 100       589 return join '', map { _render($_, $xml) } @$tree[1 .. $#$tree]
  175         348  
274             if $type eq 'root';
275              
276             # DOCTYPE
277 14 100       68 return '[1] . '>' if $type eq 'doctype';
278              
279             # Comment
280 10 100       49 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       34 return '[1] . '?>' if $type eq 'pi';
287              
288             # Everything else
289 0         0 return '';
290             }
291              
292             sub _start {
293 863     863   1659 my ($start, $attrs, $xml, $current) = @_;
294              
295             # Autoclose optional HTML elements
296 863 100 100     2912 if (!$xml && $$current->[0] ne 'root') {
297 557 100       1734 if (my $end = $END{$start}) { _end($end, 0, $current) }
  148 100       272  
298              
299             elsif (my $close = $CLOSE{$start}) {
300 146         323 my ($allowed, $scope) = @$close;
301              
302             # Close allowed parent elements in scope
303 146         232 my $parent = $$current;
304 146   66     657 while ($parent->[0] ne 'root' && !$scope->{$parent->[1]}) {
305 147 100       356 _end($parent->[1], 0, $current) if $allowed->{$parent->[1]};
306 147         469 $parent = $parent->[3];
307             }
308             }
309             }
310              
311             # New tag
312 863         2830 push @$$current, my $new = ['tag', $start, $attrs, $$current];
313 863         2565 weaken $new->[3];
314 863         1372 $$current = $new;
315             }
316              
317             sub _tag {
318 12     12   34 my $tree = ['tag', shift, undef, undef];
319              
320             # Content
321 12 100       66 push @$tree, ref $_[-1] eq 'CODE' ? ['raw', pop->()] : ['text', pop]
    100          
322             if @_ % 2;
323              
324             # Attributes
325 12         37 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         5  
  2         8  
329              
330 1         5 return $tree;
331             }
332              
333             1;
334              
335             =for Pod::Coverage *EVERYTHING*
336              
337             =cut