File Coverage

blib/lib/Mojo/DOM/HTML.pm
Criterion Covered Total %
statement 103 104 99.0
branch 88 90 97.7
condition 48 52 92.3
subroutine 14 14 100.0
pod 4 4 100.0
total 257 264 97.3


line stmt bran cond sub pod time code
1             package Mojo::DOM::HTML;
2 65     65   519 use Mojo::Base -base;
  65         156  
  65         580  
3              
4 65     65   538 use Exporter qw(import);
  65         146  
  65         2999  
5 65     65   435 use Mojo::Util qw(html_attr_unescape html_unescape xml_escape);
  65         142  
  65         7639  
6 65     65   515 use Scalar::Util qw(weaken);
  65         2190  
  65         194252  
7              
8             our @EXPORT_OK = ('tag_to_html');
9              
10             has tree => sub { ['root'] };
11             has 'xml';
12              
13             my $ATTR_RE = qr/
14             ([^<>=\s\/0-9.\-][^<>=\s\/]*|\/) # Key
15             (?:
16             \s*=\s*
17             (?s:(["'])(.*?)\g{-2}|([^>\s]*)) # Value
18             )?
19             \s*
20             /x;
21             my $TOKEN_RE = qr/
22             ([^<]+)? # Text
23             (?:
24             <(?:
25             !(?:
26             DOCTYPE(
27             \s+\w+ # Doctype
28             (?:(?:\s+\w+)?(?:\s+(?:"[^"]*"|'[^']*'))+)? # External ID
29             (?:\s+\[.+?\])? # Int Subset
30             \s*)
31             |
32             --(.*?)--\s* # Comment
33             |
34             \[CDATA\[(.*?)\]\] # CDATA
35             )
36             |
37             \?(.*?)\? # Processing Instruction
38             |
39             \s*((?:\/\s*)?[^<>\s\/0-9.\-][^<>\s\/]*\s*(?:(?:$ATTR_RE){0,32766})*+) # Tag
40             )>
41             |
42             (<) # Runaway "<"
43             )??
44             /xis;
45              
46             # HTML elements that only contain raw text
47             my %RAW = map { $_ => 1 } qw(script style);
48              
49             # HTML elements that only contain raw text and entities
50             my %RCDATA = map { $_ => 1 } qw(title textarea);
51              
52             # HTML elements with optional end tags
53             my %END = (body => 'head', optgroup => 'optgroup', option => 'option');
54              
55             # HTML elements that break paragraphs
56             map { $END{$_} = 'p' } (
57             qw(address article aside blockquote details dialog div dl fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6),
58             qw(header hgroup hr main menu nav ol p pre section table ul)
59             );
60              
61             # Container HTML elements that create their own scope
62             my %SCOPE = map { $_ => 1 } qw(math svg);
63              
64             # HTML table elements with optional end tags
65             my %TABLE = map { $_ => 1 } qw(colgroup tbody td tfoot th thead tr);
66              
67             # HTML elements with optional end tags and scoping rules
68             my %CLOSE = (li => [{li => 1}, {ul => 1, ol => 1}], tr => [{tr => 1}, {table => 1}]);
69             $CLOSE{$_} = [\%TABLE, {table => 1}] for qw(colgroup tbody tfoot thead);
70             $CLOSE{$_} = [{dd => 1, dt => 1}, {dl => 1}] for qw(dd dt);
71             $CLOSE{$_} = [{rp => 1, rt => 1}, {ruby => 1}] for qw(rp rt);
72             $CLOSE{$_} = [{th => 1, td => 1}, {table => 1}] for qw(td th);
73              
74             # HTML parent elements that signal no more content when closed, but that are also phrasing content
75             my %NO_MORE_CONTENT = (ruby => [qw(rt rp)], select => [qw(option optgroup)]);
76              
77             # HTML elements without end tags
78             my %EMPTY = map { $_ => 1 } qw(area base br col embed hr img input keygen link menuitem meta param source track wbr);
79              
80             # HTML elements categorized as phrasing content (and obsolete inline elements)
81             my @PHRASING = (
82             qw(a abbr area audio b bdi bdo br button canvas cite code data datalist del dfn em embed i iframe img input ins kbd),
83             qw(keygen label link map mark math meta meter noscript object output picture progress q ruby s samp script select),
84             qw(slot small span strong sub sup svg template textarea time u var video wbr)
85             );
86             my @OBSOLETE = qw(acronym applet basefont big font strike tt);
87             my %PHRASING = map { $_ => 1 } @OBSOLETE, @PHRASING;
88              
89             # HTML elements that don't get their self-closing flag acknowledged
90             my %BLOCK = map { $_ => 1 } (
91             qw(a address applet article aside b big blockquote body button caption center code col colgroup dd details dialog),
92             qw(dir div dl dt em fieldset figcaption figure font footer form frameset h1 h2 h3 h4 h5 h6 head header hgroup html),
93             qw(i iframe li listing main marquee menu nav nobr noembed noframes noscript object ol optgroup option p plaintext),
94             qw(pre rp rt s script section select small strike strong style summary table tbody td template textarea tfoot th),
95             qw(thead title tr tt u ul xmp)
96             );
97              
98             sub parse {
99 324     324 1 1679 my ($self, $html) = (shift, "$_[0]");
100              
101 324         1148 my $xml = $self->xml;
102 324         1075 my $current = my $tree = ['root'];
103 324         6041 while ($html =~ /\G$TOKEN_RE/gcso) {
104 10681         37302 my ($text, $doctype, $comment, $cdata, $pi, $tag, $runaway) = ($1, $2, $3, $4, $5, $6, $11);
105              
106             # Text (and runaway "<")
107 10681 100       19587 $text .= '<' if defined $runaway;
108 10681 100       22039 _node($current, 'text', html_unescape $text) if defined $text;
109              
110             # Tag
111 10681 100       271979 if (defined $tag) {
    100          
    100          
    100          
    100          
112              
113             # End
114 3632 100       13997 if ($tag =~ /^\/\s*(\S+)/) {
    50          
115 1591 100       4375 my $end = $xml ? $1 : lc $1;
116              
117             # No more content
118 1591 100 100     6139 if (!$xml && (my $tags = $NO_MORE_CONTENT{$end})) { _end($_, $xml, \$current) for @$tags }
  15         85  
119              
120 1591         3346 _end($end, $xml, \$current);
121             }
122              
123             # Start
124             elsif ($tag =~ m!^([^\s/]+)([\s\S]*)!) {
125 2041 100       7138 my ($start, $attr) = ($xml ? $1 : lc $1, $2);
126              
127             # Attributes
128 2041         3175 my (%attrs, $closing);
129 2041         8796 while ($attr =~ /$ATTR_RE/go) {
130 34217 100 100     171665 my ($key, $value) = ($xml ? $1 : lc $1, $3 // $4);
131              
132             # Empty tag
133 34217 100 50     75592 ++$closing and next if $key eq '/';
134              
135 34153 100       88781 $attrs{$key} = defined $value ? html_attr_unescape $value : $value;
136             }
137              
138             # "image" is an alias for "img"
139 2041 100 100     7651 $start = 'img' if !$xml && $start eq 'image';
140 2041         5888 _start($start, \%attrs, $xml, \$current);
141              
142             # Element without end tag (self-closing)
143 2041 100 100     14345 _end($start, $xml, \$current) if !$xml && $EMPTY{$start} || ($xml || !$BLOCK{$start}) && $closing;
      100        
      100        
      100        
144              
145             # Raw text elements
146 2041 100 100     16895 next if $xml || !$RAW{$start} && !$RCDATA{$start};
      100        
147 72 100       2568 next unless $html =~ m!\G(.*?))!gcsi;
148 71 100       441 _node($current, 'raw', $RCDATA{$start} ? html_unescape $1 : $1);
149 71         242 _end($start, 0, \$current);
150             }
151             }
152              
153             # DOCTYPE
154 18         159 elsif (defined $doctype) { _node($current, 'doctype', $doctype) }
155              
156             # Comment
157 14         44 elsif (defined $comment) { _node($current, 'comment', $comment) }
158              
159             # CDATA
160 7         20 elsif (defined $cdata) { _node($current, 'cdata', $cdata) }
161              
162             # Processing instruction (try to detect XML)
163             elsif (defined $pi) {
164 18 100 100     185 $self->xml($xml = 1) if !exists $self->{xml} && $pi =~ /xml/i;
165 18         67 _node($current, 'pi', $pi);
166             }
167             }
168              
169 324         1454 return $self->tree($tree);
170             }
171              
172 164     164 1 649 sub render { _render($_[0]->tree, $_[0]->xml) }
173              
174 11     11 1 46 sub tag { shift->tree(['root', _tag(@_)]) }
175              
176 954     954 1 2408 sub tag_to_html { _render(_tag(@_), undef) }
177              
178             sub _end {
179 2384     2384   4499 my ($end, $xml, $current) = @_;
180              
181             # Search stack for start tag
182 2384         3967 my $next = $$current;
183 2384         3142 do {
184              
185             # Ignore useless end tag
186 5205 100       10646 return if $next->[0] eq 'root';
187              
188             # Don’t traverse a container tag
189 4721 100 100     11013 return if $SCOPE{$next->[1]} && $next->[1] ne $end;
190              
191             # Right tag
192 4718 100       14642 return $$current = $next->[3] if $next->[1] eq $end;
193              
194             # Phrasing content can only cross phrasing content
195 2827 100 100     9955 return if !$xml && $PHRASING{$end} && !$PHRASING{$next->[1]};
      66        
196              
197             } while $next = $next->[3];
198             }
199              
200             sub _node {
201 3086     3086   7210 my ($current, $type, $content) = @_;
202 3086         9988 push @$current, my $new = [$type, $content, $current];
203 3086         6968 weaken $new->[2];
204             }
205              
206             sub _render {
207 2494     2494   4598 my ($tree, $xml) = @_;
208              
209             # Tag
210 2494         4260 my $type = $tree->[0];
211 2494 100       7986 if ($type eq 'tag') {
212              
213             # Start tag
214 1407         2350 my $tag = $tree->[1];
215 1407         2448 my $result = "<$tag";
216              
217             # Attributes
218 1407         2196 for my $key (sort keys %{$tree->[2]}) {
  1407         5441  
219 1672         3024 my $value = $tree->[2]{$key};
220 1672 100 50     3464 $result .= $xml ? qq{ $key="$key"} : " $key" and next unless defined $value;
    100          
221 1635         4344 $result .= qq{ $key="} . xml_escape($value) . '"';
222             }
223              
224             # No children
225 1407 100       7338 return $xml ? "$result />" : $EMPTY{$tag} ? "$result>" : "$result>" unless $tree->[4];
    100          
    100          
226              
227             # Children
228 65     65   646 no warnings 'recursion';
  65         145  
  65         75739  
229 916         2446 $result .= '>' . join '', map { _render($_, $xml) } @$tree[4 .. $#$tree];
  1188         2951  
230              
231             # End tag
232 916         5314 return "$result";
233             }
234              
235             # Text (escaped)
236 1087 100       2668 return xml_escape $tree->[1] if $type eq 'text';
237              
238             # Raw text
239 542 100       2270 return $tree->[1] if $type eq 'raw';
240              
241             # Root
242 150 100       711 return join '', map { _render($_, $xml) } @$tree[1 .. $#$tree] if $type eq 'root';
  188         483  
243              
244             # DOCTYPE
245 14 100       65 return '[1] . '>' if $type eq 'doctype';
246              
247             # Comment
248 10 100       42 return '' if $type eq 'comment';
249              
250             # CDATA
251 6 100       27 return '[1] . ']]>' if $type eq 'cdata';
252              
253             # Processing instruction
254 3 50       24 return '[1] . '?>' if $type eq 'pi';
255              
256             # Everything else
257 0         0 return '';
258             }
259              
260             sub _start {
261 2041     2041   3968 my ($start, $attrs, $xml, $current) = @_;
262              
263             # Autoclose optional HTML elements
264 2041 100 100     7311 if (!$xml && $$current->[0] ne 'root') {
265 1624 100       5410 if (my $end = $END{$start}) { _end($end, 0, $current) }
  469 100       972  
266              
267             elsif (my $close = $CLOSE{$start}) {
268 595         1075 my ($allowed, $scope) = @$close;
269              
270             # Close allowed parent elements in scope
271 595         836 my $parent = $$current;
272 595   66     2515 while ($parent->[0] ne 'root' && !$scope->{$parent->[1]}) {
273 440 100       974 _end($parent->[1], 0, $current) if $allowed->{$parent->[1]};
274 440         1605 $parent = $parent->[3];
275             }
276             }
277             }
278              
279             # New tag
280 2041         7336 push @$$current, my $new = ['tag', $start, $attrs, $$current];
281 2041         4007 weaken $new->[3];
282 2041         3867 $$current = $new;
283             }
284              
285             sub _tag {
286 965     965   2810 my $tree = ['tag', shift, undef, undef];
287              
288             # Content
289 965 100       4090 push @$tree, ref $_[-1] eq 'CODE' ? ['raw', pop->()] : ['text', pop] if @_ % 2;
    100          
290              
291             # Attributes
292 965         3677 my $attrs = $tree->[2] = {@_};
293 965 100 100     4218 return $tree unless exists $attrs->{data} && ref $attrs->{data} eq 'HASH';
294 5         13 my $data = delete $attrs->{data};
295 5         19 @$attrs{map { y/_/-/; lc "data-$_" } keys %$data} = values %$data;
  9         23  
  9         38  
296 5         18 return $tree;
297             }
298              
299             1;
300              
301             =encoding utf8
302              
303             =head1 NAME
304              
305             Mojo::DOM::HTML - HTML/XML engine
306              
307             =head1 SYNOPSIS
308              
309             use Mojo::DOM::HTML;
310              
311             # Turn HTML into DOM tree
312             my $html = Mojo::DOM::HTML->new;
313             $html->parse('

Test

123

');
314             my $tree = $html->tree;
315              
316             =head1 DESCRIPTION
317              
318             L is the HTML/XML engine used by L, based on the L
319             Standard|https://html.spec.whatwg.org> and the L.
320              
321             =head1 FUNCTIONS
322              
323             L implements the following functions, which can be imported individually.
324              
325             =head2 tag_to_html
326              
327             my $str = tag_to_html 'div', id => 'foo', 'safe content';
328              
329             Generate HTML/XML tag and render it right away. This is a significantly faster alternative to L for template
330             systems that have to generate a lot of tags.
331              
332             =head1 ATTRIBUTES
333              
334             L implements the following attributes.
335              
336             =head2 tree
337              
338             my $tree = $html->tree;
339             $html = $html->tree(['root']);
340              
341             Document Object Model. Note that this structure should only be used very carefully since it is very dynamic.
342              
343             =head2 xml
344              
345             my $bool = $html->xml;
346             $html = $html->xml($bool);
347              
348             Disable HTML semantics in parser and activate case-sensitivity, defaults to auto-detection based on XML declarations.
349              
350             =head1 METHODS
351              
352             L inherits all methods from L and implements the following new ones.
353              
354             =head2 parse
355              
356             $html = $html->parse('I ♥ Mojolicious!');
357              
358             Parse HTML/XML fragment.
359              
360             =head2 render
361              
362             my $str = $html->render;
363              
364             Render DOM to HTML/XML.
365              
366             =head2 tag
367              
368             $html = $html->tag('div', id => 'foo', 'safe content');
369              
370             Generate HTML/XML tag.
371              
372             =head1 SEE ALSO
373              
374             L, L, L.
375              
376             =cut