ported fixes from Mojolicious
[catagits/DOM-Tiny.git] / lib / DOM / Tiny.pm
index abb3cc0..0eecb5e 100644 (file)
@@ -11,19 +11,21 @@ use overload
   fallback => 1;
 
 use Carp 'croak';
-use DOM::Tiny::Collection;
-use DOM::Tiny::CSS;
-use DOM::Tiny::HTML;
+use DOM::Tiny::_Collection;
+use DOM::Tiny::_CSS;
+use DOM::Tiny::_HTML;
 use Scalar::Util qw(blessed weaken);
 
-our $VERSION = '0.001';
+our $VERSION = '0.003';
 
 sub new {
   my $class = shift;
-  my $self = bless \DOM::Tiny::HTML->new, ref $class || $class;
+  my $self = bless \DOM::Tiny::_HTML->new, ref $class || $class;
   return @_ ? $self->parse(@_) : $self;
 }
 
+sub TO_JSON { shift->_delegate('render') }
+
 sub all_text { shift->_all_text(1, @_) }
 
 sub ancestors { _select($_[0]->_collect($_[0]->_ancestors), $_[1]) }
@@ -65,7 +67,7 @@ sub content {
   my $type = $self->type;
   if ($type eq 'root' || $type eq 'tag') {
     return $self->_content(0, 1, @_) if @_;
-    my $html = DOM::Tiny::HTML->new(xml => $self->xml);
+    my $html = DOM::Tiny::_HTML->new(xml => $self->xml);
     return join '', map { $html->tree($_)->render } _nodes($self->tree);
   }
 
@@ -151,7 +153,7 @@ sub tag {
   return $self;
 }
 
-sub tap { shift->DOM::Tiny::Collection::tap(@_) }
+sub tap { DOM::Tiny::_Collection::tap(@_) }
 
 sub text { shift->_all_text(0, @_) }
 
@@ -165,7 +167,13 @@ sub val {
   my $self = shift;
 
   # "option"
-  return $self->{value} // $self->text if (my $tag = $self->tag) eq 'option';
+  return defined($self->{value}) ? $self->{value} : $self->text
+    if (my $tag = $self->tag) eq 'option';
+
+  # "input" ("type=checkbox" and "type=radio")
+  my $type = $self->{type} || '';
+  return defined $self->{value} ? $self->{value} : 'on'
+    if $tag eq 'input' && ($type eq 'radio' || $type eq 'checkbox');
 
   # "textarea", "input" or "button"
   return $tag eq 'textarea' ? $self->text : $self->{value} if $tag ne 'select';
@@ -211,7 +219,7 @@ sub _all_text {
 sub _ancestors {
   my ($self, $root) = @_;
 
-  return unless my $tree = $self->_parent;
+  return () unless my $tree = $self->_parent;
   my @ancestors;
   do { push @ancestors, $tree }
     while ($tree->[0] eq 'tag') && ($tree = $tree->[3]);
@@ -223,7 +231,7 @@ sub _build { shift->new->tree(shift)->xml(shift) }
 sub _collect {
   my $self = shift;
   my $xml  = $self->xml;
-  return DOM::Tiny::Collection->new(map { $self->_build($_, $xml) } @_);
+  return DOM::Tiny::_Collection->new(map { $self->_build($_, $xml) } @_);
 }
 
 sub _content {
@@ -232,7 +240,7 @@ sub _content {
   my $tree = $self->tree;
   unless ($tree->[0] eq 'root' || $tree->[0] eq 'tag') {
     my $old = $self->content;
-    return $self->content($start ? "$old$new" : "$new$old");
+    return $self->content($start ? $old . $new : $new . $old);
   }
 
   $start  = $start  ? ($#$tree + 1) : _start($tree);
@@ -242,7 +250,7 @@ sub _content {
   return $self;
 }
 
-sub _css { DOM::Tiny::CSS->new(tree => shift->tree) }
+sub _css { DOM::Tiny::_CSS->new(tree => shift->tree) }
 
 sub _delegate {
   my ($self, $method) = (shift, shift);
@@ -267,7 +275,7 @@ sub _link {
 sub _maybe { $_[1] ? $_[0]->_build($_[1], $_[0]->xml) : undef }
 
 sub _nodes {
-  return unless my $tree = shift;
+  return () unless my $tree = shift;
   my @nodes = @$tree[_start($tree) .. $#$tree];
   return shift() ? grep { $_->[0] eq 'tag' } @nodes : @nodes;
 }
@@ -281,7 +289,7 @@ sub _offset {
 
 sub _parent { $_[0]->tree->[$_[0]->type eq 'tag' ? 3 : 2] }
 
-sub _parse { DOM::Tiny::HTML->new(xml => shift->xml)->parse(shift)->tree }
+sub _parse { DOM::Tiny::_HTML->new(xml => shift->xml)->parse(shift)->tree }
 
 sub _replace {
   my ($self, $parent, $child, @nodes) = @_;
@@ -361,8 +369,8 @@ sub _text {
 sub _wrap {
   my ($self, $content, $new) = @_;
 
-  $content = 1 if (my $tree = $self->tree)->[0] eq 'root';
-  $content = 0 if $tree->[0] ne 'root' && $tree->[0] ne 'tag';
+  return $self if (my $tree = $self->tree)->[0] eq 'root' && !$content;
+  return $self if $tree->[0] ne 'root' && $tree->[0] ne 'tag' && $content;
 
   # Find innermost tag
   my $current;
@@ -420,9 +428,12 @@ DOM::Tiny - Minimalistic HTML/XML DOM parser with CSS selectors
 
 =head1 DESCRIPTION
 
-L<DOM::Tiny> is a minimalistic and relaxed HTML/XML DOM parser with CSS
-selector support based on L<Mojo::DOM>. It will even try to interpret broken
-HTML and XML, so you should not use it for validation.
+L<DOM::Tiny> is a minimalistic and relaxed pure-perl HTML/XML DOM parser based
+on L<Mojo::DOM>. It supports the L<HTML Living Standard|https://html.spec.whatwg.org/>
+and L<Extensible Markup Language (XML) 1.0|http://www.w3.org/TR/xml/>, and
+matching based on L<CSS3 selectors|http://www.w3.org/TR/selectors/>. It will
+even try to interpret broken HTML and XML, so you should not use it for
+validation.
 
 =head1 NODES AND ELEMENTS
 
@@ -459,8 +470,8 @@ names are lowercased and selectors need to be lowercase as well.
   my $dom = DOM::Tiny->new('<P ID="greeting">Hi!</P>');
   say $dom->at('p[id]')->text;
 
-If XML processing instructions are found, the parser will automatically switch
-into XML mode and everything becomes case-sensitive.
+If an XML declaration is found, the parser will automatically switch into XML
+mode and everything becomes case-sensitive.
 
   # XML semantics
   my $dom = DOM::Tiny->new('<?xml version="1.0"?><P ID="greeting">Hi!</P>');
@@ -476,6 +487,264 @@ XML detection can also be disabled with the L</"xml"> method.
   my $dom = DOM::Tiny->new->xml(0)->parse('<P ID="greeting">Hi!</P>');
   say $dom->at('p[id]')->text;
 
+=head1 SELECTORS
+
+L<DOM::Tiny> uses a CSS selector engine based on L<Mojo::DOM::CSS>. All CSS
+selectors that make sense for a standalone parser are supported.
+
+=over
+
+=item Z<>*
+
+Any element.
+
+  my $all = $dom->find('*');
+
+=item E
+
+An element of type C<E>.
+
+  my $title = $dom->at('title');
+
+=item E[foo]
+
+An C<E> element with a C<foo> attribute.
+
+  my $links = $dom->find('a[href]');
+
+=item E[foo="bar"]
+
+An C<E> element whose C<foo> attribute value is exactly equal to C<bar>.
+
+  my $case_sensitive = $dom->find('input[type="hidden"]');
+  my $case_sensitive = $dom->find('input[type=hidden]');
+
+=item E[foo="bar" i]
+
+An C<E> element whose C<foo> attribute value is exactly equal to any
+(ASCII-range) case-permutation of C<bar>. Note that this selector is
+EXPERIMENTAL and might change without warning!
+
+  my $case_insensitive = $dom->find('input[type="hidden" i]');
+  my $case_insensitive = $dom->find('input[type=hidden i]');
+  my $case_insensitive = $dom->find('input[class~="foo" i]');
+
+This selector is part of
+L<Selectors Level 4|http://dev.w3.org/csswg/selectors-4>, which is still a work
+in progress.
+
+=item E[foo~="bar"]
+
+An C<E> element whose C<foo> attribute value is a list of whitespace-separated
+values, one of which is exactly equal to C<bar>.
+
+  my $foo = $dom->find('input[class~="foo"]');
+  my $foo = $dom->find('input[class~=foo]');
+
+=item E[foo^="bar"]
+
+An C<E> element whose C<foo> attribute value begins exactly with the string
+C<bar>.
+
+  my $begins_with = $dom->find('input[name^="f"]');
+  my $begins_with = $dom->find('input[name^=f]');
+
+=item E[foo$="bar"]
+
+An C<E> element whose C<foo> attribute value ends exactly with the string
+C<bar>.
+
+  my $ends_with = $dom->find('input[name$="o"]');
+  my $ends_with = $dom->find('input[name$=o]');
+
+=item E[foo*="bar"]
+
+An C<E> element whose C<foo> attribute value contains the substring C<bar>.
+
+  my $contains = $dom->find('input[name*="fo"]');
+  my $contains = $dom->find('input[name*=fo]');
+
+=item E:root
+
+An C<E> element, root of the document.
+
+  my $root = $dom->at(':root');
+
+=item E:nth-child(n)
+
+An C<E> element, the C<n-th> child of its parent.
+
+  my $third = $dom->find('div:nth-child(3)');
+  my $odd   = $dom->find('div:nth-child(odd)');
+  my $even  = $dom->find('div:nth-child(even)');
+  my $top3  = $dom->find('div:nth-child(-n+3)');
+
+=item E:nth-last-child(n)
+
+An C<E> element, the C<n-th> child of its parent, counting from the last one.
+
+  my $third    = $dom->find('div:nth-last-child(3)');
+  my $odd      = $dom->find('div:nth-last-child(odd)');
+  my $even     = $dom->find('div:nth-last-child(even)');
+  my $bottom3  = $dom->find('div:nth-last-child(-n+3)');
+
+=item E:nth-of-type(n)
+
+An C<E> element, the C<n-th> sibling of its type.
+
+  my $third = $dom->find('div:nth-of-type(3)');
+  my $odd   = $dom->find('div:nth-of-type(odd)');
+  my $even  = $dom->find('div:nth-of-type(even)');
+  my $top3  = $dom->find('div:nth-of-type(-n+3)');
+
+=item E:nth-last-of-type(n)
+
+An C<E> element, the C<n-th> sibling of its type, counting from the last one.
+
+  my $third    = $dom->find('div:nth-last-of-type(3)');
+  my $odd      = $dom->find('div:nth-last-of-type(odd)');
+  my $even     = $dom->find('div:nth-last-of-type(even)');
+  my $bottom3  = $dom->find('div:nth-last-of-type(-n+3)');
+
+=item E:first-child
+
+An C<E> element, first child of its parent.
+
+  my $first = $dom->find('div p:first-child');
+
+=item E:last-child
+
+An C<E> element, last child of its parent.
+
+  my $last = $dom->find('div p:last-child');
+
+=item E:first-of-type
+
+An C<E> element, first sibling of its type.
+
+  my $first = $dom->find('div p:first-of-type');
+
+=item E:last-of-type
+
+An C<E> element, last sibling of its type.
+
+  my $last = $dom->find('div p:last-of-type');
+
+=item E:only-child
+
+An C<E> element, only child of its parent.
+
+  my $lonely = $dom->find('div p:only-child');
+
+=item E:only-of-type
+
+An C<E> element, only sibling of its type.
+
+  my $lonely = $dom->find('div p:only-of-type');
+
+=item E:empty
+
+An C<E> element that has no children (including text nodes).
+
+  my $empty = $dom->find(':empty');
+
+=item E:checked
+
+A user interface element C<E> which is checked (for instance a radio-button or
+checkbox).
+
+  my $input = $dom->find(':checked');
+
+=item E.warning
+
+An C<E> element whose class is "warning".
+
+  my $warning = $dom->find('div.warning');
+
+=item E#myid
+
+An C<E> element with C<ID> equal to "myid".
+
+  my $foo = $dom->at('div#foo');
+
+=item E:not(s)
+
+An C<E> element that does not match simple selector C<s>.
+
+  my $others = $dom->find('div p:not(:first-child)');
+
+=item E F
+
+An C<F> element descendant of an C<E> element.
+
+  my $headlines = $dom->find('div h1');
+
+=item E E<gt> F
+
+An C<F> element child of an C<E> element.
+
+  my $headlines = $dom->find('html > body > div > h1');
+
+=item E + F
+
+An C<F> element immediately preceded by an C<E> element.
+
+  my $second = $dom->find('h1 + h2');
+
+=item E ~ F
+
+An C<F> element preceded by an C<E> element.
+
+  my $second = $dom->find('h1 ~ h2');
+
+=item E, F, G
+
+Elements of type C<E>, C<F> and C<G>.
+
+  my $headlines = $dom->find('h1, h2, h3');
+
+=item E[foo=bar][bar=baz]
+
+An C<E> element whose attributes match all following attribute selectors.
+
+  my $links = $dom->find('a[foo^=b][foo$=ar]');
+
+=back
+
+=head1 OPERATORS
+
+L<DOM::Tiny> overloads the following operators.
+
+=head2 array
+
+  my @nodes = @$dom;
+
+Alias for L</"child_nodes">.
+
+  # "<!-- Test -->"
+  $dom->parse('<!-- Test --><b>123</b>')->[0];
+
+=head2 bool
+
+  my $bool = !!$dom;
+
+Always true.
+
+=head2 hash
+
+  my %attrs = %$dom;
+
+Alias for L</"attr">.
+
+  # "test"
+  $dom->parse('<div id="test">Test</div>')->at('div')->{id};
+
+=head2 stringify
+
+  my $str = "$dom";
+
+Alias for L</"to_string">.
+
 =head1 METHODS
 
 L<DOM::Tiny> implements the following methods.
@@ -508,8 +777,8 @@ whitespace trimming is enabled by default.
   my $collection = $dom->ancestors('div ~ p');
 
 Find all ancestor elements of this node matching the CSS selector and return a
-L<DOM::Tiny::Collection> object containing these elements as L<DOM::Tiny>
-objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+L<collection|/"COLLECTION METHODS"> containing these elements as L<DOM::Tiny>
+objects. All selectors listed in L</"SELECTORS"> are supported.
 
   # List tag names of ancestor elements
   say $dom->ancestors->map('tag')->join("\n");
@@ -518,7 +787,7 @@ objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are supported.
 
   $dom = $dom->append('<p>I ♥ DOM::Tiny!</p>');
 
-Append HTML/XML fragment to this node.
+Append HTML/XML fragment to this node (for all node types other than C<root>).
 
   # "<div><h1>Test</h1><h2>123</h2></div>"
   $dom->parse('<div><h1>Test</h1></div>')
@@ -551,8 +820,8 @@ node's content.
   my $result = $dom->at('div ~ p');
 
 Find first descendant element of this element matching the CSS selector and
-return it as a L<DOM::Tiny> object or return C<undef> if none could be found.
-All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+return it as a L<DOM::Tiny> object, or C<undef> if none could be found. All
+selectors listed in L</"SELECTORS"> are supported.
 
   # Find first element with "svg" namespace definition
   my $namespace = $dom->at('[xmlns\:svg]')->{'xmlns:svg'};
@@ -579,7 +848,7 @@ This element's attributes.
 
   my $collection = $dom->child_nodes;
 
-Return a L<DOM::Tiny::Collection> object containing all child nodes of this
+Return a L<collection|/"COLLECTION METHODS"> containing all child nodes of this
 element as L<DOM::Tiny> objects.
 
   # "<p><b>123</b></p>"
@@ -597,8 +866,8 @@ element as L<DOM::Tiny> objects.
   my $collection = $dom->children('div ~ p');
 
 Find all child elements of this element matching the CSS selector and return a
-L<DOM::Tiny::Collection> object containing these elements as L<DOM::Tiny>
-objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+L<collection|/"COLLECTION METHODS"> containing these elements as L<DOM::Tiny>
+objects. All selectors listed in L</"SELECTORS"> are supported.
 
   # Show tag name of random child element
   say $dom->children->shuffle->first->tag;
@@ -634,7 +903,7 @@ and C<tag> nodes) or raw content.
 
   my $collection = $dom->descendant_nodes;
 
-Return a L<DOM::Tiny::Collection> object containing all descendant nodes of
+Return a L<collection|/"COLLECTION METHODS"> containing all descendant nodes of
 this element as L<DOM::Tiny> objects.
 
   # "<p><b>123</b></p>"
@@ -652,9 +921,8 @@ this element as L<DOM::Tiny> objects.
   my $collection = $dom->find('div ~ p');
 
 Find all descendant elements of this element matching the CSS selector and
-return a L<DOM::Tiny::Collection> object containing these elements as
-L<DOM::Tiny> objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are
-supported.
+return a L<collection|/"COLLECTION METHODS"> containing these elements as
+L<DOM::Tiny> objects. All selectors listed in L</"SELECTORS"> are supported.
 
   # Find a specific element and extract information
   my $id = $dom->find('div')->[23]{id};
@@ -674,8 +942,8 @@ supported.
   my $collection = $dom->following('div ~ p');
 
 Find all sibling elements after this node matching the CSS selector and return
-a L<DOM::Tiny::Collection> object containing these elements as L<DOM::Tiny>
-objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+a L<collection|/"COLLECTION METHODS"> containing these elements as L<DOM::Tiny>
+objects. All selectors listen in L</"SELECTORS"> are supported.
 
   # List tags of sibling elements after this node
   say $dom->following->map('tag')->join("\n");
@@ -684,7 +952,7 @@ objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are supported.
 
   my $collection = $dom->following_nodes;
 
-Return a L<DOM::Tiny::Collection> object containing all sibling nodes after
+Return a L<collection|/"COLLECTION METHODS"> containing all sibling nodes after
 this node as L<DOM::Tiny> objects.
 
   # "C"
@@ -694,8 +962,8 @@ this node as L<DOM::Tiny> objects.
 
   my $bool = $dom->matches('div ~ p');
 
-Check if this element matches the CSS selector. All selectors from
-L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+Check if this element matches the CSS selector. All selectors listed in
+L</"SELECTORS"> are supported.
 
   # True
   $dom->parse('<p class="a">A</p>')->at('p')->matches('.a');
@@ -709,7 +977,7 @@ L<DOM::Tiny::CSS/"SELECTORS"> are supported.
 
   my $namespace = $dom->namespace;
 
-Find this element's namespace or return C<undef> if none could be found.
+Find this element's namespace, or return C<undef> if none could be found.
 
   # Find namespace for an element with namespace prefix
   my $namespace = $dom->at('svg > svg\:circle')->namespace;
@@ -721,8 +989,8 @@ Find this element's namespace or return C<undef> if none could be found.
 
   my $sibling = $dom->next;
 
-Return L<DOM::Tiny> object for next sibling element or C<undef> if there are no
-more siblings.
+Return L<DOM::Tiny> object for next sibling element, or C<undef> if there are
+no more siblings.
 
   # "<h2>123</h2>"
   $dom->parse('<div><h1>Test</h1><h2>123</h2></div>')->at('h1')->next;
@@ -731,7 +999,7 @@ more siblings.
 
   my $sibling = $dom->next_node;
 
-Return L<DOM::Tiny> object for next sibling node or C<undef> if there are no
+Return L<DOM::Tiny> object for next sibling node, or C<undef> if there are no
 more siblings.
 
   # "456"
@@ -746,17 +1014,20 @@ more siblings.
 
   my $parent = $dom->parent;
 
-Return L<DOM::Tiny> object for parent of this node or C<undef> if this node has
-no parent.
+Return L<DOM::Tiny> object for parent of this node, or C<undef> if this node
+has no parent.
+
+  # "<b><i>Test</i></b>"
+  $dom->parse('<p><b><i>Test</i></b></p>')->at('i')->parent;
 
 =head2 parse
 
   $dom = $dom->parse('<foo bar="baz">I ♥ DOM::Tiny!</foo>');
 
-Parse HTML/XML fragment with L<DOM::Tiny::HTML>.
+Parse HTML/XML fragment.
 
   # Parse XML
-  my $dom = DOM::Tiny->new->xml(1)->parse($xml);
+  my $dom = DOM::Tiny->new->xml(1)->parse('<foo>I ♥ DOM::Tiny!</foo>');
 
 =head2 preceding
 
@@ -764,8 +1035,8 @@ Parse HTML/XML fragment with L<DOM::Tiny::HTML>.
   my $collection = $dom->preceding('div ~ p');
 
 Find all sibling elements before this node matching the CSS selector and return
-a L<DOM::Tiny::Collection> object containing these elements as L<DOM::Tiny>
-objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+a L<collection|/"COLLECTION METHODS"> containing these elements as L<DOM::Tiny>
+objects. All selectors listed in L</"SELECTORS"> are supported.
 
   # List tags of sibling elements before this node
   say $dom->preceding->map('tag')->join("\n");
@@ -774,8 +1045,8 @@ objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are supported.
 
   my $collection = $dom->preceding_nodes;
 
-Return a L<DOM::Tiny::Collection> object containing all sibling nodes before
-this node as L<DOM::Tiny> objects.
+Return a L<collection|/"COLLECTION METHODS"> containing all sibling nodes
+before this node as L<DOM::Tiny> objects.
 
   # "A"
   $dom->parse('A<!-- B --><p>C</p>')->at('p')->preceding_nodes->first->content;
@@ -784,7 +1055,7 @@ this node as L<DOM::Tiny> objects.
 
   $dom = $dom->prepend('<p>I ♥ DOM::Tiny!</p>');
 
-Prepend HTML/XML fragment to this node.
+Prepend HTML/XML fragment to this node (for all node types other than C<root>).
 
   # "<div><h1>Test</h1><h2>123</h2></div>"
   $dom->parse('<div><h2>123</h2></div>')
@@ -816,7 +1087,7 @@ node's content.
 
   my $sibling = $dom->previous;
 
-Return L<DOM::Tiny> object for previous sibling element or C<undef> if there
+Return L<DOM::Tiny> object for previous sibling element, or C<undef> if there
 are no more siblings.
 
   # "<h1>Test</h1>"
@@ -826,7 +1097,7 @@ are no more siblings.
 
   my $sibling = $dom->previous_node;
 
-Return L<DOM::Tiny> object for previous sibling node or C<undef> if there are
+Return L<DOM::Tiny> object for previous sibling node, or C<undef> if there are
 no more siblings.
 
   # "123"
@@ -962,13 +1233,13 @@ C<root>, C<tag> or C<text>.
   my $value = $dom->val;
 
 Extract value from form element (such as C<button>, C<input>, C<option>,
-C<select> and C<textarea>) or return C<undef> if this element has no value. In
+C<select> and C<textarea>), or return C<undef> if this element has no value. In
 the case of C<select> with C<multiple> attribute, find C<option> elements with
-C<selected> attribute and return an array reference with all values or C<undef>
-if none could be found.
+C<selected> attribute and return an array reference with all values, or
+C<undef> if none could be found.
 
   # "a"
-  $dom->parse('<input name="test" value="a">')->at('input')->val;
+  $dom->parse('<input name=test value=a>')->at('input')->val;
 
   # "b"
   $dom->parse('<textarea>b</textarea>')->at('textarea')->val;
@@ -984,12 +1255,15 @@ if none could be found.
   $dom->parse('<select multiple><option selected>e</option></select>')
     ->at('select')->val->[0];
 
+  # "on"
+  $dom->parse('<input name=test type=checkbox>')->at('input')->val;
+
 =head2 wrap
 
   $dom = $dom->wrap('<div></div>');
 
-Wrap HTML/XML fragment around this node, placing it as the last child of the
-first innermost element.
+Wrap HTML/XML fragment around this node (for all node types other than C<root>),
+placing it as the last child of the first innermost element.
 
   # "<p>123<b>Test</b></p>"
   $dom->parse('<b>Test</b>')->at('b')->wrap('<p>123</p>')->root;
@@ -1007,8 +1281,8 @@ first innermost element.
 
   $dom = $dom->wrap_content('<div></div>');
 
-Wrap HTML/XML fragment around this node's content, placing it as the last
-children of the first innermost element.
+Wrap HTML/XML fragment around this node's content (for C<root> and C<tag>
+nodes), placing it as the last children of the first innermost element.
 
   # "<p><b>123Test</b></p>"
   $dom->parse('<p>Test<p>')->at('p')->wrap_content('<b>123</b>')->root;
@@ -1022,41 +1296,218 @@ children of the first innermost element.
   $dom     = $dom->xml($bool);
 
 Disable HTML semantics in parser and activate case-sensitivity, defaults to
-auto detection based on processing instructions.
+auto detection based on XML declarations.
 
-=head1 OPERATORS
+=head1 COLLECTION METHODS
 
-L<DOM::Tiny> overloads the following operators.
+Some L<DOM::Tiny> methods return an array-based collection object based on
+L<Mojo::Collection>, which can either be accessed directly as an array
+reference, or with the following methods.
 
-=head2 array
+  # Chain methods
+  $collection->map(sub { ucfirst })->shuffle->each(sub {
+    my ($word, $num) = @_;
+    say "$num: $word";
+  });
 
-  my @nodes = @$dom;
+  # Access array directly to manipulate collection
+  $collection->[23] += 100;
+  say for @$collection;
 
-Alias for L</"child_nodes">.
+=head2 compact
 
-  # "<!-- Test -->"
-  $dom->parse('<!-- Test --><b>123</b>')->[0];
+  my $new = $collection->compact;
 
-=head2 bool
+Create a new collection with all elements that are defined and not an empty
+string.
 
-  my $bool = !!$dom;
+  # $collection contains (0, 1, undef, 2, '', 3)
+  $collection->compact->join(', '); # "0, 1, 2, 3"
 
-Always true.
+=head2 each
 
-=head2 hash
+  my @elements = $collection->each;
+  $collection  = $collection->each(sub {...});
 
-  my %attrs = %$dom;
+Evaluate callback for each element in collection or return all elements as a
+list if none has been provided. The element will be the first argument passed
+to the callback and is also available as C<$_>.
 
-Alias for L</"attr">.
+  # Make a numbered list
+  $collection->each(sub {
+    my ($e, $num) = @_;
+    say "$num: $e";
+  });
 
-  # "test"
-  $dom->parse('<div id="test">Test</div>')->at('div')->{id};
+=head2 first
 
-=head2 stringify
+  my $first = $collection->first;
+  my $first = $collection->first(qr/foo/);
+  my $first = $collection->first(sub {...});
+  my $first = $collection->first($method);
+  my $first = $collection->first($method, @args);
 
-  my $str = "$dom";
+Evaluate regular expression/callback for, or call method on, each element in
+collection and return the first one that matched the regular expression, or for
+which the callback/method returned true. The element will be the first argument
+passed to the callback and is also available as C<$_>.
 
-Alias for L</"to_string">.
+  # Longer version
+  my $first = $collection->first(sub { $_->$method(@args) });
+
+  # Find first value that contains the word "tiny"
+  my $interesting = $collection->first(qr/tiny/i);
+
+  # Find first value that is greater than 5
+  my $greater = $collection->first(sub { $_ > 5 });
+
+=head2 flatten
+
+  my $new = $collection->flatten;
+
+Flatten nested collections/arrays recursively and create a new collection with
+all elements.
+
+  # $collection contains (1, [2, [3, 4], 5, [6]], 7)
+  $collection->flatten->join(', '); # "1, 2, 3, 4, 5, 6, 7"
+
+=head2 grep
+
+  my $new = $collection->grep(qr/foo/);
+  my $new = $collection->grep(sub {...});
+  my $new = $collection->grep($method);
+  my $new = $collection->grep($method, @args);
+
+Evaluate regular expression/callback for, or call method on, each element in
+collection and create a new collection with all elements that matched the
+regular expression, or for which the callback/method returned true. The element
+will be the first argument passed to the callback and is also available as
+C<$_>.
+
+  # Longer version
+  my $new = $collection->grep(sub { $_->$method(@args) });
+
+  # Find all values that contain the word "tiny"
+  my $interesting = $collection->grep(qr/tiny/i);
+
+  # Find all values that are greater than 5
+  my $greater = $collection->grep(sub { $_ > 5 });
+
+=head2 join
+
+  my $stream = $collection->join;
+  my $stream = $collection->join("\n");
+
+Turn collection into string.
+
+  # Join all values with commas
+  $collection->join(', ');
+
+=head2 last
+
+  my $last = $collection->last;
+
+Return the last element in collection.
+
+=head2 map
+
+  my $new = $collection->map(sub {...});
+  my $new = $collection->map($method);
+  my $new = $collection->map($method, @args);
+
+Evaluate callback for, or call method on, each element in collection and create
+a new collection from the results. The element will be the first argument
+passed to the callback and is also available as C<$_>.
+
+  # Longer version
+  my $new = $collection->map(sub { $_->$method(@args) });
+
+  # Append the word "tiny" to all values
+  my $domified = $collection->map(sub { $_ . 'tiny' });
+
+=head2 reduce
+
+  my $result = $collection->reduce(sub {...});
+  my $result = $collection->reduce(sub {...}, $initial);
+
+Reduce elements in collection with callback, the first element will be used as
+initial value if none has been provided.
+
+  # Calculate the sum of all values
+  my $sum = $collection->reduce(sub { $a + $b });
+
+  # Count how often each value occurs in collection
+  my $hash = $collection->reduce(sub { $a->{$b}++; $a }, {});
+
+=head2 reverse
+
+  my $new = $collection->reverse;
+
+Create a new collection with all elements in reverse order.
+
+=head2 slice
+
+  my $new = $collection->slice(4 .. 7);
+
+Create a new collection with all selected elements.
+
+  # $collection contains ('A', 'B', 'C', 'D', 'E')
+  $collection->slice(1, 2, 4)->join(' '); # "B C E"
+
+=head2 shuffle
+
+  my $new = $collection->shuffle;
+
+Create a new collection with all elements in random order.
+
+=head2 size
+
+  my $size = $collection->size;
+
+Number of elements in collection.
+
+=head2 sort
+
+  my $new = $collection->sort;
+  my $new = $collection->sort(sub {...});
+
+Sort elements based on return value of callback and create a new collection
+from the results.
+
+  # Sort values case-insensitive
+  my $case_insensitive = $collection->sort(sub { uc($a) cmp uc($b) });
+
+=head2 tap
+
+  $collection = $collection->tap(sub {...});
+
+Equivalent to L<Mojo::Base/"tap">.
+
+=head2 to_array
+
+  my $array = $collection->to_array;
+
+Turn collection into array reference.
+
+=head2 uniq
+
+  my $new = $collection->uniq;
+  my $new = $collection->uniq(sub {...});
+  my $new = $collection->uniq($method);
+  my $new = $collection->uniq($method, @args);
+
+Create a new collection without duplicate elements, using the string
+representation of either the elements or the return value of the
+callback/method.
+
+  # Longer version
+  my $new = $collection->uniq(sub { $_->$method(@args) });
+
+  # $collection contains ('foo', 'bar', 'bar', 'baz')
+  $collection->uniq->join(' '); # "foo bar baz"
+
+  # $collection contains ([1, 2], [2, 1], [3, 2])
+  $collection->uniq(sub{ $_->[1] })->to_array; # "[[1, 2], [2, 1]]"
 
 =head1 BUGS
 
@@ -1066,9 +1517,21 @@ Report any issues on the public bugtracker.
 
 Dan Book <dbook@cpan.org>
 
+Code and tests adapted from L<Mojo::DOM>, a lightweight DOM parser by the L<Mojolicious> team.
+
+=head1 CONTRIBUTORS
+
+=over
+
+=item Matt S Trout (mst)
+
+=back
+
 =head1 COPYRIGHT AND LICENSE
 
-This software is Copyright (c) 2015 by Dan Book.
+Copyright (c) 2008-2015 Sebastian Riedel.
+
+Copyright (c) 2015 L</"AUTHOR"> and L</"CONTRIBUTORS"> for adaptation to standalone format.
 
 This is free software, licensed under:
 
@@ -1076,4 +1539,8 @@ This is free software, licensed under:
 
 =head1 SEE ALSO
 
-L<Mojo::DOM>, L<XML::LibXML>, L<XML::Twig>, L<HTML::TreeBuilder>, L<XML::Smart>
+L<Mojo::DOM>, L<HTML::TreeBuilder>, L<XML::LibXML>, L<XML::Twig>, L<XML::Smart>
+
+=for Pod::Coverage TO_JSON
+
+=cut