add missing attribution
[catagits/DOM-Tiny.git] / lib / DOM / Tiny.pm
CommitLineData
a292be34 1package DOM::Tiny;
2
3use strict;
4use warnings;
5
d6512b50 6use overload
7 '@{}' => sub { shift->child_nodes },
8 '%{}' => sub { shift->attr },
9 bool => sub {1},
10 '""' => sub { shift->to_string },
11 fallback => 1;
12
13use Carp 'croak';
78ba4051 14use DOM::Tiny::_Collection;
9a5f1e3f 15use DOM::Tiny::_CSS;
16use DOM::Tiny::_HTML;
d6512b50 17use Scalar::Util qw(blessed weaken);
18
c256a8c4 19our $VERSION = '0.003';
a292be34 20
3793c28f 21sub new {
22 my $class = shift;
9a5f1e3f 23 my $self = bless \DOM::Tiny::_HTML->new, ref $class || $class;
3793c28f 24 return @_ ? $self->parse(@_) : $self;
25}
26
78ba4051 27sub TO_JSON { shift->_delegate('render') }
28
d6512b50 29sub all_text { shift->_all_text(1, @_) }
30
31sub ancestors { _select($_[0]->_collect($_[0]->_ancestors), $_[1]) }
32
33sub append { shift->_add(1, @_) }
34sub append_content { shift->_content(1, 0, @_) }
35
36sub at {
37 my $self = shift;
38 return undef unless my $result = $self->_css->select_one(@_);
39 return $self->_build($result, $self->xml);
40}
41
42sub attr {
43 my $self = shift;
44
45 # Hash
46 my $tree = $self->tree;
47 my $attrs = $tree->[0] ne 'tag' ? {} : $tree->[2];
48 return $attrs unless @_;
49
50 # Get
51 return $attrs->{$_[0]} unless @_ > 1 || ref $_[0];
52
53 # Set
54 my $values = ref $_[0] ? $_[0] : {@_};
55 @$attrs{keys %$values} = values %$values;
56
57 return $self;
58}
59
60sub child_nodes { $_[0]->_collect(_nodes($_[0]->tree)) }
61
62sub children { _select($_[0]->_collect(_nodes($_[0]->tree, 1)), $_[1]) }
63
64sub content {
65 my $self = shift;
66
67 my $type = $self->type;
68 if ($type eq 'root' || $type eq 'tag') {
69 return $self->_content(0, 1, @_) if @_;
9a5f1e3f 70 my $html = DOM::Tiny::_HTML->new(xml => $self->xml);
d6512b50 71 return join '', map { $html->tree($_)->render } _nodes($self->tree);
72 }
73
74 return $self->tree->[1] unless @_;
75 $self->tree->[1] = shift;
76 return $self;
77}
78
79sub descendant_nodes { $_[0]->_collect(_all(_nodes($_[0]->tree))) }
80
81sub find { $_[0]->_collect(@{$_[0]->_css->select($_[1])}) }
82
83sub following { _select($_[0]->_collect(@{$_[0]->_siblings(1)->[1]}), $_[1]) }
84sub following_nodes { $_[0]->_collect(@{$_[0]->_siblings->[1]}) }
85
86sub matches { shift->_css->matches(@_) }
87
88sub namespace {
89 my $self = shift;
90
91 return undef if (my $tree = $self->tree)->[0] ne 'tag';
92
93 # Extract namespace prefix and search parents
94 my $ns = $tree->[1] =~ /^(.*?):/ ? "xmlns:$1" : undef;
95 for my $node ($tree, $self->_ancestors) {
96
97 # Namespace for prefix
98 my $attrs = $node->[2];
99 if ($ns) { $_ eq $ns and return $attrs->{$_} for keys %$attrs }
100
101 # Namespace attribute
102 elsif (defined $attrs->{xmlns}) { return $attrs->{xmlns} }
103 }
104
105 return undef;
106}
107
d6512b50 108sub next { $_[0]->_maybe($_[0]->_siblings(1, 0)->[1]) }
109sub next_node { $_[0]->_maybe($_[0]->_siblings(0, 0)->[1]) }
110
111sub parent {
112 my $self = shift;
113 return undef if $self->tree->[0] eq 'root';
114 return $self->_build($self->_parent, $self->xml);
115}
116
117sub parse { shift->_delegate(parse => @_) }
118
119sub preceding { _select($_[0]->_collect(@{$_[0]->_siblings(1)->[0]}), $_[1]) }
120sub preceding_nodes { $_[0]->_collect(@{$_[0]->_siblings->[0]}) }
121
122sub prepend { shift->_add(0, @_) }
123sub prepend_content { shift->_content(0, 0, @_) }
124
125sub previous { $_[0]->_maybe($_[0]->_siblings(1, -1)->[0]) }
126sub previous_node { $_[0]->_maybe($_[0]->_siblings(0, -1)->[0]) }
127
128sub remove { shift->replace('') }
129
130sub replace {
131 my ($self, $new) = @_;
132 return $self->parse($new) if (my $tree = $self->tree)->[0] eq 'root';
133 return $self->_replace($self->_parent, $tree, _nodes($self->_parse($new)));
134}
135
136sub root {
137 my $self = shift;
138 return $self unless my $tree = $self->_ancestors(1);
139 return $self->_build($tree, $self->xml);
140}
141
142sub strip {
143 my $self = shift;
144 return $self if (my $tree = $self->tree)->[0] ne 'tag';
145 return $self->_replace($tree->[3], $tree, _nodes($tree));
146}
147
148sub tag {
149 my ($self, $tag) = @_;
150 return undef if (my $tree = $self->tree)->[0] ne 'tag';
151 return $tree->[1] unless $tag;
152 $tree->[1] = $tag;
153 return $self;
154}
155
1644a3ee 156sub tap { DOM::Tiny::_Collection::tap(@_) }
d6512b50 157
158sub text { shift->_all_text(0, @_) }
159
160sub to_string { shift->_delegate('render') }
161
162sub tree { shift->_delegate(tree => @_) }
163
164sub type { shift->tree->[0] }
165
166sub val {
167 my $self = shift;
168
169 # "option"
2d9f5165 170 return defined($self->{value}) ? $self->{value} : $self->text
171 if (my $tag = $self->tag) eq 'option';
d6512b50 172
173 # "textarea", "input" or "button"
174 return $tag eq 'textarea' ? $self->text : $self->{value} if $tag ne 'select';
175
176 # "select"
177 my $v = $self->find('option:checked')->map('val');
178 return exists $self->{multiple} ? $v->size ? $v->to_array : undef : $v->last;
179}
180
181sub wrap { shift->_wrap(0, @_) }
182sub wrap_content { shift->_wrap(1, @_) }
183
184sub xml { shift->_delegate(xml => @_) }
185
186sub _add {
187 my ($self, $offset, $new) = @_;
188
189 return $self if (my $tree = $self->tree)->[0] eq 'root';
190
191 my $parent = $self->_parent;
192 splice @$parent, _offset($parent, $tree) + $offset, 0,
193 _link($parent, _nodes($self->_parse($new)));
194
195 return $self;
196}
197
198sub _all {
199 map { $_->[0] eq 'tag' ? ($_, _all(_nodes($_))) : ($_) } @_;
200}
201
202sub _all_text {
203 my ($self, $recurse, $trim) = @_;
204
205 # Detect "pre" tag
206 my $tree = $self->tree;
207 $trim = 1 unless defined $trim;
208 map { $_->[1] eq 'pre' and $trim = 0 } $self->_ancestors, $tree
209 if $trim && $tree->[0] ne 'root';
210
211 return _text([_nodes($tree)], $recurse, $trim);
212}
213
214sub _ancestors {
215 my ($self, $root) = @_;
216
4c89d5f3 217 return () unless my $tree = $self->_parent;
d6512b50 218 my @ancestors;
219 do { push @ancestors, $tree }
220 while ($tree->[0] eq 'tag') && ($tree = $tree->[3]);
221 return $root ? $ancestors[-1] : @ancestors[0 .. $#ancestors - 1];
222}
223
224sub _build { shift->new->tree(shift)->xml(shift) }
225
226sub _collect {
227 my $self = shift;
228 my $xml = $self->xml;
78ba4051 229 return DOM::Tiny::_Collection->new(map { $self->_build($_, $xml) } @_);
d6512b50 230}
231
232sub _content {
233 my ($self, $start, $offset, $new) = @_;
234
235 my $tree = $self->tree;
236 unless ($tree->[0] eq 'root' || $tree->[0] eq 'tag') {
237 my $old = $self->content;
238 return $self->content($start ? "$old$new" : "$new$old");
239 }
240
241 $start = $start ? ($#$tree + 1) : _start($tree);
242 $offset = $offset ? $#$tree : 0;
243 splice @$tree, $start, $offset, _link($tree, _nodes($self->_parse($new)));
244
245 return $self;
246}
247
9a5f1e3f 248sub _css { DOM::Tiny::_CSS->new(tree => shift->tree) }
d6512b50 249
250sub _delegate {
251 my ($self, $method) = (shift, shift);
252 return $$self->$method unless @_;
253 $$self->$method(@_);
254 return $self;
255}
256
257sub _link {
258 my ($parent, @children) = @_;
259
260 # Link parent to children
261 for my $node (@children) {
262 my $offset = $node->[0] eq 'tag' ? 3 : 2;
263 $node->[$offset] = $parent;
264 weaken $node->[$offset];
265 }
266
267 return @children;
268}
269
270sub _maybe { $_[1] ? $_[0]->_build($_[1], $_[0]->xml) : undef }
271
272sub _nodes {
4c89d5f3 273 return () unless my $tree = shift;
d6512b50 274 my @nodes = @$tree[_start($tree) .. $#$tree];
275 return shift() ? grep { $_->[0] eq 'tag' } @nodes : @nodes;
276}
277
278sub _offset {
279 my ($parent, $child) = @_;
280 my $i = _start($parent);
281 $_ eq $child ? last : $i++ for @$parent[$i .. $#$parent];
282 return $i;
283}
284
285sub _parent { $_[0]->tree->[$_[0]->type eq 'tag' ? 3 : 2] }
286
9a5f1e3f 287sub _parse { DOM::Tiny::_HTML->new(xml => shift->xml)->parse(shift)->tree }
d6512b50 288
289sub _replace {
927f1351 290 my ($self, $parent, $child, @nodes) = @_;
291 splice @$parent, _offset($parent, $child), 1, _link($parent, @nodes);
d6512b50 292 return $self->parent;
293}
294
295sub _select {
296 my ($collection, $selector) = @_;
297 return $collection unless $selector;
298 return $collection->new(grep { $_->matches($selector) } @$collection);
299}
300
301sub _siblings {
302 my ($self, $tags, $i) = @_;
303
304 return [] unless my $parent = $self->parent;
305
306 my $tree = $self->tree;
307 my (@before, @after, $match);
308 for my $node (_nodes($parent->tree)) {
309 ++$match and next if !$match && $node eq $tree;
310 next if $tags && $node->[0] ne 'tag';
311 $match ? push @after, $node : push @before, $node;
312 }
313
314 return defined $i ? [$before[$i], $after[$i]] : [\@before, \@after];
315}
316
317sub _squish {
318 my $str = shift;
319 $str =~ s/^\s+//;
320 $str =~ s/\s+$//;
321 $str =~ s/\s+/ /g;
322 return $str;
323}
324
325sub _start { $_[0][0] eq 'root' ? 1 : 4 }
326
327sub _text {
328 my ($nodes, $recurse, $trim) = @_;
329
330 # Merge successive text nodes
331 my $i = 0;
332 while (my $next = $nodes->[$i + 1]) {
333 ++$i and next unless $nodes->[$i][0] eq 'text' && $next->[0] eq 'text';
334 splice @$nodes, $i, 2, ['text', $nodes->[$i][1] . $next->[1]];
335 }
336
337 my $text = '';
338 for my $node (@$nodes) {
339 my $type = $node->[0];
340
341 # Text
342 my $chunk = '';
343 if ($type eq 'text') { $chunk = $trim ? _squish $node->[1] : $node->[1] }
344
345 # CDATA or raw text
346 elsif ($type eq 'cdata' || $type eq 'raw') { $chunk = $node->[1] }
347
348 # Nested tag
349 elsif ($type eq 'tag' && $recurse) {
350 no warnings 'recursion';
351 $chunk = _text([_nodes($node)], 1, $node->[1] eq 'pre' ? 0 : $trim);
352 }
353
354 # Add leading whitespace if punctuation allows it
355 $chunk = " $chunk" if $text =~ /\S\z/ && $chunk =~ /^[^.!?,;:\s]+/;
356
357 # Trim whitespace blocks
358 $text .= $chunk if $chunk =~ /\S+/ || !$trim;
359 }
360
361 return $text;
362}
363
364sub _wrap {
365 my ($self, $content, $new) = @_;
366
367 $content = 1 if (my $tree = $self->tree)->[0] eq 'root';
368 $content = 0 if $tree->[0] ne 'root' && $tree->[0] ne 'tag';
369
370 # Find innermost tag
371 my $current;
372 my $first = $new = $self->_parse($new);
373 $current = $first while $first = (_nodes($first, 1))[0];
374 return $self unless $current;
375
376 # Wrap content
377 if ($content) {
378 push @$current, _link($current, _nodes($tree));
379 splice @$tree, _start($tree), $#$tree, _link($tree, _nodes($new));
380 return $self;
381 }
382
383 # Wrap element
384 $self->_replace($self->_parent, $tree, _nodes($new));
385 push @$current, _link($current, $tree);
386 return $self;
387}
388
a292be34 3891;
390
d6512b50 391=encoding utf8
392
a292be34 393=head1 NAME
394
d6512b50 395DOM::Tiny - Minimalistic HTML/XML DOM parser with CSS selectors
a292be34 396
397=head1 SYNOPSIS
398
d6512b50 399 use DOM::Tiny;
400
401 # Parse
402 my $dom = DOM::Tiny->new('<div><p id="a">Test</p><p id="b">123</p></div>');
403
404 # Find
405 say $dom->at('#b')->text;
406 say $dom->find('p')->map('text')->join("\n");
407 say $dom->find('[id]')->map(attr => 'id')->join("\n");
408
409 # Iterate
410 $dom->find('p[id]')->reverse->each(sub { say $_->{id} });
411
412 # Loop
413 for my $e ($dom->find('p[id]')->each) {
414 say $e->{id}, ':', $e->text;
415 }
416
417 # Modify
418 $dom->find('div p')->last->append('<p id="c">456</p>');
419 $dom->find(':not(p)')->map('strip');
420
421 # Render
422 say "$dom";
423
a292be34 424=head1 DESCRIPTION
425
9a5f1e3f 426L<DOM::Tiny> is a minimalistic and relaxed pure-perl HTML/XML DOM parser based
427on L<Mojo::DOM>. It supports the L<HTML Living Standard|https://html.spec.whatwg.org/>
428and L<Extensible Markup Language (XML) 1.0|http://www.w3.org/TR/xml/>, and
429matching based on L<CSS3 selectors|http://www.w3.org/TR/selectors/>. It will
430even try to interpret broken HTML and XML, so you should not use it for
5a70ee9d 431validation.
d6512b50 432
433=head1 NODES AND ELEMENTS
434
435When we parse an HTML/XML fragment, it gets turned into a tree of nodes.
436
437 <!DOCTYPE html>
438 <html>
439 <head><title>Hello</title></head>
440 <body>World!</body>
441 </html>
442
443There are currently eight different kinds of nodes, C<cdata>, C<comment>,
444C<doctype>, C<pi>, C<raw>, C<root>, C<tag> and C<text>. Elements are nodes of
445the type C<tag>.
446
447 root
448 |- doctype (html)
449 +- tag (html)
450 |- tag (head)
451 | +- tag (title)
452 | +- raw (Hello)
453 +- tag (body)
454 +- text (World!)
455
456While all node types are represented as L<DOM::Tiny> objects, some methods like
457L</"attr"> and L</"namespace"> only apply to elements.
458
459=head1 CASE-SENSITIVITY
460
461L<DOM::Tiny> defaults to HTML semantics, that means all tags and attribute
462names are lowercased and selectors need to be lowercase as well.
463
464 # HTML semantics
465 my $dom = DOM::Tiny->new('<P ID="greeting">Hi!</P>');
466 say $dom->at('p[id]')->text;
467
468If XML processing instructions are found, the parser will automatically switch
469into XML mode and everything becomes case-sensitive.
470
471 # XML semantics
472 my $dom = DOM::Tiny->new('<?xml version="1.0"?><P ID="greeting">Hi!</P>');
473 say $dom->at('P[ID]')->text;
474
475XML detection can also be disabled with the L</"xml"> method.
476
477 # Force XML semantics
478 my $dom = DOM::Tiny->new->xml(1)->parse('<P ID="greeting">Hi!</P>');
479 say $dom->at('P[ID]')->text;
480
481 # Force HTML semantics
482 my $dom = DOM::Tiny->new->xml(0)->parse('<P ID="greeting">Hi!</P>');
483 say $dom->at('p[id]')->text;
484
63873d67 485=head1 SELECTORS
486
487L<DOM::Tiny> uses a CSS selector engine based on L<Mojo::DOM::CSS>. All CSS
488selectors that make sense for a standalone parser are supported.
489
03eb5521 490=over
491
492=item Z<>*
63873d67 493
494Any element.
495
496 my $all = $dom->find('*');
497
03eb5521 498=item E
63873d67 499
500An element of type C<E>.
501
502 my $title = $dom->at('title');
503
03eb5521 504=item E[foo]
63873d67 505
506An C<E> element with a C<foo> attribute.
507
508 my $links = $dom->find('a[href]');
509
03eb5521 510=item E[foo="bar"]
63873d67 511
512An C<E> element whose C<foo> attribute value is exactly equal to C<bar>.
513
514 my $case_sensitive = $dom->find('input[type="hidden"]');
515 my $case_sensitive = $dom->find('input[type=hidden]');
516
03eb5521 517=item E[foo="bar" i]
63873d67 518
519An C<E> element whose C<foo> attribute value is exactly equal to any
520(ASCII-range) case-permutation of C<bar>. Note that this selector is
521EXPERIMENTAL and might change without warning!
522
523 my $case_insensitive = $dom->find('input[type="hidden" i]');
524 my $case_insensitive = $dom->find('input[type=hidden i]');
525 my $case_insensitive = $dom->find('input[class~="foo" i]');
526
527This selector is part of
528L<Selectors Level 4|http://dev.w3.org/csswg/selectors-4>, which is still a work
529in progress.
530
03eb5521 531=item E[foo~="bar"]
63873d67 532
533An C<E> element whose C<foo> attribute value is a list of whitespace-separated
534values, one of which is exactly equal to C<bar>.
535
536 my $foo = $dom->find('input[class~="foo"]');
537 my $foo = $dom->find('input[class~=foo]');
538
03eb5521 539=item E[foo^="bar"]
63873d67 540
541An C<E> element whose C<foo> attribute value begins exactly with the string
542C<bar>.
543
544 my $begins_with = $dom->find('input[name^="f"]');
545 my $begins_with = $dom->find('input[name^=f]');
546
03eb5521 547=item E[foo$="bar"]
63873d67 548
549An C<E> element whose C<foo> attribute value ends exactly with the string
550C<bar>.
551
552 my $ends_with = $dom->find('input[name$="o"]');
553 my $ends_with = $dom->find('input[name$=o]');
554
03eb5521 555=item E[foo*="bar"]
63873d67 556
557An C<E> element whose C<foo> attribute value contains the substring C<bar>.
558
559 my $contains = $dom->find('input[name*="fo"]');
560 my $contains = $dom->find('input[name*=fo]');
561
03eb5521 562=item E:root
63873d67 563
564An C<E> element, root of the document.
565
566 my $root = $dom->at(':root');
567
03eb5521 568=item E:nth-child(n)
63873d67 569
570An C<E> element, the C<n-th> child of its parent.
571
572 my $third = $dom->find('div:nth-child(3)');
573 my $odd = $dom->find('div:nth-child(odd)');
574 my $even = $dom->find('div:nth-child(even)');
575 my $top3 = $dom->find('div:nth-child(-n+3)');
576
03eb5521 577=item E:nth-last-child(n)
63873d67 578
579An C<E> element, the C<n-th> child of its parent, counting from the last one.
580
581 my $third = $dom->find('div:nth-last-child(3)');
582 my $odd = $dom->find('div:nth-last-child(odd)');
583 my $even = $dom->find('div:nth-last-child(even)');
584 my $bottom3 = $dom->find('div:nth-last-child(-n+3)');
585
03eb5521 586=item E:nth-of-type(n)
63873d67 587
588An C<E> element, the C<n-th> sibling of its type.
589
590 my $third = $dom->find('div:nth-of-type(3)');
591 my $odd = $dom->find('div:nth-of-type(odd)');
592 my $even = $dom->find('div:nth-of-type(even)');
593 my $top3 = $dom->find('div:nth-of-type(-n+3)');
594
03eb5521 595=item E:nth-last-of-type(n)
63873d67 596
597An C<E> element, the C<n-th> sibling of its type, counting from the last one.
598
599 my $third = $dom->find('div:nth-last-of-type(3)');
600 my $odd = $dom->find('div:nth-last-of-type(odd)');
601 my $even = $dom->find('div:nth-last-of-type(even)');
602 my $bottom3 = $dom->find('div:nth-last-of-type(-n+3)');
603
03eb5521 604=item E:first-child
63873d67 605
606An C<E> element, first child of its parent.
607
608 my $first = $dom->find('div p:first-child');
609
03eb5521 610=item E:last-child
63873d67 611
612An C<E> element, last child of its parent.
613
614 my $last = $dom->find('div p:last-child');
615
03eb5521 616=item E:first-of-type
63873d67 617
618An C<E> element, first sibling of its type.
619
620 my $first = $dom->find('div p:first-of-type');
621
03eb5521 622=item E:last-of-type
63873d67 623
624An C<E> element, last sibling of its type.
625
626 my $last = $dom->find('div p:last-of-type');
627
03eb5521 628=item E:only-child
63873d67 629
630An C<E> element, only child of its parent.
631
632 my $lonely = $dom->find('div p:only-child');
633
03eb5521 634=item E:only-of-type
63873d67 635
636An C<E> element, only sibling of its type.
637
638 my $lonely = $dom->find('div p:only-of-type');
639
03eb5521 640=item E:empty
63873d67 641
642An C<E> element that has no children (including text nodes).
643
644 my $empty = $dom->find(':empty');
645
03eb5521 646=item E:checked
63873d67 647
648A user interface element C<E> which is checked (for instance a radio-button or
649checkbox).
650
651 my $input = $dom->find(':checked');
652
03eb5521 653=item E.warning
63873d67 654
655An C<E> element whose class is "warning".
656
657 my $warning = $dom->find('div.warning');
658
03eb5521 659=item E#myid
63873d67 660
661An C<E> element with C<ID> equal to "myid".
662
663 my $foo = $dom->at('div#foo');
664
03eb5521 665=item E:not(s)
63873d67 666
667An C<E> element that does not match simple selector C<s>.
668
669 my $others = $dom->find('div p:not(:first-child)');
670
03eb5521 671=item E F
63873d67 672
673An C<F> element descendant of an C<E> element.
674
675 my $headlines = $dom->find('div h1');
676
03eb5521 677=item E E<gt> F
63873d67 678
679An C<F> element child of an C<E> element.
680
681 my $headlines = $dom->find('html > body > div > h1');
682
03eb5521 683=item E + F
63873d67 684
685An C<F> element immediately preceded by an C<E> element.
686
687 my $second = $dom->find('h1 + h2');
688
03eb5521 689=item E ~ F
63873d67 690
691An C<F> element preceded by an C<E> element.
692
693 my $second = $dom->find('h1 ~ h2');
694
03eb5521 695=item E, F, G
63873d67 696
697Elements of type C<E>, C<F> and C<G>.
698
699 my $headlines = $dom->find('h1, h2, h3');
700
03eb5521 701=item E[foo=bar][bar=baz]
63873d67 702
703An C<E> element whose attributes match all following attribute selectors.
704
705 my $links = $dom->find('a[foo^=b][foo$=ar]');
706
03eb5521 707=back
708
9a5f1e3f 709=head1 OPERATORS
710
711L<DOM::Tiny> overloads the following operators.
712
713=head2 array
714
715 my @nodes = @$dom;
716
717Alias for L</"child_nodes">.
718
719 # "<!-- Test -->"
720 $dom->parse('<!-- Test --><b>123</b>')->[0];
721
722=head2 bool
723
724 my $bool = !!$dom;
725
726Always true.
727
728=head2 hash
729
730 my %attrs = %$dom;
731
732Alias for L</"attr">.
733
734 # "test"
735 $dom->parse('<div id="test">Test</div>')->at('div')->{id};
736
737=head2 stringify
738
739 my $str = "$dom";
740
741Alias for L</"to_string">.
742
d6512b50 743=head1 METHODS
744
745L<DOM::Tiny> implements the following methods.
746
3793c28f 747=head2 new
748
749 my $dom = DOM::Tiny->new;
750 my $dom = DOM::Tiny->new('<foo bar="baz">I ♥ DOM::Tiny!</foo>');
751
752Construct a new scalar-based L<DOM::Tiny> object and L</"parse"> HTML/XML
753fragment if necessary.
754
d6512b50 755=head2 all_text
756
757 my $trimmed = $dom->all_text;
758 my $untrimmed = $dom->all_text(0);
759
760Extract text content from all descendant nodes of this element, smart
761whitespace trimming is enabled by default.
762
763 # "foo bar baz"
764 $dom->parse("<div>foo\n<p>bar</p>baz\n</div>")->at('div')->all_text;
765
766 # "foo\nbarbaz\n"
767 $dom->parse("<div>foo\n<p>bar</p>baz\n</div>")->at('div')->all_text(0);
768
769=head2 ancestors
770
771 my $collection = $dom->ancestors;
772 my $collection = $dom->ancestors('div ~ p');
773
774Find all ancestor elements of this node matching the CSS selector and return a
8563f527 775L<collection|/"COLLECTION METHODS"> containing these elements as L<DOM::Tiny>
9a5f1e3f 776objects. All selectors listed in L</"SELECTORS"> are supported.
d6512b50 777
778 # List tag names of ancestor elements
779 say $dom->ancestors->map('tag')->join("\n");
780
781=head2 append
782
783 $dom = $dom->append('<p>I ♥ DOM::Tiny!</p>');
784
785Append HTML/XML fragment to this node.
786
787 # "<div><h1>Test</h1><h2>123</h2></div>"
788 $dom->parse('<div><h1>Test</h1></div>')
789 ->at('h1')->append('<h2>123</h2>')->root;
790
791 # "<p>Test 123</p>"
792 $dom->parse('<p>Test</p>')->at('p')
793 ->child_nodes->first->append(' 123')->root;
794
795=head2 append_content
796
797 $dom = $dom->append_content('<p>I ♥ DOM::Tiny!</p>');
798
799Append HTML/XML fragment (for C<root> and C<tag> nodes) or raw content to this
800node's content.
801
802 # "<div><h1>Test123</h1></div>"
803 $dom->parse('<div><h1>Test</h1></div>')
804 ->at('h1')->append_content('123')->root;
805
806 # "<!-- Test 123 --><br>"
807 $dom->parse('<!-- Test --><br>')
808 ->child_nodes->first->append_content('123 ')->root;
809
810 # "<p>Test<i>123</i></p>"
811 $dom->parse('<p>Test</p>')->at('p')->append_content('<i>123</i>')->root;
812
813=head2 at
814
815 my $result = $dom->at('div ~ p');
816
817Find first descendant element of this element matching the CSS selector and
818return it as a L<DOM::Tiny> object or return C<undef> if none could be found.
9a5f1e3f 819All selectors listed in L</"SELECTORS"> are supported.
d6512b50 820
821 # Find first element with "svg" namespace definition
822 my $namespace = $dom->at('[xmlns\:svg]')->{'xmlns:svg'};
823
824=head2 attr
825
826 my $hash = $dom->attr;
827 my $foo = $dom->attr('foo');
828 $dom = $dom->attr({foo => 'bar'});
829 $dom = $dom->attr(foo => 'bar');
830
831This element's attributes.
832
833 # Remove an attribute
834 delete $dom->attr->{id};
835
836 # Attribute without value
837 $dom->attr(selected => undef);
838
839 # List id attributes
840 say $dom->find('*')->map(attr => 'id')->compact->join("\n");
841
842=head2 child_nodes
843
844 my $collection = $dom->child_nodes;
845
8563f527 846Return a L<collection|/"COLLECTION METHODS"> containing all child nodes of this
d6512b50 847element as L<DOM::Tiny> objects.
848
849 # "<p><b>123</b></p>"
850 $dom->parse('<p>Test<b>123</b></p>')->at('p')->child_nodes->first->remove;
851
852 # "<!DOCTYPE html>"
853 $dom->parse('<!DOCTYPE html><b>123</b>')->child_nodes->first;
854
855 # " Test "
856 $dom->parse('<b>123</b><!-- Test -->')->child_nodes->last->content;
857
858=head2 children
859
860 my $collection = $dom->children;
861 my $collection = $dom->children('div ~ p');
862
863Find all child elements of this element matching the CSS selector and return a
8563f527 864L<collection|/"COLLECTION METHODS"> containing these elements as L<DOM::Tiny>
9a5f1e3f 865objects. All selectors listed in L</"SELECTORS"> are supported.
d6512b50 866
867 # Show tag name of random child element
868 say $dom->children->shuffle->first->tag;
869
870=head2 content
871
872 my $str = $dom->content;
873 $dom = $dom->content('<p>I ♥ DOM::Tiny!</p>');
874
875Return this node's content or replace it with HTML/XML fragment (for C<root>
876and C<tag> nodes) or raw content.
877
878 # "<b>Test</b>"
879 $dom->parse('<div><b>Test</b></div>')->at('div')->content;
880
881 # "<div><h1>123</h1></div>"
882 $dom->parse('<div><h1>Test</h1></div>')->at('h1')->content('123')->root;
883
884 # "<p><i>123</i></p>"
885 $dom->parse('<p>Test</p>')->at('p')->content('<i>123</i>')->root;
886
887 # "<div><h1></h1></div>"
888 $dom->parse('<div><h1>Test</h1></div>')->at('h1')->content('')->root;
889
890 # " Test "
891 $dom->parse('<!-- Test --><br>')->child_nodes->first->content;
892
893 # "<div><!-- 123 -->456</div>"
894 $dom->parse('<div><!-- Test -->456</div>')
895 ->at('div')->child_nodes->first->content(' 123 ')->root;
896
897=head2 descendant_nodes
898
899 my $collection = $dom->descendant_nodes;
900
8563f527 901Return a L<collection|/"COLLECTION METHODS"> containing all descendant nodes of
d6512b50 902this element as L<DOM::Tiny> objects.
903
904 # "<p><b>123</b></p>"
905 $dom->parse('<p><!-- Test --><b>123<!-- 456 --></b></p>')
906 ->descendant_nodes->grep(sub { $_->type eq 'comment' })
907 ->map('remove')->first;
908
909 # "<p><b>test</b>test</p>"
910 $dom->parse('<p><b>123</b>456</p>')
911 ->at('p')->descendant_nodes->grep(sub { $_->type eq 'text' })
912 ->map(content => 'test')->first->root;
913
914=head2 find
915
916 my $collection = $dom->find('div ~ p');
917
918Find all descendant elements of this element matching the CSS selector and
8563f527 919return a L<collection|/"COLLECTION METHODS"> containing these elements as
9a5f1e3f 920L<DOM::Tiny> objects. All selectors listed in L</"SELECTORS"> are supported.
d6512b50 921
922 # Find a specific element and extract information
923 my $id = $dom->find('div')->[23]{id};
924
925 # Extract information from multiple elements
926 my @headers = $dom->find('h1, h2, h3')->map('text')->each;
927
928 # Count all the different tags
929 my $hash = $dom->find('*')->reduce(sub { $a->{$b->tag}++; $a }, {});
930
931 # Find elements with a class that contains dots
932 my @divs = $dom->find('div.foo\.bar')->each;
933
934=head2 following
935
936 my $collection = $dom->following;
937 my $collection = $dom->following('div ~ p');
938
939Find all sibling elements after this node matching the CSS selector and return
8563f527 940a L<collection|/"COLLECTION METHODS"> containing these elements as L<DOM::Tiny>
9a5f1e3f 941objects. All selectors listen in L</"SELECTORS"> are supported.
d6512b50 942
943 # List tags of sibling elements after this node
944 say $dom->following->map('tag')->join("\n");
945
946=head2 following_nodes
947
948 my $collection = $dom->following_nodes;
949
8563f527 950Return a L<collection|/"COLLECTION METHODS"> containing all sibling nodes after
d6512b50 951this node as L<DOM::Tiny> objects.
952
953 # "C"
954 $dom->parse('<p>A</p><!-- B -->C')->at('p')->following_nodes->last->content;
955
956=head2 matches
957
958 my $bool = $dom->matches('div ~ p');
959
9a5f1e3f 960Check if this element matches the CSS selector. All selectors listed in
961L</"SELECTORS"> are supported.
d6512b50 962
963 # True
964 $dom->parse('<p class="a">A</p>')->at('p')->matches('.a');
965 $dom->parse('<p class="a">A</p>')->at('p')->matches('p[class]');
966
967 # False
968 $dom->parse('<p class="a">A</p>')->at('p')->matches('.b');
969 $dom->parse('<p class="a">A</p>')->at('p')->matches('p[id]');
970
971=head2 namespace
972
973 my $namespace = $dom->namespace;
974
975Find this element's namespace or return C<undef> if none could be found.
976
977 # Find namespace for an element with namespace prefix
978 my $namespace = $dom->at('svg > svg\:circle')->namespace;
979
980 # Find namespace for an element that may or may not have a namespace prefix
981 my $namespace = $dom->at('svg > circle')->namespace;
982
d6512b50 983=head2 next
984
985 my $sibling = $dom->next;
986
987Return L<DOM::Tiny> object for next sibling element or C<undef> if there are no
988more siblings.
989
990 # "<h2>123</h2>"
991 $dom->parse('<div><h1>Test</h1><h2>123</h2></div>')->at('h1')->next;
992
993=head2 next_node
994
995 my $sibling = $dom->next_node;
996
997Return L<DOM::Tiny> object for next sibling node or C<undef> if there are no
998more siblings.
999
1000 # "456"
1001 $dom->parse('<p><b>123</b><!-- Test -->456</p>')
1002 ->at('b')->next_node->next_node;
1003
1004 # " Test "
1005 $dom->parse('<p><b>123</b><!-- Test -->456</p>')
1006 ->at('b')->next_node->content;
1007
1008=head2 parent
1009
1010 my $parent = $dom->parent;
1011
1012Return L<DOM::Tiny> object for parent of this node or C<undef> if this node has
1013no parent.
1014
1015=head2 parse
1016
1017 $dom = $dom->parse('<foo bar="baz">I ♥ DOM::Tiny!</foo>');
1018
9a5f1e3f 1019Parse HTML/XML fragment.
d6512b50 1020
1021 # Parse XML
1022 my $dom = DOM::Tiny->new->xml(1)->parse($xml);
1023
1024=head2 preceding
1025
1026 my $collection = $dom->preceding;
1027 my $collection = $dom->preceding('div ~ p');
1028
1029Find all sibling elements before this node matching the CSS selector and return
8563f527 1030a L<collection|/"COLLECTION METHODS"> containing these elements as L<DOM::Tiny>
9a5f1e3f 1031objects. All selectors listed in L</"SELECTORS"> are supported.
d6512b50 1032
1033 # List tags of sibling elements before this node
1034 say $dom->preceding->map('tag')->join("\n");
1035
1036=head2 preceding_nodes
1037
1038 my $collection = $dom->preceding_nodes;
1039
8563f527 1040Return a L<collection|/"COLLECTION METHODS"> containing all sibling nodes
1041before this node as L<DOM::Tiny> objects.
d6512b50 1042
1043 # "A"
1044 $dom->parse('A<!-- B --><p>C</p>')->at('p')->preceding_nodes->first->content;
1045
1046=head2 prepend
1047
1048 $dom = $dom->prepend('<p>I ♥ DOM::Tiny!</p>');
1049
1050Prepend HTML/XML fragment to this node.
1051
1052 # "<div><h1>Test</h1><h2>123</h2></div>"
1053 $dom->parse('<div><h2>123</h2></div>')
1054 ->at('h2')->prepend('<h1>Test</h1>')->root;
1055
1056 # "<p>Test 123</p>"
1057 $dom->parse('<p>123</p>')
1058 ->at('p')->child_nodes->first->prepend('Test ')->root;
1059
1060=head2 prepend_content
1061
1062 $dom = $dom->prepend_content('<p>I ♥ DOM::Tiny!</p>');
1063
1064Prepend HTML/XML fragment (for C<root> and C<tag> nodes) or raw content to this
1065node's content.
1066
1067 # "<div><h2>Test123</h2></div>"
1068 $dom->parse('<div><h2>123</h2></div>')
1069 ->at('h2')->prepend_content('Test')->root;
1070
1071 # "<!-- Test 123 --><br>"
1072 $dom->parse('<!-- 123 --><br>')
1073 ->child_nodes->first->prepend_content(' Test')->root;
1074
1075 # "<p><i>123</i>Test</p>"
1076 $dom->parse('<p>Test</p>')->at('p')->prepend_content('<i>123</i>')->root;
1077
1078=head2 previous
1079
1080 my $sibling = $dom->previous;
1081
1082Return L<DOM::Tiny> object for previous sibling element or C<undef> if there
1083are no more siblings.
1084
1085 # "<h1>Test</h1>"
1086 $dom->parse('<div><h1>Test</h1><h2>123</h2></div>')->at('h2')->previous;
1087
1088=head2 previous_node
1089
1090 my $sibling = $dom->previous_node;
1091
1092Return L<DOM::Tiny> object for previous sibling node or C<undef> if there are
1093no more siblings.
1094
1095 # "123"
1096 $dom->parse('<p>123<!-- Test --><b>456</b></p>')
1097 ->at('b')->previous_node->previous_node;
1098
1099 # " Test "
1100 $dom->parse('<p>123<!-- Test --><b>456</b></p>')
1101 ->at('b')->previous_node->content;
1102
1103=head2 remove
1104
1105 my $parent = $dom->remove;
1106
1107Remove this node and return L</"root"> (for C<root> nodes) or L</"parent">.
1108
1109 # "<div></div>"
1110 $dom->parse('<div><h1>Test</h1></div>')->at('h1')->remove;
1111
1112 # "<p><b>456</b></p>"
1113 $dom->parse('<p>123<b>456</b></p>')
1114 ->at('p')->child_nodes->first->remove->root;
1115
1116=head2 replace
1117
1118 my $parent = $dom->replace('<div>I ♥ DOM::Tiny!</div>');
1119
1120Replace this node with HTML/XML fragment and return L</"root"> (for C<root>
1121nodes) or L</"parent">.
1122
1123 # "<div><h2>123</h2></div>"
1124 $dom->parse('<div><h1>Test</h1></div>')->at('h1')->replace('<h2>123</h2>');
1125
1126 # "<p><b>123</b></p>"
1127 $dom->parse('<p>Test</p>')
1128 ->at('p')->child_nodes->[0]->replace('<b>123</b>')->root;
1129
1130=head2 root
1131
1132 my $root = $dom->root;
1133
1134Return L<DOM::Tiny> object for C<root> node.
1135
1136=head2 strip
1137
1138 my $parent = $dom->strip;
1139
1140Remove this element while preserving its content and return L</"parent">.
1141
1142 # "<div>Test</div>"
1143 $dom->parse('<div><h1>Test</h1></div>')->at('h1')->strip;
1144
1145=head2 tag
1146
1147 my $tag = $dom->tag;
1148 $dom = $dom->tag('div');
1149
1150This element's tag name.
1151
1152 # List tag names of child elements
1153 say $dom->children->map('tag')->join("\n");
1154
1155=head2 tap
1156
1157 $dom = $dom->tap(sub {...});
1158
e99ef07d 1159Equivalent to L<Mojo::Base/"tap">.
d6512b50 1160
1161=head2 text
1162
1163 my $trimmed = $dom->text;
1164 my $untrimmed = $dom->text(0);
1165
1166Extract text content from this element only (not including child elements),
1167smart whitespace trimming is enabled by default.
1168
1169 # "foo baz"
1170 $dom->parse("<div>foo\n<p>bar</p>baz\n</div>")->at('div')->text;
1171
1172 # "foo\nbaz\n"
1173 $dom->parse("<div>foo\n<p>bar</p>baz\n</div>")->at('div')->text(0);
1174
1175=head2 to_string
1176
1177 my $str = $dom->to_string;
1178
1179Render this node and its content to HTML/XML.
1180
1181 # "<b>Test</b>"
1182 $dom->parse('<div><b>Test</b></div>')->at('div b')->to_string;
1183
1184=head2 tree
1185
1186 my $tree = $dom->tree;
1187 $dom = $dom->tree(['root']);
1188
1189Document Object Model. Note that this structure should only be used very
1190carefully since it is very dynamic.
1191
1192=head2 type
1193
1194 my $type = $dom->type;
1195
1196This node's type, usually C<cdata>, C<comment>, C<doctype>, C<pi>, C<raw>,
1197C<root>, C<tag> or C<text>.
1198
1199 # "cdata"
1200 $dom->parse('<![CDATA[Test]]>')->child_nodes->first->type;
1201
1202 # "comment"
1203 $dom->parse('<!-- Test -->')->child_nodes->first->type;
1204
1205 # "doctype"
1206 $dom->parse('<!DOCTYPE html>')->child_nodes->first->type;
1207
1208 # "pi"
1209 $dom->parse('<?xml version="1.0"?>')->child_nodes->first->type;
1210
1211 # "raw"
1212 $dom->parse('<title>Test</title>')->at('title')->child_nodes->first->type;
1213
1214 # "root"
1215 $dom->parse('<p>Test</p>')->type;
1216
1217 # "tag"
1218 $dom->parse('<p>Test</p>')->at('p')->type;
1219
1220 # "text"
1221 $dom->parse('<p>Test</p>')->at('p')->child_nodes->first->type;
1222
1223=head2 val
1224
1225 my $value = $dom->val;
1226
1227Extract value from form element (such as C<button>, C<input>, C<option>,
1228C<select> and C<textarea>) or return C<undef> if this element has no value. In
1229the case of C<select> with C<multiple> attribute, find C<option> elements with
1230C<selected> attribute and return an array reference with all values or C<undef>
1231if none could be found.
1232
1233 # "a"
1234 $dom->parse('<input name="test" value="a">')->at('input')->val;
1235
1236 # "b"
1237 $dom->parse('<textarea>b</textarea>')->at('textarea')->val;
1238
1239 # "c"
1240 $dom->parse('<option value="c">Test</option>')->at('option')->val;
1241
1242 # "d"
1243 $dom->parse('<select><option selected>d</option></select>')
1244 ->at('select')->val;
1245
1246 # "e"
1247 $dom->parse('<select multiple><option selected>e</option></select>')
1248 ->at('select')->val->[0];
1249
1250=head2 wrap
1251
1252 $dom = $dom->wrap('<div></div>');
1253
1254Wrap HTML/XML fragment around this node, placing it as the last child of the
1255first innermost element.
1256
1257 # "<p>123<b>Test</b></p>"
1258 $dom->parse('<b>Test</b>')->at('b')->wrap('<p>123</p>')->root;
1259
1260 # "<div><p><b>Test</b></p>123</div>"
1261 $dom->parse('<b>Test</b>')->at('b')->wrap('<div><p></p>123</div>')->root;
1262
1263 # "<p><b>Test</b></p><p>123</p>"
1264 $dom->parse('<b>Test</b>')->at('b')->wrap('<p></p><p>123</p>')->root;
1265
1266 # "<p><b>Test</b></p>"
1267 $dom->parse('<p>Test</p>')->at('p')->child_nodes->first->wrap('<b>')->root;
1268
1269=head2 wrap_content
1270
1271 $dom = $dom->wrap_content('<div></div>');
1272
1273Wrap HTML/XML fragment around this node's content, placing it as the last
1274children of the first innermost element.
1275
1276 # "<p><b>123Test</b></p>"
1277 $dom->parse('<p>Test<p>')->at('p')->wrap_content('<b>123</b>')->root;
1278
1279 # "<p><b>Test</b></p><p>123</p>"
1280 $dom->parse('<b>Test</b>')->wrap_content('<p></p><p>123</p>');
1281
1282=head2 xml
1283
1284 my $bool = $dom->xml;
1285 $dom = $dom->xml($bool);
1286
1287Disable HTML semantics in parser and activate case-sensitivity, defaults to
1288auto detection based on processing instructions.
1289
78ba4051 1290=head1 COLLECTION METHODS
1291
9a5f1e3f 1292Some L<DOM::Tiny> methods return an array-based collection object based on
1293L<Mojo::Collection>, which can either be accessed directly as an array
1294reference, or with the following methods.
78ba4051 1295
1296 # Chain methods
1297 $collection->map(sub { ucfirst })->shuffle->each(sub {
1298 my ($word, $num) = @_;
1299 say "$num: $word";
1300 });
1301
1302 # Access array directly to manipulate collection
1303 $collection->[23] += 100;
1304 say for @$collection;
1305
1306=head2 compact
1307
1308 my $new = $collection->compact;
1309
1310Create a new collection with all elements that are defined and not an empty
1311string.
1312
1313 # $collection contains (0, 1, undef, 2, '', 3)
1314 $collection->compact->join(', '); # "0, 1, 2, 3"
1315
1316=head2 each
1317
1318 my @elements = $collection->each;
1319 $collection = $collection->each(sub {...});
1320
1321Evaluate callback for each element in collection or return all elements as a
1322list if none has been provided. The element will be the first argument passed
1323to the callback and is also available as C<$_>.
1324
1325 # Make a numbered list
1326 $collection->each(sub {
1327 my ($e, $num) = @_;
1328 say "$num: $e";
1329 });
1330
1331=head2 first
1332
1333 my $first = $collection->first;
1334 my $first = $collection->first(qr/foo/);
1335 my $first = $collection->first(sub {...});
1336 my $first = $collection->first($method);
1337 my $first = $collection->first($method, @args);
1338
1339Evaluate regular expression/callback for, or call method on, each element in
1340collection and return the first one that matched the regular expression, or for
1341which the callback/method returned true. The element will be the first argument
1342passed to the callback and is also available as C<$_>.
1343
1344 # Longer version
1345 my $first = $collection->first(sub { $_->$method(@args) });
1346
c7bc4d31 1347 # Find first value that contains the word "tiny"
1348 my $interesting = $collection->first(qr/tiny/i);
78ba4051 1349
1350 # Find first value that is greater than 5
1351 my $greater = $collection->first(sub { $_ > 5 });
1352
1353=head2 flatten
1354
1355 my $new = $collection->flatten;
1356
1357Flatten nested collections/arrays recursively and create a new collection with
1358all elements.
1359
1360 # $collection contains (1, [2, [3, 4], 5, [6]], 7)
1361 $collection->flatten->join(', '); # "1, 2, 3, 4, 5, 6, 7"
1362
1363=head2 grep
1364
1365 my $new = $collection->grep(qr/foo/);
1366 my $new = $collection->grep(sub {...});
1367 my $new = $collection->grep($method);
1368 my $new = $collection->grep($method, @args);
1369
1370Evaluate regular expression/callback for, or call method on, each element in
1371collection and create a new collection with all elements that matched the
1372regular expression, or for which the callback/method returned true. The element
1373will be the first argument passed to the callback and is also available as
1374C<$_>.
1375
1376 # Longer version
1377 my $new = $collection->grep(sub { $_->$method(@args) });
1378
c7bc4d31 1379 # Find all values that contain the word "tiny"
1380 my $interesting = $collection->grep(qr/tiny/i);
78ba4051 1381
1382 # Find all values that are greater than 5
1383 my $greater = $collection->grep(sub { $_ > 5 });
1384
1385=head2 join
1386
1387 my $stream = $collection->join;
1388 my $stream = $collection->join("\n");
1389
1390Turn collection into string.
1391
1392 # Join all values with commas
1393 $collection->join(', ');
1394
1395=head2 last
1396
1397 my $last = $collection->last;
1398
1399Return the last element in collection.
1400
1401=head2 map
1402
1403 my $new = $collection->map(sub {...});
1404 my $new = $collection->map($method);
1405 my $new = $collection->map($method, @args);
1406
1407Evaluate callback for, or call method on, each element in collection and create
1408a new collection from the results. The element will be the first argument
1409passed to the callback and is also available as C<$_>.
1410
1411 # Longer version
1412 my $new = $collection->map(sub { $_->$method(@args) });
1413
c7bc4d31 1414 # Append the word "tiny" to all values
1415 my $domified = $collection->map(sub { $_ . 'tiny' });
78ba4051 1416
1417=head2 reduce
1418
1419 my $result = $collection->reduce(sub {...});
1420 my $result = $collection->reduce(sub {...}, $initial);
1421
1422Reduce elements in collection with callback, the first element will be used as
1423initial value if none has been provided.
1424
1425 # Calculate the sum of all values
1426 my $sum = $collection->reduce(sub { $a + $b });
1427
1428 # Count how often each value occurs in collection
1429 my $hash = $collection->reduce(sub { $a->{$b}++; $a }, {});
1430
1431=head2 reverse
1432
1433 my $new = $collection->reverse;
1434
1435Create a new collection with all elements in reverse order.
1436
1437=head2 slice
1438
1439 my $new = $collection->slice(4 .. 7);
1440
1441Create a new collection with all selected elements.
1442
1443 # $collection contains ('A', 'B', 'C', 'D', 'E')
1444 $collection->slice(1, 2, 4)->join(' '); # "B C E"
1445
1446=head2 shuffle
1447
1448 my $new = $collection->shuffle;
1449
1450Create a new collection with all elements in random order.
1451
1452=head2 size
1453
1454 my $size = $collection->size;
1455
1456Number of elements in collection.
1457
1458=head2 sort
1459
1460 my $new = $collection->sort;
1461 my $new = $collection->sort(sub {...});
1462
1463Sort elements based on return value of callback and create a new collection
1464from the results.
1465
1466 # Sort values case-insensitive
1467 my $case_insensitive = $collection->sort(sub { uc($a) cmp uc($b) });
1468
1469=head2 tap
1470
1471 $collection = $collection->tap(sub {...});
1472
1473Equivalent to L<Mojo::Base/"tap">.
1474
1475=head2 to_array
1476
1477 my $array = $collection->to_array;
1478
1479Turn collection into array reference.
1480
1481=head2 uniq
1482
1483 my $new = $collection->uniq;
1484 my $new = $collection->uniq(sub {...});
1485 my $new = $collection->uniq($method);
1486 my $new = $collection->uniq($method, @args);
1487
1488Create a new collection without duplicate elements, using the string
1489representation of either the elements or the return value of the
1490callback/method.
1491
1492 # Longer version
1493 my $new = $collection->uniq(sub { $_->$method(@args) });
1494
1495 # $collection contains ('foo', 'bar', 'bar', 'baz')
1496 $collection->uniq->join(' '); # "foo bar baz"
1497
1498 # $collection contains ([1, 2], [2, 1], [3, 2])
1499 $collection->uniq(sub{ $_->[1] })->to_array; # "[[1, 2], [2, 1]]"
1500
a292be34 1501=head1 BUGS
1502
1503Report any issues on the public bugtracker.
1504
1505=head1 AUTHOR
1506
1507Dan Book <dbook@cpan.org>
1508
7218d584 1509Code and tests adapted from L<Mojo::DOM> by the L<Mojolicious> team.
1510
2d9f5165 1511=head1 CONTRIBUTORS
1512
1513=over
1514
1515=item Matt S Trout (mst)
1516
1517=back
1518
a292be34 1519=head1 COPYRIGHT AND LICENSE
1520
1521This software is Copyright (c) 2015 by Dan Book.
1522
1523This is free software, licensed under:
1524
1525 The Artistic License 2.0 (GPL Compatible)
1526
1527=head1 SEE ALSO
1528
31877452 1529L<Mojo::DOM>, L<HTML::TreeBuilder>, L<XML::LibXML>, L<XML::Twig>, L<XML::Smart>
9a5f1e3f 1530
1531=for Pod::Coverage TO_JSON
1532
1533=cut