blib/lib/Mojo/DOM58.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 234 | 234 | 100.0 |
branch | 123 | 126 | 97.6 |
condition | 59 | 66 | 89.3 |
subroutine | 78 | 78 | 100.0 |
pod | 43 | 44 | 97.7 |
total | 537 | 548 | 97.9 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package Mojo::DOM58; | ||||||
2 | |||||||
3 | 2 | 2 | 142808 | use strict; | |||
2 | 14 | ||||||
2 | 59 | ||||||
4 | 2 | 2 | 10 | use warnings; | |||
2 | 4 | ||||||
2 | 227 | ||||||
5 | |||||||
6 | use overload | ||||||
7 | 4 | 4 | 13 | '@{}' => sub { shift->child_nodes }, | |||
8 | 95 | 95 | 251 | '%{}' => sub { shift->attr }, | |||
9 | 32 | 32 | 3373 | bool => sub {1}, | |||
10 | 129 | 129 | 15132 | '""' => sub { shift->to_string }, | |||
11 | 2 | 2 | 2748 | fallback => 1; | |||
2 | 2076 | ||||||
2 | 22 | ||||||
12 | |||||||
13 | 2 | 2 | 212 | use Exporter 'import'; | |||
2 | 4 | ||||||
2 | 56 | ||||||
14 | 2 | 2 | 445 | use Mojo::DOM58::_Collection; | |||
2 | 5 | ||||||
2 | 50 | ||||||
15 | 2 | 2 | 952 | use Mojo::DOM58::_CSS; | |||
2 | 6 | ||||||
2 | 76 | ||||||
16 | 2 | 2 | 954 | use Mojo::DOM58::_HTML 'tag_to_html'; | |||
2 | 9 | ||||||
2 | 228 | ||||||
17 | 2 | 2 | 17 | use Scalar::Util qw(blessed weaken); | |||
2 | 4 | ||||||
2 | 107 | ||||||
18 | 2 | 2 | 1663 | use Storable 'dclone'; | |||
2 | 6686 | ||||||
2 | 8077 | ||||||
19 | |||||||
20 | our $VERSION = '3.001'; | ||||||
21 | |||||||
22 | our @EXPORT_OK = 'tag_to_html'; | ||||||
23 | |||||||
24 | sub new { | ||||||
25 | 2128 | 2128 | 1 | 325302 | my $class = shift; | ||
26 | 2128 | 66 | 5414 | my $self = bless \Mojo::DOM58::_HTML->new, ref $class || $class; | |||
27 | 2128 | 100 | 5992 | return @_ ? $self->parse(@_) : $self; | |||
28 | } | ||||||
29 | |||||||
30 | sub new_tag { | ||||||
31 | 11 | 11 | 1 | 2547 | my $self = shift; | ||
32 | 11 | 27 | my $new = $self->new; | ||||
33 | 11 | 43 | $$new->tag(@_); | ||||
34 | 11 | 100 | 30 | $$new->xml($$self->xml) if ref $self; | |||
35 | 11 | 43 | return $new; | ||||
36 | } | ||||||
37 | |||||||
38 | 1 | 1 | 0 | 90 | sub TO_JSON { ${shift()}->render } | ||
1 | 5 | ||||||
39 | |||||||
40 | 25 | 25 | 1 | 75 | sub all_text { _text(_nodes($_[0]->tree), $_[0]->xml, 1) } | ||
41 | |||||||
42 | 15 | 15 | 1 | 49 | sub ancestors { _select($_[0]->_collect([_ancestors($_[0]->tree)]), $_[1]) } | ||
43 | |||||||
44 | 9 | 9 | 1 | 36 | sub append { shift->_add(1, @_) } | ||
45 | 13 | 13 | 1 | 98 | sub append_content { shift->_content(1, 0, @_) } | ||
46 | |||||||
47 | sub at { | ||||||
48 | 612 | 612 | 1 | 2042 | my $self = shift; | ||
49 | 612 | 100 | 1459 | return undef unless my $result = $self->_css->select_one(@_); | |||
50 | 564 | 3165 | return $self->_build($result, $self->xml); | ||||
51 | } | ||||||
52 | |||||||
53 | sub attr { | ||||||
54 | 168 | 168 | 1 | 328 | my $self = shift; | ||
55 | |||||||
56 | # Hash | ||||||
57 | 168 | 298 | my $tree = $self->tree; | ||||
58 | 168 | 100 | 431 | my $attrs = $tree->[0] ne 'tag' ? {} : $tree->[2]; | |||
59 | 168 | 100 | 870 | return $attrs unless @_; | |||
60 | |||||||
61 | # Get | ||||||
62 | 40 | 100 | 100 | 285 | return $attrs->{$_[0]} unless @_ > 1 || ref $_[0]; | ||
63 | |||||||
64 | # Set | ||||||
65 | 4 | 100 | 15 | my $values = ref $_[0] ? $_[0] : {@_}; | |||
66 | 4 | 17 | @$attrs{keys %$values} = values %$values; | ||||
67 | |||||||
68 | 4 | 19 | return $self; | ||||
69 | } | ||||||
70 | |||||||
71 | 59 | 59 | 1 | 169 | sub child_nodes { $_[0]->_collect(_nodes($_[0]->tree)) } | ||
72 | |||||||
73 | 13 | 13 | 1 | 122 | sub children { _select($_[0]->_collect(_nodes($_[0]->tree, 1)), $_[1]) } | ||
74 | |||||||
75 | sub content { | ||||||
76 | 55 | 55 | 1 | 106 | my $self = shift; | ||
77 | |||||||
78 | 55 | 119 | my $type = $self->type; | ||||
79 | 55 | 100 | 100 | 223 | if ($type eq 'root' || $type eq 'tag') { | ||
80 | 24 | 100 | 79 | return $self->_content(0, 1, @_) if @_; | |||
81 | 7 | 19 | my $html = Mojo::DOM58::_HTML->new(xml => $self->xml); | ||||
82 | 7 | 13 | return join '', map { $html->tree($_)->render } @{_nodes($self->tree)}; | ||||
12 | 29 | ||||||
7 | 14 | ||||||
83 | } | ||||||
84 | |||||||
85 | 31 | 100 | 73 | return $self->tree->[1] unless @_; | |||
86 | 3 | 7 | $self->tree->[1] = shift; | ||||
87 | 3 | 10 | return $self; | ||||
88 | } | ||||||
89 | |||||||
90 | 13 | 13 | 1 | 34 | sub descendant_nodes { $_[0]->_collect(_all(_nodes($_[0]->tree))) } | ||
91 | |||||||
92 | sub find { | ||||||
93 | 427 | 427 | 1 | 2661 | my $self = shift; | ||
94 | 427 | 1084 | return $self->_collect($self->_css->select(@_)); | ||||
95 | } | ||||||
96 | |||||||
97 | 8 | 8 | 1 | 21 | sub following { _select($_[0]->_collect(_siblings($_[0]->tree, 1, 1)), $_[1]) } | ||
98 | 7 | 7 | 1 | 20 | sub following_nodes { $_[0]->_collect(_siblings($_[0]->tree, 0, 1)) } | ||
99 | |||||||
100 | 44 | 44 | 1 | 109 | sub matches { shift->_css->matches(@_) } | ||
101 | |||||||
102 | sub namespace { | ||||||
103 | 18 | 18 | 1 | 34 | my $self = shift; | ||
104 | |||||||
105 | 18 | 100 | 38 | return undef if (my $tree = $self->tree)->[0] ne 'tag'; | |||
106 | |||||||
107 | # Extract namespace prefix and search parents | ||||||
108 | 16 | 100 | 172 | my $ns = $tree->[1] =~ /^(.*?):/ ? "xmlns:$1" : undef; | |||
109 | 16 | 41 | for my $node ($tree, _ancestors($tree)) { | ||||
110 | |||||||
111 | # Namespace for prefix | ||||||
112 | 35 | 53 | my $attrs = $node->[2]; | ||||
113 | 35 | 100 | 100 | 82 | if ($ns) { $_ eq $ns and return $attrs->{$_} for keys %$attrs } | ||
13 | 100 | 71 | |||||
114 | |||||||
115 | # Namespace attribute | ||||||
116 | 10 | 55 | elsif (defined $attrs->{xmlns}) { return $attrs->{xmlns} } | ||||
117 | } | ||||||
118 | |||||||
119 | 1 | 6 | return undef; | ||||
120 | } | ||||||
121 | |||||||
122 | 13 | 13 | 1 | 30 | sub next { $_[0]->_maybe(_siblings($_[0]->tree, 1, 1, 0)) } | ||
123 | 5 | 5 | 1 | 14 | sub next_node { $_[0]->_maybe(_siblings($_[0]->tree, 0, 1, 0)) } | ||
124 | |||||||
125 | sub parent { | ||||||
126 | 48 | 48 | 1 | 93 | my $self = shift; | ||
127 | 48 | 50 | 82 | return undef if (my $tree = $self->tree)->[0] eq 'root'; | |||
128 | 48 | 109 | return $self->_build(_parent($tree), $self->xml); | ||||
129 | } | ||||||
130 | |||||||
131 | 166 | 50 | 166 | 1 | 285 | sub parse { ${$_[0]}->parse($_[1]) and return $_[0] } | |
166 | 703 | ||||||
132 | |||||||
133 | 5 | 5 | 1 | 13 | sub preceding { _select($_[0]->_collect(_siblings($_[0]->tree, 1, 0)), $_[1]) } | ||
134 | 7 | 7 | 1 | 18 | sub preceding_nodes { $_[0]->_collect(_siblings($_[0]->tree, 0)) } | ||
135 | |||||||
136 | 11 | 11 | 1 | 31 | sub prepend { shift->_add(0, @_) } | ||
137 | 6 | 6 | 1 | 24 | sub prepend_content { shift->_content(0, 0, @_) } | ||
138 | |||||||
139 | 7 | 7 | 1 | 22 | sub previous { $_[0]->_maybe(_siblings($_[0]->tree, 1, 0, -1)) } | ||
140 | 5 | 5 | 1 | 11 | sub previous_node { $_[0]->_maybe(_siblings($_[0]->tree, 0, 0, -1)) } | ||
141 | |||||||
142 | 6 | 6 | 1 | 17 | sub remove { shift->replace('') } | ||
143 | |||||||
144 | sub replace { | ||||||
145 | 24 | 24 | 1 | 65 | my ($self, $new) = @_; | ||
146 | 24 | 100 | 49 | return $self->parse($new) if (my $tree = $self->tree)->[0] eq 'root'; | |||
147 | 16 | 40 | return $self->_replace(_parent($tree), $tree, _nodes($self->_parse($new))); | ||||
148 | } | ||||||
149 | |||||||
150 | sub root { | ||||||
151 | 12 | 12 | 1 | 26 | my $self = shift; | ||
152 | 12 | 100 | 32 | return $self unless my $tree = _ancestors($self->tree, 1); | |||
153 | 9 | 25 | return $self->_build($tree, $self->xml); | ||||
154 | } | ||||||
155 | |||||||
156 | sub selector { | ||||||
157 | 13 | 100 | 13 | 1 | 30 | return undef unless (my $tree = shift->tree)->[0] eq 'tag'; | |
158 | return join ' > ', | ||||||
159 | 11 | 43 | reverse map { $_->[1] . ':nth-child(' . (@{_siblings($_, 1)} + 1) . ')' } | ||||
31 | 69 | ||||||
31 | 52 | ||||||
160 | $tree, _ancestors($tree); | ||||||
161 | } | ||||||
162 | |||||||
163 | sub strip { | ||||||
164 | 9 | 9 | 1 | 15 | my $self = shift; | ||
165 | 9 | 100 | 22 | return $self if (my $tree = $self->tree)->[0] ne 'tag'; | |||
166 | 7 | 21 | return $self->_replace($tree->[3], $tree, _nodes($tree)); | ||||
167 | } | ||||||
168 | |||||||
169 | sub tag { | ||||||
170 | 96 | 96 | 1 | 270 | my ($self, $tag) = @_; | ||
171 | 96 | 100 | 176 | return undef if (my $tree = $self->tree)->[0] ne 'tag'; | |||
172 | 94 | 100 | 440 | return $tree->[1] unless $tag; | |||
173 | 1 | 4 | $tree->[1] = $tag; | ||||
174 | 1 | 3 | return $self; | ||||
175 | } | ||||||
176 | |||||||
177 | 1 | 1 | 1 | 6 | sub tap { Mojo::DOM58::_Collection::tap(@_) } | ||
178 | |||||||
179 | 722 | 722 | 1 | 1822 | sub text { _text(_nodes(shift->tree), 0, 0) } | ||
180 | |||||||
181 | 139 | 139 | 1 | 217 | sub to_string { ${shift()}->render } | ||
139 | 459 | ||||||
182 | |||||||
183 | 4596 | 100 | 50 | 4596 | 1 | 9504 | sub tree { @_ > 1 ? (${$_[0]}->tree($_[1]) and return $_[0]) : ${$_[0]}->tree } |
2641 | 7856 | ||||||
184 | |||||||
185 | 78 | 78 | 1 | 150 | sub type { shift->tree->[0] } | ||
186 | |||||||
187 | sub val { | ||||||
188 | 27 | 27 | 1 | 45 | my $self = shift; | ||
189 | |||||||
190 | # "option" | ||||||
191 | 27 | 100 | 60 | return defined($self->{value}) ? $self->{value} : $self->text | |||
100 | |||||||
192 | if (my $tag = $self->tag) eq 'option'; | ||||||
193 | |||||||
194 | # "input" ("type=checkbox" and "type=radio") | ||||||
195 | 17 | 100 | 48 | my $type = $self->{type} || ''; | |||
196 | 17 | 100 | 100 | 70 | return defined $self->{value} ? $self->{value} : 'on' | ||
100 | 100 | ||||||
197 | if $tag eq 'input' && ($type eq 'radio' || $type eq 'checkbox'); | ||||||
198 | |||||||
199 | # "textarea", "input" or "button" | ||||||
200 | 12 | 100 | 38 | return $tag eq 'textarea' ? $self->text : $self->{value} if $tag ne 'select'; | |||
100 | |||||||
201 | |||||||
202 | # "select" | ||||||
203 | my $v = $self->find('option:checked:not([disabled])') | ||||||
204 | 5 | 6 | 12 | ->grep(sub { !$_->ancestors('optgroup[disabled]')->size })->map('val'); | |||
6 | 17 | ||||||
205 | 5 | 100 | 31 | return exists $self->{multiple} ? $v->size ? $v->to_array : undef : $v->last; | |||
100 | |||||||
206 | } | ||||||
207 | |||||||
208 | 1 | 1 | 1 | 1429 | sub with_roles { Mojo::DOM58::_Collection::with_roles(@_) } | ||
209 | |||||||
210 | 9 | 9 | 1 | 36 | sub wrap { shift->_wrap(0, @_) } | ||
211 | 7 | 7 | 1 | 20 | sub wrap_content { shift->_wrap(1, @_) } | ||
212 | |||||||
213 | 3275 | 100 | 50 | 3275 | 1 | 6099 | sub xml { @_ > 1 ? (${$_[0]}->xml($_[1]) and return $_[0]) : ${$_[0]}->xml } |
1309 | 4272 | ||||||
214 | |||||||
215 | sub _add { | ||||||
216 | 20 | 20 | 44 | my ($self, $offset, $new) = @_; | |||
217 | |||||||
218 | 20 | 100 | 47 | return $self if (my $tree = $self->tree)->[0] eq 'root'; | |||
219 | |||||||
220 | 16 | 38 | my $parent = _parent($tree); | ||||
221 | splice @$parent, _offset($parent, $tree) + $offset, 0, | ||||||
222 | 16 | 46 | @{_link($parent, _nodes($self->_parse($new)))}; | ||||
16 | 45 | ||||||
223 | |||||||
224 | 16 | 76 | return $self; | ||||
225 | } | ||||||
226 | |||||||
227 | sub _all { | ||||||
228 | 21 | 21 | 30 | my $nodes = shift; | |||
229 | 21 | 100 | 39 | @$nodes = map { $_->[0] eq 'tag' ? ($_, @{_all(_nodes($_))}) : ($_) } @$nodes; | |||
60 | 131 | ||||||
8 | 16 | ||||||
230 | 21 | 53 | return $nodes; | ||||
231 | } | ||||||
232 | |||||||
233 | sub _ancestors { | ||||||
234 | 54 | 54 | 124 | my ($tree, $root) = @_; | |||
235 | |||||||
236 | 54 | 100 | 117 | return () unless $tree = _parent($tree); | |||
237 | 51 | 91 | my @ancestors; | ||||
238 | 51 | 66 | 76 | do { push @ancestors, $tree } | |||
137 | 450 | ||||||
239 | while ($tree->[0] eq 'tag') && ($tree = $tree->[3]); | ||||||
240 | 51 | 100 | 237 | return $root ? $ancestors[-1] : @ancestors[0 .. $#ancestors - 1]; | |||
241 | } | ||||||
242 | |||||||
243 | 1955 | 1955 | 4096 | sub _build { shift->new->tree(shift)->xml(shift) } | |||
244 | |||||||
245 | sub _collect { | ||||||
246 | 553 | 50 | 553 | 1529 | my ($self, $nodes) = (shift, shift || []); | ||
247 | 553 | 1281 | my $xml = $self->xml; | ||||
248 | 553 | 1208 | return Mojo::DOM58::_Collection->new(map { $self->_build($_, $xml) } @$nodes); | ||||
1314 | 2279 | ||||||
249 | } | ||||||
250 | |||||||
251 | sub _content { | ||||||
252 | 36 | 36 | 86 | my ($self, $start, $offset, $new) = @_; | |||
253 | |||||||
254 | 36 | 67 | my $tree = $self->tree; | ||||
255 | 36 | 100 | 100 | 149 | unless ($tree->[0] eq 'root' || $tree->[0] eq 'tag') { | ||
256 | 2 | 6 | my $old = $self->content; | ||||
257 | 2 | 100 | 11 | return $self->content($start ? $old . $new : $new . $old); | |||
258 | } | ||||||
259 | |||||||
260 | 34 | 100 | 84 | $start = $start ? ($#$tree + 1) : _start($tree); | |||
261 | 34 | 100 | 76 | $offset = $offset ? $#$tree : 0; | |||
262 | 34 | 51 | splice @$tree, $start, $offset, @{_link($tree, _nodes($self->_parse($new)))}; | ||||
34 | 79 | ||||||
263 | |||||||
264 | 34 | 153 | return $self; | ||||
265 | } | ||||||
266 | |||||||
267 | 1083 | 1083 | 2329 | sub _css { Mojo::DOM58::_CSS->new(tree => shift->tree) } | |||
268 | |||||||
269 | 1 | 1 | 8 | sub _fragment { _link(my $r = ['root', @_], [@_]); $r } | |||
1 | 5 | ||||||
270 | |||||||
271 | sub _link { | ||||||
272 | 98 | 98 | 176 | my ($parent, $children) = @_; | |||
273 | |||||||
274 | # Link parent to children | ||||||
275 | 98 | 183 | for my $node (@$children) { | ||||
276 | 102 | 100 | 234 | my $offset = $node->[0] eq 'tag' ? 3 : 2; | |||
277 | 102 | 187 | $node->[$offset] = $parent; | ||||
278 | 102 | 301 | weaken $node->[$offset]; | ||||
279 | } | ||||||
280 | |||||||
281 | 98 | 322 | return $children; | ||||
282 | } | ||||||
283 | |||||||
284 | 30 | 100 | 30 | 106 | sub _maybe { $_[1] ? $_[0]->_build($_[1], $_[0]->xml) : undef } | ||
285 | |||||||
286 | sub _nodes { | ||||||
287 | 1191 | 50 | 1191 | 2612 | return () unless my $tree = shift; | ||
288 | 1191 | 2297 | my @nodes = @$tree[_start($tree) .. $#$tree]; | ||||
289 | 1191 | 100 | 3562 | return shift() ? [grep { $_->[0] eq 'tag' } @nodes] : \@nodes; | |||
84 | 262 | ||||||
290 | } | ||||||
291 | |||||||
292 | sub _offset { | ||||||
293 | 46 | 46 | 136 | my ($parent, $child) = @_; | |||
294 | 46 | 92 | my $i = _start($parent); | ||||
295 | 46 | 100 | 289 | $_ eq $child ? last : $i++ for @$parent[$i .. $#$parent]; | |||
296 | 46 | 99 | return $i; | ||||
297 | } | ||||||
298 | |||||||
299 | 223 | 100 | 223 | 733 | sub _parent { $_[0]->[$_[0][0] eq 'tag' ? 3 : 2] } | ||
300 | |||||||
301 | sub _parse { | ||||||
302 | 80 | 80 | 166 | my ($self, $input) = @_; | |||
303 | 80 | 100 | 66 | 420 | return Mojo::DOM58::_HTML->new(xml => $self->xml)->parse($input)->tree | ||
304 | unless blessed $input && $input->isa('Mojo::DOM58'); | ||||||
305 | 21 | 51 | my $tree = dclone $input->tree; | ||||
306 | 21 | 100 | 125 | return $tree->[0] eq 'root' ? $tree : _fragment($tree); | |||
307 | } | ||||||
308 | |||||||
309 | sub _replace { | ||||||
310 | 30 | 30 | 71 | my ($self, $parent, $child, $nodes) = @_; | |||
311 | 30 | 88 | splice @$parent, _offset($parent, $child), 1, @{_link($parent, $nodes)}; | ||||
30 | 66 | ||||||
312 | 30 | 76 | return $self->parent; | ||||
313 | } | ||||||
314 | |||||||
315 | 41 | 100 | 41 | 219 | sub _select { $_[1] ? $_[0]->grep(matches => $_[1]) : $_[0] } | ||
316 | |||||||
317 | sub _siblings { | ||||||
318 | 88 | 88 | 178 | my ($tree, $tags, $tail, $i) = @_; | |||
319 | |||||||
320 | 88 | 100 | 211 | return defined $i ? undef : [] if $tree->[0] eq 'root'; | |||
100 | |||||||
321 | |||||||
322 | 82 | 147 | my $nodes = _nodes(_parent($tree)); | ||||
323 | 82 | 124 | my $match = -1; | ||||
324 | 82 | 66 | 652 | defined($match++) and $_ eq $tree and last for @$nodes; | |||
100 | |||||||
325 | |||||||
326 | 82 | 100 | 152 | if ($tail) { splice @$nodes, 0, $match + 1 } | |||
30 | 64 | ||||||
327 | 52 | 122 | else { splice @$nodes, $match, ($#$nodes + 1) - $match } | ||||
328 | |||||||
329 | 82 | 100 | 187 | @$nodes = grep { $_->[0] eq 'tag' } @$nodes if $tags; | |||
171 | 354 | ||||||
330 | |||||||
331 | 82 | 100 | 100 | 376 | return defined $i ? $i == -1 && !@$nodes ? undef : $nodes->[$i] : $nodes; | ||
100 | |||||||
332 | } | ||||||
333 | |||||||
334 | 1264 | 100 | 1264 | 4164 | sub _start { $_[0][0] eq 'root' ? 1 : 4 } | ||
335 | |||||||
336 | sub _text { | ||||||
337 | 747 | 747 | 1364 | my ($nodes, $xml, $all) = @_; | |||
338 | |||||||
339 | 747 | 1219 | my $text = ''; | ||||
340 | 747 | 1682 | while (my $node = shift @$nodes) { | ||||
341 | 1076 | 1569 | my $type = $node->[0]; | ||||
342 | |||||||
343 | # Text | ||||||
344 | 1076 | 100 | 100 | 3436 | if ($type eq 'text' || $type eq 'cdata' || $type eq 'raw') { | ||
100 | 100 | ||||||
100 | |||||||
345 | 867 | 2525 | $text .= $node->[1]; | ||||
346 | } | ||||||
347 | |||||||
348 | # Nested tag | ||||||
349 | elsif ($type eq 'tag' && $all) { | ||||||
350 | 152 | 100 | 100 | 561 | unshift @$nodes, @{_nodes($node)} if $xml || ($node->[1] ne 'script' && $node->[1] ne 'style'); | ||
140 | 100 | 223 | |||||
351 | } | ||||||
352 | } | ||||||
353 | |||||||
354 | 747 | 3489 | return $text; | ||||
355 | } | ||||||
356 | |||||||
357 | sub _wrap { | ||||||
358 | 16 | 16 | 38 | my ($self, $content, $new) = @_; | |||
359 | |||||||
360 | 16 | 100 | 100 | 30 | return $self if (my $tree = $self->tree)->[0] eq 'root' && !$content; | ||
361 | 15 | 100 | 100 | 74 | return $self if $tree->[0] ne 'root' && $tree->[0] ne 'tag' && $content; | ||
100 | |||||||
362 | |||||||
363 | # Find innermost tag | ||||||
364 | 14 | 26 | my $current; | ||||
365 | 14 | 34 | my $first = $new = $self->_parse($new); | ||||
366 | 14 | 48 | $current = $first while $first = _nodes($first, 1)->[0]; | ||||
367 | 14 | 100 | 51 | return $self unless $current; | |||
368 | |||||||
369 | # Wrap content | ||||||
370 | 12 | 100 | 24 | if ($content) { | |||
371 | 5 | 11 | push @$current, @{_link($current, _nodes($tree))}; | ||||
5 | 11 | ||||||
372 | 5 | 18 | splice @$tree, _start($tree), $#$tree, @{_link($tree, _nodes($new))}; | ||||
5 | 10 | ||||||
373 | 5 | 23 | return $self; | ||||
374 | } | ||||||
375 | |||||||
376 | # Wrap element | ||||||
377 | 7 | 18 | $self->_replace(_parent($tree), $tree, _nodes($new)); | ||||
378 | 7 | 27 | push @$current, @{_link($current, [$tree])}; | ||||
7 | 24 | ||||||
379 | 7 | 33 | return $self; | ||||
380 | } | ||||||
381 | |||||||
382 | 1; | ||||||
383 | |||||||
384 | =encoding utf8 | ||||||
385 | |||||||
386 | =head1 NAME | ||||||
387 | |||||||
388 | Mojo::DOM58 - Minimalistic HTML/XML DOM parser with CSS selectors | ||||||
389 | |||||||
390 | =head1 SYNOPSIS | ||||||
391 | |||||||
392 | use Mojo::DOM58; | ||||||
393 | |||||||
394 | # Parse | ||||||
395 | my $dom = Mojo::DOM58->new(' Test 123 |
||||||
396 | |||||||
397 | # Find | ||||||
398 | say $dom->at('#b')->text; | ||||||
399 | say $dom->find('p')->map('text')->join("\n"); | ||||||
400 | say $dom->find('[id]')->map(attr => 'id')->join("\n"); | ||||||
401 | |||||||
402 | # Iterate | ||||||
403 | $dom->find('p[id]')->reverse->each(sub { say $_->{id} }); | ||||||
404 | |||||||
405 | # Loop | ||||||
406 | for my $e ($dom->find('p[id]')->each) { | ||||||
407 | say $e->{id}, ':', $e->text; | ||||||
408 | } | ||||||
409 | |||||||
410 | # Modify | ||||||
411 | $dom->find('div p')->last->append(' 456 '); |
||||||
412 | $dom->at('#c')->prepend($dom->new_tag('p', id => 'd', '789')); | ||||||
413 | $dom->find(':not(p)')->map('strip'); | ||||||
414 | |||||||
415 | # Render | ||||||
416 | say "$dom"; | ||||||
417 | |||||||
418 | =head1 DESCRIPTION | ||||||
419 | |||||||
420 | L |
||||||
421 | on L |
||||||
422 | and L |
||||||
423 | matching based on L |
||||||
424 | even try to interpret broken HTML and XML, so you should not use it for | ||||||
425 | validation. | ||||||
426 | |||||||
427 | =head1 FORK INFO | ||||||
428 | |||||||
429 | L |
||||||
430 | closely compatible with upstream. It differs only in the standalone format and | ||||||
431 | compatibility with Perl 5.8. Any bugs or patches not related to these changes | ||||||
432 | should be reported directly to the L |
||||||
433 | |||||||
434 | This release of L |
||||||
435 | L |
||||||
436 | |||||||
437 | =head1 NODES AND ELEMENTS | ||||||
438 | |||||||
439 | When we parse an HTML/XML fragment, it gets turned into a tree of nodes. | ||||||
440 | |||||||
441 | |||||||
442 | |||||||
443 | |
||||||
444 | World! | ||||||
445 | |||||||
446 | |||||||
447 | There are currently eight different kinds of nodes, C |
||||||
448 | C |
||||||
449 | the type C |
||||||
450 | |||||||
451 | root | ||||||
452 | |- doctype (html) | ||||||
453 | +- tag (html) | ||||||
454 | |- tag (head) | ||||||
455 | | +- tag (title) | ||||||
456 | | +- raw (Hello) | ||||||
457 | +- tag (body) | ||||||
458 | +- text (World!) | ||||||
459 | |||||||
460 | While all node types are represented as L |
||||||
461 | L"attr"> and L"namespace"> only apply to elements. | ||||||
462 | |||||||
463 | =head1 CASE-SENSITIVITY | ||||||
464 | |||||||
465 | L |
||||||
466 | names are lowercased and selectors need to be lowercase as well. | ||||||
467 | |||||||
468 | # HTML semantics | ||||||
469 | my $dom = Mojo::DOM58->new(' Hi! '); |
||||||
470 | say $dom->at('p[id]')->text; | ||||||
471 | |||||||
472 | If an XML declaration is found, the parser will automatically switch into XML | ||||||
473 | mode and everything becomes case-sensitive. | ||||||
474 | |||||||
475 | # XML semantics | ||||||
476 | my $dom = Mojo::DOM58->new(' Hi! '); |
||||||
477 | say $dom->at('P[ID]')->text; | ||||||
478 | |||||||
479 | HTML or XML semantics can also be forced with the L"xml"> method. | ||||||
480 | |||||||
481 | # Force HTML semantics | ||||||
482 | my $dom = Mojo::DOM58->new->xml(0)->parse(' Hi! '); |
||||||
483 | say $dom->at('p[id]')->text; | ||||||
484 | |||||||
485 | # Force XML semantics | ||||||
486 | my $dom = Mojo::DOM58->new->xml(1)->parse(' Hi! '); |
||||||
487 | say $dom->at('P[ID]')->text; | ||||||
488 | |||||||
489 | =head1 SELECTORS | ||||||
490 | |||||||
491 | L |
||||||
492 | selectors that make sense for a standalone parser are supported. | ||||||
493 | |||||||
494 | =over | ||||||
495 | |||||||
496 | =item Z<>* | ||||||
497 | |||||||
498 | Any element. | ||||||
499 | |||||||
500 | my $all = $dom->find('*'); | ||||||
501 | |||||||
502 | =item E | ||||||
503 | |||||||
504 | An element of type C |
||||||
505 | |||||||
506 | my $title = $dom->at('title'); | ||||||
507 | |||||||
508 | =item E[foo] | ||||||
509 | |||||||
510 | An C |
||||||
511 | |||||||
512 | my $links = $dom->find('a[href]'); | ||||||
513 | |||||||
514 | =item E[foo="bar"] | ||||||
515 | |||||||
516 | An C |
||||||
517 | |||||||
518 | my $case_sensitive = $dom->find('input[type="hidden"]'); | ||||||
519 | my $case_sensitive = $dom->find('input[type=hidden]'); | ||||||
520 | |||||||
521 | =item E[foo="bar" i] | ||||||
522 | |||||||
523 | An C |
||||||
524 | (ASCII-range) case-permutation of C |
||||||
525 | B |
||||||
526 | |||||||
527 | my $case_insensitive = $dom->find('input[type="hidden" i]'); | ||||||
528 | my $case_insensitive = $dom->find('input[type=hidden i]'); | ||||||
529 | my $case_insensitive = $dom->find('input[class~="foo" i]'); | ||||||
530 | |||||||
531 | This selector is part of | ||||||
532 | L |
||||||
533 | in progress. | ||||||
534 | |||||||
535 | =item E[foo="bar" s] | ||||||
536 | |||||||
537 | An C |
||||||
538 | equal to C |
||||||
539 | without warning! | ||||||
540 | |||||||
541 | my $case_sensitive = $dom->find('input[type="hidden" s]'); | ||||||
542 | |||||||
543 | This selector is part of | ||||||
544 | L |
||||||
545 | in progress. | ||||||
546 | |||||||
547 | =item E[foo~="bar"] | ||||||
548 | |||||||
549 | An C |
||||||
550 | values, one of which is exactly equal to C |
||||||
551 | |||||||
552 | my $foo = $dom->find('input[class~="foo"]'); | ||||||
553 | my $foo = $dom->find('input[class~=foo]'); | ||||||
554 | |||||||
555 | =item E[foo^="bar"] | ||||||
556 | |||||||
557 | An C |
||||||
558 | C |
||||||
559 | |||||||
560 | my $begins_with = $dom->find('input[name^="f"]'); | ||||||
561 | my $begins_with = $dom->find('input[name^=f]'); | ||||||
562 | |||||||
563 | =item E[foo$="bar"] | ||||||
564 | |||||||
565 | An C |
||||||
566 | C |
||||||
567 | |||||||
568 | my $ends_with = $dom->find('input[name$="o"]'); | ||||||
569 | my $ends_with = $dom->find('input[name$=o]'); | ||||||
570 | |||||||
571 | =item E[foo*="bar"] | ||||||
572 | |||||||
573 | An C |
||||||
574 | |||||||
575 | my $contains = $dom->find('input[name*="fo"]'); | ||||||
576 | my $contains = $dom->find('input[name*=fo]'); | ||||||
577 | |||||||
578 | =item E[foo|="en"] | ||||||
579 | |||||||
580 | An C |
||||||
581 | beginning (from the left) with C |
||||||
582 | |||||||
583 | my $english = $dom->find('link[hreflang|=en]'); | ||||||
584 | |||||||
585 | =item E:root | ||||||
586 | |||||||
587 | An C |
||||||
588 | |||||||
589 | my $root = $dom->at(':root'); | ||||||
590 | |||||||
591 | =item E:nth-child(n) | ||||||
592 | |||||||
593 | An C |
||||||
594 | |||||||
595 | my $third = $dom->find('div:nth-child(3)'); | ||||||
596 | my $odd = $dom->find('div:nth-child(odd)'); | ||||||
597 | my $even = $dom->find('div:nth-child(even)'); | ||||||
598 | my $top3 = $dom->find('div:nth-child(-n+3)'); | ||||||
599 | |||||||
600 | =item E:nth-last-child(n) | ||||||
601 | |||||||
602 | An C |
||||||
603 | |||||||
604 | my $third = $dom->find('div:nth-last-child(3)'); | ||||||
605 | my $odd = $dom->find('div:nth-last-child(odd)'); | ||||||
606 | my $even = $dom->find('div:nth-last-child(even)'); | ||||||
607 | my $bottom3 = $dom->find('div:nth-last-child(-n+3)'); | ||||||
608 | |||||||
609 | =item E:nth-of-type(n) | ||||||
610 | |||||||
611 | An C |
||||||
612 | |||||||
613 | my $third = $dom->find('div:nth-of-type(3)'); | ||||||
614 | my $odd = $dom->find('div:nth-of-type(odd)'); | ||||||
615 | my $even = $dom->find('div:nth-of-type(even)'); | ||||||
616 | my $top3 = $dom->find('div:nth-of-type(-n+3)'); | ||||||
617 | |||||||
618 | =item E:nth-last-of-type(n) | ||||||
619 | |||||||
620 | An C |
||||||
621 | |||||||
622 | my $third = $dom->find('div:nth-last-of-type(3)'); | ||||||
623 | my $odd = $dom->find('div:nth-last-of-type(odd)'); | ||||||
624 | my $even = $dom->find('div:nth-last-of-type(even)'); | ||||||
625 | my $bottom3 = $dom->find('div:nth-last-of-type(-n+3)'); | ||||||
626 | |||||||
627 | =item E:first-child | ||||||
628 | |||||||
629 | An C |
||||||
630 | |||||||
631 | my $first = $dom->find('div p:first-child'); | ||||||
632 | |||||||
633 | =item E:last-child | ||||||
634 | |||||||
635 | An C |
||||||
636 | |||||||
637 | my $last = $dom->find('div p:last-child'); | ||||||
638 | |||||||
639 | =item E:first-of-type | ||||||
640 | |||||||
641 | An C |
||||||
642 | |||||||
643 | my $first = $dom->find('div p:first-of-type'); | ||||||
644 | |||||||
645 | =item E:last-of-type | ||||||
646 | |||||||
647 | An C |
||||||
648 | |||||||
649 | my $last = $dom->find('div p:last-of-type'); | ||||||
650 | |||||||
651 | =item E:only-child | ||||||
652 | |||||||
653 | An C |
||||||
654 | |||||||
655 | my $lonely = $dom->find('div p:only-child'); | ||||||
656 | |||||||
657 | =item E:only-of-type | ||||||
658 | |||||||
659 | An C |
||||||
660 | |||||||
661 | my $lonely = $dom->find('div p:only-of-type'); | ||||||
662 | |||||||
663 | =item E:empty | ||||||
664 | |||||||
665 | An C |
||||||
666 | |||||||
667 | my $empty = $dom->find(':empty'); | ||||||
668 | |||||||
669 | =item E:any-link | ||||||
670 | |||||||
671 | Alias for L"E:link">. Note that this selector is B |
||||||
672 | change without warning! This selector is part of | ||||||
673 | L |
||||||
674 | work in progress. | ||||||
675 | |||||||
676 | =item E:link | ||||||
677 | |||||||
678 | An C |
||||||
679 | not yet visited (C<:link>) or already visited (C<:visited>). Note that | ||||||
680 | L |
||||||
681 | C<:visited> yield exactly the same results. | ||||||
682 | |||||||
683 | my $links = $dom->find(':any-link'); | ||||||
684 | my $links = $dom->find(':link'); | ||||||
685 | my $links = $dom->find(':visited'); | ||||||
686 | |||||||
687 | =item E:visited | ||||||
688 | |||||||
689 | Alias for L"E:link">. | ||||||
690 | |||||||
691 | =item E:scope | ||||||
692 | |||||||
693 | An C |
||||||
694 | without warning! | ||||||
695 | |||||||
696 | my $scoped = $dom->find('a:not(:scope > a)'); | ||||||
697 | my $scoped = $dom->find('div :scope p'); | ||||||
698 | my $scoped = $dom->find('~ p'); | ||||||
699 | |||||||
700 | This selector is part of L |
||||||
701 | |||||||
702 | =item E:checked | ||||||
703 | |||||||
704 | A user interface element C |
||||||
705 | checkbox). | ||||||
706 | |||||||
707 | my $input = $dom->find(':checked'); | ||||||
708 | |||||||
709 | =item E.warning | ||||||
710 | |||||||
711 | An C |
||||||
712 | |||||||
713 | my $warning = $dom->find('div.warning'); | ||||||
714 | |||||||
715 | =item E#myid | ||||||
716 | |||||||
717 | An C |
||||||
718 | |||||||
719 | my $foo = $dom->at('div#foo'); | ||||||
720 | |||||||
721 | =item E:not(s1, s2) | ||||||
722 | |||||||
723 | An C |
||||||
724 | selector C |
||||||
725 | might change without warning! | ||||||
726 | |||||||
727 | my $others = $dom->find('div p:not(:first-child, :last-child)'); | ||||||
728 | |||||||
729 | Support for compound selectors was added as part of | ||||||
730 | L |
||||||
731 | in progress. | ||||||
732 | |||||||
733 | =item E:is(s1, s2) | ||||||
734 | |||||||
735 | An C |
||||||
736 | C |
||||||
737 | |||||||
738 | my $headers = $dom->find(':is(section, article, aside, nav) h1'); | ||||||
739 | |||||||
740 | This selector is part of | ||||||
741 | L |
||||||
742 | in progress. | ||||||
743 | |||||||
744 | =item E:has(rs1, rs2) | ||||||
745 | |||||||
746 | An C |
||||||
747 | match an element. Note that this selector is B |
||||||
748 | |||||||
749 | my $link = $dom->find('a:has(> img)'); | ||||||
750 | |||||||
751 | This selector is part of L |
||||||
752 | Also be aware that this feature is currently marked C |
||||||
753 | completely. | ||||||
754 | |||||||
755 | =item A|E | ||||||
756 | |||||||
757 | An C |
||||||
758 | L |
||||||
759 | Key/value pairs passed to selector methods are used to declare namespace | ||||||
760 | aliases. | ||||||
761 | |||||||
762 | my $elem = $dom->find('lq|elem', lq => 'http://example.com/q-markup'); | ||||||
763 | |||||||
764 | Using an empty alias searches for an element that belongs to no namespace. | ||||||
765 | |||||||
766 | my $div = $dom->find('|div'); | ||||||
767 | |||||||
768 | =item E F | ||||||
769 | |||||||
770 | An C |
||||||
771 | |||||||
772 | my $headlines = $dom->find('div h1'); | ||||||
773 | |||||||
774 | =item E E |
||||||
775 | |||||||
776 | An C |
||||||
777 | |||||||
778 | my $headlines = $dom->find('html > body > div > h1'); | ||||||
779 | |||||||
780 | =item E + F | ||||||
781 | |||||||
782 | An C |
||||||
783 | |||||||
784 | my $second = $dom->find('h1 + h2'); | ||||||
785 | |||||||
786 | =item E ~ F | ||||||
787 | |||||||
788 | An C |
||||||
789 | |||||||
790 | my $second = $dom->find('h1 ~ h2'); | ||||||
791 | |||||||
792 | =item E, F, G | ||||||
793 | |||||||
794 | Elements of type C |
||||||
795 | |||||||
796 | my $headlines = $dom->find('h1, h2, h3'); | ||||||
797 | |||||||
798 | =item E[foo=bar][bar=baz] | ||||||
799 | |||||||
800 | An C |
||||||
801 | |||||||
802 | my $links = $dom->find('a[foo^=b][foo$=ar]'); | ||||||
803 | |||||||
804 | =back | ||||||
805 | |||||||
806 | =head1 OPERATORS | ||||||
807 | |||||||
808 | L |
||||||
809 | |||||||
810 | =head2 array | ||||||
811 | |||||||
812 | my @nodes = @$dom; | ||||||
813 | |||||||
814 | Alias for L"child_nodes">. | ||||||
815 | |||||||
816 | # "" | ||||||
817 | $dom->parse('123')->[0]; | ||||||
818 | |||||||
819 | =head2 bool | ||||||
820 | |||||||
821 | my $bool = !!$dom; | ||||||
822 | |||||||
823 | Always true. | ||||||
824 | |||||||
825 | =head2 hash | ||||||
826 | |||||||
827 | my %attrs = %$dom; | ||||||
828 | |||||||
829 | Alias for L"attr">. | ||||||
830 | |||||||
831 | # "test" | ||||||
832 | $dom->parse(' Test ')->at('div')->{id}; |
||||||
833 | |||||||
834 | =head2 stringify | ||||||
835 | |||||||
836 | my $str = "$dom"; | ||||||
837 | |||||||
838 | Alias for L"to_string">. | ||||||
839 | |||||||
840 | =head1 FUNCTIONS | ||||||
841 | |||||||
842 | L |
||||||
843 | individually. | ||||||
844 | |||||||
845 | =head2 tag_to_html | ||||||
846 | |||||||
847 | my $str = tag_to_html 'div', id => 'foo', 'safe content'; | ||||||
848 | |||||||
849 | Generate HTML/XML tag and render it right away. This is a significantly faster | ||||||
850 | alternative to L"new_tag"> for template systems that have to generate a lot | ||||||
851 | of tags. | ||||||
852 | |||||||
853 | =head1 METHODS | ||||||
854 | |||||||
855 | L |
||||||
856 | |||||||
857 | =head2 new | ||||||
858 | |||||||
859 | my $dom = Mojo::DOM58->new; | ||||||
860 | my $dom = Mojo::DOM58->new(' |
||||||
861 | |||||||
862 | Construct a new scalar-based L |
||||||
863 | fragment if necessary. | ||||||
864 | |||||||
865 | =head2 new_tag | ||||||
866 | |||||||
867 | my $tag = Mojo::DOM58->new_tag('div'); | ||||||
868 | my $tag = $dom->new_tag('div'); | ||||||
869 | my $tag = $dom->new_tag('div', id => 'foo', hidden => undef); | ||||||
870 | my $tag = $dom->new_tag('div', 'safe content'); | ||||||
871 | my $tag = $dom->new_tag('div', id => 'foo', 'safe content'); | ||||||
872 | my $tag = $dom->new_tag('div', data => {mojo => 'rocks'}, 'safe content'); | ||||||
873 | my $tag = $dom->new_tag('div', id => 'foo', sub { 'unsafe content' }); | ||||||
874 | |||||||
875 | Construct a new L |
||||||
876 | attributes and content. The C attribute may contain a hash reference with | ||||||
877 | key/value pairs to generate attributes from. | ||||||
878 | |||||||
879 | # " " |
||||||
880 | $dom->new_tag('br'); | ||||||
881 | |||||||
882 | # "" | ||||||
883 | $dom->new_tag('div'); | ||||||
884 | |||||||
885 | # "" | ||||||
886 | $dom->new_tag('div', id => 'foo', hidden => undef); | ||||||
887 | |||||||
888 | # " test & 123 " |
||||||
889 | $dom->new_tag('div', 'test & 123'); | ||||||
890 | |||||||
891 | # " test & 123 " |
||||||
892 | $dom->new_tag('div', id => 'foo', 'test & 123'); | ||||||
893 | |||||||
894 | # " test & 123 "" |
||||||
895 | $dom->new_tag('div', data => {foo => 1, Bar => 'test'}, 'test & 123'); | ||||||
896 | |||||||
897 | # " test & 123 " |
||||||
898 | $dom->new_tag('div', id => 'foo', sub { 'test & 123' }); | ||||||
899 | |||||||
900 | # " HelloMojo! " |
||||||
901 | $dom->parse(' Hello ')->at('div') |
||||||
902 | ->append_content($dom->new_tag('b', 'Mojo!'))->root; | ||||||
903 | |||||||
904 | =head2 all_text | ||||||
905 | |||||||
906 | my $text = $dom->all_text; | ||||||
907 | |||||||
908 | Extract text content from all descendant nodes of this element. For HTML documents C |