File Coverage

blib/lib/Mojo/DOM58/_HTML.pm
Criterion Covered Total %
statement 113 115 98.2
branch 97 100 97.0
condition 39 46 84.7
subroutine 18 18 100.0
pod 0 7 0.0
total 267 286 93.3


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         58  
8 2     2   12 use warnings;
  2         25  
  2         55  
9 2     2   18 use Exporter 'import';
  2         6  
  2         79  
10 2     2   1073 use Mojo::DOM58::Entities qw(html_attr_unescape html_escape html_unescape);
  2         4  
  2         317  
11 2     2   18 use Scalar::Util 'weaken';
  2         3  
  2         2938  
12              
13             our $VERSION = '2.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 elements without end tags
78             my %EMPTY = map { $_ => 1 } (
79             qw(area base br col embed hr img input keygen link menuitem meta param),
80             qw(source track wbr)
81             );
82              
83             # HTML elements categorized as phrasing content (and obsolete inline elements)
84             my @PHRASING = (
85             qw(a abbr area audio b bdi bdo br button canvas cite code data datalist),
86             qw(del dfn em embed i iframe img input ins kbd keygen label link map mark),
87             qw(math meta meter noscript object output picture progress q ruby s samp),
88             qw(script select slot small span strong sub sup svg template textarea time u),
89             qw(var video wbr)
90             );
91             my @OBSOLETE = qw(acronym applet basefont big font strike tt);
92             my %PHRASING = map { $_ => 1 } @OBSOLETE, @PHRASING;
93              
94             # HTML elements that don't get their self-closing flag acknowledged
95             my %BLOCK = map { $_ => 1 } (
96             qw(a address applet article aside b big blockquote body button caption),
97             qw(center code col colgroup dd details dialog dir div dl dt em fieldset),
98             qw(figcaption figure font footer form frameset h1 h2 h3 h4 h5 h6 head),
99             qw(header hgroup html i iframe li listing main marquee menu nav nobr),
100             qw(noembed noframes noscript object ol optgroup option p plaintext pre rp),
101             qw(rt s script section select small strike strong style summary table),
102             qw(tbody td template textarea tfoot th thead title tr tt u ul xmp)
103             );
104              
105             sub new {
106 2061     2061 0 3327 my $class = shift;
107 2061 50 33     11573 bless @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {}, ref $class || $class;
  0 100       0  
108             }
109              
110 11     11 0 23 sub tag { shift->tree(['root', _tag(@_)]) }
111              
112 1     1 0 3 sub tag_to_html { _render(_tag(@_), undef) }
113              
114             sub tree {
115 4742     4742 0 7118 my $self = shift;
116 4742 100       16745 return exists $self->{tree} ? $self->{tree} : ($self->{tree} = ['root']) unless @_;
    100          
117 2072         4253 $self->{tree} = shift;
118 2072         8431 return $self;
119             }
120              
121             sub xml {
122 3417     3417 0 4952 my $self = shift;
123 3417 100       8081 return $self->{xml} unless @_;
124 1860         3537 $self->{xml} = shift;
125 1860         7827 return $self;
126             }
127              
128             sub parse {
129 213     213 0 768 my ($self, $html) = (shift, "$_[0]");
130              
131 213         490 my $xml = $self->xml;
132 213         482 my $current = my $tree = ['root'];
133 213         1609 while ($html =~ /\G$TOKEN_RE/gcso) {
134 3936         11822 my ($text, $doctype, $comment, $cdata, $pi, $tag, $runaway)
135             = ($1, $2, $3, $4, $5, $6, $11);
136              
137             # Text (and runaway "<")
138 3936 100       6711 $text .= '<' if defined $runaway;
139 3936 100       7220 _node($current, 'text', html_unescape $text) if defined $text;
140              
141             # Tag
142 3936 100       138955 if (defined $tag) {
    100          
    100          
    100          
    100          
143              
144             # End
145 1329 100       4210 if ($tag =~ /^\/\s*(\S+)/) { _end($xml ? $1 : lc $1, $xml, \$current) }
  550 100       1720  
    50          
146              
147             # Start
148             elsif ($tag =~ m!^([^\s/]+)([\s\S]*)!) {
149 779 100       2549 my ($start, $attr) = ($xml ? $1 : lc $1, $2);
150              
151             # Attributes
152 779         1809 my (%attrs, $closing);
153 779         2282 while ($attr =~ /$ATTR_RE/go) {
154 33091 100       59920 my $key = $xml ? $1 : lc $1;
155 33091 100       57882 my $value = defined($2) ? $2 : defined($3) ? $3 : $4;
    100          
156              
157             # Empty tag
158 33091 100 50     48414 ++$closing and next if $key eq '/';
159              
160 33047 100       58965 $attrs{$key} = defined $value ? html_attr_unescape $value : $value;
161             }
162              
163             # "image" is an alias for "img"
164 779 100 100     2363 $start = 'img' if !$xml && $start eq 'image';
165 779         2095 _start($start, \%attrs, $xml, \$current);
166              
167             # Element without end tag (self-closing)
168             _end($start, $xml, \$current)
169 779 100 100     4481 if !$xml && $EMPTY{$start} || ($xml || !$BLOCK{$start}) && $closing;
      100        
      100        
      100        
170              
171             # Raw text elements
172 779 100 100     5135 next if $xml || !$RAW{$start} && !$RCDATA{$start};
      100        
173 29 100       579 next unless $html =~ m!\G(.*?)<\s*/\s*\Q$start\E\s*>!gcsi;
174 28 100       141 _node($current, 'raw', $RCDATA{$start} ? html_unescape $1 : $1);
175 28         81 _end($start, 0, \$current);
176             }
177             }
178              
179             # DOCTYPE
180 11         30 elsif (defined $doctype) { _node($current, 'doctype', $doctype) }
181              
182             # Comment
183 10         27 elsif (defined $comment) { _node($current, 'comment', $comment) }
184              
185             # CDATA
186 7         38 elsif (defined $cdata) { _node($current, 'cdata', $cdata) }
187              
188             # Processing instruction (try to detect XML)
189             elsif (defined $pi) {
190 17 100 100     127 $self->xml($xml = 1) if !exists $self->{xml} && $pi =~ /xml/i;
191 17         42 _node($current, 'pi', $pi);
192             }
193             }
194              
195 213         603 return $self->tree($tree);
196             }
197              
198 152     152 0 332 sub render { _render($_[0]->tree, $_[0]->xml) }
199              
200             sub _end {
201 823     823   1522 my ($end, $xml, $current) = @_;
202              
203             # Search stack for start tag
204 823         1134 my $next = $$current;
205 823         1050 do {
206              
207             # Ignore useless end tag
208 1119 100       2149 return if $next->[0] eq 'root';
209              
210             # Right tag
211 998 100       3591 return $$current = $next->[3] if $next->[1] eq $end;
212              
213             # Phrasing content can only cross phrasing content
214 301 100 100     1204 return if !$xml && $PHRASING{$end} && !$PHRASING{$next->[1]};
      66        
215              
216             } while $next = $next->[3];
217             }
218              
219             sub _node {
220 1041     1041   1790 my ($current, $type, $content) = @_;
221 1041         3665 push @$current, my $new = [$type, $content, $current];
222 1041         2779 weaken $new->[2];
223             }
224              
225             sub _render {
226 981     981   1550 my ($tree, $xml) = @_;
227              
228             # Tag
229 981         1444 my $type = $tree->[0];
230 981 100       1733 if ($type eq 'tag') {
231              
232             # Start tag
233 434         673 my $tag = $tree->[1];
234 434         694 my $result = "<$tag";
235              
236             # Attributes
237 434         499 for my $key (sort keys %{$tree->[2]}) {
  434         1072  
238 64         121 my $value = $tree->[2]{$key};
239 64 100 50     157 $result .= $xml ? qq{ $key="$key"} : " $key" and next
    100          
240             unless defined $value;
241 54         168 $result .= qq{ $key="} . html_escape($value) . '"';
242             }
243              
244             # No children
245 434 100       1105 return $xml ? "$result />" : $EMPTY{$tag} ? "$result>" : "$result>"
    100          
    100          
246             unless $tree->[4];
247              
248             # Children
249 2     2   18 no warnings 'recursion';
  2         4  
  2         1111  
250 394         715 $result .= '>' . join '', map { _render($_, $xml) } @$tree[4 .. $#$tree];
  653         1226  
251              
252             # End tag
253 394         2146 return "$result";
254             }
255              
256             # Text (escaped)
257 547 100       1276 return html_escape($tree->[1]) if $type eq 'text';
258              
259             # Raw text
260 144 100       269 return $tree->[1] if $type eq 'raw';
261              
262             # Root
263 138 100       496 return join '', map { _render($_, $xml) } @$tree[1 .. $#$tree]
  175         348  
264             if $type eq 'root';
265              
266             # DOCTYPE
267 14 100       62 return '[1] . '>' if $type eq 'doctype';
268              
269             # Comment
270 10 100       47 return '' if $type eq 'comment';
271              
272             # CDATA
273 6 100       34 return '[1] . ']]>' if $type eq 'cdata';
274              
275             # Processing instruction
276 3 50       28 return '[1] . '?>' if $type eq 'pi';
277              
278             # Everything else
279 0         0 return '';
280             }
281              
282             sub _start {
283 779     779   1387 my ($start, $attrs, $xml, $current) = @_;
284              
285             # Autoclose optional HTML elements
286 779 100 100     2300 if (!$xml && $$current->[0] ne 'root') {
287 491 100       1299 if (my $end = $END{$start}) { _end($end, 0, $current) }
  126 100       234  
288              
289             elsif (my $close = $CLOSE{$start}) {
290 123         261 my ($allowed, $scope) = @$close;
291              
292             # Close allowed parent elements in scope
293 123         182 my $parent = $$current;
294 123   66     520 while ($parent->[0] ne 'root' && !$scope->{$parent->[1]}) {
295 119 100       288 _end($parent->[1], 0, $current) if $allowed->{$parent->[1]};
296 119         373 $parent = $parent->[3];
297             }
298             }
299             }
300              
301             # New tag
302 779         2347 push @$$current, my $new = ['tag', $start, $attrs, $$current];
303 779         2211 weaken $new->[3];
304 779         1185 $$current = $new;
305             }
306              
307             sub _tag {
308 12     12   23 my $tree = ['tag', shift, undef, undef];
309              
310             # Content
311 12 100       44 push @$tree, ref $_[-1] eq 'CODE' ? ['raw', pop->()] : ['text', pop]
    100          
312             if @_ % 2;
313              
314             # Attributes
315 12         32 my $attrs = $tree->[2] = {@_};
316 12 100 66     45 return $tree unless exists $attrs->{data} && ref $attrs->{data} eq 'HASH';
317 1         3 my $data = delete $attrs->{data};
318 1         4 @$attrs{map { y/_/-/; lc "data-$_" } keys %$data} = values %$data;
  2         5  
  2         6  
319              
320 1         4 return $tree;
321             }
322              
323             1;
324              
325             =for Pod::Coverage *EVERYTHING*
326              
327             =cut