code, tests, docs
Dan Book [Fri, 30 Oct 2015 05:43:46 +0000 (01:43 -0400)]
15 files changed:
Build.PL [new file with mode: 0644]
INSTALL [new file with mode: 0644]
META.json [new file with mode: 0644]
README.pod [new file with mode: 0644]
cpanfile
dist.ini
examples/entities.pl [new file with mode: 0644]
lib/DOM/Tiny.pm
lib/DOM/Tiny/CSS.pm [new file with mode: 0644]
lib/DOM/Tiny/Collection.pm [new file with mode: 0644]
lib/DOM/Tiny/Entities.pm [new file with mode: 0644]
lib/DOM/Tiny/HTML.pm [new file with mode: 0644]
t/collection.t [new file with mode: 0644]
t/dom.t [new file with mode: 0644]
t/entities.t [new file with mode: 0644]

diff --git a/Build.PL b/Build.PL
new file mode 100644 (file)
index 0000000..237996a
--- /dev/null
+++ b/Build.PL
@@ -0,0 +1,114 @@
+# This Build.PL for DOM-Tiny was generated by
+# Dist::Zilla::Plugin::ModuleBuildTiny::Fallback 0.020
+use strict;
+use warnings;
+
+my %configure_requires = (
+    'Module::Build::Tiny' => '0.034',
+);
+
+my @missing = grep {
+    ! eval "require $_; $_->VERSION($configure_requires{$_}); 1"
+} keys %configure_requires;
+
+if (not @missing)
+{
+    # This section for DOM-Tiny was generated by Dist::Zilla::Plugin::ModuleBuildTiny 0.014.
+    use strict;
+    use warnings;
+
+    use 5.010001;
+    # use Module::Build::Tiny 0.034;
+    Module::Build::Tiny::Build_PL();
+}
+else
+{
+    if (not $ENV{PERL_MB_FALLBACK_SILENCE_WARNING})
+    {
+        warn <<'EOW';
+*** WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING ***
+
+If you're seeing this warning, your toolchain is really, really old* and you'll
+almost certainly have problems installing CPAN modules from this century. But
+never fear, dear user, for we have the technology to fix this!
+
+If you're using CPAN.pm to install things, then you can upgrade it using:
+
+    cpan CPAN
+
+If you're using CPANPLUS to install things, then you can upgrade it using:
+
+    cpanp CPANPLUS
+
+If you're using cpanminus, you shouldn't be seeing this message in the first
+place, so please file an issue on github.
+
+This public service announcement was brought to you by the Perl Toolchain
+Gang, the irc.perl.org #toolchain IRC channel, and the number 42.
+
+----
+
+* Alternatively, you are running this file manually, in which case you need
+to learn to first fulfill all configure requires prerequisites listed in
+META.yml or META.json -- or use a cpan client to install this distribution.
+
+You can also silence this warning for future installations by setting the
+PERL_MB_FALLBACK_SILENCE_WARNING environment variable, but please don't do
+that until you fix your toolchain as described above.
+
+EOW
+        sleep 10 if -t STDIN && (-t STDOUT || !(-f STDOUT || -c STDOUT));
+    }
+
+
+    # This section was automatically generated by Dist::Zilla::Plugin::ModuleBuild v5.040.
+    use strict;
+    use warnings;
+
+    require Module::Build; Module::Build->VERSION(0.28);
+
+
+    my %module_build_args = (
+      "configure_requires" => {
+        "Module::Build::Tiny" => "0.034"
+      },
+      "dist_abstract" => "Minimalistic HTML/XML DOM parser with CSS selectors",
+      "dist_author" => [
+        "Dan Book <dbook\@cpan.org>"
+      ],
+      "dist_name" => "DOM-Tiny",
+      "dist_version" => "0.001",
+      "license" => "artistic_2",
+      "module_name" => "DOM::Tiny",
+      "recursive_test_files" => 1,
+      "requires" => {
+        "Carp" => 0,
+        "Class::Tiny::Chained" => 0,
+        "Exporter" => 0,
+        "List::Util" => 0,
+        "Scalar::Util" => 0,
+        "perl" => "5.010001"
+      },
+      "test_requires" => {
+        "JSON::Tiny" => "0.41",
+        "Test::More" => "0.88"
+      }
+    );
+
+
+    my %fallback_build_requires = (
+      "JSON::Tiny" => "0.41",
+      "Test::More" => "0.88"
+    );
+
+
+    unless ( eval { Module::Build->VERSION(0.4004) } ) {
+      delete $module_build_args{test_requires};
+      $module_build_args{build_requires} = \%fallback_build_requires;
+    }
+
+    my $build = Module::Build->new(%module_build_args);
+
+
+    $build->create_build_script;
+}
diff --git a/INSTALL b/INSTALL
new file mode 100644 (file)
index 0000000..17283c8
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,43 @@
+This is the Perl distribution DOM-Tiny.
+
+Installing DOM-Tiny is straightforward.
+
+## Installation with cpanm
+
+If you have cpanm, you only need one line:
+
+    % cpanm DOM::Tiny
+
+If you are installing into a system-wide directory, you may need to pass the
+"-S" flag to cpanm, which uses sudo to install the module:
+
+    % cpanm -S DOM::Tiny
+
+## Installing with the CPAN shell
+
+Alternatively, if your CPAN shell is set up, you should just be able to do:
+
+    % cpan DOM::Tiny
+
+## Manual installation
+
+As a last resort, you can manually install it. Download the tarball, untar it,
+then build it:
+
+    % perl Build.PL
+    % ./Build && ./Build test
+
+Then install it:
+
+    % ./Build install
+
+If you are installing into a system-wide directory, you may need to run:
+
+    % sudo ./Build install
+
+## Documentation
+
+DOM-Tiny documentation is available as POD.
+You can run perldoc from a shell to read the documentation:
+
+    % perldoc DOM::Tiny
diff --git a/META.json b/META.json
new file mode 100644 (file)
index 0000000..4b7d925
--- /dev/null
+++ b/META.json
@@ -0,0 +1,84 @@
+{
+   "abstract" : "Minimalistic HTML/XML DOM parser with CSS selectors",
+   "author" : [
+      "Dan Book <dbook@cpan.org>"
+   ],
+   "dynamic_config" : 0,
+   "generated_by" : "Dist::Zilla version 5.040, CPAN::Meta::Converter version 2.150005",
+   "license" : [
+      "artistic_2"
+   ],
+   "meta-spec" : {
+      "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",
+      "version" : 2
+   },
+   "name" : "DOM-Tiny",
+   "no_index" : {
+      "directory" : [
+         "eg",
+         "examples",
+         "inc",
+         "share",
+         "t",
+         "xt"
+      ]
+   },
+   "prereqs" : {
+      "configure" : {
+         "requires" : {
+            "Module::Build::Tiny" : "0.034"
+         }
+      },
+      "develop" : {
+         "requires" : {
+            "Pod::Coverage::TrustPod" : "0",
+            "Test::Pod" : "1.41",
+            "Test::Pod::Coverage" : "1.08"
+         }
+      },
+      "runtime" : {
+         "requires" : {
+            "Carp" : "0",
+            "Class::Tiny::Chained" : "0",
+            "Exporter" : "0",
+            "List::Util" : "0",
+            "Scalar::Util" : "0",
+            "perl" : "5.010001"
+         }
+      },
+      "test" : {
+         "requires" : {
+            "JSON::Tiny" : "0.41",
+            "Test::More" : "0.88"
+         }
+      }
+   },
+   "provides" : {
+      "DOM::Tiny" : {
+         "file" : "lib/DOM/Tiny.pm",
+         "version" : "0.001"
+      },
+      "DOM::Tiny::CSS" : {
+         "file" : "lib/DOM/Tiny/CSS.pm",
+         "version" : "0.001"
+      },
+      "DOM::Tiny::Collection" : {
+         "file" : "lib/DOM/Tiny/Collection.pm",
+         "version" : "0.001"
+      },
+      "DOM::Tiny::Entities" : {
+         "file" : "lib/DOM/Tiny/Entities.pm",
+         "version" : "0.001"
+      },
+      "DOM::Tiny::HTML" : {
+         "file" : "lib/DOM/Tiny/HTML.pm",
+         "version" : "0.001"
+      }
+   },
+   "release_status" : "stable",
+   "version" : "0.001",
+   "x_contributors" : [
+      "Dan Book <grinnz@grinnz.com>"
+   ]
+}
+
diff --git a/README.pod b/README.pod
new file mode 100644 (file)
index 0000000..2f6ea69
--- /dev/null
@@ -0,0 +1,696 @@
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+DOM::Tiny - Minimalistic HTML/XML DOM parser with CSS selectors
+
+=head1 SYNOPSIS
+
+  use DOM::Tiny;
+
+  # Parse
+  my $dom = DOM::Tiny->new('<div><p id="a">Test</p><p id="b">123</p></div>');
+
+  # Find
+  say $dom->at('#b')->text;
+  say $dom->find('p')->map('text')->join("\n");
+  say $dom->find('[id]')->map(attr => 'id')->join("\n");
+
+  # Iterate
+  $dom->find('p[id]')->reverse->each(sub { say $_->{id} });
+
+  # Loop
+  for my $e ($dom->find('p[id]')->each) {
+    say $e->{id}, ':', $e->text;
+  }
+
+  # Modify
+  $dom->find('div p')->last->append('<p id="c">456</p>');
+  $dom->find(':not(p)')->map('strip');
+
+  # Render
+  say "$dom";
+
+=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.
+
+=head1 NODES AND ELEMENTS
+
+When we parse an HTML/XML fragment, it gets turned into a tree of nodes.
+
+  <!DOCTYPE html>
+  <html>
+    <head><title>Hello</title></head>
+    <body>World!</body>
+  </html>
+
+There are currently eight different kinds of nodes, C<cdata>, C<comment>,
+C<doctype>, C<pi>, C<raw>, C<root>, C<tag> and C<text>. Elements are nodes of
+the type C<tag>.
+
+  root
+  |- doctype (html)
+  +- tag (html)
+     |- tag (head)
+     |  +- tag (title)
+     |     +- raw (Hello)
+     +- tag (body)
+        +- text (World!)
+
+While all node types are represented as L<DOM::Tiny> objects, some methods like
+L</"attr"> and L</"namespace"> only apply to elements.
+
+=head1 CASE-SENSITIVITY
+
+L<DOM::Tiny> defaults to HTML semantics, that means all tags and attribute
+names are lowercased and selectors need to be lowercase as well.
+
+  # HTML semantics
+  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.
+
+  # XML semantics
+  my $dom = DOM::Tiny->new('<?xml version="1.0"?><P ID="greeting">Hi!</P>');
+  say $dom->at('P[ID]')->text;
+
+XML detection can also be disabled with the L</"xml"> method.
+
+  # Force XML semantics
+  my $dom = DOM::Tiny->new->xml(1)->parse('<P ID="greeting">Hi!</P>');
+  say $dom->at('P[ID]')->text;
+
+  # Force HTML semantics
+  my $dom = DOM::Tiny->new->xml(0)->parse('<P ID="greeting">Hi!</P>');
+  say $dom->at('p[id]')->text;
+
+=head1 METHODS
+
+L<DOM::Tiny> implements the following methods.
+
+=head2 all_text
+
+  my $trimmed   = $dom->all_text;
+  my $untrimmed = $dom->all_text(0);
+
+Extract text content from all descendant nodes of this element, smart
+whitespace trimming is enabled by default.
+
+  # "foo bar baz"
+  $dom->parse("<div>foo\n<p>bar</p>baz\n</div>")->at('div')->all_text;
+
+  # "foo\nbarbaz\n"
+  $dom->parse("<div>foo\n<p>bar</p>baz\n</div>")->at('div')->all_text(0);
+
+=head2 ancestors
+
+  my $collection = $dom->ancestors;
+  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.
+
+  # List tag names of ancestor elements
+  say $dom->ancestors->map('tag')->join("\n");
+
+=head2 append
+
+  $dom = $dom->append('<p>I ♥ DOM::Tiny!</p>');
+
+Append HTML/XML fragment to this node.
+
+  # "<div><h1>Test</h1><h2>123</h2></div>"
+  $dom->parse('<div><h1>Test</h1></div>')
+    ->at('h1')->append('<h2>123</h2>')->root;
+
+  # "<p>Test 123</p>"
+  $dom->parse('<p>Test</p>')->at('p')
+    ->child_nodes->first->append(' 123')->root;
+
+=head2 append_content
+
+  $dom = $dom->append_content('<p>I ♥ DOM::Tiny!</p>');
+
+Append HTML/XML fragment (for C<root> and C<tag> nodes) or raw content to this
+node's content.
+
+  # "<div><h1>Test123</h1></div>"
+  $dom->parse('<div><h1>Test</h1></div>')
+    ->at('h1')->append_content('123')->root;
+
+  # "<!-- Test 123 --><br>"
+  $dom->parse('<!-- Test --><br>')
+    ->child_nodes->first->append_content('123 ')->root;
+
+  # "<p>Test<i>123</i></p>"
+  $dom->parse('<p>Test</p>')->at('p')->append_content('<i>123</i>')->root;
+
+=head2 at
+
+  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.
+
+  # Find first element with "svg" namespace definition
+  my $namespace = $dom->at('[xmlns\:svg]')->{'xmlns:svg'};
+
+=head2 attr
+
+  my $hash = $dom->attr;
+  my $foo  = $dom->attr('foo');
+  $dom     = $dom->attr({foo => 'bar'});
+  $dom     = $dom->attr(foo => 'bar');
+
+This element's attributes.
+
+  # Remove an attribute
+  delete $dom->attr->{id};
+
+  # Attribute without value
+  $dom->attr(selected => undef);
+
+  # List id attributes
+  say $dom->find('*')->map(attr => 'id')->compact->join("\n");
+
+=head2 child_nodes
+
+  my $collection = $dom->child_nodes;
+
+Return a L<DOM::Tiny::Collection> object containing all child nodes of this
+element as L<DOM::Tiny> objects.
+
+  # "<p><b>123</b></p>"
+  $dom->parse('<p>Test<b>123</b></p>')->at('p')->child_nodes->first->remove;
+
+  # "<!DOCTYPE html>"
+  $dom->parse('<!DOCTYPE html><b>123</b>')->child_nodes->first;
+
+  # " Test "
+  $dom->parse('<b>123</b><!-- Test -->')->child_nodes->last->content;
+
+=head2 children
+
+  my $collection = $dom->children;
+  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.
+
+  # Show tag name of random child element
+  say $dom->children->shuffle->first->tag;
+
+=head2 content
+
+  my $str = $dom->content;
+  $dom    = $dom->content('<p>I ♥ DOM::Tiny!</p>');
+
+Return this node's content or replace it with HTML/XML fragment (for C<root>
+and C<tag> nodes) or raw content.
+
+  # "<b>Test</b>"
+  $dom->parse('<div><b>Test</b></div>')->at('div')->content;
+
+  # "<div><h1>123</h1></div>"
+  $dom->parse('<div><h1>Test</h1></div>')->at('h1')->content('123')->root;
+
+  # "<p><i>123</i></p>"
+  $dom->parse('<p>Test</p>')->at('p')->content('<i>123</i>')->root;
+
+  # "<div><h1></h1></div>"
+  $dom->parse('<div><h1>Test</h1></div>')->at('h1')->content('')->root;
+
+  # " Test "
+  $dom->parse('<!-- Test --><br>')->child_nodes->first->content;
+
+  # "<div><!-- 123 -->456</div>"
+  $dom->parse('<div><!-- Test -->456</div>')
+    ->at('div')->child_nodes->first->content(' 123 ')->root;
+
+=head2 descendant_nodes
+
+  my $collection = $dom->descendant_nodes;
+
+Return a L<DOM::Tiny::Collection> object containing all descendant nodes of
+this element as L<DOM::Tiny> objects.
+
+  # "<p><b>123</b></p>"
+  $dom->parse('<p><!-- Test --><b>123<!-- 456 --></b></p>')
+    ->descendant_nodes->grep(sub { $_->type eq 'comment' })
+    ->map('remove')->first;
+
+  # "<p><b>test</b>test</p>"
+  $dom->parse('<p><b>123</b>456</p>')
+    ->at('p')->descendant_nodes->grep(sub { $_->type eq 'text' })
+    ->map(content => 'test')->first->root;
+
+=head2 find
+
+  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.
+
+  # Find a specific element and extract information
+  my $id = $dom->find('div')->[23]{id};
+
+  # Extract information from multiple elements
+  my @headers = $dom->find('h1, h2, h3')->map('text')->each;
+
+  # Count all the different tags
+  my $hash = $dom->find('*')->reduce(sub { $a->{$b->tag}++; $a }, {});
+
+  # Find elements with a class that contains dots
+  my @divs = $dom->find('div.foo\.bar')->each;
+
+=head2 following
+
+  my $collection = $dom->following;
+  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.
+
+  # List tags of sibling elements after this node
+  say $dom->following->map('tag')->join("\n");
+
+=head2 following_nodes
+
+  my $collection = $dom->following_nodes;
+
+Return a L<DOM::Tiny::Collection> object containing all sibling nodes after
+this node as L<DOM::Tiny> objects.
+
+  # "C"
+  $dom->parse('<p>A</p><!-- B -->C')->at('p')->following_nodes->last->content;
+
+=head2 matches
+
+  my $bool = $dom->matches('div ~ p');
+
+Check if this element matches the CSS selector. All selectors from
+L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+
+  # True
+  $dom->parse('<p class="a">A</p>')->at('p')->matches('.a');
+  $dom->parse('<p class="a">A</p>')->at('p')->matches('p[class]');
+
+  # False
+  $dom->parse('<p class="a">A</p>')->at('p')->matches('.b');
+  $dom->parse('<p class="a">A</p>')->at('p')->matches('p[id]');
+
+=head2 namespace
+
+  my $namespace = $dom->namespace;
+
+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;
+
+  # Find namespace for an element that may or may not have a namespace prefix
+  my $namespace = $dom->at('svg > circle')->namespace;
+
+=head2 new
+
+  my $dom = DOM::Tiny->new;
+  my $dom = DOM::Tiny->new('<foo bar="baz">I ♥ DOM::Tiny!</foo>');
+
+Construct a new scalar-based L<DOM::Tiny> object and L</"parse"> HTML/XML
+fragment if necessary.
+
+=head2 next
+
+  my $sibling = $dom->next;
+
+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;
+
+=head2 next_node
+
+  my $sibling = $dom->next_node;
+
+Return L<DOM::Tiny> object for next sibling node or C<undef> if there are no
+more siblings.
+
+  # "456"
+  $dom->parse('<p><b>123</b><!-- Test -->456</p>')
+    ->at('b')->next_node->next_node;
+
+  # " Test "
+  $dom->parse('<p><b>123</b><!-- Test -->456</p>')
+    ->at('b')->next_node->content;
+
+=head2 parent
+
+  my $parent = $dom->parent;
+
+Return L<DOM::Tiny> object for parent of this node or C<undef> if this node has
+no parent.
+
+=head2 parse
+
+  $dom = $dom->parse('<foo bar="baz">I ♥ DOM::Tiny!</foo>');
+
+Parse HTML/XML fragment with L<DOM::Tiny::HTML>.
+
+  # Parse XML
+  my $dom = DOM::Tiny->new->xml(1)->parse($xml);
+
+=head2 preceding
+
+  my $collection = $dom->preceding;
+  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.
+
+  # List tags of sibling elements before this node
+  say $dom->preceding->map('tag')->join("\n");
+
+=head2 preceding_nodes
+
+  my $collection = $dom->preceding_nodes;
+
+Return a L<DOM::Tiny::Collection> object 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;
+
+=head2 prepend
+
+  $dom = $dom->prepend('<p>I ♥ DOM::Tiny!</p>');
+
+Prepend HTML/XML fragment to this node.
+
+  # "<div><h1>Test</h1><h2>123</h2></div>"
+  $dom->parse('<div><h2>123</h2></div>')
+    ->at('h2')->prepend('<h1>Test</h1>')->root;
+
+  # "<p>Test 123</p>"
+  $dom->parse('<p>123</p>')
+    ->at('p')->child_nodes->first->prepend('Test ')->root;
+
+=head2 prepend_content
+
+  $dom = $dom->prepend_content('<p>I ♥ DOM::Tiny!</p>');
+
+Prepend HTML/XML fragment (for C<root> and C<tag> nodes) or raw content to this
+node's content.
+
+  # "<div><h2>Test123</h2></div>"
+  $dom->parse('<div><h2>123</h2></div>')
+    ->at('h2')->prepend_content('Test')->root;
+
+  # "<!-- Test 123 --><br>"
+  $dom->parse('<!-- 123 --><br>')
+    ->child_nodes->first->prepend_content(' Test')->root;
+
+  # "<p><i>123</i>Test</p>"
+  $dom->parse('<p>Test</p>')->at('p')->prepend_content('<i>123</i>')->root;
+
+=head2 previous
+
+  my $sibling = $dom->previous;
+
+Return L<DOM::Tiny> object for previous sibling element or C<undef> if there
+are no more siblings.
+
+  # "<h1>Test</h1>"
+  $dom->parse('<div><h1>Test</h1><h2>123</h2></div>')->at('h2')->previous;
+
+=head2 previous_node
+
+  my $sibling = $dom->previous_node;
+
+Return L<DOM::Tiny> object for previous sibling node or C<undef> if there are
+no more siblings.
+
+  # "123"
+  $dom->parse('<p>123<!-- Test --><b>456</b></p>')
+    ->at('b')->previous_node->previous_node;
+
+  # " Test "
+  $dom->parse('<p>123<!-- Test --><b>456</b></p>')
+    ->at('b')->previous_node->content;
+
+=head2 remove
+
+  my $parent = $dom->remove;
+
+Remove this node and return L</"root"> (for C<root> nodes) or L</"parent">.
+
+  # "<div></div>"
+  $dom->parse('<div><h1>Test</h1></div>')->at('h1')->remove;
+
+  # "<p><b>456</b></p>"
+  $dom->parse('<p>123<b>456</b></p>')
+    ->at('p')->child_nodes->first->remove->root;
+
+=head2 replace
+
+  my $parent = $dom->replace('<div>I ♥ DOM::Tiny!</div>');
+
+Replace this node with HTML/XML fragment and return L</"root"> (for C<root>
+nodes) or L</"parent">.
+
+  # "<div><h2>123</h2></div>"
+  $dom->parse('<div><h1>Test</h1></div>')->at('h1')->replace('<h2>123</h2>');
+
+  # "<p><b>123</b></p>"
+  $dom->parse('<p>Test</p>')
+    ->at('p')->child_nodes->[0]->replace('<b>123</b>')->root;
+
+=head2 root
+
+  my $root = $dom->root;
+
+Return L<DOM::Tiny> object for C<root> node.
+
+=head2 strip
+
+  my $parent = $dom->strip;
+
+Remove this element while preserving its content and return L</"parent">.
+
+  # "<div>Test</div>"
+  $dom->parse('<div><h1>Test</h1></div>')->at('h1')->strip;
+
+=head2 tag
+
+  my $tag = $dom->tag;
+  $dom    = $dom->tag('div');
+
+This element's tag name.
+
+  # List tag names of child elements
+  say $dom->children->map('tag')->join("\n");
+
+=head2 tap
+
+  $dom = $dom->tap(sub {...});
+
+Alias for L<Mojo::Base/"tap">.
+
+=head2 text
+
+  my $trimmed   = $dom->text;
+  my $untrimmed = $dom->text(0);
+
+Extract text content from this element only (not including child elements),
+smart whitespace trimming is enabled by default.
+
+  # "foo baz"
+  $dom->parse("<div>foo\n<p>bar</p>baz\n</div>")->at('div')->text;
+
+  # "foo\nbaz\n"
+  $dom->parse("<div>foo\n<p>bar</p>baz\n</div>")->at('div')->text(0);
+
+=head2 to_string
+
+  my $str = $dom->to_string;
+
+Render this node and its content to HTML/XML.
+
+  # "<b>Test</b>"
+  $dom->parse('<div><b>Test</b></div>')->at('div b')->to_string;
+
+=head2 tree
+
+  my $tree = $dom->tree;
+  $dom     = $dom->tree(['root']);
+
+Document Object Model. Note that this structure should only be used very
+carefully since it is very dynamic.
+
+=head2 type
+
+  my $type = $dom->type;
+
+This node's type, usually C<cdata>, C<comment>, C<doctype>, C<pi>, C<raw>,
+C<root>, C<tag> or C<text>.
+
+  # "cdata"
+  $dom->parse('<![CDATA[Test]]>')->child_nodes->first->type;
+
+  # "comment"
+  $dom->parse('<!-- Test -->')->child_nodes->first->type;
+
+  # "doctype"
+  $dom->parse('<!DOCTYPE html>')->child_nodes->first->type;
+
+  # "pi"
+  $dom->parse('<?xml version="1.0"?>')->child_nodes->first->type;
+
+  # "raw"
+  $dom->parse('<title>Test</title>')->at('title')->child_nodes->first->type;
+
+  # "root"
+  $dom->parse('<p>Test</p>')->type;
+
+  # "tag"
+  $dom->parse('<p>Test</p>')->at('p')->type;
+
+  # "text"
+  $dom->parse('<p>Test</p>')->at('p')->child_nodes->first->type;
+
+=head2 val
+
+  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
+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.
+
+  # "a"
+  $dom->parse('<input name="test" value="a">')->at('input')->val;
+
+  # "b"
+  $dom->parse('<textarea>b</textarea>')->at('textarea')->val;
+
+  # "c"
+  $dom->parse('<option value="c">Test</option>')->at('option')->val;
+
+  # "d"
+  $dom->parse('<select><option selected>d</option></select>')
+    ->at('select')->val;
+
+  # "e"
+  $dom->parse('<select multiple><option selected>e</option></select>')
+    ->at('select')->val->[0];
+
+=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.
+
+  # "<p>123<b>Test</b></p>"
+  $dom->parse('<b>Test</b>')->at('b')->wrap('<p>123</p>')->root;
+
+  # "<div><p><b>Test</b></p>123</div>"
+  $dom->parse('<b>Test</b>')->at('b')->wrap('<div><p></p>123</div>')->root;
+
+  # "<p><b>Test</b></p><p>123</p>"
+  $dom->parse('<b>Test</b>')->at('b')->wrap('<p></p><p>123</p>')->root;
+
+  # "<p><b>Test</b></p>"
+  $dom->parse('<p>Test</p>')->at('p')->child_nodes->first->wrap('<b>')->root;
+
+=head2 wrap_content
+
+  $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.
+
+  # "<p><b>123Test</b></p>"
+  $dom->parse('<p>Test<p>')->at('p')->wrap_content('<b>123</b>')->root;
+
+  # "<p><b>Test</b></p><p>123</p>"
+  $dom->parse('<b>Test</b>')->wrap_content('<p></p><p>123</p>');
+
+=head2 xml
+
+  my $bool = $dom->xml;
+  $dom     = $dom->xml($bool);
+
+Disable HTML semantics in parser and activate case-sensitivity, defaults to
+auto detection based on processing instructions.
+
+=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 BUGS
+
+Report any issues on the public bugtracker.
+
+=head1 AUTHOR
+
+Dan Book <dbook@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is Copyright (c) 2015 by Dan Book.
+
+This is free software, licensed under:
+
+  The Artistic License 2.0 (GPL Compatible)
+
+=head1 SEE ALSO
+
+L<Mojo::DOM>, L<XML::LibXML>, L<XML::Twig>, L<HTML::TreeBuilder>, L<XML::Smart>
+
+=cut
index e69de29..1a7e6f4 100644 (file)
--- a/cpanfile
+++ b/cpanfile
@@ -0,0 +1,8 @@
+requires 'perl' => '5.010001';
+requires 'Carp';
+requires 'Class::Tiny::Chained';
+requires 'Exporter';
+requires 'List::Util';
+requires 'Scalar::Util';
+test_requires 'JSON::Tiny' => '0.41';
+test_requires 'Test::More' => '0.88';
index 9791cf3..c203e92 100644 (file)
--- a/dist.ini
+++ b/dist.ini
@@ -5,3 +5,5 @@ copyright_holder = Dan Book
 copyright_year   = 2015
 
 [@Author::DBOOK]
+installer = ModuleBuildTiny::Fallback
+pod_tests = 1
diff --git a/examples/entities.pl b/examples/entities.pl
new file mode 100644 (file)
index 0000000..8b7cba4
--- /dev/null
@@ -0,0 +1,17 @@
+use strict;
+use warnings;
+use HTTP::Tiny;
+use DOM::Tiny;
+use Encode 'decode';
+
+# Extract named character references from HTML Living Standard
+my $res = HTTP::Tiny->new->get('https://html.spec.whatwg.org');
+my $dom = DOM::Tiny->new(decode 'UTF-8', $res->{content});
+my $rows = $dom->find('#named-character-references-table tbody > tr');
+for my $row ($rows->each) {
+  my $entity     = $row->at('td > code')->text;
+  my $codepoints = $row->children('td')->[1]->text;
+  print "$entity $codepoints\n";
+}
+
+1;
index cf226c4..247855f 100644 (file)
@@ -3,18 +3,1061 @@ package DOM::Tiny;
 use strict;
 use warnings;
 
+use overload
+  '@{}'    => sub { shift->child_nodes },
+  '%{}'    => sub { shift->attr },
+  bool     => sub {1},
+  '""'     => sub { shift->to_string },
+  fallback => 1;
+
+use Carp 'croak';
+use DOM::Tiny::Collection;
+use DOM::Tiny::CSS;
+use DOM::Tiny::HTML;
+use Scalar::Util qw(blessed weaken);
+
 our $VERSION = '0.001';
 
+sub all_text { shift->_all_text(1, @_) }
+
+sub ancestors { _select($_[0]->_collect($_[0]->_ancestors), $_[1]) }
+
+sub append { shift->_add(1, @_) }
+sub append_content { shift->_content(1, 0, @_) }
+
+sub at {
+  my $self = shift;
+  return undef unless my $result = $self->_css->select_one(@_);
+  return $self->_build($result, $self->xml);
+}
+
+sub attr {
+  my $self = shift;
+
+  # Hash
+  my $tree = $self->tree;
+  my $attrs = $tree->[0] ne 'tag' ? {} : $tree->[2];
+  return $attrs unless @_;
+
+  # Get
+  return $attrs->{$_[0]} unless @_ > 1 || ref $_[0];
+
+  # Set
+  my $values = ref $_[0] ? $_[0] : {@_};
+  @$attrs{keys %$values} = values %$values;
+
+  return $self;
+}
+
+sub child_nodes { $_[0]->_collect(_nodes($_[0]->tree)) }
+
+sub children { _select($_[0]->_collect(_nodes($_[0]->tree, 1)), $_[1]) }
+
+sub content {
+  my $self = shift;
+
+  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);
+    return join '', map { $html->tree($_)->render } _nodes($self->tree);
+  }
+
+  return $self->tree->[1] unless @_;
+  $self->tree->[1] = shift;
+  return $self;
+}
+
+sub descendant_nodes { $_[0]->_collect(_all(_nodes($_[0]->tree))) }
+
+sub find { $_[0]->_collect(@{$_[0]->_css->select($_[1])}) }
+
+sub following { _select($_[0]->_collect(@{$_[0]->_siblings(1)->[1]}), $_[1]) }
+sub following_nodes { $_[0]->_collect(@{$_[0]->_siblings->[1]}) }
+
+sub matches { shift->_css->matches(@_) }
+
+sub namespace {
+  my $self = shift;
+
+  return undef if (my $tree = $self->tree)->[0] ne 'tag';
+
+  # Extract namespace prefix and search parents
+  my $ns = $tree->[1] =~ /^(.*?):/ ? "xmlns:$1" : undef;
+  for my $node ($tree, $self->_ancestors) {
+
+    # Namespace for prefix
+    my $attrs = $node->[2];
+    if ($ns) { $_ eq $ns and return $attrs->{$_} for keys %$attrs }
+
+    # Namespace attribute
+    elsif (defined $attrs->{xmlns}) { return $attrs->{xmlns} }
+  }
+
+  return undef;
+}
+
+sub new {
+  my $class = shift;
+  my $self = bless \DOM::Tiny::HTML->new, ref $class || $class;
+  return @_ ? $self->parse(@_) : $self;
+}
+
+sub next      { $_[0]->_maybe($_[0]->_siblings(1, 0)->[1]) }
+sub next_node { $_[0]->_maybe($_[0]->_siblings(0, 0)->[1]) }
+
+sub parent {
+  my $self = shift;
+  return undef if $self->tree->[0] eq 'root';
+  return $self->_build($self->_parent, $self->xml);
+}
+
+sub parse { shift->_delegate(parse => @_) }
+
+sub preceding { _select($_[0]->_collect(@{$_[0]->_siblings(1)->[0]}), $_[1]) }
+sub preceding_nodes { $_[0]->_collect(@{$_[0]->_siblings->[0]}) }
+
+sub prepend { shift->_add(0, @_) }
+sub prepend_content { shift->_content(0, 0, @_) }
+
+sub previous      { $_[0]->_maybe($_[0]->_siblings(1, -1)->[0]) }
+sub previous_node { $_[0]->_maybe($_[0]->_siblings(0, -1)->[0]) }
+
+sub remove { shift->replace('') }
+
+sub replace {
+  my ($self, $new) = @_;
+  return $self->parse($new) if (my $tree = $self->tree)->[0] eq 'root';
+  return $self->_replace($self->_parent, $tree, _nodes($self->_parse($new)));
+}
+
+sub root {
+  my $self = shift;
+  return $self unless my $tree = $self->_ancestors(1);
+  return $self->_build($tree, $self->xml);
+}
+
+sub strip {
+  my $self = shift;
+  return $self if (my $tree = $self->tree)->[0] ne 'tag';
+  return $self->_replace($tree->[3], $tree, _nodes($tree));
+}
+
+sub tag {
+  my ($self, $tag) = @_;
+  return undef if (my $tree = $self->tree)->[0] ne 'tag';
+  return $tree->[1] unless $tag;
+  $tree->[1] = $tag;
+  return $self;
+}
+
+sub tap { shift->DOM::Tiny::Collection::tap(@_) }
+
+sub text { shift->_all_text(0, @_) }
+
+sub to_string { shift->_delegate('render') }
+
+sub tree { shift->_delegate(tree => @_) }
+
+sub type { shift->tree->[0] }
+
+sub val {
+  my $self = shift;
+
+  # "option"
+  return $self->{value} // $self->text if (my $tag = $self->tag) eq 'option';
+
+  # "textarea", "input" or "button"
+  return $tag eq 'textarea' ? $self->text : $self->{value} if $tag ne 'select';
+
+  # "select"
+  my $v = $self->find('option:checked')->map('val');
+  return exists $self->{multiple} ? $v->size ? $v->to_array : undef : $v->last;
+}
+
+sub wrap         { shift->_wrap(0, @_) }
+sub wrap_content { shift->_wrap(1, @_) }
+
+sub xml { shift->_delegate(xml => @_) }
+
+sub _add {
+  my ($self, $offset, $new) = @_;
+
+  return $self if (my $tree = $self->tree)->[0] eq 'root';
+
+  my $parent = $self->_parent;
+  splice @$parent, _offset($parent, $tree) + $offset, 0,
+    _link($parent, _nodes($self->_parse($new)));
+
+  return $self;
+}
+
+sub _all {
+  map { $_->[0] eq 'tag' ? ($_, _all(_nodes($_))) : ($_) } @_;
+}
+
+sub _all_text {
+  my ($self, $recurse, $trim) = @_;
+
+  # Detect "pre" tag
+  my $tree = $self->tree;
+  $trim = 1 unless defined $trim;
+  map { $_->[1] eq 'pre' and $trim = 0 } $self->_ancestors, $tree
+    if $trim && $tree->[0] ne 'root';
+
+  return _text([_nodes($tree)], $recurse, $trim);
+}
+
+sub _ancestors {
+  my ($self, $root) = @_;
+
+  return unless my $tree = $self->_parent;
+  my @ancestors;
+  do { push @ancestors, $tree }
+    while ($tree->[0] eq 'tag') && ($tree = $tree->[3]);
+  return $root ? $ancestors[-1] : @ancestors[0 .. $#ancestors - 1];
+}
+
+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) } @_);
+}
+
+sub _content {
+  my ($self, $start, $offset, $new) = @_;
+
+  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");
+  }
+
+  $start  = $start  ? ($#$tree + 1) : _start($tree);
+  $offset = $offset ? $#$tree       : 0;
+  splice @$tree, $start, $offset, _link($tree, _nodes($self->_parse($new)));
+
+  return $self;
+}
+
+sub _css { DOM::Tiny::CSS->new(tree => shift->tree) }
+
+sub _delegate {
+  my ($self, $method) = (shift, shift);
+  return $$self->$method unless @_;
+  $$self->$method(@_);
+  return $self;
+}
+
+sub _link {
+  my ($parent, @children) = @_;
+
+  # Link parent to children
+  for my $node (@children) {
+    my $offset = $node->[0] eq 'tag' ? 3 : 2;
+    $node->[$offset] = $parent;
+    weaken $node->[$offset];
+  }
+
+  return @children;
+}
+
+sub _maybe { $_[1] ? $_[0]->_build($_[1], $_[0]->xml) : undef }
+
+sub _nodes {
+  return unless my $tree = shift;
+  my @nodes = @$tree[_start($tree) .. $#$tree];
+  return shift() ? grep { $_->[0] eq 'tag' } @nodes : @nodes;
+}
+
+sub _offset {
+  my ($parent, $child) = @_;
+  my $i = _start($parent);
+  $_ eq $child ? last : $i++ for @$parent[$i .. $#$parent];
+  return $i;
+}
+
+sub _parent { $_[0]->tree->[$_[0]->type eq 'tag' ? 3 : 2] }
+
+sub _parse { DOM::Tiny::HTML->new(xml => shift->xml)->parse(shift)->tree }
+
+sub _replace {
+  my ($self, $parent, $tree, @nodes) = @_;
+  splice @$parent, _offset($parent, $tree), 1, _link($parent, @nodes);
+  return $self->parent;
+}
+
+sub _select {
+  my ($collection, $selector) = @_;
+  return $collection unless $selector;
+  return $collection->new(grep { $_->matches($selector) } @$collection);
+}
+
+sub _siblings {
+  my ($self, $tags, $i) = @_;
+
+  return [] unless my $parent = $self->parent;
+
+  my $tree = $self->tree;
+  my (@before, @after, $match);
+  for my $node (_nodes($parent->tree)) {
+    ++$match and next if !$match && $node eq $tree;
+    next if $tags && $node->[0] ne 'tag';
+    $match ? push @after, $node : push @before, $node;
+  }
+
+  return defined $i ? [$before[$i], $after[$i]] : [\@before, \@after];
+}
+
+sub _squish {
+  my $str = shift;
+  $str =~ s/^\s+//;
+  $str =~ s/\s+$//;
+  $str =~ s/\s+/ /g;
+  return $str;
+}
+
+sub _start { $_[0][0] eq 'root' ? 1 : 4 }
+
+sub _text {
+  my ($nodes, $recurse, $trim) = @_;
+
+  # Merge successive text nodes
+  my $i = 0;
+  while (my $next = $nodes->[$i + 1]) {
+    ++$i and next unless $nodes->[$i][0] eq 'text' && $next->[0] eq 'text';
+    splice @$nodes, $i, 2, ['text', $nodes->[$i][1] . $next->[1]];
+  }
+
+  my $text = '';
+  for my $node (@$nodes) {
+    my $type = $node->[0];
+
+    # Text
+    my $chunk = '';
+    if ($type eq 'text') { $chunk = $trim ? _squish $node->[1] : $node->[1] }
+
+    # CDATA or raw text
+    elsif ($type eq 'cdata' || $type eq 'raw') { $chunk = $node->[1] }
+
+    # Nested tag
+    elsif ($type eq 'tag' && $recurse) {
+      no warnings 'recursion';
+      $chunk = _text([_nodes($node)], 1, $node->[1] eq 'pre' ? 0 : $trim);
+    }
+
+    # Add leading whitespace if punctuation allows it
+    $chunk = " $chunk" if $text =~ /\S\z/ && $chunk =~ /^[^.!?,;:\s]+/;
+
+    # Trim whitespace blocks
+    $text .= $chunk if $chunk =~ /\S+/ || !$trim;
+  }
+
+  return $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';
+
+  # Find innermost tag
+  my $current;
+  my $first = $new = $self->_parse($new);
+  $current = $first while $first = (_nodes($first, 1))[0];
+  return $self unless $current;
+
+  # Wrap content
+  if ($content) {
+    push @$current, _link($current, _nodes($tree));
+    splice @$tree, _start($tree), $#$tree, _link($tree, _nodes($new));
+    return $self;
+  }
+
+  # Wrap element
+  $self->_replace($self->_parent, $tree, _nodes($new));
+  push @$current, _link($current, $tree);
+  return $self;
+}
+
 1;
 
+=encoding utf8
+
 =head1 NAME
 
-DOM::Tiny - Module abstract
+DOM::Tiny - Minimalistic HTML/XML DOM parser with CSS selectors
 
 =head1 SYNOPSIS
 
+  use DOM::Tiny;
+
+  # Parse
+  my $dom = DOM::Tiny->new('<div><p id="a">Test</p><p id="b">123</p></div>');
+
+  # Find
+  say $dom->at('#b')->text;
+  say $dom->find('p')->map('text')->join("\n");
+  say $dom->find('[id]')->map(attr => 'id')->join("\n");
+
+  # Iterate
+  $dom->find('p[id]')->reverse->each(sub { say $_->{id} });
+
+  # Loop
+  for my $e ($dom->find('p[id]')->each) {
+    say $e->{id}, ':', $e->text;
+  }
+
+  # Modify
+  $dom->find('div p')->last->append('<p id="c">456</p>');
+  $dom->find(':not(p)')->map('strip');
+
+  # Render
+  say "$dom";
+
 =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.
+
+=head1 NODES AND ELEMENTS
+
+When we parse an HTML/XML fragment, it gets turned into a tree of nodes.
+
+  <!DOCTYPE html>
+  <html>
+    <head><title>Hello</title></head>
+    <body>World!</body>
+  </html>
+
+There are currently eight different kinds of nodes, C<cdata>, C<comment>,
+C<doctype>, C<pi>, C<raw>, C<root>, C<tag> and C<text>. Elements are nodes of
+the type C<tag>.
+
+  root
+  |- doctype (html)
+  +- tag (html)
+     |- tag (head)
+     |  +- tag (title)
+     |     +- raw (Hello)
+     +- tag (body)
+        +- text (World!)
+
+While all node types are represented as L<DOM::Tiny> objects, some methods like
+L</"attr"> and L</"namespace"> only apply to elements.
+
+=head1 CASE-SENSITIVITY
+
+L<DOM::Tiny> defaults to HTML semantics, that means all tags and attribute
+names are lowercased and selectors need to be lowercase as well.
+
+  # HTML semantics
+  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.
+
+  # XML semantics
+  my $dom = DOM::Tiny->new('<?xml version="1.0"?><P ID="greeting">Hi!</P>');
+  say $dom->at('P[ID]')->text;
+
+XML detection can also be disabled with the L</"xml"> method.
+
+  # Force XML semantics
+  my $dom = DOM::Tiny->new->xml(1)->parse('<P ID="greeting">Hi!</P>');
+  say $dom->at('P[ID]')->text;
+
+  # Force HTML semantics
+  my $dom = DOM::Tiny->new->xml(0)->parse('<P ID="greeting">Hi!</P>');
+  say $dom->at('p[id]')->text;
+
+=head1 METHODS
+
+L<DOM::Tiny> implements the following methods.
+
+=head2 all_text
+
+  my $trimmed   = $dom->all_text;
+  my $untrimmed = $dom->all_text(0);
+
+Extract text content from all descendant nodes of this element, smart
+whitespace trimming is enabled by default.
+
+  # "foo bar baz"
+  $dom->parse("<div>foo\n<p>bar</p>baz\n</div>")->at('div')->all_text;
+
+  # "foo\nbarbaz\n"
+  $dom->parse("<div>foo\n<p>bar</p>baz\n</div>")->at('div')->all_text(0);
+
+=head2 ancestors
+
+  my $collection = $dom->ancestors;
+  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.
+
+  # List tag names of ancestor elements
+  say $dom->ancestors->map('tag')->join("\n");
+
+=head2 append
+
+  $dom = $dom->append('<p>I ♥ DOM::Tiny!</p>');
+
+Append HTML/XML fragment to this node.
+
+  # "<div><h1>Test</h1><h2>123</h2></div>"
+  $dom->parse('<div><h1>Test</h1></div>')
+    ->at('h1')->append('<h2>123</h2>')->root;
+
+  # "<p>Test 123</p>"
+  $dom->parse('<p>Test</p>')->at('p')
+    ->child_nodes->first->append(' 123')->root;
+
+=head2 append_content
+
+  $dom = $dom->append_content('<p>I ♥ DOM::Tiny!</p>');
+
+Append HTML/XML fragment (for C<root> and C<tag> nodes) or raw content to this
+node's content.
+
+  # "<div><h1>Test123</h1></div>"
+  $dom->parse('<div><h1>Test</h1></div>')
+    ->at('h1')->append_content('123')->root;
+
+  # "<!-- Test 123 --><br>"
+  $dom->parse('<!-- Test --><br>')
+    ->child_nodes->first->append_content('123 ')->root;
+
+  # "<p>Test<i>123</i></p>"
+  $dom->parse('<p>Test</p>')->at('p')->append_content('<i>123</i>')->root;
+
+=head2 at
+
+  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.
+
+  # Find first element with "svg" namespace definition
+  my $namespace = $dom->at('[xmlns\:svg]')->{'xmlns:svg'};
+
+=head2 attr
+
+  my $hash = $dom->attr;
+  my $foo  = $dom->attr('foo');
+  $dom     = $dom->attr({foo => 'bar'});
+  $dom     = $dom->attr(foo => 'bar');
+
+This element's attributes.
+
+  # Remove an attribute
+  delete $dom->attr->{id};
+
+  # Attribute without value
+  $dom->attr(selected => undef);
+
+  # List id attributes
+  say $dom->find('*')->map(attr => 'id')->compact->join("\n");
+
+=head2 child_nodes
+
+  my $collection = $dom->child_nodes;
+
+Return a L<DOM::Tiny::Collection> object containing all child nodes of this
+element as L<DOM::Tiny> objects.
+
+  # "<p><b>123</b></p>"
+  $dom->parse('<p>Test<b>123</b></p>')->at('p')->child_nodes->first->remove;
+
+  # "<!DOCTYPE html>"
+  $dom->parse('<!DOCTYPE html><b>123</b>')->child_nodes->first;
+
+  # " Test "
+  $dom->parse('<b>123</b><!-- Test -->')->child_nodes->last->content;
+
+=head2 children
+
+  my $collection = $dom->children;
+  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.
+
+  # Show tag name of random child element
+  say $dom->children->shuffle->first->tag;
+
+=head2 content
+
+  my $str = $dom->content;
+  $dom    = $dom->content('<p>I ♥ DOM::Tiny!</p>');
+
+Return this node's content or replace it with HTML/XML fragment (for C<root>
+and C<tag> nodes) or raw content.
+
+  # "<b>Test</b>"
+  $dom->parse('<div><b>Test</b></div>')->at('div')->content;
+
+  # "<div><h1>123</h1></div>"
+  $dom->parse('<div><h1>Test</h1></div>')->at('h1')->content('123')->root;
+
+  # "<p><i>123</i></p>"
+  $dom->parse('<p>Test</p>')->at('p')->content('<i>123</i>')->root;
+
+  # "<div><h1></h1></div>"
+  $dom->parse('<div><h1>Test</h1></div>')->at('h1')->content('')->root;
+
+  # " Test "
+  $dom->parse('<!-- Test --><br>')->child_nodes->first->content;
+
+  # "<div><!-- 123 -->456</div>"
+  $dom->parse('<div><!-- Test -->456</div>')
+    ->at('div')->child_nodes->first->content(' 123 ')->root;
+
+=head2 descendant_nodes
+
+  my $collection = $dom->descendant_nodes;
+
+Return a L<DOM::Tiny::Collection> object containing all descendant nodes of
+this element as L<DOM::Tiny> objects.
+
+  # "<p><b>123</b></p>"
+  $dom->parse('<p><!-- Test --><b>123<!-- 456 --></b></p>')
+    ->descendant_nodes->grep(sub { $_->type eq 'comment' })
+    ->map('remove')->first;
+
+  # "<p><b>test</b>test</p>"
+  $dom->parse('<p><b>123</b>456</p>')
+    ->at('p')->descendant_nodes->grep(sub { $_->type eq 'text' })
+    ->map(content => 'test')->first->root;
+
+=head2 find
+
+  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.
+
+  # Find a specific element and extract information
+  my $id = $dom->find('div')->[23]{id};
+
+  # Extract information from multiple elements
+  my @headers = $dom->find('h1, h2, h3')->map('text')->each;
+
+  # Count all the different tags
+  my $hash = $dom->find('*')->reduce(sub { $a->{$b->tag}++; $a }, {});
+
+  # Find elements with a class that contains dots
+  my @divs = $dom->find('div.foo\.bar')->each;
+
+=head2 following
+
+  my $collection = $dom->following;
+  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.
+
+  # List tags of sibling elements after this node
+  say $dom->following->map('tag')->join("\n");
+
+=head2 following_nodes
+
+  my $collection = $dom->following_nodes;
+
+Return a L<DOM::Tiny::Collection> object containing all sibling nodes after
+this node as L<DOM::Tiny> objects.
+
+  # "C"
+  $dom->parse('<p>A</p><!-- B -->C')->at('p')->following_nodes->last->content;
+
+=head2 matches
+
+  my $bool = $dom->matches('div ~ p');
+
+Check if this element matches the CSS selector. All selectors from
+L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+
+  # True
+  $dom->parse('<p class="a">A</p>')->at('p')->matches('.a');
+  $dom->parse('<p class="a">A</p>')->at('p')->matches('p[class]');
+
+  # False
+  $dom->parse('<p class="a">A</p>')->at('p')->matches('.b');
+  $dom->parse('<p class="a">A</p>')->at('p')->matches('p[id]');
+
+=head2 namespace
+
+  my $namespace = $dom->namespace;
+
+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;
+
+  # Find namespace for an element that may or may not have a namespace prefix
+  my $namespace = $dom->at('svg > circle')->namespace;
+
+=head2 new
+
+  my $dom = DOM::Tiny->new;
+  my $dom = DOM::Tiny->new('<foo bar="baz">I ♥ DOM::Tiny!</foo>');
+
+Construct a new scalar-based L<DOM::Tiny> object and L</"parse"> HTML/XML
+fragment if necessary.
+
+=head2 next
+
+  my $sibling = $dom->next;
+
+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;
+
+=head2 next_node
+
+  my $sibling = $dom->next_node;
+
+Return L<DOM::Tiny> object for next sibling node or C<undef> if there are no
+more siblings.
+
+  # "456"
+  $dom->parse('<p><b>123</b><!-- Test -->456</p>')
+    ->at('b')->next_node->next_node;
+
+  # " Test "
+  $dom->parse('<p><b>123</b><!-- Test -->456</p>')
+    ->at('b')->next_node->content;
+
+=head2 parent
+
+  my $parent = $dom->parent;
+
+Return L<DOM::Tiny> object for parent of this node or C<undef> if this node has
+no parent.
+
+=head2 parse
+
+  $dom = $dom->parse('<foo bar="baz">I ♥ DOM::Tiny!</foo>');
+
+Parse HTML/XML fragment with L<DOM::Tiny::HTML>.
+
+  # Parse XML
+  my $dom = DOM::Tiny->new->xml(1)->parse($xml);
+
+=head2 preceding
+
+  my $collection = $dom->preceding;
+  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.
+
+  # List tags of sibling elements before this node
+  say $dom->preceding->map('tag')->join("\n");
+
+=head2 preceding_nodes
+
+  my $collection = $dom->preceding_nodes;
+
+Return a L<DOM::Tiny::Collection> object 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;
+
+=head2 prepend
+
+  $dom = $dom->prepend('<p>I ♥ DOM::Tiny!</p>');
+
+Prepend HTML/XML fragment to this node.
+
+  # "<div><h1>Test</h1><h2>123</h2></div>"
+  $dom->parse('<div><h2>123</h2></div>')
+    ->at('h2')->prepend('<h1>Test</h1>')->root;
+
+  # "<p>Test 123</p>"
+  $dom->parse('<p>123</p>')
+    ->at('p')->child_nodes->first->prepend('Test ')->root;
+
+=head2 prepend_content
+
+  $dom = $dom->prepend_content('<p>I ♥ DOM::Tiny!</p>');
+
+Prepend HTML/XML fragment (for C<root> and C<tag> nodes) or raw content to this
+node's content.
+
+  # "<div><h2>Test123</h2></div>"
+  $dom->parse('<div><h2>123</h2></div>')
+    ->at('h2')->prepend_content('Test')->root;
+
+  # "<!-- Test 123 --><br>"
+  $dom->parse('<!-- 123 --><br>')
+    ->child_nodes->first->prepend_content(' Test')->root;
+
+  # "<p><i>123</i>Test</p>"
+  $dom->parse('<p>Test</p>')->at('p')->prepend_content('<i>123</i>')->root;
+
+=head2 previous
+
+  my $sibling = $dom->previous;
+
+Return L<DOM::Tiny> object for previous sibling element or C<undef> if there
+are no more siblings.
+
+  # "<h1>Test</h1>"
+  $dom->parse('<div><h1>Test</h1><h2>123</h2></div>')->at('h2')->previous;
+
+=head2 previous_node
+
+  my $sibling = $dom->previous_node;
+
+Return L<DOM::Tiny> object for previous sibling node or C<undef> if there are
+no more siblings.
+
+  # "123"
+  $dom->parse('<p>123<!-- Test --><b>456</b></p>')
+    ->at('b')->previous_node->previous_node;
+
+  # " Test "
+  $dom->parse('<p>123<!-- Test --><b>456</b></p>')
+    ->at('b')->previous_node->content;
+
+=head2 remove
+
+  my $parent = $dom->remove;
+
+Remove this node and return L</"root"> (for C<root> nodes) or L</"parent">.
+
+  # "<div></div>"
+  $dom->parse('<div><h1>Test</h1></div>')->at('h1')->remove;
+
+  # "<p><b>456</b></p>"
+  $dom->parse('<p>123<b>456</b></p>')
+    ->at('p')->child_nodes->first->remove->root;
+
+=head2 replace
+
+  my $parent = $dom->replace('<div>I ♥ DOM::Tiny!</div>');
+
+Replace this node with HTML/XML fragment and return L</"root"> (for C<root>
+nodes) or L</"parent">.
+
+  # "<div><h2>123</h2></div>"
+  $dom->parse('<div><h1>Test</h1></div>')->at('h1')->replace('<h2>123</h2>');
+
+  # "<p><b>123</b></p>"
+  $dom->parse('<p>Test</p>')
+    ->at('p')->child_nodes->[0]->replace('<b>123</b>')->root;
+
+=head2 root
+
+  my $root = $dom->root;
+
+Return L<DOM::Tiny> object for C<root> node.
+
+=head2 strip
+
+  my $parent = $dom->strip;
+
+Remove this element while preserving its content and return L</"parent">.
+
+  # "<div>Test</div>"
+  $dom->parse('<div><h1>Test</h1></div>')->at('h1')->strip;
+
+=head2 tag
+
+  my $tag = $dom->tag;
+  $dom    = $dom->tag('div');
+
+This element's tag name.
+
+  # List tag names of child elements
+  say $dom->children->map('tag')->join("\n");
+
+=head2 tap
+
+  $dom = $dom->tap(sub {...});
+
+Alias for L<Mojo::Base/"tap">.
+
+=head2 text
+
+  my $trimmed   = $dom->text;
+  my $untrimmed = $dom->text(0);
+
+Extract text content from this element only (not including child elements),
+smart whitespace trimming is enabled by default.
+
+  # "foo baz"
+  $dom->parse("<div>foo\n<p>bar</p>baz\n</div>")->at('div')->text;
+
+  # "foo\nbaz\n"
+  $dom->parse("<div>foo\n<p>bar</p>baz\n</div>")->at('div')->text(0);
+
+=head2 to_string
+
+  my $str = $dom->to_string;
+
+Render this node and its content to HTML/XML.
+
+  # "<b>Test</b>"
+  $dom->parse('<div><b>Test</b></div>')->at('div b')->to_string;
+
+=head2 tree
+
+  my $tree = $dom->tree;
+  $dom     = $dom->tree(['root']);
+
+Document Object Model. Note that this structure should only be used very
+carefully since it is very dynamic.
+
+=head2 type
+
+  my $type = $dom->type;
+
+This node's type, usually C<cdata>, C<comment>, C<doctype>, C<pi>, C<raw>,
+C<root>, C<tag> or C<text>.
+
+  # "cdata"
+  $dom->parse('<![CDATA[Test]]>')->child_nodes->first->type;
+
+  # "comment"
+  $dom->parse('<!-- Test -->')->child_nodes->first->type;
+
+  # "doctype"
+  $dom->parse('<!DOCTYPE html>')->child_nodes->first->type;
+
+  # "pi"
+  $dom->parse('<?xml version="1.0"?>')->child_nodes->first->type;
+
+  # "raw"
+  $dom->parse('<title>Test</title>')->at('title')->child_nodes->first->type;
+
+  # "root"
+  $dom->parse('<p>Test</p>')->type;
+
+  # "tag"
+  $dom->parse('<p>Test</p>')->at('p')->type;
+
+  # "text"
+  $dom->parse('<p>Test</p>')->at('p')->child_nodes->first->type;
+
+=head2 val
+
+  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
+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.
+
+  # "a"
+  $dom->parse('<input name="test" value="a">')->at('input')->val;
+
+  # "b"
+  $dom->parse('<textarea>b</textarea>')->at('textarea')->val;
+
+  # "c"
+  $dom->parse('<option value="c">Test</option>')->at('option')->val;
+
+  # "d"
+  $dom->parse('<select><option selected>d</option></select>')
+    ->at('select')->val;
+
+  # "e"
+  $dom->parse('<select multiple><option selected>e</option></select>')
+    ->at('select')->val->[0];
+
+=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.
+
+  # "<p>123<b>Test</b></p>"
+  $dom->parse('<b>Test</b>')->at('b')->wrap('<p>123</p>')->root;
+
+  # "<div><p><b>Test</b></p>123</div>"
+  $dom->parse('<b>Test</b>')->at('b')->wrap('<div><p></p>123</div>')->root;
+
+  # "<p><b>Test</b></p><p>123</p>"
+  $dom->parse('<b>Test</b>')->at('b')->wrap('<p></p><p>123</p>')->root;
+
+  # "<p><b>Test</b></p>"
+  $dom->parse('<p>Test</p>')->at('p')->child_nodes->first->wrap('<b>')->root;
+
+=head2 wrap_content
+
+  $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.
+
+  # "<p><b>123Test</b></p>"
+  $dom->parse('<p>Test<p>')->at('p')->wrap_content('<b>123</b>')->root;
+
+  # "<p><b>Test</b></p><p>123</p>"
+  $dom->parse('<b>Test</b>')->wrap_content('<p></p><p>123</p>');
+
+=head2 xml
+
+  my $bool = $dom->xml;
+  $dom     = $dom->xml($bool);
+
+Disable HTML semantics in parser and activate case-sensitivity, defaults to
+auto detection based on processing instructions.
+
+=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 BUGS
 
 Report any issues on the public bugtracker.
@@ -33,3 +1076,4 @@ This is free software, licensed under:
 
 =head1 SEE ALSO
 
+L<Mojo::DOM>, L<XML::LibXML>, L<XML::Twig>, L<HTML::TreeBuilder>, L<XML::Smart>
diff --git a/lib/DOM/Tiny/CSS.pm b/lib/DOM/Tiny/CSS.pm
new file mode 100644 (file)
index 0000000..dbe552a
--- /dev/null
@@ -0,0 +1,585 @@
+package DOM::Tiny::CSS;
+
+use strict;
+use warnings;
+use Class::Tiny::Chained 'tree';
+
+our $VERSION = '0.001';
+
+my $ESCAPE_RE = qr/\\[^0-9a-fA-F]|\\[0-9a-fA-F]{1,6}/;
+my $ATTR_RE   = qr/
+  \[
+  ((?:$ESCAPE_RE|[\w\-])+)                              # Key
+  (?:
+    (\W)?=                                              # Operator
+    (?:"((?:\\"|[^"])*)"|'((?:\\'|[^'])*)'|([^\]]+?))   # Value
+    (?:\s+(i))?                                         # Case-sensitivity
+  )?
+  \]
+/x;
+
+sub matches {
+  my $tree = shift->tree;
+  return $tree->[0] ne 'tag' ? undef : _match(_compile(shift), $tree, $tree);
+}
+
+sub select     { _select(0, shift->tree, _compile(@_)) }
+sub select_one { _select(1, shift->tree, _compile(@_)) }
+
+sub _ancestor {
+  my ($selectors, $current, $tree, $one, $pos) = @_;
+
+  while ($current = $current->[3]) {
+    return undef if $current->[0] eq 'root' || $current eq $tree;
+    return 1 if _combinator($selectors, $current, $tree, $pos);
+    last if $one;
+  }
+
+  return undef;
+}
+
+sub _attr {
+  my ($name_re, $value_re, $current) = @_;
+
+  my $attrs = $current->[2];
+  for my $name (keys %$attrs) {
+    next unless $name =~ $name_re;
+    return 1 unless defined $attrs->{$name} && defined $value_re;
+    return 1 if $attrs->{$name} =~ $value_re;
+  }
+
+  return undef;
+}
+
+sub _combinator {
+  my ($selectors, $current, $tree, $pos) = @_;
+
+  # Selector
+  return undef unless my $c = $selectors->[$pos];
+  if (ref $c) {
+    return undef unless _selector($c, $current);
+    return 1 unless $c = $selectors->[++$pos];
+  }
+
+  # ">" (parent only)
+  return _ancestor($selectors, $current, $tree, 1, ++$pos) if $c eq '>';
+
+  # "~" (preceding siblings)
+  return _sibling($selectors, $current, $tree, 0, ++$pos) if $c eq '~';
+
+  # "+" (immediately preceding siblings)
+  return _sibling($selectors, $current, $tree, 1, ++$pos) if $c eq '+';
+
+  # " " (ancestor)
+  return _ancestor($selectors, $current, $tree, 0, ++$pos);
+}
+
+sub _compile {
+  my $css = "$_[0]";
+  $css =~ s/^\s+//;
+  $css =~ s/\s+$//;
+
+  my $group = [[]];
+  while (my $selectors = $group->[-1]) {
+    push @$selectors, [] unless @$selectors && ref $selectors->[-1];
+    my $last = $selectors->[-1];
+
+    # Separator
+    if ($css =~ /\G\s*,\s*/gc) { push @$group, [] }
+
+    # Combinator
+    elsif ($css =~ /\G\s*([ >+~])\s*/gc) { push @$selectors, $1 }
+
+    # Class or ID
+    elsif ($css =~ /\G([.#])((?:$ESCAPE_RE\s|\\.|[^,.#:[ >~+])+)/gco) {
+      my ($name, $op) = $1 eq '.' ? ('class', '~') : ('id', '');
+      push @$last, ['attr', _name($name), _value($op, $2)];
+    }
+
+    # Attributes
+    elsif ($css =~ /\G$ATTR_RE/gco) {
+      push @$last, ['attr', _name($1), _value($2 // '', $3 // $4 // $5, $6)];
+    }
+
+    # Pseudo-class (":not" contains more selectors)
+    elsif ($css =~ /\G:([\w\-]+)(?:\(((?:\([^)]+\)|[^)])+)\))?/gcs) {
+      push @$last, ['pc', lc $1, $1 eq 'not' ? _compile($2) : _equation($2)];
+    }
+
+    # Tag
+    elsif ($css =~ /\G((?:$ESCAPE_RE\s|\\.|[^,.#:[ >~+])+)/gco) {
+      push @$last, ['tag', _name($1)] unless $1 eq '*';
+    }
+
+    else {last}
+  }
+
+  return $group;
+}
+
+sub _empty { $_[0][0] eq 'comment' || $_[0][0] eq 'pi' }
+
+sub _equation {
+  return [] unless my $equation = shift;
+
+  # "even"
+  return [2, 2] if $equation =~ /^\s*even\s*$/i;
+
+  # "odd"
+  return [2, 1] if $equation =~ /^\s*odd\s*$/i;
+
+  # Equation
+  my $num = [1, 1];
+  return $num if $equation !~ /(?:(-?(?:\d+)?)?(n))?\s*\+?\s*(-?\s*\d+)?\s*$/i;
+  $num->[0] = defined($1) && $1 ne '' ? $1 : $2 ? 1 : 0;
+  $num->[0] = -1 if $num->[0] eq '-';
+  $num->[1] = $3 // 0;
+  $num->[1] =~ s/\s+//g;
+  return $num;
+}
+
+sub _match {
+  my ($group, $current, $tree) = @_;
+  _combinator([reverse @$_], $current, $tree, 0) and return 1 for @$group;
+  return undef;
+}
+
+sub _name {qr/(?:^|:)\Q@{[_unescape(shift)]}\E$/}
+
+sub _pc {
+  my ($class, $args, $current) = @_;
+
+  # ":empty"
+  return !grep { !_empty($_) } @$current[4 .. $#$current] if $class eq 'empty';
+
+  # ":root"
+  return $current->[3] && $current->[3][0] eq 'root' if $class eq 'root';
+
+  # ":not"
+  return !_match($args, $current, $current) if $class eq 'not';
+
+  # ":checked"
+  return exists $current->[2]{checked} || exists $current->[2]{selected}
+    if $class eq 'checked';
+
+  # ":first-*" or ":last-*" (rewrite with equation)
+  ($class, $args) = $1 ? ("nth-$class", [0, 1]) : ("nth-last-$class", [-1, 1])
+    if $class =~ s/^(?:(first)|last)-//;
+
+  # ":nth-*"
+  if ($class =~ /^nth-/) {
+    my $type = $class =~ /of-type$/ ? $current->[1] : undef;
+    my @siblings = @{_siblings($current, $type)};
+
+    # ":nth-last-*"
+    @siblings = reverse @siblings if $class =~ /^nth-last/;
+
+    for my $i (0 .. $#siblings) {
+      next if (my $result = $args->[0] * $i + $args->[1]) < 1;
+      last unless my $sibling = $siblings[$result - 1];
+      return 1 if $sibling eq $current;
+    }
+  }
+
+  # ":only-*"
+  elsif ($class =~ /^only-(?:child|(of-type))$/) {
+    $_ ne $current and return undef
+      for @{_siblings($current, $1 ? $current->[1] : undef)};
+    return 1;
+  }
+
+  return undef;
+}
+
+sub _select {
+  my ($one, $tree, $group) = @_;
+
+  my @results;
+  my @queue = @$tree[($tree->[0] eq 'root' ? 1 : 4) .. $#$tree];
+  while (my $current = shift @queue) {
+    next unless $current->[0] eq 'tag';
+
+    unshift @queue, @$current[4 .. $#$current];
+    next unless _match($group, $current, $tree);
+    $one ? return $current : push @results, $current;
+  }
+
+  return $one ? undef : \@results;
+}
+
+sub _selector {
+  my ($selector, $current) = @_;
+
+  for my $s (@$selector) {
+    my $type = $s->[0];
+
+    # Tag
+    if ($type eq 'tag') { return undef unless $current->[1] =~ $s->[1] }
+
+    # Attribute
+    elsif ($type eq 'attr') { return undef unless _attr(@$s[1, 2], $current) }
+
+    # Pseudo-class
+    elsif ($type eq 'pc') { return undef unless _pc(@$s[1, 2], $current) }
+  }
+
+  return 1;
+}
+
+sub _sibling {
+  my ($selectors, $current, $tree, $immediate, $pos) = @_;
+
+  my $found;
+  for my $sibling (@{_siblings($current)}) {
+    return $found if $sibling eq $current;
+
+    # "+" (immediately preceding sibling)
+    if ($immediate) { $found = _combinator($selectors, $sibling, $tree, $pos) }
+
+    # "~" (preceding sibling)
+    else { return 1 if _combinator($selectors, $sibling, $tree, $pos) }
+  }
+
+  return undef;
+}
+
+sub _siblings {
+  my ($current, $type) = @_;
+
+  my $parent = $current->[3];
+  my @siblings = grep { $_->[0] eq 'tag' }
+    @$parent[($parent->[0] eq 'root' ? 1 : 4) .. $#$parent];
+  @siblings = grep { $type eq $_->[1] } @siblings if defined $type;
+
+  return \@siblings;
+}
+
+sub _unescape {
+  my $value = shift;
+
+  # Remove escaped newlines
+  $value =~ s/\\\n//g;
+
+  # Unescape Unicode characters
+  $value =~ s/\\([0-9a-fA-F]{1,6})\s?/pack 'U', hex $1/ge;
+
+  # Remove backslash
+  $value =~ s/\\//g;
+
+  return $value;
+}
+
+sub _value {
+  my ($op, $value, $insensitive) = @_;
+  return undef unless defined $value;
+  $value = ($insensitive ? '(?i)' : '') . quotemeta _unescape($value);
+
+  # "~=" (word)
+  return qr/(?:^|\s+)$value(?:\s+|$)/ if $op eq '~';
+
+  # "*=" (contains)
+  return qr/$value/ if $op eq '*';
+
+  # "^=" (begins with)
+  return qr/^$value/ if $op eq '^';
+
+  # "$=" (ends with)
+  return qr/$value$/ if $op eq '$';
+
+  # Everything else
+  return qr/^$value$/;
+}
+
+1;
+
+=encoding utf8
+
+=head1 NAME
+
+DOM::Tiny::CSS - CSS selector engine
+
+=head1 SYNOPSIS
+
+  use DOM::Tiny::CSS;
+
+  # Select elements from DOM tree
+  my $css = DOM::Tiny::CSS->new(tree => $tree);
+  my $elements = $css->select('h1, h2, h3');
+
+=head1 DESCRIPTION
+
+L<DOM::Tiny::CSS> is the CSS selector engine used by L<DOM::Tiny> based on
+L<Mojo::DOM::CSS>, which is based on L<Selectors Level 3|http://www.w3.org/TR/css3-selectors/>.
+
+=head1 SELECTORS
+
+All CSS selectors that make sense for a standalone parser are supported.
+
+=head2 *
+
+Any element.
+
+  my $all = $css->select('*');
+
+=head2 E
+
+An element of type C<E>.
+
+  my $title = $css->select('title');
+
+=head2 E[foo]
+
+An C<E> element with a C<foo> attribute.
+
+  my $links = $css->select('a[href]');
+
+=head2 E[foo="bar"]
+
+An C<E> element whose C<foo> attribute value is exactly equal to C<bar>.
+
+  my $case_sensitive = $css->select('input[type="hidden"]');
+  my $case_sensitive = $css->select('input[type=hidden]');
+
+=head2 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 = $css->select('input[type="hidden" i]');
+  my $case_insensitive = $css->select('input[type=hidden i]');
+  my $case_insensitive = $css->select('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.
+
+=head2 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 = $css->select('input[class~="foo"]');
+  my $foo = $css->select('input[class~=foo]');
+
+=head2 E[foo^="bar"]
+
+An C<E> element whose C<foo> attribute value begins exactly with the string
+C<bar>.
+
+  my $begins_with = $css->select('input[name^="f"]');
+  my $begins_with = $css->select('input[name^=f]');
+
+=head2 E[foo$="bar"]
+
+An C<E> element whose C<foo> attribute value ends exactly with the string
+C<bar>.
+
+  my $ends_with = $css->select('input[name$="o"]');
+  my $ends_with = $css->select('input[name$=o]');
+
+=head2 E[foo*="bar"]
+
+An C<E> element whose C<foo> attribute value contains the substring C<bar>.
+
+  my $contains = $css->select('input[name*="fo"]');
+  my $contains = $css->select('input[name*=fo]');
+
+=head2 E:root
+
+An C<E> element, root of the document.
+
+  my $root = $css->select(':root');
+
+=head2 E:nth-child(n)
+
+An C<E> element, the C<n-th> child of its parent.
+
+  my $third = $css->select('div:nth-child(3)');
+  my $odd   = $css->select('div:nth-child(odd)');
+  my $even  = $css->select('div:nth-child(even)');
+  my $top3  = $css->select('div:nth-child(-n+3)');
+
+=head2 E:nth-last-child(n)
+
+An C<E> element, the C<n-th> child of its parent, counting from the last one.
+
+  my $third    = $css->select('div:nth-last-child(3)');
+  my $odd      = $css->select('div:nth-last-child(odd)');
+  my $even     = $css->select('div:nth-last-child(even)');
+  my $bottom3  = $css->select('div:nth-last-child(-n+3)');
+
+=head2 E:nth-of-type(n)
+
+An C<E> element, the C<n-th> sibling of its type.
+
+  my $third = $css->select('div:nth-of-type(3)');
+  my $odd   = $css->select('div:nth-of-type(odd)');
+  my $even  = $css->select('div:nth-of-type(even)');
+  my $top3  = $css->select('div:nth-of-type(-n+3)');
+
+=head2 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    = $css->select('div:nth-last-of-type(3)');
+  my $odd      = $css->select('div:nth-last-of-type(odd)');
+  my $even     = $css->select('div:nth-last-of-type(even)');
+  my $bottom3  = $css->select('div:nth-last-of-type(-n+3)');
+
+=head2 E:first-child
+
+An C<E> element, first child of its parent.
+
+  my $first = $css->select('div p:first-child');
+
+=head2 E:last-child
+
+An C<E> element, last child of its parent.
+
+  my $last = $css->select('div p:last-child');
+
+=head2 E:first-of-type
+
+An C<E> element, first sibling of its type.
+
+  my $first = $css->select('div p:first-of-type');
+
+=head2 E:last-of-type
+
+An C<E> element, last sibling of its type.
+
+  my $last = $css->select('div p:last-of-type');
+
+=head2 E:only-child
+
+An C<E> element, only child of its parent.
+
+  my $lonely = $css->select('div p:only-child');
+
+=head2 E:only-of-type
+
+An C<E> element, only sibling of its type.
+
+  my $lonely = $css->select('div p:only-of-type');
+
+=head2 E:empty
+
+An C<E> element that has no children (including text nodes).
+
+  my $empty = $css->select(':empty');
+
+=head2 E:checked
+
+A user interface element C<E> which is checked (for instance a radio-button or
+checkbox).
+
+  my $input = $css->select(':checked');
+
+=head2 E.warning
+
+An C<E> element whose class is "warning".
+
+  my $warning = $css->select('div.warning');
+
+=head2 E#myid
+
+An C<E> element with C<ID> equal to "myid".
+
+  my $foo = $css->select('div#foo');
+
+=head2 E:not(s)
+
+An C<E> element that does not match simple selector C<s>.
+
+  my $others = $css->select('div p:not(:first-child)');
+
+=head2 E F
+
+An C<F> element descendant of an C<E> element.
+
+  my $headlines = $css->select('div h1');
+
+=head2 E E<gt> F
+
+An C<F> element child of an C<E> element.
+
+  my $headlines = $css->select('html > body > div > h1');
+
+=head2 E + F
+
+An C<F> element immediately preceded by an C<E> element.
+
+  my $second = $css->select('h1 + h2');
+
+=head2 E ~ F
+
+An C<F> element preceded by an C<E> element.
+
+  my $second = $css->select('h1 ~ h2');
+
+=head2 E, F, G
+
+Elements of type C<E>, C<F> and C<G>.
+
+  my $headlines = $css->select('h1, h2, h3');
+
+=head2 E[foo=bar][bar=baz]
+
+An C<E> element whose attributes match all following attribute selectors.
+
+  my $links = $css->select('a[foo^=b][foo$=ar]');
+
+=head1 ATTRIBUTES
+
+L<DOM::Tiny::CSS> implements the following attributes.
+
+=head2 tree
+
+  my $tree = $css->tree;
+  $css     = $css->tree(['root']);
+
+Document Object Model. Note that this structure should only be used very
+carefully since it is very dynamic.
+
+=head1 METHODS
+
+L<DOM::Tiny::CSS> implements the following methods.
+
+=head2 matches
+
+  my $bool = $css->matches('head > title');
+
+Check if first node in L</"tree"> matches the CSS selector.
+
+=head2 select
+
+  my $results = $css->select('head > title');
+
+Run CSS selector against L</"tree">.
+
+=head2 select_one
+
+  my $result = $css->select_one('head > title');
+
+Run CSS selector against L</"tree"> and stop as soon as the first node matched.
+
+=head1 BUGS
+
+Report any issues on the public bugtracker.
+
+=head1 AUTHOR
+
+Dan Book <dbook@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is Copyright (c) 2015 by Dan Book.
+
+This is free software, licensed under:
+
+  The Artistic License 2.0 (GPL Compatible)
+
+=head1 SEE ALSO
+
+L<Mojo::DOM::CSS>
diff --git a/lib/DOM/Tiny/Collection.pm b/lib/DOM/Tiny/Collection.pm
new file mode 100644 (file)
index 0000000..857a92b
--- /dev/null
@@ -0,0 +1,388 @@
+package DOM::Tiny::Collection;
+
+use strict;
+use warnings;
+use Carp 'croak';
+use Exporter 'import';
+use List::Util;
+use Scalar::Util 'blessed';
+
+our @EXPORT_OK = ('c');
+
+sub TO_JSON { [@{shift()}] }
+
+sub c { __PACKAGE__->new(@_) }
+
+sub compact {
+  my $self = shift;
+  return $self->new(grep { defined && (ref || length) } @$self);
+}
+
+sub each {
+  my ($self, $cb) = @_;
+  return @$self unless $cb;
+  my $i = 1;
+  $_->$cb($i++) for @$self;
+  return $self;
+}
+
+sub first {
+  my ($self, $cb) = (shift, shift);
+  return $self->[0] unless $cb;
+  return List::Util::first { $_ =~ $cb } @$self if ref $cb eq 'Regexp';
+  return List::Util::first { $_->$cb(@_) } @$self;
+}
+
+sub flatten { $_[0]->new(_flatten(@{$_[0]})) }
+
+sub grep {
+  my ($self, $cb) = (shift, shift);
+  return $self->new(grep { $_ =~ $cb } @$self) if ref $cb eq 'Regexp';
+  return $self->new(grep { $_->$cb(@_) } @$self);
+}
+
+sub join {
+  join $_[1] // '', map {"$_"} @{$_[0]};
+}
+
+sub last { shift->[-1] }
+
+sub map {
+  my ($self, $cb) = (shift, shift);
+  return $self->new(map { $_->$cb(@_) } @$self);
+}
+
+sub new {
+  my $class = shift;
+  return bless [@_], ref $class || $class;
+}
+
+sub reduce {
+  my $self = shift;
+  @_ = (@_, @$self);
+  goto &List::Util::reduce;
+}
+
+sub reverse { $_[0]->new(reverse @{$_[0]}) }
+
+sub shuffle { $_[0]->new(List::Util::shuffle @{$_[0]}) }
+
+sub size { scalar @{$_[0]} }
+
+sub slice {
+  my $self = shift;
+  return $self->new(@$self[@_]);
+}
+
+sub sort {
+  my ($self, $cb) = @_;
+
+  return $self->new(sort @$self) unless $cb;
+
+  my $caller = caller;
+  no strict 'refs';
+  my @sorted = sort {
+    local (*{"${caller}::a"}, *{"${caller}::b"}) = (\$a, \$b);
+    $a->$cb($b);
+  } @$self;
+  return $self->new(@sorted);
+}
+
+sub tap {
+  my ($self, $cb) = (shift, shift);
+  $_->$cb(@_) for $self;
+  return $self;
+}
+
+sub to_array { [@{shift()}] }
+
+sub uniq {
+  my ($self, $cb) = (shift, shift);
+  my %seen;
+  return $self->new(grep { !$seen{$_->$cb(@_)}++ } @$self) if $cb;
+  return $self->new(grep { !$seen{$_}++ } @$self);
+}
+
+sub _flatten {
+  map { _ref($_) ? _flatten(@$_) : $_ } @_;
+}
+
+sub _ref { ref $_[0] eq 'ARRAY' || blessed $_[0] && $_[0]->isa(__PACKAGE__) }
+
+1;
+
+=encoding utf8
+
+=head1 NAME
+
+DOM::Tiny::Collection - Collection
+
+=head1 SYNOPSIS
+
+  use Mojo::Collection;
+
+  # Manipulate collection
+  my $collection = Mojo::Collection->new(qw(just works));
+  unshift @$collection, 'it';
+  say $collection->join("\n");
+
+  # Chain methods
+  $collection->map(sub { ucfirst })->shuffle->each(sub {
+    my ($word, $num) = @_;
+    say "$num: $word";
+  });
+
+  # Use the alternative constructor
+  use Mojo::Collection 'c';
+  c(qw(a b c))->join('/')->url_escape->say;
+
+=head1 DESCRIPTION
+
+L<Mojo::Collection> is an array-based container for collections.
+
+  # Access array directly to manipulate collection
+  my $collection = Mojo::Collection->new(1 .. 25);
+  $collection->[23] += 100;
+  say for @$collection;
+
+=head1 FUNCTIONS
+
+L<Mojo::Collection> implements the following functions, which can be imported
+individually.
+
+=head2 c
+
+  my $collection = c(1, 2, 3);
+
+Construct a new array-based L<Mojo::Collection> object.
+
+=head1 METHODS
+
+L<Mojo::Collection> implements the following methods.
+
+=head2 TO_JSON
+
+  my $array = $collection->TO_JSON;
+
+Alias for L</"to_array">.
+
+=head2 compact
+
+  my $new = $collection->compact;
+
+Create a new collection with all elements that are defined and not an empty
+string.
+
+  # "0, 1, 2, 3"
+  Mojo::Collection->new(0, 1, undef, 2, '', 3)->compact->join(', ');
+
+=head2 each
+
+  my @elements = $collection->each;
+  $collection  = $collection->each(sub {...});
+
+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<$_>.
+
+  # Make a numbered list
+  $collection->each(sub {
+    my ($e, $num) = @_;
+    say "$num: $e";
+  });
+
+=head2 first
+
+  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);
+
+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<$_>.
+
+  # Longer version
+  my $first = $collection->first(sub { $_->$method(@args) });
+
+  # Find first value that contains the word "mojo"
+  my $interesting = $collection->first(qr/mojo/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.
+
+  # "1, 2, 3, 4, 5, 6, 7"
+  Mojo::Collection->new(1, [2, [3, 4], 5, [6]], 7)->flatten->join(', ');
+
+=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 "mojo"
+  my $interesting = $collection->grep(qr/mojo/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(', ')->say;
+
+=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 "mojo" to all values
+  my $mojoified = $collection->map(sub { $_ . 'mojo' });
+
+=head2 new
+
+  my $collection = Mojo::Collection->new(1, 2, 3);
+
+Construct a new array-based L<Mojo::Collection> object.
+
+=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.
+
+  # "B C E"
+  Mojo::Collection->new('A', 'B', 'C', 'D', 'E')->slice(1, 2, 4)->join(' ');
+
+=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 {...});
+
+Alias for 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) });
+
+  # "foo bar baz"
+  Mojo::Collection->new('foo', 'bar', 'bar', 'baz')->uniq->join(' ');
+
+  # "[[1, 2], [2, 1]]"
+  Mojo::Collection->new([1, 2], [2, 1], [3, 2])->uniq(sub{ $_->[1] })->to_array;
+
+=head1 BUGS
+
+Report any issues on the public bugtracker.
+
+=head1 AUTHOR
+
+Dan Book <dbook@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is Copyright (c) 2015 by Dan Book.
+
+This is free software, licensed under:
+
+  The Artistic License 2.0 (GPL Compatible)
+
+=head1 SEE ALSO
+
+L<Mojo::Collection>
diff --git a/lib/DOM/Tiny/Entities.pm b/lib/DOM/Tiny/Entities.pm
new file mode 100644 (file)
index 0000000..54208f6
--- /dev/null
@@ -0,0 +1,2237 @@
+package DOM::Tiny::Entities;
+
+use strict;
+use warnings;
+use utf8;
+use Exporter 'import';
+
+our @EXPORT_OK = qw(html_unescape xml_escape);
+
+# To generate a new HTML entity table run this command
+# perl examples/entities.pl
+my %ENTITIES;
+for my $line (split "\n", join('', <DATA>)) {
+  next unless $line =~ /^(\S+)\s+U\+(\S+)(?:\s+U\+(\S+))?/;
+  $ENTITIES{$1} = defined $3 ? (chr(hex $2) . chr(hex $3)) : chr(hex $2);
+}
+
+# Characters that should be escaped in XML
+my %XML = (
+  '&'  => '&amp;',
+  '<'  => '&lt;',
+  '>'  => '&gt;',
+  '"'  => '&quot;',
+  '\'' => '&#39;'
+);
+
+sub html_unescape {
+  my $str = shift;
+  $str =~ s/&(?:\#((?:\d{1,7}|x[0-9a-fA-F]{1,6}));|(\w+;))/_decode($1, $2)/ge;
+  return $str;
+}
+
+sub xml_escape {
+  my $str = shift;
+  $str =~ s/([&<>"'])/$XML{$1}/ge;
+  return $str;
+}
+
+sub _decode {
+  my ($point, $name) = @_;
+  
+  # Code point
+  return chr($point !~ /^x/ ? $point : hex $point) unless defined $name;
+  
+  # Named character reference
+  return exists $ENTITIES{$name} ? $ENTITIES{$name} : "&$name";
+}
+
+1;
+
+=encoding utf8
+
+=head1 NAME
+
+DOM::Tiny::Entities - Encode or decode HTML entities in strings
+
+=head1 SYNOPSIS
+
+  use DOM::Tiny::Entities qw(html_unescape xml_escape);
+  
+  my $str = 'foo &amp; bar';
+  $str = html_unescape $str; # "foo & bar"
+  $str = xml_escape $str; # "foo &amp; bar"
+
+=head1 DESCRIPTION
+
+L<DOM::Tiny::Entities> contains functions for escaping and unescaping HTML
+entities for L<DOM::Tiny>, based on functions from L<Mojo::Util>. All functions
+are exported on demand.
+
+=head1 FUNCTIONS
+
+=head2 html_unescape
+
+ my $str = html_unescape $escaped;
+
+Unescape all HTML entities in string, according to the
+L<HTML Living Standard|https://html.spec.whatwg.org/#named-character-references-table>.
+
+ html_unescape '&lt;div&gt; # "<div>"
+
+=head2 xml_escape
+
+ my $escaped = xml_escape $str;
+
+Escape unsafe characters C<&>, C<< < >>, C<< > >>, C<">, and C<'> in string.
+
+ xml_escape '<div>'; # "&lt;div&gt;"
+
+=head1 BUGS
+
+Report any issues on the public bugtracker.
+
+=head1 AUTHOR
+
+Dan Book <dbook@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is Copyright (c) 2015 by Dan Book.
+
+This is free software, licensed under:
+
+  The Artistic License 2.0 (GPL Compatible)
+
+=head1 SEE ALSO
+
+L<HTML::Entities>
+
+=cut
+
+__DATA__
+Aacute; U+000C1
+aacute; U+000E1
+Abreve; U+00102
+abreve; U+00103
+ac; U+0223E
+acd; U+0223F
+acE; U+0223E U+00333
+Acirc; U+000C2
+acirc; U+000E2
+acute; U+000B4
+Acy; U+00410
+acy; U+00430
+AElig; U+000C6
+aelig; U+000E6
+af; U+02061
+Afr; U+1D504
+afr; U+1D51E
+Agrave; U+000C0
+agrave; U+000E0
+alefsym; U+02135
+aleph; U+02135
+Alpha; U+00391
+alpha; U+003B1
+Amacr; U+00100
+amacr; U+00101
+amalg; U+02A3F
+AMP; U+00026
+amp; U+00026
+And; U+02A53
+and; U+02227
+andand; U+02A55
+andd; U+02A5C
+andslope; U+02A58
+andv; U+02A5A
+ang; U+02220
+ange; U+029A4
+angle; U+02220
+angmsd; U+02221
+angmsdaa; U+029A8
+angmsdab; U+029A9
+angmsdac; U+029AA
+angmsdad; U+029AB
+angmsdae; U+029AC
+angmsdaf; U+029AD
+angmsdag; U+029AE
+angmsdah; U+029AF
+angrt; U+0221F
+angrtvb; U+022BE
+angrtvbd; U+0299D
+angsph; U+02222
+angst; U+000C5
+angzarr; U+0237C
+Aogon; U+00104
+aogon; U+00105
+Aopf; U+1D538
+aopf; U+1D552
+ap; U+02248
+apacir; U+02A6F
+apE; U+02A70
+ape; U+0224A
+apid; U+0224B
+apos; U+00027
+ApplyFunction; U+02061
+approx; U+02248
+approxeq; U+0224A
+Aring; U+000C5
+aring; U+000E5
+Ascr; U+1D49C
+ascr; U+1D4B6
+Assign; U+02254
+ast; U+0002A
+asymp; U+02248
+asympeq; U+0224D
+Atilde; U+000C3
+atilde; U+000E3
+Auml; U+000C4
+auml; U+000E4
+awconint; U+02233
+awint; U+02A11
+backcong; U+0224C
+backepsilon; U+003F6
+backprime; U+02035
+backsim; U+0223D
+backsimeq; U+022CD
+Backslash; U+02216
+Barv; U+02AE7
+barvee; U+022BD
+Barwed; U+02306
+barwed; U+02305
+barwedge; U+02305
+bbrk; U+023B5
+bbrktbrk; U+023B6
+bcong; U+0224C
+Bcy; U+00411
+bcy; U+00431
+bdquo; U+0201E
+becaus; U+02235
+Because; U+02235
+because; U+02235
+bemptyv; U+029B0
+bepsi; U+003F6
+bernou; U+0212C
+Bernoullis; U+0212C
+Beta; U+00392
+beta; U+003B2
+beth; U+02136
+between; U+0226C
+Bfr; U+1D505
+bfr; U+1D51F
+bigcap; U+022C2
+bigcirc; U+025EF
+bigcup; U+022C3
+bigodot; U+02A00
+bigoplus; U+02A01
+bigotimes; U+02A02
+bigsqcup; U+02A06
+bigstar; U+02605
+bigtriangledown; U+025BD
+bigtriangleup; U+025B3
+biguplus; U+02A04
+bigvee; U+022C1
+bigwedge; U+022C0
+bkarow; U+0290D
+blacklozenge; U+029EB
+blacksquare; U+025AA
+blacktriangle; U+025B4
+blacktriangledown; U+025BE
+blacktriangleleft; U+025C2
+blacktriangleright; U+025B8
+blank; U+02423
+blk12; U+02592
+blk14; U+02591
+blk34; U+02593
+block; U+02588
+bne; U+0003D U+020E5
+bnequiv; U+02261 U+020E5
+bNot; U+02AED
+bnot; U+02310
+Bopf; U+1D539
+bopf; U+1D553
+bot; U+022A5
+bottom; U+022A5
+bowtie; U+022C8
+boxbox; U+029C9
+boxDL; U+02557
+boxDl; U+02556
+boxdL; U+02555
+boxdl; U+02510
+boxDR; U+02554
+boxDr; U+02553
+boxdR; U+02552
+boxdr; U+0250C
+boxH; U+02550
+boxh; U+02500
+boxHD; U+02566
+boxHd; U+02564
+boxhD; U+02565
+boxhd; U+0252C
+boxHU; U+02569
+boxHu; U+02567
+boxhU; U+02568
+boxhu; U+02534
+boxminus; U+0229F
+boxplus; U+0229E
+boxtimes; U+022A0
+boxUL; U+0255D
+boxUl; U+0255C
+boxuL; U+0255B
+boxul; U+02518
+boxUR; U+0255A
+boxUr; U+02559
+boxuR; U+02558
+boxur; U+02514
+boxV; U+02551
+boxv; U+02502
+boxVH; U+0256C
+boxVh; U+0256B
+boxvH; U+0256A
+boxvh; U+0253C
+boxVL; U+02563
+boxVl; U+02562
+boxvL; U+02561
+boxvl; U+02524
+boxVR; U+02560
+boxVr; U+0255F
+boxvR; U+0255E
+boxvr; U+0251C
+bprime; U+02035
+Breve; U+002D8
+breve; U+002D8
+brvbar; U+000A6
+Bscr; U+0212C
+bscr; U+1D4B7
+bsemi; U+0204F
+bsim; U+0223D
+bsime; U+022CD
+bsol; U+0005C
+bsolb; U+029C5
+bsolhsub; U+027C8
+bull; U+02022
+bullet; U+02022
+bump; U+0224E
+bumpE; U+02AAE
+bumpe; U+0224F
+Bumpeq; U+0224E
+bumpeq; U+0224F
+Cacute; U+00106
+cacute; U+00107
+Cap; U+022D2
+cap; U+02229
+capand; U+02A44
+capbrcup; U+02A49
+capcap; U+02A4B
+capcup; U+02A47
+capdot; U+02A40
+CapitalDifferentialD; U+02145
+caps; U+02229 U+0FE00
+caret; U+02041
+caron; U+002C7
+Cayleys; U+0212D
+ccaps; U+02A4D
+Ccaron; U+0010C
+ccaron; U+0010D
+Ccedil; U+000C7
+ccedil; U+000E7
+Ccirc; U+00108
+ccirc; U+00109
+Cconint; U+02230
+ccups; U+02A4C
+ccupssm; U+02A50
+Cdot; U+0010A
+cdot; U+0010B
+cedil; U+000B8
+Cedilla; U+000B8
+cemptyv; U+029B2
+cent; U+000A2
+CenterDot; U+000B7
+centerdot; U+000B7
+Cfr; U+0212D
+cfr; U+1D520
+CHcy; U+00427
+chcy; U+00447
+check; U+02713
+checkmark; U+02713
+Chi; U+003A7
+chi; U+003C7
+cir; U+025CB
+circ; U+002C6
+circeq; U+02257
+circlearrowleft; U+021BA
+circlearrowright; U+021BB
+circledast; U+0229B
+circledcirc; U+0229A
+circleddash; U+0229D
+CircleDot; U+02299
+circledR; U+000AE
+circledS; U+024C8
+CircleMinus; U+02296
+CirclePlus; U+02295
+CircleTimes; U+02297
+cirE; U+029C3
+cire; U+02257
+cirfnint; U+02A10
+cirmid; U+02AEF
+cirscir; U+029C2
+ClockwiseContourIntegral; U+02232
+CloseCurlyDoubleQuote; U+0201D
+CloseCurlyQuote; U+02019
+clubs; U+02663
+clubsuit; U+02663
+Colon; U+02237
+colon; U+0003A
+Colone; U+02A74
+colone; U+02254
+coloneq; U+02254
+comma; U+0002C
+commat; U+00040
+comp; U+02201
+compfn; U+02218
+complement; U+02201
+complexes; U+02102
+cong; U+02245
+congdot; U+02A6D
+Congruent; U+02261
+Conint; U+0222F
+conint; U+0222E
+ContourIntegral; U+0222E
+Copf; U+02102
+copf; U+1D554
+coprod; U+02210
+Coproduct; U+02210
+COPY; U+000A9
+copy; U+000A9
+copysr; U+02117
+CounterClockwiseContourIntegral; U+02233
+crarr; U+021B5
+Cross; U+02A2F
+cross; U+02717
+Cscr; U+1D49E
+cscr; U+1D4B8
+csub; U+02ACF
+csube; U+02AD1
+csup; U+02AD0
+csupe; U+02AD2
+ctdot; U+022EF
+cudarrl; U+02938
+cudarrr; U+02935
+cuepr; U+022DE
+cuesc; U+022DF
+cularr; U+021B6
+cularrp; U+0293D
+Cup; U+022D3
+cup; U+0222A
+cupbrcap; U+02A48
+CupCap; U+0224D
+cupcap; U+02A46
+cupcup; U+02A4A
+cupdot; U+0228D
+cupor; U+02A45
+cups; U+0222A U+0FE00
+curarr; U+021B7
+curarrm; U+0293C
+curlyeqprec; U+022DE
+curlyeqsucc; U+022DF
+curlyvee; U+022CE
+curlywedge; U+022CF
+curren; U+000A4
+curvearrowleft; U+021B6
+curvearrowright; U+021B7
+cuvee; U+022CE
+cuwed; U+022CF
+cwconint; U+02232
+cwint; U+02231
+cylcty; U+0232D
+Dagger; U+02021
+dagger; U+02020
+daleth; U+02138
+Darr; U+021A1
+dArr; U+021D3
+darr; U+02193
+dash; U+02010
+Dashv; U+02AE4
+dashv; U+022A3
+dbkarow; U+0290F
+dblac; U+002DD
+Dcaron; U+0010E
+dcaron; U+0010F
+Dcy; U+00414
+dcy; U+00434
+DD; U+02145
+dd; U+02146
+ddagger; U+02021
+ddarr; U+021CA
+DDotrahd; U+02911
+ddotseq; U+02A77
+deg; U+000B0
+Del; U+02207
+Delta; U+00394
+delta; U+003B4
+demptyv; U+029B1
+dfisht; U+0297F
+Dfr; U+1D507
+dfr; U+1D521
+dHar; U+02965
+dharl; U+021C3
+dharr; U+021C2
+DiacriticalAcute; U+000B4
+DiacriticalDot; U+002D9
+DiacriticalDoubleAcute; U+002DD
+DiacriticalGrave; U+00060
+DiacriticalTilde; U+002DC
+diam; U+022C4
+Diamond; U+022C4
+diamond; U+022C4
+diamondsuit; U+02666
+diams; U+02666
+die; U+000A8
+DifferentialD; U+02146
+digamma; U+003DD
+disin; U+022F2
+div; U+000F7
+divide; U+000F7
+divideontimes; U+022C7
+divonx; U+022C7
+DJcy; U+00402
+djcy; U+00452
+dlcorn; U+0231E
+dlcrop; U+0230D
+dollar; U+00024
+Dopf; U+1D53B
+dopf; U+1D555
+Dot; U+000A8
+dot; U+002D9
+DotDot; U+020DC
+doteq; U+02250
+doteqdot; U+02251
+DotEqual; U+02250
+dotminus; U+02238
+dotplus; U+02214
+dotsquare; U+022A1
+doublebarwedge; U+02306
+DoubleContourIntegral; U+0222F
+DoubleDot; U+000A8
+DoubleDownArrow; U+021D3
+DoubleLeftArrow; U+021D0
+DoubleLeftRightArrow; U+021D4
+DoubleLeftTee; U+02AE4
+DoubleLongLeftArrow; U+027F8
+DoubleLongLeftRightArrow; U+027FA
+DoubleLongRightArrow; U+027F9
+DoubleRightArrow; U+021D2
+DoubleRightTee; U+022A8
+DoubleUpArrow; U+021D1
+DoubleUpDownArrow; U+021D5
+DoubleVerticalBar; U+02225
+DownArrow; U+02193
+Downarrow; U+021D3
+downarrow; U+02193
+DownArrowBar; U+02913
+DownArrowUpArrow; U+021F5
+DownBreve; U+00311
+downdownarrows; U+021CA
+downharpoonleft; U+021C3
+downharpoonright; U+021C2
+DownLeftRightVector; U+02950
+DownLeftTeeVector; U+0295E
+DownLeftVector; U+021BD
+DownLeftVectorBar; U+02956
+DownRightTeeVector; U+0295F
+DownRightVector; U+021C1
+DownRightVectorBar; U+02957
+DownTee; U+022A4
+DownTeeArrow; U+021A7
+drbkarow; U+02910
+drcorn; U+0231F
+drcrop; U+0230C
+Dscr; U+1D49F
+dscr; U+1D4B9
+DScy; U+00405
+dscy; U+00455
+dsol; U+029F6
+Dstrok; U+00110
+dstrok; U+00111
+dtdot; U+022F1
+dtri; U+025BF
+dtrif; U+025BE
+duarr; U+021F5
+duhar; U+0296F
+dwangle; U+029A6
+DZcy; U+0040F
+dzcy; U+0045F
+dzigrarr; U+027FF
+Eacute; U+000C9
+eacute; U+000E9
+easter; U+02A6E
+Ecaron; U+0011A
+ecaron; U+0011B
+ecir; U+02256
+Ecirc; U+000CA
+ecirc; U+000EA
+ecolon; U+02255
+Ecy; U+0042D
+ecy; U+0044D
+eDDot; U+02A77
+Edot; U+00116
+eDot; U+02251
+edot; U+00117
+ee; U+02147
+efDot; U+02252
+Efr; U+1D508
+efr; U+1D522
+eg; U+02A9A
+Egrave; U+000C8
+egrave; U+000E8
+egs; U+02A96
+egsdot; U+02A98
+el; U+02A99
+Element; U+02208
+elinters; U+023E7
+ell; U+02113
+els; U+02A95
+elsdot; U+02A97
+Emacr; U+00112
+emacr; U+00113
+empty; U+02205
+emptyset; U+02205
+EmptySmallSquare; U+025FB
+emptyv; U+02205
+EmptyVerySmallSquare; U+025AB
+emsp; U+02003
+emsp13; U+02004
+emsp14; U+02005
+ENG; U+0014A
+eng; U+0014B
+ensp; U+02002
+Eogon; U+00118
+eogon; U+00119
+Eopf; U+1D53C
+eopf; U+1D556
+epar; U+022D5
+eparsl; U+029E3
+eplus; U+02A71
+epsi; U+003B5
+Epsilon; U+00395
+epsilon; U+003B5
+epsiv; U+003F5
+eqcirc; U+02256
+eqcolon; U+02255
+eqsim; U+02242
+eqslantgtr; U+02A96
+eqslantless; U+02A95
+Equal; U+02A75
+equals; U+0003D
+EqualTilde; U+02242
+equest; U+0225F
+Equilibrium; U+021CC
+equiv; U+02261
+equivDD; U+02A78
+eqvparsl; U+029E5
+erarr; U+02971
+erDot; U+02253
+Escr; U+02130
+escr; U+0212F
+esdot; U+02250
+Esim; U+02A73
+esim; U+02242
+Eta; U+00397
+eta; U+003B7
+ETH; U+000D0
+eth; U+000F0
+Euml; U+000CB
+euml; U+000EB
+euro; U+020AC
+excl; U+00021
+exist; U+02203
+Exists; U+02203
+expectation; U+02130
+ExponentialE; U+02147
+exponentiale; U+02147
+fallingdotseq; U+02252
+Fcy; U+00424
+fcy; U+00444
+female; U+02640
+ffilig; U+0FB03
+fflig; U+0FB00
+ffllig; U+0FB04
+Ffr; U+1D509
+ffr; U+1D523
+filig; U+0FB01
+FilledSmallSquare; U+025FC
+FilledVerySmallSquare; U+025AA
+fjlig; U+00066 U+0006A
+flat; U+0266D
+fllig; U+0FB02
+fltns; U+025B1
+fnof; U+00192
+Fopf; U+1D53D
+fopf; U+1D557
+ForAll; U+02200
+forall; U+02200
+fork; U+022D4
+forkv; U+02AD9
+Fouriertrf; U+02131
+fpartint; U+02A0D
+frac12; U+000BD
+frac13; U+02153
+frac14; U+000BC
+frac15; U+02155
+frac16; U+02159
+frac18; U+0215B
+frac23; U+02154
+frac25; U+02156
+frac34; U+000BE
+frac35; U+02157
+frac38; U+0215C
+frac45; U+02158
+frac56; U+0215A
+frac58; U+0215D
+frac78; U+0215E
+frasl; U+02044
+frown; U+02322
+Fscr; U+02131
+fscr; U+1D4BB
+gacute; U+001F5
+Gamma; U+00393
+gamma; U+003B3
+Gammad; U+003DC
+gammad; U+003DD
+gap; U+02A86
+Gbreve; U+0011E
+gbreve; U+0011F
+Gcedil; U+00122
+Gcirc; U+0011C
+gcirc; U+0011D
+Gcy; U+00413
+gcy; U+00433
+Gdot; U+00120
+gdot; U+00121
+gE; U+02267
+ge; U+02265
+gEl; U+02A8C
+gel; U+022DB
+geq; U+02265
+geqq; U+02267
+geqslant; U+02A7E
+ges; U+02A7E
+gescc; U+02AA9
+gesdot; U+02A80
+gesdoto; U+02A82
+gesdotol; U+02A84
+gesl; U+022DB U+0FE00
+gesles; U+02A94
+Gfr; U+1D50A
+gfr; U+1D524
+Gg; U+022D9
+gg; U+0226B
+ggg; U+022D9
+gimel; U+02137
+GJcy; U+00403
+gjcy; U+00453
+gl; U+02277
+gla; U+02AA5
+glE; U+02A92
+glj; U+02AA4
+gnap; U+02A8A
+gnapprox; U+02A8A
+gnE; U+02269
+gne; U+02A88
+gneq; U+02A88
+gneqq; U+02269
+gnsim; U+022E7
+Gopf; U+1D53E
+gopf; U+1D558
+grave; U+00060
+GreaterEqual; U+02265
+GreaterEqualLess; U+022DB
+GreaterFullEqual; U+02267
+GreaterGreater; U+02AA2
+GreaterLess; U+02277
+GreaterSlantEqual; U+02A7E
+GreaterTilde; U+02273
+Gscr; U+1D4A2
+gscr; U+0210A
+gsim; U+02273
+gsime; U+02A8E
+gsiml; U+02A90
+GT; U+0003E
+Gt; U+0226B
+gt; U+0003E
+gtcc; U+02AA7
+gtcir; U+02A7A
+gtdot; U+022D7
+gtlPar; U+02995
+gtquest; U+02A7C
+gtrapprox; U+02A86
+gtrarr; U+02978
+gtrdot; U+022D7
+gtreqless; U+022DB
+gtreqqless; U+02A8C
+gtrless; U+02277
+gtrsim; U+02273
+gvertneqq; U+02269 U+0FE00
+gvnE; U+02269 U+0FE00
+Hacek; U+002C7
+hairsp; U+0200A
+half; U+000BD
+hamilt; U+0210B
+HARDcy; U+0042A
+hardcy; U+0044A
+hArr; U+021D4
+harr; U+02194
+harrcir; U+02948
+harrw; U+021AD
+Hat; U+0005E
+hbar; U+0210F
+Hcirc; U+00124
+hcirc; U+00125
+hearts; U+02665
+heartsuit; U+02665
+hellip; U+02026
+hercon; U+022B9
+Hfr; U+0210C
+hfr; U+1D525
+HilbertSpace; U+0210B
+hksearow; U+02925
+hkswarow; U+02926
+hoarr; U+021FF
+homtht; U+0223B
+hookleftarrow; U+021A9
+hookrightarrow; U+021AA
+Hopf; U+0210D
+hopf; U+1D559
+horbar; U+02015
+HorizontalLine; U+02500
+Hscr; U+0210B
+hscr; U+1D4BD
+hslash; U+0210F
+Hstrok; U+00126
+hstrok; U+00127
+HumpDownHump; U+0224E
+HumpEqual; U+0224F
+hybull; U+02043
+hyphen; U+02010
+Iacute; U+000CD
+iacute; U+000ED
+ic; U+02063
+Icirc; U+000CE
+icirc; U+000EE
+Icy; U+00418
+icy; U+00438
+Idot; U+00130
+IEcy; U+00415
+iecy; U+00435
+iexcl; U+000A1
+iff; U+021D4
+Ifr; U+02111
+ifr; U+1D526
+Igrave; U+000CC
+igrave; U+000EC
+ii; U+02148
+iiiint; U+02A0C
+iiint; U+0222D
+iinfin; U+029DC
+iiota; U+02129
+IJlig; U+00132
+ijlig; U+00133
+Im; U+02111
+Imacr; U+0012A
+imacr; U+0012B
+image; U+02111
+ImaginaryI; U+02148
+imagline; U+02110
+imagpart; U+02111
+imath; U+00131
+imof; U+022B7
+imped; U+001B5
+Implies; U+021D2
+in; U+02208
+incare; U+02105
+infin; U+0221E
+infintie; U+029DD
+inodot; U+00131
+Int; U+0222C
+int; U+0222B
+intcal; U+022BA
+integers; U+02124
+Integral; U+0222B
+intercal; U+022BA
+Intersection; U+022C2
+intlarhk; U+02A17
+intprod; U+02A3C
+InvisibleComma; U+02063
+InvisibleTimes; U+02062
+IOcy; U+00401
+iocy; U+00451
+Iogon; U+0012E
+iogon; U+0012F
+Iopf; U+1D540
+iopf; U+1D55A
+Iota; U+00399
+iota; U+003B9
+iprod; U+02A3C
+iquest; U+000BF
+Iscr; U+02110
+iscr; U+1D4BE
+isin; U+02208
+isindot; U+022F5
+isinE; U+022F9
+isins; U+022F4
+isinsv; U+022F3
+isinv; U+02208
+it; U+02062
+Itilde; U+00128
+itilde; U+00129
+Iukcy; U+00406
+iukcy; U+00456
+Iuml; U+000CF
+iuml; U+000EF
+Jcirc; U+00134
+jcirc; U+00135
+Jcy; U+00419
+jcy; U+00439
+Jfr; U+1D50D
+jfr; U+1D527
+jmath; U+00237
+Jopf; U+1D541
+jopf; U+1D55B
+Jscr; U+1D4A5
+jscr; U+1D4BF
+Jsercy; U+00408
+jsercy; U+00458
+Jukcy; U+00404
+jukcy; U+00454
+Kappa; U+0039A
+kappa; U+003BA
+kappav; U+003F0
+Kcedil; U+00136
+kcedil; U+00137
+Kcy; U+0041A
+kcy; U+0043A
+Kfr; U+1D50E
+kfr; U+1D528
+kgreen; U+00138
+KHcy; U+00425
+khcy; U+00445
+KJcy; U+0040C
+kjcy; U+0045C
+Kopf; U+1D542
+kopf; U+1D55C
+Kscr; U+1D4A6
+kscr; U+1D4C0
+lAarr; U+021DA
+Lacute; U+00139
+lacute; U+0013A
+laemptyv; U+029B4
+lagran; U+02112
+Lambda; U+0039B
+lambda; U+003BB
+Lang; U+027EA
+lang; U+027E8
+langd; U+02991
+langle; U+027E8
+lap; U+02A85
+Laplacetrf; U+02112
+laquo; U+000AB
+Larr; U+0219E
+lArr; U+021D0
+larr; U+02190
+larrb; U+021E4
+larrbfs; U+0291F
+larrfs; U+0291D
+larrhk; U+021A9
+larrlp; U+021AB
+larrpl; U+02939
+larrsim; U+02973
+larrtl; U+021A2
+lat; U+02AAB
+lAtail; U+0291B
+latail; U+02919
+late; U+02AAD
+lates; U+02AAD U+0FE00
+lBarr; U+0290E
+lbarr; U+0290C
+lbbrk; U+02772
+lbrace; U+0007B
+lbrack; U+0005B
+lbrke; U+0298B
+lbrksld; U+0298F
+lbrkslu; U+0298D
+Lcaron; U+0013D
+lcaron; U+0013E
+Lcedil; U+0013B
+lcedil; U+0013C
+lceil; U+02308
+lcub; U+0007B
+Lcy; U+0041B
+lcy; U+0043B
+ldca; U+02936
+ldquo; U+0201C
+ldquor; U+0201E
+ldrdhar; U+02967
+ldrushar; U+0294B
+ldsh; U+021B2
+lE; U+02266
+le; U+02264
+LeftAngleBracket; U+027E8
+LeftArrow; U+02190
+Leftarrow; U+021D0
+leftarrow; U+02190
+LeftArrowBar; U+021E4
+LeftArrowRightArrow; U+021C6
+leftarrowtail; U+021A2
+LeftCeiling; U+02308
+LeftDoubleBracket; U+027E6
+LeftDownTeeVector; U+02961
+LeftDownVector; U+021C3
+LeftDownVectorBar; U+02959
+LeftFloor; U+0230A
+leftharpoondown; U+021BD
+leftharpoonup; U+021BC
+leftleftarrows; U+021C7
+LeftRightArrow; U+02194
+Leftrightarrow; U+021D4
+leftrightarrow; U+02194
+leftrightarrows; U+021C6
+leftrightharpoons; U+021CB
+leftrightsquigarrow; U+021AD
+LeftRightVector; U+0294E
+LeftTee; U+022A3
+LeftTeeArrow; U+021A4
+LeftTeeVector; U+0295A
+leftthreetimes; U+022CB
+LeftTriangle; U+022B2
+LeftTriangleBar; U+029CF
+LeftTriangleEqual; U+022B4
+LeftUpDownVector; U+02951
+LeftUpTeeVector; U+02960
+LeftUpVector; U+021BF
+LeftUpVectorBar; U+02958
+LeftVector; U+021BC
+LeftVectorBar; U+02952
+lEg; U+02A8B
+leg; U+022DA
+leq; U+02264
+leqq; U+02266
+leqslant; U+02A7D
+les; U+02A7D
+lescc; U+02AA8
+lesdot; U+02A7F
+lesdoto; U+02A81
+lesdotor; U+02A83
+lesg; U+022DA U+0FE00
+lesges; U+02A93
+lessapprox; U+02A85
+lessdot; U+022D6
+lesseqgtr; U+022DA
+lesseqqgtr; U+02A8B
+LessEqualGreater; U+022DA
+LessFullEqual; U+02266
+LessGreater; U+02276
+lessgtr; U+02276
+LessLess; U+02AA1
+lesssim; U+02272
+LessSlantEqual; U+02A7D
+LessTilde; U+02272
+lfisht; U+0297C
+lfloor; U+0230A
+Lfr; U+1D50F
+lfr; U+1D529
+lg; U+02276
+lgE; U+02A91
+lHar; U+02962
+lhard; U+021BD
+lharu; U+021BC
+lharul; U+0296A
+lhblk; U+02584
+LJcy; U+00409
+ljcy; U+00459
+Ll; U+022D8
+ll; U+0226A
+llarr; U+021C7
+llcorner; U+0231E
+Lleftarrow; U+021DA
+llhard; U+0296B
+lltri; U+025FA
+Lmidot; U+0013F
+lmidot; U+00140
+lmoust; U+023B0
+lmoustache; U+023B0
+lnap; U+02A89
+lnapprox; U+02A89
+lnE; U+02268
+lne; U+02A87
+lneq; U+02A87
+lneqq; U+02268
+lnsim; U+022E6
+loang; U+027EC
+loarr; U+021FD
+lobrk; U+027E6
+LongLeftArrow; U+027F5
+Longleftarrow; U+027F8
+longleftarrow; U+027F5
+LongLeftRightArrow; U+027F7
+Longleftrightarrow; U+027FA
+longleftrightarrow; U+027F7
+longmapsto; U+027FC
+LongRightArrow; U+027F6
+Longrightarrow; U+027F9
+longrightarrow; U+027F6
+looparrowleft; U+021AB
+looparrowright; U+021AC
+lopar; U+02985
+Lopf; U+1D543
+lopf; U+1D55D
+loplus; U+02A2D
+lotimes; U+02A34
+lowast; U+02217
+lowbar; U+0005F
+LowerLeftArrow; U+02199
+LowerRightArrow; U+02198
+loz; U+025CA
+lozenge; U+025CA
+lozf; U+029EB
+lpar; U+00028
+lparlt; U+02993
+lrarr; U+021C6
+lrcorner; U+0231F
+lrhar; U+021CB
+lrhard; U+0296D
+lrm; U+0200E
+lrtri; U+022BF
+lsaquo; U+02039
+Lscr; U+02112
+lscr; U+1D4C1
+Lsh; U+021B0
+lsh; U+021B0
+lsim; U+02272
+lsime; U+02A8D
+lsimg; U+02A8F
+lsqb; U+0005B
+lsquo; U+02018
+lsquor; U+0201A
+Lstrok; U+00141
+lstrok; U+00142
+LT; U+0003C
+Lt; U+0226A
+lt; U+0003C
+ltcc; U+02AA6
+ltcir; U+02A79
+ltdot; U+022D6
+lthree; U+022CB
+ltimes; U+022C9
+ltlarr; U+02976
+ltquest; U+02A7B
+ltri; U+025C3
+ltrie; U+022B4
+ltrif; U+025C2
+ltrPar; U+02996
+lurdshar; U+0294A
+luruhar; U+02966
+lvertneqq; U+02268 U+0FE00
+lvnE; U+02268 U+0FE00
+macr; U+000AF
+male; U+02642
+malt; U+02720
+maltese; U+02720
+Map; U+02905
+map; U+021A6
+mapsto; U+021A6
+mapstodown; U+021A7
+mapstoleft; U+021A4
+mapstoup; U+021A5
+marker; U+025AE
+mcomma; U+02A29
+Mcy; U+0041C
+mcy; U+0043C
+mdash; U+02014
+mDDot; U+0223A
+measuredangle; U+02221
+MediumSpace; U+0205F
+Mellintrf; U+02133
+Mfr; U+1D510
+mfr; U+1D52A
+mho; U+02127
+micro; U+000B5
+mid; U+02223
+midast; U+0002A
+midcir; U+02AF0
+middot; U+000B7
+minus; U+02212
+minusb; U+0229F
+minusd; U+02238
+minusdu; U+02A2A
+MinusPlus; U+02213
+mlcp; U+02ADB
+mldr; U+02026
+mnplus; U+02213
+models; U+022A7
+Mopf; U+1D544
+mopf; U+1D55E
+mp; U+02213
+Mscr; U+02133
+mscr; U+1D4C2
+mstpos; U+0223E
+Mu; U+0039C
+mu; U+003BC
+multimap; U+022B8
+mumap; U+022B8
+nabla; U+02207
+Nacute; U+00143
+nacute; U+00144
+nang; U+02220 U+020D2
+nap; U+02249
+napE; U+02A70 U+00338
+napid; U+0224B U+00338
+napos; U+00149
+napprox; U+02249
+natur; U+0266E
+natural; U+0266E
+naturals; U+02115
+nbsp; U+000A0
+nbump; U+0224E U+00338
+nbumpe; U+0224F U+00338
+ncap; U+02A43
+Ncaron; U+00147
+ncaron; U+00148
+Ncedil; U+00145
+ncedil; U+00146
+ncong; U+02247
+ncongdot; U+02A6D U+00338
+ncup; U+02A42
+Ncy; U+0041D
+ncy; U+0043D
+ndash; U+02013
+ne; U+02260
+nearhk; U+02924
+neArr; U+021D7
+nearr; U+02197
+nearrow; U+02197
+nedot; U+02250 U+00338
+NegativeMediumSpace; U+0200B
+NegativeThickSpace; U+0200B
+NegativeThinSpace; U+0200B
+NegativeVeryThinSpace; U+0200B
+nequiv; U+02262
+nesear; U+02928
+nesim; U+02242 U+00338
+NestedGreaterGreater; U+0226B
+NestedLessLess; U+0226A
+NewLine; U+0000A
+nexist; U+02204
+nexists; U+02204
+Nfr; U+1D511
+nfr; U+1D52B
+ngE; U+02267 U+00338
+nge; U+02271
+ngeq; U+02271
+ngeqq; U+02267 U+00338
+ngeqslant; U+02A7E U+00338
+nges; U+02A7E U+00338
+nGg; U+022D9 U+00338
+ngsim; U+02275
+nGt; U+0226B U+020D2
+ngt; U+0226F
+ngtr; U+0226F
+nGtv; U+0226B U+00338
+nhArr; U+021CE
+nharr; U+021AE
+nhpar; U+02AF2
+ni; U+0220B
+nis; U+022FC
+nisd; U+022FA
+niv; U+0220B
+NJcy; U+0040A
+njcy; U+0045A
+nlArr; U+021CD
+nlarr; U+0219A
+nldr; U+02025
+nlE; U+02266 U+00338
+nle; U+02270
+nLeftarrow; U+021CD
+nleftarrow; U+0219A
+nLeftrightarrow; U+021CE
+nleftrightarrow; U+021AE
+nleq; U+02270
+nleqq; U+02266 U+00338
+nleqslant; U+02A7D U+00338
+nles; U+02A7D U+00338
+nless; U+0226E
+nLl; U+022D8 U+00338
+nlsim; U+02274
+nLt; U+0226A U+020D2
+nlt; U+0226E
+nltri; U+022EA
+nltrie; U+022EC
+nLtv; U+0226A U+00338
+nmid; U+02224
+NoBreak; U+02060
+NonBreakingSpace; U+000A0
+Nopf; U+02115
+nopf; U+1D55F
+Not; U+02AEC
+not; U+000AC
+NotCongruent; U+02262
+NotCupCap; U+0226D
+NotDoubleVerticalBar; U+02226
+NotElement; U+02209
+NotEqual; U+02260
+NotEqualTilde; U+02242 U+00338
+NotExists; U+02204
+NotGreater; U+0226F
+NotGreaterEqual; U+02271
+NotGreaterFullEqual; U+02267 U+00338
+NotGreaterGreater; U+0226B U+00338
+NotGreaterLess; U+02279
+NotGreaterSlantEqual; U+02A7E U+00338
+NotGreaterTilde; U+02275
+NotHumpDownHump; U+0224E U+00338
+NotHumpEqual; U+0224F U+00338
+notin; U+02209
+notindot; U+022F5 U+00338
+notinE; U+022F9 U+00338
+notinva; U+02209
+notinvb; U+022F7
+notinvc; U+022F6
+NotLeftTriangle; U+022EA
+NotLeftTriangleBar; U+029CF U+00338
+NotLeftTriangleEqual; U+022EC
+NotLess; U+0226E
+NotLessEqual; U+02270
+NotLessGreater; U+02278
+NotLessLess; U+0226A U+00338
+NotLessSlantEqual; U+02A7D U+00338
+NotLessTilde; U+02274
+NotNestedGreaterGreater; U+02AA2 U+00338
+NotNestedLessLess; U+02AA1 U+00338
+notni; U+0220C
+notniva; U+0220C
+notnivb; U+022FE
+notnivc; U+022FD
+NotPrecedes; U+02280
+NotPrecedesEqual; U+02AAF U+00338
+NotPrecedesSlantEqual; U+022E0
+NotReverseElement; U+0220C
+NotRightTriangle; U+022EB
+NotRightTriangleBar; U+029D0 U+00338
+NotRightTriangleEqual; U+022ED
+NotSquareSubset; U+0228F U+00338
+NotSquareSubsetEqual; U+022E2
+NotSquareSuperset; U+02290 U+00338
+NotSquareSupersetEqual; U+022E3
+NotSubset; U+02282 U+020D2
+NotSubsetEqual; U+02288
+NotSucceeds; U+02281
+NotSucceedsEqual; U+02AB0 U+00338
+NotSucceedsSlantEqual; U+022E1
+NotSucceedsTilde; U+0227F U+00338
+NotSuperset; U+02283 U+020D2
+NotSupersetEqual; U+02289
+NotTilde; U+02241
+NotTildeEqual; U+02244
+NotTildeFullEqual; U+02247
+NotTildeTilde; U+02249
+NotVerticalBar; U+02224
+npar; U+02226
+nparallel; U+02226
+nparsl; U+02AFD U+020E5
+npart; U+02202 U+00338
+npolint; U+02A14
+npr; U+02280
+nprcue; U+022E0
+npre; U+02AAF U+00338
+nprec; U+02280
+npreceq; U+02AAF U+00338
+nrArr; U+021CF
+nrarr; U+0219B
+nrarrc; U+02933 U+00338
+nrarrw; U+0219D U+00338
+nRightarrow; U+021CF
+nrightarrow; U+0219B
+nrtri; U+022EB
+nrtrie; U+022ED
+nsc; U+02281
+nsccue; U+022E1
+nsce; U+02AB0 U+00338
+Nscr; U+1D4A9
+nscr; U+1D4C3
+nshortmid; U+02224
+nshortparallel; U+02226
+nsim; U+02241
+nsime; U+02244
+nsimeq; U+02244
+nsmid; U+02224
+nspar; U+02226
+nsqsube; U+022E2
+nsqsupe; U+022E3
+nsub; U+02284
+nsubE; U+02AC5 U+00338
+nsube; U+02288
+nsubset; U+02282 U+020D2
+nsubseteq; U+02288
+nsubseteqq; U+02AC5 U+00338
+nsucc; U+02281
+nsucceq; U+02AB0 U+00338
+nsup; U+02285
+nsupE; U+02AC6 U+00338
+nsupe; U+02289
+nsupset; U+02283 U+020D2
+nsupseteq; U+02289
+nsupseteqq; U+02AC6 U+00338
+ntgl; U+02279
+Ntilde; U+000D1
+ntilde; U+000F1
+ntlg; U+02278
+ntriangleleft; U+022EA
+ntrianglelefteq; U+022EC
+ntriangleright; U+022EB
+ntrianglerighteq; U+022ED
+Nu; U+0039D
+nu; U+003BD
+num; U+00023
+numero; U+02116
+numsp; U+02007
+nvap; U+0224D U+020D2
+nVDash; U+022AF
+nVdash; U+022AE
+nvDash; U+022AD
+nvdash; U+022AC
+nvge; U+02265 U+020D2
+nvgt; U+0003E U+020D2
+nvHarr; U+02904
+nvinfin; U+029DE
+nvlArr; U+02902
+nvle; U+02264 U+020D2
+nvlt; U+0003C U+020D2
+nvltrie; U+022B4 U+020D2
+nvrArr; U+02903
+nvrtrie; U+022B5 U+020D2
+nvsim; U+0223C U+020D2
+nwarhk; U+02923
+nwArr; U+021D6
+nwarr; U+02196
+nwarrow; U+02196
+nwnear; U+02927
+Oacute; U+000D3
+oacute; U+000F3
+oast; U+0229B
+ocir; U+0229A
+Ocirc; U+000D4
+ocirc; U+000F4
+Ocy; U+0041E
+ocy; U+0043E
+odash; U+0229D
+Odblac; U+00150
+odblac; U+00151
+odiv; U+02A38
+odot; U+02299
+odsold; U+029BC
+OElig; U+00152
+oelig; U+00153
+ofcir; U+029BF
+Ofr; U+1D512
+ofr; U+1D52C
+ogon; U+002DB
+Ograve; U+000D2
+ograve; U+000F2
+ogt; U+029C1
+ohbar; U+029B5
+ohm; U+003A9
+oint; U+0222E
+olarr; U+021BA
+olcir; U+029BE
+olcross; U+029BB
+oline; U+0203E
+olt; U+029C0
+Omacr; U+0014C
+omacr; U+0014D
+Omega; U+003A9
+omega; U+003C9
+Omicron; U+0039F
+omicron; U+003BF
+omid; U+029B6
+ominus; U+02296
+Oopf; U+1D546
+oopf; U+1D560
+opar; U+029B7
+OpenCurlyDoubleQuote; U+0201C
+OpenCurlyQuote; U+02018
+operp; U+029B9
+oplus; U+02295
+Or; U+02A54
+or; U+02228
+orarr; U+021BB
+ord; U+02A5D
+order; U+02134
+orderof; U+02134
+ordf; U+000AA
+ordm; U+000BA
+origof; U+022B6
+oror; U+02A56
+orslope; U+02A57
+orv; U+02A5B
+oS; U+024C8
+Oscr; U+1D4AA
+oscr; U+02134
+Oslash; U+000D8
+oslash; U+000F8
+osol; U+02298
+Otilde; U+000D5
+otilde; U+000F5
+Otimes; U+02A37
+otimes; U+02297
+otimesas; U+02A36
+Ouml; U+000D6
+ouml; U+000F6
+ovbar; U+0233D
+OverBar; U+0203E
+OverBrace; U+023DE
+OverBracket; U+023B4
+OverParenthesis; U+023DC
+par; U+02225
+para; U+000B6
+parallel; U+02225
+parsim; U+02AF3
+parsl; U+02AFD
+part; U+02202
+PartialD; U+02202
+Pcy; U+0041F
+pcy; U+0043F
+percnt; U+00025
+period; U+0002E
+permil; U+02030
+perp; U+022A5
+pertenk; U+02031
+Pfr; U+1D513
+pfr; U+1D52D
+Phi; U+003A6
+phi; U+003C6
+phiv; U+003D5
+phmmat; U+02133
+phone; U+0260E
+Pi; U+003A0
+pi; U+003C0
+pitchfork; U+022D4
+piv; U+003D6
+planck; U+0210F
+planckh; U+0210E
+plankv; U+0210F
+plus; U+0002B
+plusacir; U+02A23
+plusb; U+0229E
+pluscir; U+02A22
+plusdo; U+02214
+plusdu; U+02A25
+pluse; U+02A72
+PlusMinus; U+000B1
+plusmn; U+000B1
+plussim; U+02A26
+plustwo; U+02A27
+pm; U+000B1
+Poincareplane; U+0210C
+pointint; U+02A15
+Popf; U+02119
+popf; U+1D561
+pound; U+000A3
+Pr; U+02ABB
+pr; U+0227A
+prap; U+02AB7
+prcue; U+0227C
+prE; U+02AB3
+pre; U+02AAF
+prec; U+0227A
+precapprox; U+02AB7
+preccurlyeq; U+0227C
+Precedes; U+0227A
+PrecedesEqual; U+02AAF
+PrecedesSlantEqual; U+0227C
+PrecedesTilde; U+0227E
+preceq; U+02AAF
+precnapprox; U+02AB9
+precneqq; U+02AB5
+precnsim; U+022E8
+precsim; U+0227E
+Prime; U+02033
+prime; U+02032
+primes; U+02119
+prnap; U+02AB9
+prnE; U+02AB5
+prnsim; U+022E8
+prod; U+0220F
+Product; U+0220F
+profalar; U+0232E
+profline; U+02312
+profsurf; U+02313
+prop; U+0221D
+Proportion; U+02237
+Proportional; U+0221D
+propto; U+0221D
+prsim; U+0227E
+prurel; U+022B0
+Pscr; U+1D4AB
+pscr; U+1D4C5
+Psi; U+003A8
+psi; U+003C8
+puncsp; U+02008
+Qfr; U+1D514
+qfr; U+1D52E
+qint; U+02A0C
+Qopf; U+0211A
+qopf; U+1D562
+qprime; U+02057
+Qscr; U+1D4AC
+qscr; U+1D4C6
+quaternions; U+0210D
+quatint; U+02A16
+quest; U+0003F
+questeq; U+0225F
+QUOT; U+00022
+quot; U+00022
+rAarr; U+021DB
+race; U+0223D U+00331
+Racute; U+00154
+racute; U+00155
+radic; U+0221A
+raemptyv; U+029B3
+Rang; U+027EB
+rang; U+027E9
+rangd; U+02992
+range; U+029A5
+rangle; U+027E9
+raquo; U+000BB
+Rarr; U+021A0
+rArr; U+021D2
+rarr; U+02192
+rarrap; U+02975
+rarrb; U+021E5
+rarrbfs; U+02920
+rarrc; U+02933
+rarrfs; U+0291E
+rarrhk; U+021AA
+rarrlp; U+021AC
+rarrpl; U+02945
+rarrsim; U+02974
+Rarrtl; U+02916
+rarrtl; U+021A3
+rarrw; U+0219D
+rAtail; U+0291C
+ratail; U+0291A
+ratio; U+02236
+rationals; U+0211A
+RBarr; U+02910
+rBarr; U+0290F
+rbarr; U+0290D
+rbbrk; U+02773
+rbrace; U+0007D
+rbrack; U+0005D
+rbrke; U+0298C
+rbrksld; U+0298E
+rbrkslu; U+02990
+Rcaron; U+00158
+rcaron; U+00159
+Rcedil; U+00156
+rcedil; U+00157
+rceil; U+02309
+rcub; U+0007D
+Rcy; U+00420
+rcy; U+00440
+rdca; U+02937
+rdldhar; U+02969
+rdquo; U+0201D
+rdquor; U+0201D
+rdsh; U+021B3
+Re; U+0211C
+real; U+0211C
+realine; U+0211B
+realpart; U+0211C
+reals; U+0211D
+rect; U+025AD
+REG; U+000AE
+reg; U+000AE
+ReverseElement; U+0220B
+ReverseEquilibrium; U+021CB
+ReverseUpEquilibrium; U+0296F
+rfisht; U+0297D
+rfloor; U+0230B
+Rfr; U+0211C
+rfr; U+1D52F
+rHar; U+02964
+rhard; U+021C1
+rharu; U+021C0
+rharul; U+0296C
+Rho; U+003A1
+rho; U+003C1
+rhov; U+003F1
+RightAngleBracket; U+027E9
+RightArrow; U+02192
+Rightarrow; U+021D2
+rightarrow; U+02192
+RightArrowBar; U+021E5
+RightArrowLeftArrow; U+021C4
+rightarrowtail; U+021A3
+RightCeiling; U+02309
+RightDoubleBracket; U+027E7
+RightDownTeeVector; U+0295D
+RightDownVector; U+021C2
+RightDownVectorBar; U+02955
+RightFloor; U+0230B
+rightharpoondown; U+021C1
+rightharpoonup; U+021C0
+rightleftarrows; U+021C4
+rightleftharpoons; U+021CC
+rightrightarrows; U+021C9
+rightsquigarrow; U+0219D
+RightTee; U+022A2
+RightTeeArrow; U+021A6
+RightTeeVector; U+0295B
+rightthreetimes; U+022CC
+RightTriangle; U+022B3
+RightTriangleBar; U+029D0
+RightTriangleEqual; U+022B5
+RightUpDownVector; U+0294F
+RightUpTeeVector; U+0295C
+RightUpVector; U+021BE
+RightUpVectorBar; U+02954
+RightVector; U+021C0
+RightVectorBar; U+02953
+ring; U+002DA
+risingdotseq; U+02253
+rlarr; U+021C4
+rlhar; U+021CC
+rlm; U+0200F
+rmoust; U+023B1
+rmoustache; U+023B1
+rnmid; U+02AEE
+roang; U+027ED
+roarr; U+021FE
+robrk; U+027E7
+ropar; U+02986
+Ropf; U+0211D
+ropf; U+1D563
+roplus; U+02A2E
+rotimes; U+02A35
+RoundImplies; U+02970
+rpar; U+00029
+rpargt; U+02994
+rppolint; U+02A12
+rrarr; U+021C9
+Rrightarrow; U+021DB
+rsaquo; U+0203A
+Rscr; U+0211B
+rscr; U+1D4C7
+Rsh; U+021B1
+rsh; U+021B1
+rsqb; U+0005D
+rsquo; U+02019
+rsquor; U+02019
+rthree; U+022CC
+rtimes; U+022CA
+rtri; U+025B9
+rtrie; U+022B5
+rtrif; U+025B8
+rtriltri; U+029CE
+RuleDelayed; U+029F4
+ruluhar; U+02968
+rx; U+0211E
+Sacute; U+0015A
+sacute; U+0015B
+sbquo; U+0201A
+Sc; U+02ABC
+sc; U+0227B
+scap; U+02AB8
+Scaron; U+00160
+scaron; U+00161
+sccue; U+0227D
+scE; U+02AB4
+sce; U+02AB0
+Scedil; U+0015E
+scedil; U+0015F
+Scirc; U+0015C
+scirc; U+0015D
+scnap; U+02ABA
+scnE; U+02AB6
+scnsim; U+022E9
+scpolint; U+02A13
+scsim; U+0227F
+Scy; U+00421
+scy; U+00441
+sdot; U+022C5
+sdotb; U+022A1
+sdote; U+02A66
+searhk; U+02925
+seArr; U+021D8
+searr; U+02198
+searrow; U+02198
+sect; U+000A7
+semi; U+0003B
+seswar; U+02929
+setminus; U+02216
+setmn; U+02216
+sext; U+02736
+Sfr; U+1D516
+sfr; U+1D530
+sfrown; U+02322
+sharp; U+0266F
+SHCHcy; U+00429
+shchcy; U+00449
+SHcy; U+00428
+shcy; U+00448
+ShortDownArrow; U+02193
+ShortLeftArrow; U+02190
+shortmid; U+02223
+shortparallel; U+02225
+ShortRightArrow; U+02192
+ShortUpArrow; U+02191
+shy; U+000AD
+Sigma; U+003A3
+sigma; U+003C3
+sigmaf; U+003C2
+sigmav; U+003C2
+sim; U+0223C
+simdot; U+02A6A
+sime; U+02243
+simeq; U+02243
+simg; U+02A9E
+simgE; U+02AA0
+siml; U+02A9D
+simlE; U+02A9F
+simne; U+02246
+simplus; U+02A24
+simrarr; U+02972
+slarr; U+02190
+SmallCircle; U+02218
+smallsetminus; U+02216
+smashp; U+02A33
+smeparsl; U+029E4
+smid; U+02223
+smile; U+02323
+smt; U+02AAA
+smte; U+02AAC
+smtes; U+02AAC U+0FE00
+SOFTcy; U+0042C
+softcy; U+0044C
+sol; U+0002F
+solb; U+029C4
+solbar; U+0233F
+Sopf; U+1D54A
+sopf; U+1D564
+spades; U+02660
+spadesuit; U+02660
+spar; U+02225
+sqcap; U+02293
+sqcaps; U+02293 U+0FE00
+sqcup; U+02294
+sqcups; U+02294 U+0FE00
+Sqrt; U+0221A
+sqsub; U+0228F
+sqsube; U+02291
+sqsubset; U+0228F
+sqsubseteq; U+02291
+sqsup; U+02290
+sqsupe; U+02292
+sqsupset; U+02290
+sqsupseteq; U+02292
+squ; U+025A1
+Square; U+025A1
+square; U+025A1
+SquareIntersection; U+02293
+SquareSubset; U+0228F
+SquareSubsetEqual; U+02291
+SquareSuperset; U+02290
+SquareSupersetEqual; U+02292
+SquareUnion; U+02294
+squarf; U+025AA
+squf; U+025AA
+srarr; U+02192
+Sscr; U+1D4AE
+sscr; U+1D4C8
+ssetmn; U+02216
+ssmile; U+02323
+sstarf; U+022C6
+Star; U+022C6
+star; U+02606
+starf; U+02605
+straightepsilon; U+003F5
+straightphi; U+003D5
+strns; U+000AF
+Sub; U+022D0
+sub; U+02282
+subdot; U+02ABD
+subE; U+02AC5
+sube; U+02286
+subedot; U+02AC3
+submult; U+02AC1
+subnE; U+02ACB
+subne; U+0228A
+subplus; U+02ABF
+subrarr; U+02979
+Subset; U+022D0
+subset; U+02282
+subseteq; U+02286
+subseteqq; U+02AC5
+SubsetEqual; U+02286
+subsetneq; U+0228A
+subsetneqq; U+02ACB
+subsim; U+02AC7
+subsub; U+02AD5
+subsup; U+02AD3
+succ; U+0227B
+succapprox; U+02AB8
+succcurlyeq; U+0227D
+Succeeds; U+0227B
+SucceedsEqual; U+02AB0
+SucceedsSlantEqual; U+0227D
+SucceedsTilde; U+0227F
+succeq; U+02AB0
+succnapprox; U+02ABA
+succneqq; U+02AB6
+succnsim; U+022E9
+succsim; U+0227F
+SuchThat; U+0220B
+Sum; U+02211
+sum; U+02211
+sung; U+0266A
+Sup; U+022D1
+sup; U+02283
+sup1; U+000B9
+sup2; U+000B2
+sup3; U+000B3
+supdot; U+02ABE
+supdsub; U+02AD8
+supE; U+02AC6
+supe; U+02287
+supedot; U+02AC4
+Superset; U+02283
+SupersetEqual; U+02287
+suphsol; U+027C9
+suphsub; U+02AD7
+suplarr; U+0297B
+supmult; U+02AC2
+supnE; U+02ACC
+supne; U+0228B
+supplus; U+02AC0
+Supset; U+022D1
+supset; U+02283
+supseteq; U+02287
+supseteqq; U+02AC6
+supsetneq; U+0228B
+supsetneqq; U+02ACC
+supsim; U+02AC8
+supsub; U+02AD4
+supsup; U+02AD6
+swarhk; U+02926
+swArr; U+021D9
+swarr; U+02199
+swarrow; U+02199
+swnwar; U+0292A
+szlig; U+000DF
+Tab; U+00009
+target; U+02316
+Tau; U+003A4
+tau; U+003C4
+tbrk; U+023B4
+Tcaron; U+00164
+tcaron; U+00165
+Tcedil; U+00162
+tcedil; U+00163
+Tcy; U+00422
+tcy; U+00442
+tdot; U+020DB
+telrec; U+02315
+Tfr; U+1D517
+tfr; U+1D531
+there4; U+02234
+Therefore; U+02234
+therefore; U+02234
+Theta; U+00398
+theta; U+003B8
+thetasym; U+003D1
+thetav; U+003D1
+thickapprox; U+02248
+thicksim; U+0223C
+ThickSpace; U+0205F U+0200A
+thinsp; U+02009
+ThinSpace; U+02009
+thkap; U+02248
+thksim; U+0223C
+THORN; U+000DE
+thorn; U+000FE
+Tilde; U+0223C
+tilde; U+002DC
+TildeEqual; U+02243
+TildeFullEqual; U+02245
+TildeTilde; U+02248
+times; U+000D7
+timesb; U+022A0
+timesbar; U+02A31
+timesd; U+02A30
+tint; U+0222D
+toea; U+02928
+top; U+022A4
+topbot; U+02336
+topcir; U+02AF1
+Topf; U+1D54B
+topf; U+1D565
+topfork; U+02ADA
+tosa; U+02929
+tprime; U+02034
+TRADE; U+02122
+trade; U+02122
+triangle; U+025B5
+triangledown; U+025BF
+triangleleft; U+025C3
+trianglelefteq; U+022B4
+triangleq; U+0225C
+triangleright; U+025B9
+trianglerighteq; U+022B5
+tridot; U+025EC
+trie; U+0225C
+triminus; U+02A3A
+TripleDot; U+020DB
+triplus; U+02A39
+trisb; U+029CD
+tritime; U+02A3B
+trpezium; U+023E2
+Tscr; U+1D4AF
+tscr; U+1D4C9
+TScy; U+00426
+tscy; U+00446
+TSHcy; U+0040B
+tshcy; U+0045B
+Tstrok; U+00166
+tstrok; U+00167
+twixt; U+0226C
+twoheadleftarrow; U+0219E
+twoheadrightarrow; U+021A0
+Uacute; U+000DA
+uacute; U+000FA
+Uarr; U+0219F
+uArr; U+021D1
+uarr; U+02191
+Uarrocir; U+02949
+Ubrcy; U+0040E
+ubrcy; U+0045E
+Ubreve; U+0016C
+ubreve; U+0016D
+Ucirc; U+000DB
+ucirc; U+000FB
+Ucy; U+00423
+ucy; U+00443
+udarr; U+021C5
+Udblac; U+00170
+udblac; U+00171
+udhar; U+0296E
+ufisht; U+0297E
+Ufr; U+1D518
+ufr; U+1D532
+Ugrave; U+000D9
+ugrave; U+000F9
+uHar; U+02963
+uharl; U+021BF
+uharr; U+021BE
+uhblk; U+02580
+ulcorn; U+0231C
+ulcorner; U+0231C
+ulcrop; U+0230F
+ultri; U+025F8
+Umacr; U+0016A
+umacr; U+0016B
+uml; U+000A8
+UnderBar; U+0005F
+UnderBrace; U+023DF
+UnderBracket; U+023B5
+UnderParenthesis; U+023DD
+Union; U+022C3
+UnionPlus; U+0228E
+Uogon; U+00172
+uogon; U+00173
+Uopf; U+1D54C
+uopf; U+1D566
+UpArrow; U+02191
+Uparrow; U+021D1
+uparrow; U+02191
+UpArrowBar; U+02912
+UpArrowDownArrow; U+021C5
+UpDownArrow; U+02195
+Updownarrow; U+021D5
+updownarrow; U+02195
+UpEquilibrium; U+0296E
+upharpoonleft; U+021BF
+upharpoonright; U+021BE
+uplus; U+0228E
+UpperLeftArrow; U+02196
+UpperRightArrow; U+02197
+Upsi; U+003D2
+upsi; U+003C5
+upsih; U+003D2
+Upsilon; U+003A5
+upsilon; U+003C5
+UpTee; U+022A5
+UpTeeArrow; U+021A5
+upuparrows; U+021C8
+urcorn; U+0231D
+urcorner; U+0231D
+urcrop; U+0230E
+Uring; U+0016E
+uring; U+0016F
+urtri; U+025F9
+Uscr; U+1D4B0
+uscr; U+1D4CA
+utdot; U+022F0
+Utilde; U+00168
+utilde; U+00169
+utri; U+025B5
+utrif; U+025B4
+uuarr; U+021C8
+Uuml; U+000DC
+uuml; U+000FC
+uwangle; U+029A7
+vangrt; U+0299C
+varepsilon; U+003F5
+varkappa; U+003F0
+varnothing; U+02205
+varphi; U+003D5
+varpi; U+003D6
+varpropto; U+0221D
+vArr; U+021D5
+varr; U+02195
+varrho; U+003F1
+varsigma; U+003C2
+varsubsetneq; U+0228A U+0FE00
+varsubsetneqq; U+02ACB U+0FE00
+varsupsetneq; U+0228B U+0FE00
+varsupsetneqq; U+02ACC U+0FE00
+vartheta; U+003D1
+vartriangleleft; U+022B2
+vartriangleright; U+022B3
+Vbar; U+02AEB
+vBar; U+02AE8
+vBarv; U+02AE9
+Vcy; U+00412
+vcy; U+00432
+VDash; U+022AB
+Vdash; U+022A9
+vDash; U+022A8
+vdash; U+022A2
+Vdashl; U+02AE6
+Vee; U+022C1
+vee; U+02228
+veebar; U+022BB
+veeeq; U+0225A
+vellip; U+022EE
+Verbar; U+02016
+verbar; U+0007C
+Vert; U+02016
+vert; U+0007C
+VerticalBar; U+02223
+VerticalLine; U+0007C
+VerticalSeparator; U+02758
+VerticalTilde; U+02240
+VeryThinSpace; U+0200A
+Vfr; U+1D519
+vfr; U+1D533
+vltri; U+022B2
+vnsub; U+02282 U+020D2
+vnsup; U+02283 U+020D2
+Vopf; U+1D54D
+vopf; U+1D567
+vprop; U+0221D
+vrtri; U+022B3
+Vscr; U+1D4B1
+vscr; U+1D4CB
+vsubnE; U+02ACB U+0FE00
+vsubne; U+0228A U+0FE00
+vsupnE; U+02ACC U+0FE00
+vsupne; U+0228B U+0FE00
+Vvdash; U+022AA
+vzigzag; U+0299A
+Wcirc; U+00174
+wcirc; U+00175
+wedbar; U+02A5F
+Wedge; U+022C0
+wedge; U+02227
+wedgeq; U+02259
+weierp; U+02118
+Wfr; U+1D51A
+wfr; U+1D534
+Wopf; U+1D54E
+wopf; U+1D568
+wp; U+02118
+wr; U+02240
+wreath; U+02240
+Wscr; U+1D4B2
+wscr; U+1D4CC
+xcap; U+022C2
+xcirc; U+025EF
+xcup; U+022C3
+xdtri; U+025BD
+Xfr; U+1D51B
+xfr; U+1D535
+xhArr; U+027FA
+xharr; U+027F7
+Xi; U+0039E
+xi; U+003BE
+xlArr; U+027F8
+xlarr; U+027F5
+xmap; U+027FC
+xnis; U+022FB
+xodot; U+02A00
+Xopf; U+1D54F
+xopf; U+1D569
+xoplus; U+02A01
+xotime; U+02A02
+xrArr; U+027F9
+xrarr; U+027F6
+Xscr; U+1D4B3
+xscr; U+1D4CD
+xsqcup; U+02A06
+xuplus; U+02A04
+xutri; U+025B3
+xvee; U+022C1
+xwedge; U+022C0
+Yacute; U+000DD
+yacute; U+000FD
+YAcy; U+0042F
+yacy; U+0044F
+Ycirc; U+00176
+ycirc; U+00177
+Ycy; U+0042B
+ycy; U+0044B
+yen; U+000A5
+Yfr; U+1D51C
+yfr; U+1D536
+YIcy; U+00407
+yicy; U+00457
+Yopf; U+1D550
+yopf; U+1D56A
+Yscr; U+1D4B4
+yscr; U+1D4CE
+YUcy; U+0042E
+yucy; U+0044E
+Yuml; U+00178
+yuml; U+000FF
+Zacute; U+00179
+zacute; U+0017A
+Zcaron; U+0017D
+zcaron; U+0017E
+Zcy; U+00417
+zcy; U+00437
+Zdot; U+0017B
+zdot; U+0017C
+zeetrf; U+02128
+ZeroWidthSpace; U+0200B
+Zeta; U+00396
+zeta; U+003B6
+Zfr; U+02128
+zfr; U+1D537
+ZHcy; U+00416
+zhcy; U+00436
+zigrarr; U+021DD
+Zopf; U+02124
+zopf; U+1D56B
+Zscr; U+1D4B5
+zscr; U+1D4CF
+zwj; U+0200D
+zwnj; U+0200C
diff --git a/lib/DOM/Tiny/HTML.pm b/lib/DOM/Tiny/HTML.pm
new file mode 100644 (file)
index 0000000..fc84d09
--- /dev/null
@@ -0,0 +1,344 @@
+package DOM::Tiny::HTML;
+
+use strict;
+use warnings;
+use DOM::Tiny::Entities qw(html_unescape xml_escape);
+use Scalar::Util 'weaken';
+use Class::Tiny::Chained 'xml', { tree => sub { ['root'] } };
+
+my $ATTR_RE = qr/
+  ([^<>=\s\/]+|\/)                    # Key
+  (?:
+    \s*=\s*
+    (?s:(["'])(.*?)\g{-2}|([^>\s]*))   # Value
+  )?
+  \s*
+/x;
+my $TOKEN_RE = qr/
+  ([^<]+)?                                            # Text
+  (?:
+    <(?:
+      !(?:
+        DOCTYPE(
+        \s+\w+                                        # Doctype
+        (?:(?:\s+\w+)?(?:\s+(?:"[^"]*"|'[^']*'))+)?   # External ID
+        (?:\s+\[.+?\])?                               # Int Subset
+        \s*)
+      |
+        --(.*?)--\s*                                  # Comment
+      |
+        \[CDATA\[(.*?)\]\]                            # CDATA
+      )
+    |
+      \?(.*?)\?                                       # Processing Instruction
+    |
+      \s*([^<>\s]+\s*(?:(?:$ATTR_RE){0,32766})*+)     # Tag
+    )>
+  |
+    (<)                                               # Runaway "<"
+  )??
+/xis;
+
+# HTML elements that only contain raw text
+my %RAW = map { $_ => 1 } qw(script style);
+
+# HTML elements that only contain raw text and entities
+my %RCDATA = map { $_ => 1 } qw(title textarea);
+
+# HTML elements with optional end tags
+my %END = (body => 'head', optgroup => 'optgroup', option => 'option');
+
+# HTML elements that break paragraphs
+map { $END{$_} = 'p' } (
+  qw(address article aside blockquote dir div dl fieldset footer form h1 h2),
+  qw(h3 h4 h5 h6 header hr main menu nav ol p pre section table ul)
+);
+
+# HTML table elements with optional end tags
+my %TABLE = map { $_ => 1 } qw(colgroup tbody td tfoot th thead tr);
+
+# HTML elements with optional end tags and scoping rules
+my %CLOSE
+  = (li => [{li => 1}, {ul => 1, ol => 1}], tr => [{tr => 1}, {table => 1}]);
+$CLOSE{$_} = [\%TABLE, {table => 1}] for qw(colgroup tbody tfoot thead);
+$CLOSE{$_} = [{dd => 1, dt => 1}, {dl    => 1}] for qw(dd dt);
+$CLOSE{$_} = [{rp => 1, rt => 1}, {ruby  => 1}] for qw(rp rt);
+$CLOSE{$_} = [{th => 1, td => 1}, {table => 1}] for qw(td th);
+
+# HTML elements without end tags
+my %EMPTY = map { $_ => 1 } (
+  qw(area base br col embed hr img input keygen link menuitem meta param),
+  qw(source track wbr)
+);
+
+# HTML elements categorized as phrasing content (and obsolete inline elements)
+my @PHRASING = (
+  qw(a abbr area audio b bdi bdo br button canvas cite code data datalist),
+  qw(del dfn em embed i iframe img input ins kbd keygen label link map mark),
+  qw(math meta meter noscript object output picture progress q ruby s samp),
+  qw(script select small span strong sub sup svg template textarea time u),
+  qw(var video wbr)
+);
+my @OBSOLETE = qw(acronym applet basefont big font strike tt);
+my %PHRASING = map { $_ => 1 } @OBSOLETE, @PHRASING;
+
+# HTML elements that don't get their self-closing flag acknowledged
+my %BLOCK = map { $_ => 1 } (
+  qw(a address applet article aside b big blockquote body button caption),
+  qw(center code col colgroup dd details dialog dir div dl dt em fieldset),
+  qw(figcaption figure font footer form frameset h1 h2 h3 h4 h5 h6 head),
+  qw(header hgroup html i iframe li listing main marquee menu nav nobr),
+  qw(noembed noframes noscript object ol optgroup option p plaintext pre rp),
+  qw(rt s script section select small strike strong style summary table),
+  qw(tbody td template textarea tfoot th thead title tr tt u ul xmp)
+);
+
+sub parse {
+  my ($self, $html) = (shift, "$_[0]");
+
+  my $xml = $self->xml;
+  my $current = my $tree = ['root'];
+  while ($html =~ /\G$TOKEN_RE/gcso) {
+    my ($text, $doctype, $comment, $cdata, $pi, $tag, $runaway)
+      = ($1, $2, $3, $4, $5, $6, $11);
+
+    # Text (and runaway "<")
+    $text .= '<' if defined $runaway;
+    _node($current, 'text', html_unescape $text) if defined $text;
+
+    # Tag
+    if (defined $tag) {
+
+      # End
+      if ($tag =~ /^\/\s*(\S+)/) { _end($xml ? $1 : lc $1, $xml, \$current) }
+
+      # Start
+      elsif ($tag =~ m!^([^\s/]+)([\s\S]*)!) {
+        my ($start, $attr) = ($xml ? $1 : lc $1, $2);
+
+        # Attributes
+        my (%attrs, $closing);
+        while ($attr =~ /$ATTR_RE/go) {
+          my ($key, $value) = ($xml ? $1 : lc $1, $3 // $4);
+
+          # Empty tag
+          ++$closing and next if $key eq '/';
+
+          $attrs{$key} = defined $value ? html_unescape $value : $value;
+        }
+
+        # "image" is an alias for "img"
+        $start = 'img' if !$xml && $start eq 'image';
+        _start($start, \%attrs, $xml, \$current);
+
+        # Element without end tag (self-closing)
+        _end($start, $xml, \$current)
+          if !$xml && $EMPTY{$start} || ($xml || !$BLOCK{$start}) && $closing;
+
+        # Raw text elements
+        next if $xml || !$RAW{$start} && !$RCDATA{$start};
+        next unless $html =~ m!\G(.*?)<\s*/\s*\Q$start\E\s*>!gcsi;
+        _node($current, 'raw', $RCDATA{$start} ? html_unescape $1 : $1);
+        _end($start, 0, \$current);
+      }
+    }
+
+    # DOCTYPE
+    elsif (defined $doctype) { _node($current, 'doctype', $doctype) }
+
+    # Comment
+    elsif (defined $comment) { _node($current, 'comment', $comment) }
+
+    # CDATA
+    elsif (defined $cdata) { _node($current, 'cdata', $cdata) }
+
+    # Processing instruction (try to detect XML)
+    elsif (defined $pi) {
+      $self->xml($xml = 1) if !exists $self->{xml} && $pi =~ /xml/i;
+      _node($current, 'pi', $pi);
+    }
+  }
+
+  return $self->tree($tree);
+}
+
+sub render { _render($_[0]->tree, $_[0]->xml) }
+
+sub _end {
+  my ($end, $xml, $current) = @_;
+
+  # Search stack for start tag
+  my $next = $$current;
+  do {
+
+    # Ignore useless end tag
+    return if $next->[0] eq 'root';
+
+    # Right tag
+    return $$current = $next->[3] if $next->[1] eq $end;
+
+    # Phrasing content can only cross phrasing content
+    return if !$xml && $PHRASING{$end} && !$PHRASING{$next->[1]};
+
+  } while $next = $next->[3];
+}
+
+sub _node {
+  my ($current, $type, $content) = @_;
+  push @$current, my $new = [$type, $content, $current];
+  weaken $new->[2];
+}
+
+sub _render {
+  my ($tree, $xml) = @_;
+
+  # Text (escaped)
+  my $type = $tree->[0];
+  return xml_escape($tree->[1]) if $type eq 'text';
+
+  # Raw text
+  return $tree->[1] if $type eq 'raw';
+
+  # DOCTYPE
+  return '<!DOCTYPE' . $tree->[1] . '>' if $type eq 'doctype';
+
+  # Comment
+  return '<!--' . $tree->[1] . '-->' if $type eq 'comment';
+
+  # CDATA
+  return '<![CDATA[' . $tree->[1] . ']]>' if $type eq 'cdata';
+
+  # Processing instruction
+  return '<?' . $tree->[1] . '?>' if $type eq 'pi';
+
+  # Root
+  return join '', map { _render($_, $xml) } @$tree[1 .. $#$tree]
+    if $type eq 'root';
+
+  # Start tag
+  my $tag    = $tree->[1];
+  my $result = "<$tag";
+
+  # Attributes
+  for my $key (sort keys %{$tree->[2]}) {
+    my $value = $tree->[2]{$key};
+    $result .= $xml ? qq{ $key="$key"} : " $key" and next unless defined $value;
+    $result .= qq{ $key="} . xml_escape($value) . '"';
+  }
+
+  # No children
+  return $xml ? "$result />" : $EMPTY{$tag} ? "$result>" : "$result></$tag>"
+    unless $tree->[4];
+
+  # Children
+  no warnings 'recursion';
+  $result .= '>' . join '', map { _render($_, $xml) } @$tree[4 .. $#$tree];
+
+  # End tag
+  return "$result</$tag>";
+}
+
+sub _start {
+  my ($start, $attrs, $xml, $current) = @_;
+
+  # Autoclose optional HTML elements
+  if (!$xml && $$current->[0] ne 'root') {
+    if (my $end = $END{$start}) { _end($end, 0, $current) }
+
+    elsif (my $close = $CLOSE{$start}) {
+      my ($allowed, $scope) = @$close;
+
+      # Close allowed parent elements in scope
+      my $parent = $$current;
+      while ($parent->[0] ne 'root' && !$scope->{$parent->[1]}) {
+        _end($parent->[1], 0, $current) if $allowed->{$parent->[1]};
+        $parent = $parent->[3];
+      }
+    }
+  }
+
+  # New tag
+  push @$$current, my $new = ['tag', $start, $attrs, $$current];
+  weaken $new->[3];
+  $$current = $new;
+}
+
+1;
+
+=encoding utf8
+
+=head1 NAME
+
+DOM::Tiny::HTML - HTML/XML engine
+
+=head1 SYNOPSIS
+
+  use DOM::Tiny::HTML;
+
+  # Turn HTML into DOM tree
+  my $html = DOM::Tiny::HTML->new;
+  $html->parse('<div><p id="a">Test</p><p id="b">123</p></div>');
+  my $tree = $html->tree;
+
+=head1 DESCRIPTION
+
+L<DOM::Tiny::HTML> is the HTML/XML engine used by L<DOM::Tiny> based on
+L<Mojo::DOM::HTML>, which is based on the
+L<HTML Living Standard|https://html.spec.whatwg.org> as well as the
+L<Extensible Markup Language (XML) 1.0|http://www.w3.org/TR/xml/>.
+
+=head1 ATTRIBUTES
+
+L<DOM::Tiny::HTML> implements the following attributes.
+
+=head2 tree
+
+  my $tree = $html->tree;
+  $html    = $html->tree(['root']);
+
+Document Object Model. Note that this structure should only be used very
+carefully since it is very dynamic.
+
+=head2 xml
+
+  my $bool = $html->xml;
+  $html    = $html->xml($bool);
+
+Disable HTML semantics in parser and activate case-sensitivity, defaults to
+auto detection based on processing instructions.
+
+=head1 METHODS
+
+L<DOM::Tiny::HTML> implements the following methods.
+
+=head2 parse
+
+  $html = $html->parse('<foo bar="baz">I ♥ DOM::Tiny!</foo>');
+
+Parse HTML/XML fragment.
+
+=head2 render
+
+  my $str = $html->render;
+
+Render DOM to HTML/XML.
+
+=head1 BUGS
+
+Report any issues on the public bugtracker.
+
+=head1 AUTHOR
+
+Dan Book <dbook@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is Copyright (c) 2015 by Dan Book.
+
+This is free software, licensed under:
+
+  The Artistic License 2.0 (GPL Compatible)
+
+=head1 SEE ALSO
+
+L<Mojo::DOM::HTML>
diff --git a/t/collection.t b/t/collection.t
new file mode 100644 (file)
index 0000000..65b5647
--- /dev/null
@@ -0,0 +1,171 @@
+use strict;
+use warnings;
+use Test::More;
+use DOM::Tiny::Collection 'c';
+use JSON::Tiny 'encode_json';
+
+# Array
+is c(1, 2, 3)->[1], 2, 'right result';
+is_deeply [@{c(3, 2, 1)}], [3, 2, 1], 'right result';
+my $collection = c(1, 2);
+push @$collection, 3, 4, 5;
+is_deeply [@$collection], [1, 2, 3, 4, 5], 'right result';
+
+# Tap into method chain
+is_deeply c(1, 2, 3)->tap(sub { $_->[1] += 2 })->to_array, [1, 4, 3],
+  'right result';
+
+# compact
+is_deeply c(undef, 0, 1, '', 2, 3)->compact->to_array, [0, 1, 2, 3],
+  'right result';
+is_deeply c(3, 2, 1)->compact->to_array, [3, 2, 1], 'right result';
+is_deeply c()->compact->to_array, [], 'right result';
+
+# flatten
+is_deeply c(1, 2, [3, 4], 5, c(6, 7))->flatten->to_array,
+  [1, 2, 3, 4, 5, 6, 7], 'right result';
+is_deeply c(undef, 1, [2, {}, [3, c(4, 5)]], undef, 6)->flatten->to_array,
+  [undef, 1, 2, {}, 3, 4, 5, undef, 6], 'right result';
+
+# each
+$collection = c(3, 2, 1);
+is_deeply [$collection->each], [3, 2, 1], 'right elements';
+$collection = c([3], [2], [1]);
+my @results;
+$collection->each(sub { push @results, $_->[0] });
+is_deeply \@results, [3, 2, 1], 'right elements';
+@results = ();
+$collection->each(sub { push @results, shift->[0], shift });
+is_deeply \@results, [3, 1, 2, 2, 1, 3], 'right elements';
+
+# first
+$collection = c(5, 4, [3, 2], 1);
+is $collection->first, 5, 'right result';
+is_deeply $collection->first(sub { ref $_ eq 'ARRAY' }), [3, 2], 'right result';
+is $collection->first(sub { shift() < 5 }), 4, 'right result';
+is $collection->first(qr/[1-4]/), 4, 'right result';
+is $collection->first(sub { ref $_ eq 'CODE' }), undef, 'no result';
+$collection = c(c(1, 2, 3), c(4, 5, 6), c(7, 8, 9));
+is_deeply $collection->first(first => sub { $_ == 5 })->to_array, [4, 5, 6],
+  'right result';
+$collection = c();
+is $collection->first, undef, 'no result';
+is $collection->first(sub {defined}), undef, 'no result';
+
+# last
+is c(5, 4, 3)->last, 3, 'right result';
+is c(5, 4, 3)->reverse->last, 5, 'right result';
+is c()->last, undef, 'no result';
+
+# grep
+$collection = c(1, 2, 3, 4, 5, 6, 7, 8, 9);
+is_deeply $collection->grep(qr/[6-9]/)->to_array, [6, 7, 8, 9],
+  'right elements';
+is_deeply $collection->grep(sub {/[6-9]/})->to_array, [6, 7, 8, 9],
+  'right elements';
+is_deeply $collection->grep(sub { $_ > 5 })->to_array, [6, 7, 8, 9],
+  'right elements';
+is_deeply $collection->grep(sub { $_ < 5 })->to_array, [1, 2, 3, 4],
+  'right elements';
+is_deeply $collection->grep(sub { shift == 5 })->to_array, [5],
+  'right elements';
+is_deeply $collection->grep(sub { $_ < 1 })->to_array, [], 'no elements';
+is_deeply $collection->grep(sub { $_ > 9 })->to_array, [], 'no elements';
+$collection = c(c(1, 2, 3), c(4, 5, 6), c(7, 8, 9));
+is_deeply $collection->grep(first => sub { $_ >= 5 })->flatten->to_array,
+  [4, 5, 6, 7, 8, 9], 'right result';
+
+# join
+$collection = c(1, 2, 3);
+is $collection->join, '123', 'right result';
+is $collection->join(''),    '123',       'right result';
+is $collection->join('---'), '1---2---3', 'right result';
+is $collection->join("\n"),  "1\n2\n3",   'right result';
+#is $collection->join('/')->url_escape, '1%2F2%2F3', 'right result'; # no bytestream object
+
+# map
+$collection = c(1, 2, 3);
+is $collection->map(sub { $_ + 1 })->join(''), '234', 'right result';
+is_deeply [@$collection], [1, 2, 3], 'right elements';
+is $collection->map(sub { shift() + 2 })->join(''), '345', 'right result';
+is_deeply [@$collection], [1, 2, 3], 'right elements';
+$collection = c(c(1, 2, 3), c(4, 5, 6), c(7, 8, 9));
+is $collection->map('reverse')->map(join => "\n")->join("\n"),
+  "3\n2\n1\n6\n5\n4\n9\n8\n7", 'right result';
+is $collection->map(join => '-')->join("\n"), "1-2-3\n4-5-6\n7-8-9",
+  'right result';
+
+# reverse
+$collection = c(3, 2, 1);
+is_deeply $collection->reverse->to_array, [1, 2, 3], 'right order';
+$collection = c(3);
+is_deeply $collection->reverse->to_array, [3], 'right order';
+$collection = c();
+is_deeply $collection->reverse->to_array, [], 'no elements';
+
+# shuffle
+$collection = c(0 .. 10000);
+my $random = $collection->shuffle;
+is $collection->size, $random->size, 'same number of elements';
+isnt "@$collection", "@$random", 'different order';
+is_deeply c()->shuffle->to_array, [], 'no elements';
+
+# size
+$collection = c();
+is $collection->size, 0, 'right size';
+$collection = c(undef);
+is $collection->size, 1, 'right size';
+$collection = c(23);
+is $collection->size, 1, 'right size';
+$collection = c([2, 3]);
+is $collection->size, 1, 'right size';
+$collection = c(5, 4, 3, 2, 1);
+is $collection->size, 5, 'right size';
+
+# reduce
+$collection = c(2, 5, 4, 1);
+is $collection->reduce(sub { $a + $b }), 12, 'right result';
+is $collection->reduce(sub { $a + $b }, 5), 17, 'right result';
+is c()->reduce(sub { $a + $b }), undef, 'no result';
+
+# sort
+$collection = c(2, 5, 4, 1);
+is_deeply $collection->sort->to_array, [1, 2, 4, 5], 'right order';
+is_deeply $collection->sort(sub { $b cmp $a })->to_array, [5, 4, 2, 1],
+  'right order';
+is_deeply $collection->sort(sub { $_[1] cmp $_[0] })->to_array, [5, 4, 2, 1],
+  'right order';
+$collection = c(qw(Test perl Mojo));
+is_deeply $collection->sort(sub { uc(shift) cmp uc(shift) })->to_array,
+  [qw(Mojo perl Test)], 'right order';
+$collection = c();
+is_deeply $collection->sort->to_array, [], 'no elements';
+is_deeply $collection->sort(sub { $a cmp $b })->to_array, [], 'no elements';
+
+# slice
+$collection = c(1, 2, 3, 4, 5, 6, 7, 10, 9, 8);
+is_deeply $collection->slice(0)->to_array,  [1], 'right result';
+is_deeply $collection->slice(1)->to_array,  [2], 'right result';
+is_deeply $collection->slice(2)->to_array,  [3], 'right result';
+is_deeply $collection->slice(-1)->to_array, [8], 'right result';
+is_deeply $collection->slice(-3, -5)->to_array, [10, 6], 'right result';
+is_deeply $collection->slice(1, 2, 3)->to_array, [2, 3, 4], 'right result';
+is_deeply $collection->slice(6, 1, 4)->to_array, [7, 2, 5], 'right result';
+is_deeply $collection->slice(6 .. 9)->to_array, [7, 10, 9, 8], 'right result';
+
+# uniq
+$collection = c(1, 2, 3, 2, 3, 4, 5, 4);
+is_deeply $collection->uniq->to_array, [1, 2, 3, 4, 5], 'right result';
+is_deeply $collection->uniq->reverse->uniq->to_array, [5, 4, 3, 2, 1],
+  'right result';
+$collection = c([1, 2, 3], [3, 2, 1], [3, 1, 2]);
+is_deeply $collection->uniq(sub { $_->[1] }), [[1, 2, 3], [3, 1, 2]],
+  'right result';
+$collection = c(c(1, 2), c(1, 2), c(2, 1));
+is_deeply $collection->uniq(join => ',')->flatten->to_array, [1, 2, 2, 1],
+  'right result';
+
+# TO_JSON
+is encode_json(c(1, 2, 3)), '[1,2,3]', 'right result';
+
+done_testing();
diff --git a/t/dom.t b/t/dom.t
new file mode 100644 (file)
index 0000000..ed69234
--- /dev/null
+++ b/t/dom.t
@@ -0,0 +1,2501 @@
+use strict;
+use warnings;
+use utf8;
+use Test::More;
+use DOM::Tiny;
+
+# Empty
+is(DOM::Tiny->new,                     '',    'right result');
+is(DOM::Tiny->new(''),                 '',    'right result');
+is(DOM::Tiny->new->parse(''),          '',    'right result');
+is(DOM::Tiny->new->at('p'),            undef, 'no result');
+is(DOM::Tiny->new->append_content(''), '',    'right result');
+is(DOM::Tiny->new->all_text,           '',    'right result');
+
+# Simple (basics)
+my $dom
+  = DOM::Tiny->new('<div><div FOO="0" id="a">A</div><div id="b">B</div></div>');
+is $dom->at('#b')->text, 'B', 'right text';
+my @div;
+push @div, $dom->find('div[id]')->map('text')->each;
+is_deeply \@div, [qw(A B)], 'found all div elements with id';
+@div = ();
+$dom->find('div[id]')->each(sub { push @div, $_->text });
+is_deeply \@div, [qw(A B)], 'found all div elements with id';
+is $dom->at('#a')->attr('foo'), 0, 'right attribute';
+is $dom->at('#a')->attr->{foo}, 0, 'right attribute';
+is "$dom", '<div><div foo="0" id="a">A</div><div id="b">B</div></div>',
+  'right result';
+
+# Tap into method chain
+$dom = DOM::Tiny->new->parse('<div id="a">A</div><div id="b">B</div>');
+is_deeply [$dom->find('[id]')->map(attr => 'id')->each], [qw(a b)],
+  'right result';
+is $dom->tap(sub { $_->at('#b')->remove }), '<div id="a">A</div>',
+  'right result';
+
+# Build tree from scratch
+is(DOM::Tiny->new->append_content('<p>')->at('p')->append_content('0')->text,
+  '0', 'right text');
+
+# Simple nesting with healing (tree structure)
+$dom = DOM::Tiny->new(<<EOF);
+<foo><bar a="b&lt;c">ju<baz a23>s<bazz />t</bar>works</foo>
+EOF
+is $dom->tree->[0], 'root', 'right type';
+is $dom->tree->[1][0], 'tag', 'right type';
+is $dom->tree->[1][1], 'foo', 'right tag';
+is_deeply $dom->tree->[1][2], {}, 'empty attributes';
+is $dom->tree->[1][3], $dom->tree, 'right parent';
+is $dom->tree->[1][4][0], 'tag', 'right type';
+is $dom->tree->[1][4][1], 'bar', 'right tag';
+is_deeply $dom->tree->[1][4][2], {a => 'b<c'}, 'right attributes';
+is $dom->tree->[1][4][3], $dom->tree->[1], 'right parent';
+is $dom->tree->[1][4][4][0], 'text', 'right type';
+is $dom->tree->[1][4][4][1], 'ju',   'right text';
+is $dom->tree->[1][4][4][2], $dom->tree->[1][4], 'right parent';
+is $dom->tree->[1][4][5][0], 'tag', 'right type';
+is $dom->tree->[1][4][5][1], 'baz', 'right tag';
+is_deeply $dom->tree->[1][4][5][2], {a23 => undef}, 'right attributes';
+is $dom->tree->[1][4][5][3], $dom->tree->[1][4], 'right parent';
+is $dom->tree->[1][4][5][4][0], 'text', 'right type';
+is $dom->tree->[1][4][5][4][1], 's',    'right text';
+is $dom->tree->[1][4][5][4][2], $dom->tree->[1][4][5], 'right parent';
+is $dom->tree->[1][4][5][5][0], 'tag',  'right type';
+is $dom->tree->[1][4][5][5][1], 'bazz', 'right tag';
+is_deeply $dom->tree->[1][4][5][5][2], {}, 'empty attributes';
+is $dom->tree->[1][4][5][5][3], $dom->tree->[1][4][5], 'right parent';
+is $dom->tree->[1][4][5][6][0], 'text', 'right type';
+is $dom->tree->[1][4][5][6][1], 't',    'right text';
+is $dom->tree->[1][4][5][6][2], $dom->tree->[1][4][5], 'right parent';
+is $dom->tree->[1][5][0], 'text',  'right type';
+is $dom->tree->[1][5][1], 'works', 'right text';
+is $dom->tree->[1][5][2], $dom->tree->[1], 'right parent';
+is "$dom", <<EOF, 'right result';
+<foo><bar a="b&lt;c">ju<baz a23>s<bazz></bazz>t</baz></bar>works</foo>
+EOF
+
+# Select based on parent
+$dom = DOM::Tiny->new(<<EOF);
+<body>
+  <div>test1</div>
+  <div><div>test2</div></div>
+<body>
+EOF
+is $dom->find('body > div')->[0]->text, 'test1', 'right text';
+is $dom->find('body > div')->[1]->text, '',      'no content';
+is $dom->find('body > div')->[2], undef, 'no result';
+is $dom->find('body > div')->size, 2, 'right number of elements';
+is $dom->find('body > div > div')->[0]->text, 'test2', 'right text';
+is $dom->find('body > div > div')->[1], undef, 'no result';
+is $dom->find('body > div > div')->size, 1, 'right number of elements';
+
+# A bit of everything (basic navigation)
+$dom = DOM::Tiny->new->parse(<<EOF);
+<!doctype foo>
+<foo bar="ba&lt;z">
+  test
+  <simple class="working">easy</simple>
+  <test foo="bar" id="test" />
+  <!-- lala -->
+  works well
+  <![CDATA[ yada yada]]>
+  <?boom lalalala ?>
+  <a little bit broken>
+  < very broken
+  <br />
+  more text
+</foo>
+EOF
+ok !$dom->xml, 'XML mode not detected';
+is $dom->tag, undef, 'no tag';
+is $dom->attr('foo'), undef, 'no attribute';
+is $dom->attr(foo => 'bar')->attr('foo'), undef, 'no attribute';
+is $dom->tree->[1][0], 'doctype', 'right type';
+is $dom->tree->[1][1], ' foo',    'right doctype';
+is "$dom", <<EOF, 'right result';
+<!DOCTYPE foo>
+<foo bar="ba&lt;z">
+  test
+  <simple class="working">easy</simple>
+  <test foo="bar" id="test"></test>
+  <!-- lala -->
+  works well
+  <![CDATA[ yada yada]]>
+  <?boom lalalala ?>
+  <a bit broken little>
+  &lt; very broken
+  <br>
+  more text
+</a></foo>
+EOF
+my $simple = $dom->at('foo simple.working[class^="wor"]');
+is $simple->parent->all_text,
+  'test easy works well yada yada < very broken more text', 'right text';
+is $simple->tag, 'simple', 'right tag';
+is $simple->attr('class'), 'working', 'right class attribute';
+is $simple->text, 'easy', 'right text';
+is $simple->parent->tag, 'foo', 'right parent tag';
+is $simple->parent->attr->{bar}, 'ba<z', 'right parent attribute';
+is $simple->parent->children->[1]->tag, 'test', 'right sibling';
+is $simple->to_string, '<simple class="working">easy</simple>',
+  'stringified right';
+$simple->parent->attr(bar => 'baz')->attr({this => 'works', too => 'yea'});
+is $simple->parent->attr('bar'),  'baz',   'right parent attribute';
+is $simple->parent->attr('this'), 'works', 'right parent attribute';
+is $simple->parent->attr('too'),  'yea',   'right parent attribute';
+is $dom->at('test#test')->tag,              'test',   'right tag';
+is $dom->at('[class$="ing"]')->tag,         'simple', 'right tag';
+is $dom->at('[class="working"]')->tag,      'simple', 'right tag';
+is $dom->at('[class$=ing]')->tag,           'simple', 'right tag';
+is $dom->at('[class=working][class]')->tag, 'simple', 'right tag';
+is $dom->at('foo > simple')->next->tag, 'test', 'right tag';
+is $dom->at('foo > simple')->next->next->tag, 'a', 'right tag';
+is $dom->at('foo > test')->previous->tag, 'simple', 'right tag';
+is $dom->next,     undef, 'no siblings';
+is $dom->previous, undef, 'no siblings';
+is $dom->at('foo > a')->next,          undef, 'no next sibling';
+is $dom->at('foo > simple')->previous, undef, 'no previous sibling';
+is_deeply [$dom->at('simple')->ancestors->map('tag')->each], ['foo'],
+  'right results';
+ok !$dom->at('simple')->ancestors->first->xml, 'XML mode not active';
+
+# Nodes
+$dom = DOM::Tiny->new(
+  '<!DOCTYPE before><p>test<![CDATA[123]]><!-- 456 --></p><?after?>');
+is $dom->at('p')->preceding_nodes->first->content, ' before', 'right content';
+is $dom->at('p')->preceding_nodes->size, 1, 'right number of nodes';
+is $dom->at('p')->child_nodes->last->preceding_nodes->first->content, 'test',
+  'right content';
+is $dom->at('p')->child_nodes->last->preceding_nodes->last->content, '123',
+  'right content';
+is $dom->at('p')->child_nodes->last->preceding_nodes->size, 2,
+  'right number of nodes';
+is $dom->preceding_nodes->size, 0, 'no preceding nodes';
+is $dom->at('p')->following_nodes->first->content, 'after', 'right content';
+is $dom->at('p')->following_nodes->size, 1, 'right number of nodes';
+is $dom->child_nodes->first->following_nodes->first->tag, 'p', 'right tag';
+is $dom->child_nodes->first->following_nodes->last->content, 'after',
+  'right content';
+is $dom->child_nodes->first->following_nodes->size, 2, 'right number of nodes';
+is $dom->following_nodes->size, 0, 'no following nodes';
+is $dom->at('p')->previous_node->content,       ' before', 'right content';
+is $dom->at('p')->previous_node->previous_node, undef,     'no more siblings';
+is $dom->at('p')->next_node->content,           'after',   'right content';
+is $dom->at('p')->next_node->next_node,         undef,     'no more siblings';
+is $dom->at('p')->child_nodes->last->previous_node->previous_node->content,
+  'test', 'right content';
+is $dom->at('p')->child_nodes->first->next_node->next_node->content, ' 456 ',
+  'right content';
+is $dom->descendant_nodes->[0]->type,    'doctype', 'right type';
+is $dom->descendant_nodes->[0]->content, ' before', 'right content';
+is $dom->descendant_nodes->[0], '<!DOCTYPE before>', 'right content';
+is $dom->descendant_nodes->[1]->tag,     'p',     'right tag';
+is $dom->descendant_nodes->[2]->type,    'text',  'right type';
+is $dom->descendant_nodes->[2]->content, 'test',  'right content';
+is $dom->descendant_nodes->[5]->type,    'pi',    'right type';
+is $dom->descendant_nodes->[5]->content, 'after', 'right content';
+is $dom->at('p')->descendant_nodes->[0]->type,    'text', 'right type';
+is $dom->at('p')->descendant_nodes->[0]->content, 'test', 'right type';
+is $dom->at('p')->descendant_nodes->last->type,    'comment', 'right type';
+is $dom->at('p')->descendant_nodes->last->content, ' 456 ',   'right type';
+is $dom->child_nodes->[1]->child_nodes->first->parent->tag, 'p', 'right tag';
+is $dom->child_nodes->[1]->child_nodes->first->content, 'test', 'right content';
+is $dom->child_nodes->[1]->child_nodes->first, 'test', 'right content';
+is $dom->at('p')->child_nodes->first->type, 'text', 'right type';
+is $dom->at('p')->child_nodes->first->remove->tag, 'p', 'right tag';
+is $dom->at('p')->child_nodes->first->type,    'cdata', 'right type';
+is $dom->at('p')->child_nodes->first->content, '123',   'right content';
+is $dom->at('p')->child_nodes->[1]->type,    'comment', 'right type';
+is $dom->at('p')->child_nodes->[1]->content, ' 456 ',   'right content';
+is $dom->[0]->type,    'doctype', 'right type';
+is $dom->[0]->content, ' before', 'right content';
+is $dom->child_nodes->[2]->type,    'pi',    'right type';
+is $dom->child_nodes->[2]->content, 'after', 'right content';
+is $dom->child_nodes->first->content(' again')->content, ' again',
+  'right content';
+is $dom->child_nodes->grep(sub { $_->type eq 'pi' })->map('remove')
+  ->first->type, 'root', 'right type';
+is "$dom", '<!DOCTYPE again><p><![CDATA[123]]><!-- 456 --></p>', 'right result';
+
+# Modify nodes
+$dom = DOM::Tiny->new('<script>la<la>la</script>');
+is $dom->at('script')->type, 'tag', 'right type';
+is $dom->at('script')->[0]->type,    'raw',      'right type';
+is $dom->at('script')->[0]->content, 'la<la>la', 'right content';
+is "$dom", '<script>la<la>la</script>', 'right result';
+is $dom->at('script')->child_nodes->first->replace('a<b>c</b>1<b>d</b>')->tag,
+  'script', 'right tag';
+is "$dom", '<script>a<b>c</b>1<b>d</b></script>', 'right result';
+is $dom->at('b')->child_nodes->first->append('e')->content, 'c',
+  'right content';
+is $dom->at('b')->child_nodes->first->prepend('f')->type, 'text', 'right type';
+is "$dom", '<script>a<b>fce</b>1<b>d</b></script>', 'right result';
+is $dom->at('script')->child_nodes->first->following->first->tag, 'b',
+  'right tag';
+is $dom->at('script')->child_nodes->first->next->content, 'fce',
+  'right content';
+is $dom->at('script')->child_nodes->first->previous, undef, 'no siblings';
+is $dom->at('script')->child_nodes->[2]->previous->content, 'fce',
+  'right content';
+is $dom->at('b')->child_nodes->[1]->next, undef, 'no siblings';
+is $dom->at('script')->child_nodes->first->wrap('<i>:)</i>')->root,
+  '<script><i>:)a</i><b>fce</b>1<b>d</b></script>', 'right result';
+is $dom->at('i')->child_nodes->first->wrap_content('<b></b>')->root,
+  '<script><i><b>:)</b>a</i><b>fce</b>1<b>d</b></script>', 'right result';
+is $dom->at('b')->child_nodes->first->ancestors->map('tag')->join(','),
+  'b,i,script', 'right result';
+is $dom->at('b')->child_nodes->first->append_content('g')->content, ':)g',
+  'right content';
+is $dom->at('b')->child_nodes->first->prepend_content('h')->content, 'h:)g',
+  'right content';
+is "$dom", '<script><i><b>h:)g</b>a</i><b>fce</b>1<b>d</b></script>',
+  'right result';
+is $dom->at('script > b:last-of-type')->append('<!--y-->')
+  ->following_nodes->first->content, 'y', 'right content';
+is $dom->at('i')->prepend('z')->preceding_nodes->first->content, 'z',
+  'right content';
+is $dom->at('i')->following->last->text, 'd', 'right text';
+is $dom->at('i')->following->size, 2, 'right number of following elements';
+is $dom->at('i')->following('b:last-of-type')->first->text, 'd', 'right text';
+is $dom->at('i')->following('b:last-of-type')->size, 1,
+  'right number of following elements';
+is $dom->following->size, 0, 'no following elements';
+is $dom->at('script > b:last-of-type')->preceding->first->tag, 'i', 'right tag';
+is $dom->at('script > b:last-of-type')->preceding->size, 2,
+  'right number of preceding elements';
+is $dom->at('script > b:last-of-type')->preceding('b')->first->tag, 'b',
+  'right tag';
+is $dom->at('script > b:last-of-type')->preceding('b')->size, 1,
+  'right number of preceding elements';
+is $dom->preceding->size, 0, 'no preceding elements';
+is "$dom", '<script>z<i><b>h:)g</b>a</i><b>fce</b>1<b>d</b><!--y--></script>',
+  'right result';
+
+# XML nodes
+$dom = DOM::Tiny->new->xml(1)->parse('<b>test<image /></b>');
+ok $dom->at('b')->child_nodes->first->xml, 'XML mode active';
+ok $dom->at('b')->child_nodes->first->replace('<br>')->child_nodes->first->xml,
+  'XML mode active';
+is "$dom", '<b><br /><image /></b>', 'right result';
+
+# Treating nodes as elements
+$dom = DOM::Tiny->new('foo<b>bar</b>baz');
+is $dom->child_nodes->first->child_nodes->size,      0, 'no nodes';
+is $dom->child_nodes->first->descendant_nodes->size, 0, 'no nodes';
+is $dom->child_nodes->first->children->size,         0, 'no children';
+is $dom->child_nodes->first->strip->parent, 'foo<b>bar</b>baz', 'no changes';
+is $dom->child_nodes->first->at('b'), undef, 'no result';
+is $dom->child_nodes->first->find('*')->size, 0, 'no results';
+ok !$dom->child_nodes->first->matches('*'), 'no match';
+is_deeply $dom->child_nodes->first->attr, {}, 'no attributes';
+is $dom->child_nodes->first->namespace, undef, 'no namespace';
+is $dom->child_nodes->first->tag,       undef, 'no tag';
+is $dom->child_nodes->first->text,      '',    'no text';
+is $dom->child_nodes->first->all_text,  '',    'no text';
+
+# Class and ID
+$dom = DOM::Tiny->new('<div id="id" class="class">a</div>');
+is $dom->at('div#id.class')->text, 'a', 'right text';
+
+# Deep nesting (parent combinator)
+$dom = DOM::Tiny->new(<<EOF);
+<html>
+  <head>
+    <title>Foo</title>
+  </head>
+  <body>
+    <div id="container">
+      <div id="header">
+        <div id="logo">Hello World</div>
+        <div id="buttons">
+          <p id="foo">Foo</p>
+        </div>
+      </div>
+      <form>
+        <div id="buttons">
+          <p id="bar">Bar</p>
+        </div>
+      </form>
+      <div id="content">More stuff</div>
+    </div>
+  </body>
+</html>
+EOF
+my $p = $dom->find('body > #container > div p[id]');
+is $p->[0]->attr('id'), 'foo', 'right id attribute';
+is $p->[1], undef, 'no second result';
+is $p->size, 1, 'right number of elements';
+my @p;
+@div = ();
+$dom->find('div')->each(sub { push @div, $_->attr('id') });
+$dom->find('p')->each(sub { push @p, $_->attr('id') });
+is_deeply \@p, [qw(foo bar)], 'found all p elements';
+my $ids = [qw(container header logo buttons buttons content)];
+is_deeply \@div, $ids, 'found all div elements';
+is_deeply [$dom->at('p')->ancestors->map('tag')->each],
+  [qw(div div div body html)], 'right results';
+is_deeply [$dom->at('html')->ancestors->each], [], 'no results';
+is_deeply [$dom->ancestors->each],             [], 'no results';
+
+# Script tag
+$dom = DOM::Tiny->new(<<EOF);
+<script charset="utf-8">alert('lalala');</script>
+EOF
+is $dom->at('script')->text, "alert('lalala');", 'right script content';
+
+# HTML5 (unquoted values)
+$dom = DOM::Tiny->new(
+  '<div id = test foo ="bar" class=tset bar=/baz/ baz=//>works</div>');
+is $dom->at('#test')->text,                'works', 'right text';
+is $dom->at('div')->text,                  'works', 'right text';
+is $dom->at('[foo=bar][foo="bar"]')->text, 'works', 'right text';
+is $dom->at('[foo="ba"]'), undef, 'no result';
+is $dom->at('[foo=bar]')->text, 'works', 'right text';
+is $dom->at('[foo=ba]'), undef, 'no result';
+is $dom->at('.tset')->text,       'works', 'right text';
+is $dom->at('[bar=/baz/]')->text, 'works', 'right text';
+is $dom->at('[baz=//]')->text,    'works', 'right text';
+
+# HTML1 (single quotes, uppercase tags and whitespace in attributes)
+$dom = DOM::Tiny->new(q{<DIV id = 'test' foo ='bar' class= "tset">works</DIV>});
+is $dom->at('#test')->text,       'works', 'right text';
+is $dom->at('div')->text,         'works', 'right text';
+is $dom->at('[foo="bar"]')->text, 'works', 'right text';
+is $dom->at('[foo="ba"]'), undef, 'no result';
+is $dom->at('[foo=bar]')->text, 'works', 'right text';
+is $dom->at('[foo=ba]'), undef, 'no result';
+is $dom->at('.tset')->text, 'works', 'right text';
+
+# Already decoded Unicode snowman and quotes in selector
+$dom = DOM::Tiny->new('<div id="snow&apos;m&quot;an">☃</div>');
+is $dom->at('[id="snow\'m\"an"]')->text,      '☃', 'right text';
+is $dom->at('[id="snow\'m\22 an"]')->text,    '☃', 'right text';
+is $dom->at('[id="snow\'m\000022an"]')->text, '☃', 'right text';
+is $dom->at('[id="snow\'m\22an"]'),      undef, 'no result';
+is $dom->at('[id="snow\'m\21 an"]'),     undef, 'no result';
+is $dom->at('[id="snow\'m\000021an"]'),  undef, 'no result';
+is $dom->at('[id="snow\'m\000021 an"]'), undef, 'no result';
+is $dom->at("[id='snow\\'m\"an']")->text,  '☃', 'right text';
+is $dom->at("[id='snow\\27m\"an']")->text, '☃', 'right text';
+
+# Unicode and escaped selectors
+my $html
+  = '<html><div id="☃x">Snowman</div><div class="x ♥">Heart</div></html>';
+$dom = DOM::Tiny->new($html);
+is $dom->at("#\\\n\\002603x")->text,                  'Snowman', 'right text';
+is $dom->at('#\\2603 x')->text,                       'Snowman', 'right text';
+is $dom->at("#\\\n\\2603 x")->text,                   'Snowman', 'right text';
+is $dom->at(qq{[id="\\\n\\2603 x"]})->text,           'Snowman', 'right text';
+is $dom->at(qq{[id="\\\n\\002603x"]})->text,          'Snowman', 'right text';
+is $dom->at(qq{[id="\\\\2603 x"]})->text,             'Snowman', 'right text';
+is $dom->at("html #\\\n\\002603x")->text,             'Snowman', 'right text';
+is $dom->at('html #\\2603 x')->text,                  'Snowman', 'right text';
+is $dom->at("html #\\\n\\2603 x")->text,              'Snowman', 'right text';
+is $dom->at(qq{html [id="\\\n\\2603 x"]})->text,      'Snowman', 'right text';
+is $dom->at(qq{html [id="\\\n\\002603x"]})->text,     'Snowman', 'right text';
+is $dom->at(qq{html [id="\\\\2603 x"]})->text,        'Snowman', 'right text';
+is $dom->at('#☃x')->text,                           'Snowman', 'right text';
+is $dom->at('div#☃x')->text,                        'Snowman', 'right text';
+is $dom->at('html div#☃x')->text,                   'Snowman', 'right text';
+is $dom->at('[id^="☃"]')->text,                     'Snowman', 'right text';
+is $dom->at('div[id^="☃"]')->text,                  'Snowman', 'right text';
+is $dom->at('html div[id^="☃"]')->text,             'Snowman', 'right text';
+is $dom->at('html > div[id^="☃"]')->text,           'Snowman', 'right text';
+is $dom->at('[id^=☃]')->text,                       'Snowman', 'right text';
+is $dom->at('div[id^=☃]')->text,                    'Snowman', 'right text';
+is $dom->at('html div[id^=☃]')->text,               'Snowman', 'right text';
+is $dom->at('html > div[id^=☃]')->text,             'Snowman', 'right text';
+is $dom->at(".\\\n\\002665")->text,                   'Heart',   'right text';
+is $dom->at('.\\2665')->text,                         'Heart',   'right text';
+is $dom->at("html .\\\n\\002665")->text,              'Heart',   'right text';
+is $dom->at('html .\\2665')->text,                    'Heart',   'right text';
+is $dom->at(qq{html [class\$="\\\n\\002665"]})->text, 'Heart',   'right text';
+is $dom->at(qq{html [class\$="\\2665"]})->text,       'Heart',   'right text';
+is $dom->at(qq{[class\$="\\\n\\002665"]})->text,      'Heart',   'right text';
+is $dom->at(qq{[class\$="\\2665"]})->text,            'Heart',   'right text';
+is $dom->at('.x')->text,                              'Heart',   'right text';
+is $dom->at('html .x')->text,                         'Heart',   'right text';
+is $dom->at('.♥')->text,                            'Heart',   'right text';
+is $dom->at('html .♥')->text,                       'Heart',   'right text';
+is $dom->at('div.♥')->text,                         'Heart',   'right text';
+is $dom->at('html div.♥')->text,                    'Heart',   'right text';
+is $dom->at('[class$="♥"]')->text,                  'Heart',   'right text';
+is $dom->at('div[class$="♥"]')->text,               'Heart',   'right text';
+is $dom->at('html div[class$="♥"]')->text,          'Heart',   'right text';
+is $dom->at('html > div[class$="♥"]')->text,        'Heart',   'right text';
+is $dom->at('[class$=♥]')->text,                    'Heart',   'right text';
+is $dom->at('div[class$=♥]')->text,                 'Heart',   'right text';
+is $dom->at('html div[class$=♥]')->text,            'Heart',   'right text';
+is $dom->at('html > div[class$=♥]')->text,          'Heart',   'right text';
+is $dom->at('[class~="♥"]')->text,                  'Heart',   'right text';
+is $dom->at('div[class~="♥"]')->text,               'Heart',   'right text';
+is $dom->at('html div[class~="♥"]')->text,          'Heart',   'right text';
+is $dom->at('html > div[class~="♥"]')->text,        'Heart',   'right text';
+is $dom->at('[class~=♥]')->text,                    'Heart',   'right text';
+is $dom->at('div[class~=♥]')->text,                 'Heart',   'right text';
+is $dom->at('html div[class~=♥]')->text,            'Heart',   'right text';
+is $dom->at('html > div[class~=♥]')->text,          'Heart',   'right text';
+is $dom->at('[class~="x"]')->text,                    'Heart',   'right text';
+is $dom->at('div[class~="x"]')->text,                 'Heart',   'right text';
+is $dom->at('html div[class~="x"]')->text,            'Heart',   'right text';
+is $dom->at('html > div[class~="x"]')->text,          'Heart',   'right text';
+is $dom->at('[class~=x]')->text,                      'Heart',   'right text';
+is $dom->at('div[class~=x]')->text,                   'Heart',   'right text';
+is $dom->at('html div[class~=x]')->text,              'Heart',   'right text';
+is $dom->at('html > div[class~=x]')->text,            'Heart',   'right text';
+is $dom->at('html'), $html, 'right result';
+is $dom->at('#☃x')->parent,     $html, 'right result';
+is $dom->at('#☃x')->root,       $html, 'right result';
+is $dom->children('html')->first, $html, 'right result';
+is $dom->to_string, $html, 'right result';
+is $dom->content,   $html, 'right result';
+
+# Looks remotely like HTML
+$dom = DOM::Tiny->new(
+  '<!DOCTYPE H "-/W/D HT 4/E">☃<title class=test>♥</title>☃');
+is $dom->at('title')->text, '♥', 'right text';
+is $dom->at('*')->text,     '♥', 'right text';
+is $dom->at('.test')->text, '♥', 'right text';
+
+# Replace elements
+$dom = DOM::Tiny->new('<div>foo<p>lalala</p>bar</div>');
+is $dom->at('p')->replace('<foo>bar</foo>'), '<div>foo<foo>bar</foo>bar</div>',
+  'right result';
+is "$dom", '<div>foo<foo>bar</foo>bar</div>', 'right result';
+$dom->at('foo')->replace(DOM::Tiny->new('text'));
+is "$dom", '<div>footextbar</div>', 'right result';
+$dom = DOM::Tiny->new('<div>foo</div><div>bar</div>');
+$dom->find('div')->each(sub { shift->replace('<p>test</p>') });
+is "$dom", '<p>test</p><p>test</p>', 'right result';
+$dom = DOM::Tiny->new('<div>foo<p>lalala</p>bar</div>');
+is $dom->replace('♥'), '♥', 'right result';
+is "$dom", '♥', 'right result';
+$dom->replace('<div>foo<p>lalala</p>bar</div>');
+is "$dom", '<div>foo<p>lalala</p>bar</div>', 'right result';
+is $dom->at('p')->replace(''), '<div>foobar</div>', 'right result';
+is "$dom", '<div>foobar</div>', 'right result';
+is $dom->replace(''), '', 'no result';
+is "$dom", '', 'no result';
+$dom->replace('<div>foo<p>lalala</p>bar</div>');
+is "$dom", '<div>foo<p>lalala</p>bar</div>', 'right result';
+$dom->find('p')->map(replace => '');
+is "$dom", '<div>foobar</div>', 'right result';
+$dom = DOM::Tiny->new('<div>♥</div>');
+$dom->at('div')->content('☃');
+is "$dom", '<div>☃</div>', 'right result';
+$dom = DOM::Tiny->new('<div>♥</div>');
+$dom->at('div')->content("\x{2603}");
+is $dom->to_string, '<div>☃</div>', 'right result';
+is $dom->at('div')->replace('<p>♥</p>')->root, '<p>♥</p>', 'right result';
+is $dom->to_string, '<p>♥</p>', 'right result';
+is $dom->replace('<b>whatever</b>')->root, '<b>whatever</b>', 'right result';
+is $dom->to_string, '<b>whatever</b>', 'right result';
+$dom->at('b')->prepend('<p>foo</p>')->append('<p>bar</p>');
+is "$dom", '<p>foo</p><b>whatever</b><p>bar</p>', 'right result';
+is $dom->find('p')->map('remove')->first->root->at('b')->text, 'whatever',
+  'right result';
+is "$dom", '<b>whatever</b>', 'right result';
+is $dom->at('b')->strip, 'whatever', 'right result';
+is $dom->strip,  'whatever', 'right result';
+is $dom->remove, '',         'right result';
+$dom->replace('A<div>B<p>C<b>D<i><u>E</u></i>F</b>G</p><div>H</div></div>I');
+is $dom->find(':not(div):not(i):not(u)')->map('strip')->first->root,
+  'A<div>BCD<i><u>E</u></i>FG<div>H</div></div>I', 'right result';
+is $dom->at('i')->to_string, '<i><u>E</u></i>', 'right result';
+$dom = DOM::Tiny->new('<div><div>A</div><div>B</div>C</div>');
+is $dom->at('div')->at('div')->text, 'A', 'right text';
+$dom->at('div')->find('div')->map('strip');
+is "$dom", '<div>ABC</div>', 'right result';
+
+# Replace element content
+$dom = DOM::Tiny->new('<div>foo<p>lalala</p>bar</div>');
+is $dom->at('p')->content('bar'), '<p>bar</p>', 'right result';
+is "$dom", '<div>foo<p>bar</p>bar</div>', 'right result';
+$dom->at('p')->content(DOM::Tiny->new('text'));
+is "$dom", '<div>foo<p>text</p>bar</div>', 'right result';
+$dom = DOM::Tiny->new('<div>foo</div><div>bar</div>');
+$dom->find('div')->each(sub { shift->content('<p>test</p>') });
+is "$dom", '<div><p>test</p></div><div><p>test</p></div>', 'right result';
+$dom->find('p')->each(sub { shift->content('') });
+is "$dom", '<div><p></p></div><div><p></p></div>', 'right result';
+$dom = DOM::Tiny->new('<div><p id="☃" /></div>');
+$dom->at('#☃')->content('♥');
+is "$dom", '<div><p id="☃">♥</p></div>', 'right result';
+$dom = DOM::Tiny->new('<div>foo<p>lalala</p>bar</div>');
+$dom->content('♥');
+is "$dom", '♥', 'right result';
+is $dom->content('<div>foo<p>lalala</p>bar</div>'),
+  '<div>foo<p>lalala</p>bar</div>', 'right result';
+is "$dom", '<div>foo<p>lalala</p>bar</div>', 'right result';
+is $dom->content(''), '', 'no result';
+is "$dom", '', 'no result';
+$dom->content('<div>foo<p>lalala</p>bar</div>');
+is "$dom", '<div>foo<p>lalala</p>bar</div>', 'right result';
+is $dom->at('p')->content(''), '<p></p>', 'right result';
+
+# Mixed search and tree walk
+$dom = DOM::Tiny->new(<<EOF);
+<table>
+  <tr>
+    <td>text1</td>
+    <td>text2</td>
+  </tr>
+</table>
+EOF
+my @data;
+for my $tr ($dom->find('table tr')->each) {
+  for my $td (@{$tr->children}) {
+    push @data, $td->tag, $td->all_text;
+  }
+}
+is $data[0], 'td',    'right tag';
+is $data[1], 'text1', 'right text';
+is $data[2], 'td',    'right tag';
+is $data[3], 'text2', 'right text';
+is $data[4], undef,   'no tag';
+
+# RSS
+$dom = DOM::Tiny->new(<<EOF);
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
+  <channel>
+    <title>Test Blog</title>
+    <link>http://blog.example.com</link>
+    <description>lalala</description>
+    <generator>DOM::Tiny</generator>
+    <item>
+      <pubDate>Mon, 12 Jul 2010 20:42:00</pubDate>
+      <title>Works!</title>
+      <link>http://blog.example.com/test</link>
+      <guid>http://blog.example.com/test</guid>
+      <description>
+        <![CDATA[<p>trololololo>]]>
+      </description>
+      <my:extension foo:id="works">
+        <![CDATA[
+          [awesome]]
+        ]]>
+      </my:extension>
+    </item>
+  </channel>
+</rss>
+EOF
+ok $dom->xml, 'XML mode detected';
+is $dom->find('rss')->[0]->attr('version'), '2.0', 'right version';
+is_deeply [$dom->at('title')->ancestors->map('tag')->each], [qw(channel rss)],
+  'right results';
+is $dom->at('extension')->attr('foo:id'), 'works', 'right id';
+like $dom->at('#works')->text,       qr/\[awesome\]\]/, 'right text';
+like $dom->at('[id="works"]')->text, qr/\[awesome\]\]/, 'right text';
+is $dom->find('description')->[1]->text, '<p>trololololo>', 'right text';
+is $dom->at('pubDate')->text,        'Mon, 12 Jul 2010 20:42:00', 'right text';
+like $dom->at('[id*="ork"]')->text,  qr/\[awesome\]\]/,           'right text';
+like $dom->at('[id*="orks"]')->text, qr/\[awesome\]\]/,           'right text';
+like $dom->at('[id*="work"]')->text, qr/\[awesome\]\]/,           'right text';
+like $dom->at('[id*="or"]')->text,   qr/\[awesome\]\]/,           'right text';
+ok $dom->at('rss')->xml,             'XML mode active';
+ok $dom->at('extension')->parent->xml, 'XML mode active';
+ok $dom->at('extension')->root->xml,   'XML mode active';
+ok $dom->children('rss')->first->xml,  'XML mode active';
+ok $dom->at('title')->ancestors->first->xml, 'XML mode active';
+
+# Namespace
+$dom = DOM::Tiny->new(<<EOF);
+<?xml version="1.0"?>
+<bk:book xmlns='uri:default-ns'
+         xmlns:bk='uri:book-ns'
+         xmlns:isbn='uri:isbn-ns'>
+  <bk:title>Programming Perl</bk:title>
+  <comment>rocks!</comment>
+  <nons xmlns=''>
+    <section>Nothing</section>
+  </nons>
+  <meta xmlns='uri:meta-ns'>
+    <isbn:number>978-0596000271</isbn:number>
+  </meta>
+</bk:book>
+EOF
+ok $dom->xml, 'XML mode detected';
+is $dom->namespace, undef, 'no namespace';
+is $dom->at('book comment')->namespace, 'uri:default-ns', 'right namespace';
+is $dom->at('book comment')->text,      'rocks!',         'right text';
+is $dom->at('book nons section')->namespace, '',            'no namespace';
+is $dom->at('book nons section')->text,      'Nothing',     'right text';
+is $dom->at('book meta number')->namespace,  'uri:isbn-ns', 'right namespace';
+is $dom->at('book meta number')->text, '978-0596000271', 'right text';
+is $dom->children('bk\:book')->first->{xmlns}, 'uri:default-ns',
+  'right attribute';
+is $dom->children('book')->first->{xmlns}, 'uri:default-ns', 'right attribute';
+is $dom->children('k\:book')->first, undef, 'no result';
+is $dom->children('ook')->first,     undef, 'no result';
+is $dom->at('k\:book'), undef, 'no result';
+is $dom->at('ook'),     undef, 'no result';
+is $dom->at('[xmlns\:bk]')->{'xmlns:bk'}, 'uri:book-ns', 'right attribute';
+is $dom->at('[bk]')->{'xmlns:bk'},        'uri:book-ns', 'right attribute';
+is $dom->at('[bk]')->attr('xmlns:bk'), 'uri:book-ns', 'right attribute';
+is $dom->at('[bk]')->attr('s:bk'),     undef,         'no attribute';
+is $dom->at('[bk]')->attr('bk'),       undef,         'no attribute';
+is $dom->at('[bk]')->attr('k'),        undef,         'no attribute';
+is $dom->at('[s\:bk]'), undef, 'no result';
+is $dom->at('[k]'),     undef, 'no result';
+is $dom->at('number')->ancestors('meta')->first->{xmlns}, 'uri:meta-ns',
+  'right attribute';
+ok $dom->at('nons')->matches('book > nons'), 'element did match';
+ok !$dom->at('title')->matches('book > nons > section'),
+  'element did not match';
+
+# Dots
+$dom = DOM::Tiny->new(<<EOF);
+<?xml version="1.0"?>
+<foo xmlns:foo.bar="uri:first">
+  <bar xmlns:fooxbar="uri:second">
+    <foo.bar:baz>First</fooxbar:baz>
+    <fooxbar:ya.da>Second</foo.bar:ya.da>
+  </bar>
+</foo>
+EOF
+is $dom->at('foo bar baz')->text,    'First',      'right text';
+is $dom->at('baz')->namespace,       'uri:first',  'right namespace';
+is $dom->at('foo bar ya\.da')->text, 'Second',     'right text';
+is $dom->at('ya\.da')->namespace,    'uri:second', 'right namespace';
+is $dom->at('foo')->namespace,       undef,        'no namespace';
+is $dom->at('[xml\.s]'), undef, 'no result';
+is $dom->at('b\.z'),     undef, 'no result';
+
+# Yadis
+$dom = DOM::Tiny->new(<<'EOF');
+<?xml version="1.0" encoding="UTF-8"?>
+<XRDS xmlns="xri://$xrds">
+  <XRD xmlns="xri://$xrd*($v*2.0)">
+    <Service>
+      <Type>http://o.r.g/sso/2.0</Type>
+    </Service>
+    <Service>
+      <Type>http://o.r.g/sso/1.0</Type>
+    </Service>
+  </XRD>
+</XRDS>
+EOF
+ok $dom->xml, 'XML mode detected';
+is $dom->at('XRDS')->namespace, 'xri://$xrds',         'right namespace';
+is $dom->at('XRD')->namespace,  'xri://$xrd*($v*2.0)', 'right namespace';
+my $s = $dom->find('XRDS XRD Service');
+is $s->[0]->at('Type')->text, 'http://o.r.g/sso/2.0', 'right text';
+is $s->[0]->namespace, 'xri://$xrd*($v*2.0)', 'right namespace';
+is $s->[1]->at('Type')->text, 'http://o.r.g/sso/1.0', 'right text';
+is $s->[1]->namespace, 'xri://$xrd*($v*2.0)', 'right namespace';
+is $s->[2], undef, 'no result';
+is $s->size, 2, 'right number of elements';
+
+# Yadis (roundtrip with namespace)
+my $yadis = <<'EOF';
+<?xml version="1.0" encoding="UTF-8"?>
+<xrds:XRDS xmlns="xri://$xrd*($v*2.0)" xmlns:xrds="xri://$xrds">
+  <XRD>
+    <Service>
+      <Type>http://o.r.g/sso/3.0</Type>
+    </Service>
+    <xrds:Service>
+      <Type>http://o.r.g/sso/4.0</Type>
+    </xrds:Service>
+  </XRD>
+  <XRD>
+    <Service>
+      <Type test="23">http://o.r.g/sso/2.0</Type>
+    </Service>
+    <Service>
+      <Type Test="23" test="24">http://o.r.g/sso/1.0</Type>
+    </Service>
+  </XRD>
+</xrds:XRDS>
+EOF
+$dom = DOM::Tiny->new($yadis);
+ok $dom->xml, 'XML mode detected';
+is $dom->at('XRDS')->namespace, 'xri://$xrds',         'right namespace';
+is $dom->at('XRD')->namespace,  'xri://$xrd*($v*2.0)', 'right namespace';
+$s = $dom->find('XRDS XRD Service');
+is $s->[0]->at('Type')->text, 'http://o.r.g/sso/3.0', 'right text';
+is $s->[0]->namespace, 'xri://$xrd*($v*2.0)', 'right namespace';
+is $s->[1]->at('Type')->text, 'http://o.r.g/sso/4.0', 'right text';
+is $s->[1]->namespace, 'xri://$xrds', 'right namespace';
+is $s->[2]->at('Type')->text, 'http://o.r.g/sso/2.0', 'right text';
+is $s->[2]->namespace, 'xri://$xrd*($v*2.0)', 'right namespace';
+is $s->[3]->at('Type')->text, 'http://o.r.g/sso/1.0', 'right text';
+is $s->[3]->namespace, 'xri://$xrd*($v*2.0)', 'right namespace';
+is $s->[4], undef, 'no result';
+is $s->size, 4, 'right number of elements';
+is $dom->at('[Test="23"]')->text, 'http://o.r.g/sso/1.0', 'right text';
+is $dom->at('[test="23"]')->text, 'http://o.r.g/sso/2.0', 'right text';
+is $dom->find('xrds\:Service > Type')->[0]->text, 'http://o.r.g/sso/4.0',
+  'right text';
+is $dom->find('xrds\:Service > Type')->[1], undef, 'no result';
+is $dom->find('xrds\3AService > Type')->[0]->text, 'http://o.r.g/sso/4.0',
+  'right text';
+is $dom->find('xrds\3AService > Type')->[1], undef, 'no result';
+is $dom->find('xrds\3A Service > Type')->[0]->text, 'http://o.r.g/sso/4.0',
+  'right text';
+is $dom->find('xrds\3A Service > Type')->[1], undef, 'no result';
+is $dom->find('xrds\00003AService > Type')->[0]->text, 'http://o.r.g/sso/4.0',
+  'right text';
+is $dom->find('xrds\00003AService > Type')->[1], undef, 'no result';
+is $dom->find('xrds\00003A Service > Type')->[0]->text, 'http://o.r.g/sso/4.0',
+  'right text';
+is $dom->find('xrds\00003A Service > Type')->[1], undef, 'no result';
+is "$dom", $yadis, 'successful roundtrip';
+
+# Result and iterator order
+$dom = DOM::Tiny->new('<a><b>1</b></a><b>2</b><b>3</b>');
+my @numbers;
+$dom->find('b')->each(sub { push @numbers, pop, shift->text });
+is_deeply \@numbers, [1, 1, 2, 2, 3, 3], 'right order';
+
+# Attributes on multiple lines
+$dom = DOM::Tiny->new("<div test=23 id='a' \n class='x' foo=bar />");
+is $dom->at('div.x')->attr('test'),        23,  'right attribute';
+is $dom->at('[foo="bar"]')->attr('class'), 'x', 'right attribute';
+is $dom->at('div')->attr(baz => undef)->root->to_string,
+  '<div baz class="x" foo="bar" id="a" test="23"></div>', 'right result';
+
+# Markup characters in attribute values
+$dom = DOM::Tiny->new(qq{<div id="<a>" \n test='='>Test<div id='><' /></div>});
+is $dom->at('div[id="<a>"]')->attr->{test}, '=', 'right attribute';
+is $dom->at('[id="<a>"]')->text, 'Test', 'right text';
+is $dom->at('[id="><"]')->attr->{id}, '><', 'right attribute';
+
+# Empty attributes
+$dom = DOM::Tiny->new(qq{<div test="" test2='' />});
+is $dom->at('div')->attr->{test},  '', 'empty attribute value';
+is $dom->at('div')->attr->{test2}, '', 'empty attribute value';
+is $dom->at('[test]')->tag,  'div', 'right tag';
+is $dom->at('[test2]')->tag, 'div', 'right tag';
+is $dom->at('[test3]'), undef, 'no result';
+is $dom->at('[test=""]')->tag,  'div', 'right tag';
+is $dom->at('[test2=""]')->tag, 'div', 'right tag';
+is $dom->at('[test3=""]'), undef, 'no result';
+
+# Multi-line in attribute
+$dom = DOM::Tiny->new(qq{<div test="line1\nline2" />});
+is $dom->at('div')->attr->{test}, "line1\nline2", 'multi-line attribute';
+
+# Whitespaces before closing bracket
+$dom = DOM::Tiny->new('<div >content</div>');
+ok $dom->at('div'), 'tag found';
+is $dom->at('div')->text,    'content', 'right text';
+is $dom->at('div')->content, 'content', 'right text';
+
+# Class with hyphen
+$dom = DOM::Tiny->new('<div class="a">A</div><div class="a-1">A1</div>');
+@div = ();
+$dom->find('.a')->each(sub { push @div, shift->text });
+is_deeply \@div, ['A'], 'found first element only';
+@div = ();
+$dom->find('.a-1')->each(sub { push @div, shift->text });
+is_deeply \@div, ['A1'], 'found last element only';
+
+# Defined but false text
+$dom = DOM::Tiny->new(
+  '<div><div id="a">A</div><div id="b">B</div></div><div id="0">0</div>');
+@div = ();
+$dom->find('div[id]')->each(sub { push @div, shift->text });
+is_deeply \@div, [qw(A B 0)], 'found all div elements with id';
+
+# Empty tags
+$dom = DOM::Tiny->new('<hr /><br/><br id="br"/><br />');
+is "$dom", '<hr><br><br id="br"><br>', 'right result';
+is $dom->at('br')->content, '', 'empty result';
+
+# Inner XML
+$dom = DOM::Tiny->new('<a>xxx<x>x</x>xxx</a>');
+is $dom->at('a')->content, 'xxx<x>x</x>xxx', 'right result';
+is $dom->content, '<a>xxx<x>x</x>xxx</a>', 'right result';
+
+# Multiple selectors
+$dom = DOM::Tiny->new(
+  '<div id="a">A</div><div id="b">B</div><div id="c">C</div><p>D</p>');
+@div = ();
+$dom->find('p, div')->each(sub { push @div, shift->text });
+is_deeply \@div, [qw(A B C D)], 'found all elements';
+@div = ();
+$dom->find('#a, #c')->each(sub { push @div, shift->text });
+is_deeply \@div, [qw(A C)], 'found all div elements with the right ids';
+@div = ();
+$dom->find('div#a, div#b')->each(sub { push @div, shift->text });
+is_deeply \@div, [qw(A B)], 'found all div elements with the right ids';
+@div = ();
+$dom->find('div[id="a"], div[id="c"]')->each(sub { push @div, shift->text });
+is_deeply \@div, [qw(A C)], 'found all div elements with the right ids';
+$dom = DOM::Tiny->new(
+  '<div id="☃">A</div><div id="b">B</div><div id="♥x">C</div>');
+@div = ();
+$dom->find('#☃, #♥x')->each(sub { push @div, shift->text });
+is_deeply \@div, [qw(A C)], 'found all div elements with the right ids';
+@div = ();
+$dom->find('div#☃, div#b')->each(sub { push @div, shift->text });
+is_deeply \@div, [qw(A B)], 'found all div elements with the right ids';
+@div = ();
+$dom->find('div[id="☃"], div[id="♥x"]')
+  ->each(sub { push @div, shift->text });
+is_deeply \@div, [qw(A C)], 'found all div elements with the right ids';
+
+# Multiple attributes
+$dom = DOM::Tiny->new(<<EOF);
+<div foo="bar" bar="baz">A</div>
+<div foo="bar">B</div>
+<div foo="bar" bar="baz">C</div>
+<div foo="baz" bar="baz">D</div>
+EOF
+@div = ();
+$dom->find('div[foo="bar"][bar="baz"]')->each(sub { push @div, shift->text });
+is_deeply \@div, [qw(A C)], 'found all div elements with the right atributes';
+@div = ();
+$dom->find('div[foo^="b"][foo$="r"]')->each(sub { push @div, shift->text });
+is_deeply \@div, [qw(A B C)], 'found all div elements with the right atributes';
+is $dom->at('[foo="bar"]')->previous, undef, 'no previous sibling';
+is $dom->at('[foo="bar"]')->next->text, 'B', 'right text';
+is $dom->at('[foo="bar"]')->next->previous->text, 'A', 'right text';
+is $dom->at('[foo="bar"]')->next->next->next->next, undef, 'no next sibling';
+
+# Pseudo-classes
+$dom = DOM::Tiny->new(<<EOF);
+<form action="/foo">
+    <input type="text" name="user" value="test" />
+    <input type="checkbox" checked="checked" name="groovy">
+    <select name="a">
+        <option value="b">b</option>
+        <optgroup label="c">
+            <option value="d">d</option>
+            <option selected="selected" value="e">E</option>
+            <option value="f">f</option>
+        </optgroup>
+        <option value="g">g</option>
+        <option selected value="h">H</option>
+    </select>
+    <input type="submit" value="Ok!" />
+    <input type="checkbox" checked name="I">
+    <p id="content">test 123</p>
+    <p id="no_content"><? test ?><!-- 123 --></p>
+</form>
+EOF
+is $dom->find(':root')->[0]->tag,     'form', 'right tag';
+is $dom->find('*:root')->[0]->tag,    'form', 'right tag';
+is $dom->find('form:root')->[0]->tag, 'form', 'right tag';
+is $dom->find(':root')->[1], undef, 'no result';
+is $dom->find(':checked')->[0]->attr->{name},        'groovy', 'right name';
+is $dom->find('option:checked')->[0]->attr->{value}, 'e',      'right value';
+is $dom->find(':checked')->[1]->text,  'E', 'right text';
+is $dom->find('*:checked')->[1]->text, 'E', 'right text';
+is $dom->find(':checked')->[2]->text,  'H', 'right name';
+is $dom->find(':checked')->[3]->attr->{name}, 'I', 'right name';
+is $dom->find(':checked')->[4], undef, 'no result';
+is $dom->find('option[selected]')->[0]->attr->{value}, 'e', 'right value';
+is $dom->find('option[selected]')->[1]->text, 'H', 'right text';
+is $dom->find('option[selected]')->[2], undef, 'no result';
+is $dom->find(':checked[value="e"]')->[0]->text,       'E', 'right text';
+is $dom->find('*:checked[value="e"]')->[0]->text,      'E', 'right text';
+is $dom->find('option:checked[value="e"]')->[0]->text, 'E', 'right text';
+is $dom->at('optgroup option:checked[value="e"]')->text, 'E', 'right text';
+is $dom->at('select option:checked[value="e"]')->text,   'E', 'right text';
+is $dom->at('select :checked[value="e"]')->text,         'E', 'right text';
+is $dom->at('optgroup > :checked[value="e"]')->text,     'E', 'right text';
+is $dom->at('select *:checked[value="e"]')->text,        'E', 'right text';
+is $dom->at('optgroup > *:checked[value="e"]')->text,    'E', 'right text';
+is $dom->find(':checked[value="e"]')->[1], undef, 'no result';
+is $dom->find(':empty')->[0]->attr->{name},      'user', 'right name';
+is $dom->find('input:empty')->[0]->attr->{name}, 'user', 'right name';
+is $dom->at(':empty[type^="ch"]')->attr->{name}, 'groovy',  'right name';
+is $dom->at('p')->attr->{id},                    'content', 'right attribute';
+is $dom->at('p:empty')->attr->{id}, 'no_content', 'right attribute';
+
+# More pseudo-classes
+$dom = DOM::Tiny->new(<<EOF);
+<ul>
+    <li>A</li>
+    <li>B</li>
+    <li>C</li>
+    <li>D</li>
+    <li>E</li>
+    <li>F</li>
+    <li>G</li>
+    <li>H</li>
+</ul>
+EOF
+my @li;
+$dom->find('li:nth-child(odd)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A C E G)], 'found all odd li elements';
+@li = ();
+$dom->find('li:NTH-CHILD(ODD)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A C E G)], 'found all odd li elements';
+@li = ();
+$dom->find('li:nth-last-child(odd)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(B D F H)], 'found all odd li elements';
+is $dom->find(':nth-child(odd)')->[0]->tag,      'ul', 'right tag';
+is $dom->find(':nth-child(odd)')->[1]->text,     'A',  'right text';
+is $dom->find(':nth-child(1)')->[0]->tag,        'ul', 'right tag';
+is $dom->find(':nth-child(1)')->[1]->text,       'A',  'right text';
+is $dom->find(':nth-last-child(odd)')->[0]->tag, 'ul', 'right tag';
+is $dom->find(':nth-last-child(odd)')->last->text, 'H', 'right text';
+is $dom->find(':nth-last-child(1)')->[0]->tag,  'ul', 'right tag';
+is $dom->find(':nth-last-child(1)')->[1]->text, 'H',  'right text';
+@li = ();
+$dom->find('li:nth-child(2n+1)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A C E G)], 'found all odd li elements';
+@li = ();
+$dom->find('li:nth-child(2n + 1)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A C E G)], 'found all odd li elements';
+@li = ();
+$dom->find('li:nth-last-child(2n+1)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(B D F H)], 'found all odd li elements';
+@li = ();
+$dom->find('li:nth-child(even)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(B D F H)], 'found all even li elements';
+@li = ();
+$dom->find('li:NTH-CHILD(EVEN)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(B D F H)], 'found all even li elements';
+@li = ();
+$dom->find('li:nth-last-child( even )')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A C E G)], 'found all even li elements';
+@li = ();
+$dom->find('li:nth-child(2n+2)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(B D F H)], 'found all even li elements';
+@li = ();
+$dom->find('li:nTh-chILd(2N+2)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(B D F H)], 'found all even li elements';
+@li = ();
+$dom->find('li:nth-child( 2n + 2 )')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(B D F H)], 'found all even li elements';
+@li = ();
+$dom->find('li:nth-last-child(2n+2)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A C E G)], 'found all even li elements';
+@li = ();
+$dom->find('li:nth-child(4n+1)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A E)], 'found the right li elements';
+@li = ();
+$dom->find('li:nth-last-child(4n+1)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(D H)], 'found the right li elements';
+@li = ();
+$dom->find('li:nth-child(4n+4)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(D H)], 'found the right li element';
+@li = ();
+$dom->find('li:nth-last-child(4n+4)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A E)], 'found the right li element';
+@li = ();
+$dom->find('li:nth-child(4n)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(D H)], 'found the right li element';
+@li = ();
+$dom->find('li:nth-child( 4n )')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(D H)], 'found the right li element';
+@li = ();
+$dom->find('li:nth-last-child(4n)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A E)], 'found the right li element';
+@li = ();
+$dom->find('li:nth-child(5n-2)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(C H)], 'found the right li element';
+@li = ();
+$dom->find('li:nth-child( 5n - 2 )')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(C H)], 'found the right li element';
+@li = ();
+$dom->find('li:nth-last-child(5n-2)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A F)], 'found the right li element';
+@li = ();
+$dom->find('li:nth-child(-n+3)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A B C)], 'found first three li elements';
+@li = ();
+$dom->find('li:nth-child( -n + 3 )')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A B C)], 'found first three li elements';
+@li = ();
+$dom->find('li:nth-last-child(-n+3)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(F G H)], 'found last three li elements';
+@li = ();
+$dom->find('li:nth-child(-1n+3)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A B C)], 'found first three li elements';
+@li = ();
+$dom->find('li:nth-last-child(-1n+3)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(F G H)], 'found first three li elements';
+@li = ();
+$dom->find('li:nth-child(3n)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(C F)], 'found every third li elements';
+@li = ();
+$dom->find('li:nth-last-child(3n)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(C F)], 'found every third li elements';
+@li = ();
+$dom->find('li:NTH-LAST-CHILD(3N)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(C F)], 'found every third li elements';
+@li = ();
+$dom->find('li:Nth-Last-Child(3N)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(C F)], 'found every third li elements';
+@li = ();
+$dom->find('li:nth-child(3)')->each(sub { push @li, shift->text });
+is_deeply \@li, ['C'], 'found third li element';
+@li = ();
+$dom->find('li:nth-last-child(3)')->each(sub { push @li, shift->text });
+is_deeply \@li, ['F'], 'found third last li element';
+@li = ();
+$dom->find('li:nth-child(1n+0)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A B C D E F G)], 'found first three li elements';
+@li = ();
+$dom->find('li:nth-child(n+0)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A B C D E F G)], 'found first three li elements';
+@li = ();
+$dom->find('li:NTH-CHILD(N+0)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A B C D E F G)], 'found first three li elements';
+@li = ();
+$dom->find('li:Nth-Child(N+0)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A B C D E F G)], 'found first three li elements';
+@li = ();
+$dom->find('li:nth-child(n)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A B C D E F G)], 'found first three li elements';
+
+# Even more pseudo-classes
+$dom = DOM::Tiny->new(<<EOF);
+<ul>
+    <li>A</li>
+    <p>B</p>
+    <li class="test ♥">C</li>
+    <p>D</p>
+    <li>E</li>
+    <li>F</li>
+    <p>G</p>
+    <li>H</li>
+    <li>I</li>
+</ul>
+<div>
+    <div class="☃">J</div>
+</div>
+<div>
+    <a href="http://www.w3.org/DOM/">DOM</a>
+    <div class="☃">K</div>
+    <a href="http://www.w3.org/DOM/">DOM</a>
+</div>
+EOF
+my @e;
+$dom->find('ul :nth-child(odd)')->each(sub { push @e, shift->text });
+is_deeply \@e, [qw(A C E G I)], 'found all odd elements';
+@e = ();
+$dom->find('li:nth-of-type(odd)')->each(sub { push @e, shift->text });
+is_deeply \@e, [qw(A E H)], 'found all odd li elements';
+@e = ();
+$dom->find('li:nth-last-of-type( odd )')->each(sub { push @e, shift->text });
+is_deeply \@e, [qw(C F I)], 'found all odd li elements';
+@e = ();
+$dom->find('p:nth-of-type(odd)')->each(sub { push @e, shift->text });
+is_deeply \@e, [qw(B G)], 'found all odd p elements';
+@e = ();
+$dom->find('p:nth-last-of-type(odd)')->each(sub { push @e, shift->text });
+is_deeply \@e, [qw(B G)], 'found all odd li elements';
+@e = ();
+$dom->find('ul :nth-child(1)')->each(sub { push @e, shift->text });
+is_deeply \@e, ['A'], 'found first child';
+@e = ();
+$dom->find('ul :first-child')->each(sub { push @e, shift->text });
+is_deeply \@e, ['A'], 'found first child';
+@e = ();
+$dom->find('p:nth-of-type(1)')->each(sub { push @e, shift->text });
+is_deeply \@e, ['B'], 'found first child';
+@e = ();
+$dom->find('p:first-of-type')->each(sub { push @e, shift->text });
+is_deeply \@e, ['B'], 'found first child';
+@e = ();
+$dom->find('li:nth-of-type(1)')->each(sub { push @e, shift->text });
+is_deeply \@e, ['A'], 'found first child';
+@e = ();
+$dom->find('li:first-of-type')->each(sub { push @e, shift->text });
+is_deeply \@e, ['A'], 'found first child';
+@e = ();
+$dom->find('ul :nth-last-child(-n+1)')->each(sub { push @e, shift->text });
+is_deeply \@e, ['I'], 'found last child';
+@e = ();
+$dom->find('ul :last-child')->each(sub { push @e, shift->text });
+is_deeply \@e, ['I'], 'found last child';
+@e = ();
+$dom->find('p:nth-last-of-type(-n+1)')->each(sub { push @e, shift->text });
+is_deeply \@e, ['G'], 'found last child';
+@e = ();
+$dom->find('p:last-of-type')->each(sub { push @e, shift->text });
+is_deeply \@e, ['G'], 'found last child';
+@e = ();
+$dom->find('li:nth-last-of-type(-n+1)')->each(sub { push @e, shift->text });
+is_deeply \@e, ['I'], 'found last child';
+@e = ();
+$dom->find('li:last-of-type')->each(sub { push @e, shift->text });
+is_deeply \@e, ['I'], 'found last child';
+@e = ();
+$dom->find('ul :nth-child(-n+3):not(li)')->each(sub { push @e, shift->text });
+is_deeply \@e, ['B'], 'found first p element';
+@e = ();
+$dom->find('ul :nth-child(-n+3):not(:first-child)')
+  ->each(sub { push @e, shift->text });
+is_deeply \@e, [qw(B C)], 'found second and third element';
+@e = ();
+$dom->find('ul :nth-child(-n+3):not(.♥)')->each(sub { push @e, shift->text });
+is_deeply \@e, [qw(A B)], 'found first and second element';
+@e = ();
+$dom->find('ul :nth-child(-n+3):not([class$="♥"])')
+  ->each(sub { push @e, shift->text });
+is_deeply \@e, [qw(A B)], 'found first and second element';
+@e = ();
+$dom->find('ul :nth-child(-n+3):not(li[class$="♥"])')
+  ->each(sub { push @e, shift->text });
+is_deeply \@e, [qw(A B)], 'found first and second element';
+@e = ();
+$dom->find('ul :nth-child(-n+3):not([class$="♥"][class^="test"])')
+  ->each(sub { push @e, shift->text });
+is_deeply \@e, [qw(A B)], 'found first and second element';
+@e = ();
+$dom->find('ul :nth-child(-n+3):not(*[class$="♥"])')
+  ->each(sub { push @e, shift->text });
+is_deeply \@e, [qw(A B)], 'found first and second element';
+@e = ();
+$dom->find('ul :nth-child(-n+3):not(:nth-child(-n+2))')
+  ->each(sub { push @e, shift->text });
+is_deeply \@e, ['C'], 'found third element';
+@e = ();
+$dom->find('ul :nth-child(-n+3):not(:nth-child(1)):not(:nth-child(2))')
+  ->each(sub { push @e, shift->text });
+is_deeply \@e, ['C'], 'found third element';
+@e = ();
+$dom->find(':only-child')->each(sub { push @e, shift->text });
+is_deeply \@e, ['J'], 'found only child';
+@e = ();
+$dom->find('div :only-of-type')->each(sub { push @e, shift->text });
+is_deeply \@e, [qw(J K)], 'found only child';
+@e = ();
+$dom->find('div:only-child')->each(sub { push @e, shift->text });
+is_deeply \@e, ['J'], 'found only child';
+@e = ();
+$dom->find('div div:only-of-type')->each(sub { push @e, shift->text });
+is_deeply \@e, [qw(J K)], 'found only child';
+
+# Sibling combinator
+$dom = DOM::Tiny->new(<<EOF);
+<ul>
+    <li>A</li>
+    <p>B</p>
+    <li>C</li>
+</ul>
+<h1>D</h1>
+<p id="♥">E</p>
+<p id="☃">F<b>H</b></p>
+<div>G</div>
+EOF
+is $dom->at('li ~ p')->text,       'B', 'right text';
+is $dom->at('li + p')->text,       'B', 'right text';
+is $dom->at('h1 ~ p ~ p')->text,   'F', 'right text';
+is $dom->at('h1 + p ~ p')->text,   'F', 'right text';
+is $dom->at('h1 ~ p + p')->text,   'F', 'right text';
+is $dom->at('h1 + p + p')->text,   'F', 'right text';
+is $dom->at('h1  +  p+p')->text,   'F', 'right text';
+is $dom->at('ul > li ~ li')->text, 'C', 'right text';
+is $dom->at('ul li ~ li')->text,   'C', 'right text';
+is $dom->at('ul>li~li')->text,     'C', 'right text';
+is $dom->at('ul li li'),     undef, 'no result';
+is $dom->at('ul ~ li ~ li'), undef, 'no result';
+is $dom->at('ul + li ~ li'), undef, 'no result';
+is $dom->at('ul > li + li'), undef, 'no result';
+is $dom->at('h1 ~ div')->text, 'G', 'right text';
+is $dom->at('h1 + div'), undef, 'no result';
+is $dom->at('p + div')->text,               'G', 'right text';
+is $dom->at('ul + h1 + p + p + div')->text, 'G', 'right text';
+is $dom->at('ul + h1 ~ p + div')->text,     'G', 'right text';
+is $dom->at('h1 ~ #♥')->text,             'E', 'right text';
+is $dom->at('h1 + #♥')->text,             'E', 'right text';
+is $dom->at('#♥~#☃')->text,             'F', 'right text';
+is $dom->at('#♥+#☃')->text,             'F', 'right text';
+is $dom->at('#♥+#☃>b')->text,           'H', 'right text';
+is $dom->at('#♥ > #☃'), undef, 'no result';
+is $dom->at('#♥ #☃'),   undef, 'no result';
+is $dom->at('#♥ + #☃ + :nth-last-child(1)')->text,  'G', 'right text';
+is $dom->at('#♥ ~ #☃ + :nth-last-child(1)')->text,  'G', 'right text';
+is $dom->at('#♥ + #☃ ~ :nth-last-child(1)')->text,  'G', 'right text';
+is $dom->at('#♥ ~ #☃ ~ :nth-last-child(1)')->text,  'G', 'right text';
+is $dom->at('#♥ + :nth-last-child(2)')->text,         'F', 'right text';
+is $dom->at('#♥ ~ :nth-last-child(2)')->text,         'F', 'right text';
+is $dom->at('#♥ + #☃ + *:nth-last-child(1)')->text, 'G', 'right text';
+is $dom->at('#♥ ~ #☃ + *:nth-last-child(1)')->text, 'G', 'right text';
+is $dom->at('#♥ + #☃ ~ *:nth-last-child(1)')->text, 'G', 'right text';
+is $dom->at('#♥ ~ #☃ ~ *:nth-last-child(1)')->text, 'G', 'right text';
+is $dom->at('#♥ + *:nth-last-child(2)')->text,        'F', 'right text';
+is $dom->at('#♥ ~ *:nth-last-child(2)')->text,        'F', 'right text';
+
+# Adding nodes
+$dom = DOM::Tiny->new(<<EOF);
+<ul>
+    <li>A</li>
+    <p>B</p>
+    <li>C</li>
+</ul>
+<div>D</div>
+EOF
+$dom->at('li')->append('<p>A1</p>23');
+is "$dom", <<EOF, 'right result';
+<ul>
+    <li>A</li><p>A1</p>23
+    <p>B</p>
+    <li>C</li>
+</ul>
+<div>D</div>
+EOF
+$dom->at('li')->prepend('24')->prepend('<div>A-1</div>25');
+is "$dom", <<EOF, 'right result';
+<ul>
+    24<div>A-1</div>25<li>A</li><p>A1</p>23
+    <p>B</p>
+    <li>C</li>
+</ul>
+<div>D</div>
+EOF
+is $dom->at('div')->text, 'A-1', 'right text';
+is $dom->at('iv'), undef, 'no result';
+$dom->prepend('l')->prepend('alal')->prepend('a');
+is "$dom", <<EOF, 'no change';
+<ul>
+    24<div>A-1</div>25<li>A</li><p>A1</p>23
+    <p>B</p>
+    <li>C</li>
+</ul>
+<div>D</div>
+EOF
+$dom->append('lalala');
+is "$dom", <<EOF, 'no change';
+<ul>
+    24<div>A-1</div>25<li>A</li><p>A1</p>23
+    <p>B</p>
+    <li>C</li>
+</ul>
+<div>D</div>
+EOF
+$dom->find('div')->each(sub { shift->append('works') });
+is "$dom", <<EOF, 'right result';
+<ul>
+    24<div>A-1</div>works25<li>A</li><p>A1</p>23
+    <p>B</p>
+    <li>C</li>
+</ul>
+<div>D</div>works
+EOF
+$dom->at('li')->prepend_content('A3<p>A2</p>')->prepend_content('A4');
+is $dom->at('li')->text, 'A4A3 A', 'right text';
+is "$dom", <<EOF, 'right result';
+<ul>
+    24<div>A-1</div>works25<li>A4A3<p>A2</p>A</li><p>A1</p>23
+    <p>B</p>
+    <li>C</li>
+</ul>
+<div>D</div>works
+EOF
+$dom->find('li')->[1]->append_content('<p>C2</p>C3')->append_content(' C4')
+  ->append_content('C5');
+is $dom->find('li')->[1]->text, 'C C3 C4C5', 'right text';
+is "$dom", <<EOF, 'right result';
+<ul>
+    24<div>A-1</div>works25<li>A4A3<p>A2</p>A</li><p>A1</p>23
+    <p>B</p>
+    <li>C<p>C2</p>C3 C4C5</li>
+</ul>
+<div>D</div>works
+EOF
+
+# Optional "head" and "body" tags
+$dom = DOM::Tiny->new(<<EOF);
+<html>
+  <head>
+    <title>foo</title>
+  <body>bar
+EOF
+is $dom->at('html > head > title')->text, 'foo', 'right text';
+is $dom->at('html > body')->text,         'bar', 'right text';
+
+# Optional "li" tag
+$dom = DOM::Tiny->new(<<EOF);
+<ul>
+  <li>
+    <ol>
+      <li>F
+      <li>G
+    </ol>
+  <li>A</li>
+  <LI>B
+  <li>C</li>
+  <li>D
+  <li>E
+</ul>
+EOF
+is $dom->find('ul > li > ol > li')->[0]->text, 'F', 'right text';
+is $dom->find('ul > li > ol > li')->[1]->text, 'G', 'right text';
+is $dom->find('ul > li')->[1]->text,           'A', 'right text';
+is $dom->find('ul > li')->[2]->text,           'B', 'right text';
+is $dom->find('ul > li')->[3]->text,           'C', 'right text';
+is $dom->find('ul > li')->[4]->text,           'D', 'right text';
+is $dom->find('ul > li')->[5]->text,           'E', 'right text';
+
+# Optional "p" tag
+$dom = DOM::Tiny->new(<<EOF);
+<div>
+  <p>A</p>
+  <P>B
+  <p>C</p>
+  <p>D<div>X</div>
+  <p>E<img src="foo.png">
+  <p>F<br>G
+  <p>H
+</div>
+EOF
+is $dom->find('div > p')->[0]->text, 'A',   'right text';
+is $dom->find('div > p')->[1]->text, 'B',   'right text';
+is $dom->find('div > p')->[2]->text, 'C',   'right text';
+is $dom->find('div > p')->[3]->text, 'D',   'right text';
+is $dom->find('div > p')->[4]->text, 'E',   'right text';
+is $dom->find('div > p')->[5]->text, 'F G', 'right text';
+is $dom->find('div > p')->[6]->text, 'H',   'right text';
+is $dom->find('div > p > p')->[0], undef, 'no results';
+is $dom->at('div > p > img')->attr->{src}, 'foo.png', 'right attribute';
+is $dom->at('div > div')->text, 'X', 'right text';
+
+# Optional "dt" and "dd" tags
+$dom = DOM::Tiny->new(<<EOF);
+<dl>
+  <dt>A</dt>
+  <DD>B
+  <dt>C</dt>
+  <dd>D
+  <dt>E
+  <dd>F
+</dl>
+EOF
+is $dom->find('dl > dt')->[0]->text, 'A', 'right text';
+is $dom->find('dl > dd')->[0]->text, 'B', 'right text';
+is $dom->find('dl > dt')->[1]->text, 'C', 'right text';
+is $dom->find('dl > dd')->[1]->text, 'D', 'right text';
+is $dom->find('dl > dt')->[2]->text, 'E', 'right text';
+is $dom->find('dl > dd')->[2]->text, 'F', 'right text';
+
+# Optional "rp" and "rt" tags
+$dom = DOM::Tiny->new(<<EOF);
+<ruby>
+  <rp>A</rp>
+  <RT>B
+  <rp>C</rp>
+  <rt>D
+  <rp>E
+  <rt>F
+</ruby>
+EOF
+is $dom->find('ruby > rp')->[0]->text, 'A', 'right text';
+is $dom->find('ruby > rt')->[0]->text, 'B', 'right text';
+is $dom->find('ruby > rp')->[1]->text, 'C', 'right text';
+is $dom->find('ruby > rt')->[1]->text, 'D', 'right text';
+is $dom->find('ruby > rp')->[2]->text, 'E', 'right text';
+is $dom->find('ruby > rt')->[2]->text, 'F', 'right text';
+
+# Optional "optgroup" and "option" tags
+$dom = DOM::Tiny->new(<<EOF);
+<div>
+  <optgroup>A
+    <option id="foo">B
+    <option>C</option>
+    <option>D
+  <OPTGROUP>E
+    <option>F
+  <optgroup>G
+    <option>H
+</div>
+EOF
+is $dom->find('div > optgroup')->[0]->text,          'A', 'right text';
+is $dom->find('div > optgroup > #foo')->[0]->text,   'B', 'right text';
+is $dom->find('div > optgroup > option')->[1]->text, 'C', 'right text';
+is $dom->find('div > optgroup > option')->[2]->text, 'D', 'right text';
+is $dom->find('div > optgroup')->[1]->text,          'E', 'right text';
+is $dom->find('div > optgroup > option')->[3]->text, 'F', 'right text';
+is $dom->find('div > optgroup')->[2]->text,          'G', 'right text';
+is $dom->find('div > optgroup > option')->[4]->text, 'H', 'right text';
+
+# Optional "colgroup" tag
+$dom = DOM::Tiny->new(<<EOF);
+<table>
+  <col id=morefail>
+  <col id=fail>
+  <colgroup>
+    <col id=foo>
+    <col class=foo>
+  <colgroup>
+    <col id=bar>
+</table>
+EOF
+is $dom->find('table > col')->[0]->attr->{id}, 'morefail', 'right attribute';
+is $dom->find('table > col')->[1]->attr->{id}, 'fail',     'right attribute';
+is $dom->find('table > colgroup > col')->[0]->attr->{id}, 'foo',
+  'right attribute';
+is $dom->find('table > colgroup > col')->[1]->attr->{class}, 'foo',
+  'right attribute';
+is $dom->find('table > colgroup > col')->[2]->attr->{id}, 'bar',
+  'right attribute';
+
+# Optional "thead", "tbody", "tfoot", "tr", "th" and "td" tags
+$dom = DOM::Tiny->new(<<EOF);
+<table>
+  <thead>
+    <tr>
+      <th>A</th>
+      <th>D
+  <tfoot>
+    <tr>
+      <td>C
+  <tbody>
+    <tr>
+      <td>B
+</table>
+EOF
+is $dom->at('table > thead > tr > th')->text, 'A', 'right text';
+is $dom->find('table > thead > tr > th')->[1]->text, 'D', 'right text';
+is $dom->at('table > tbody > tr > td')->text, 'B', 'right text';
+is $dom->at('table > tfoot > tr > td')->text, 'C', 'right text';
+
+# Optional "colgroup", "thead", "tbody", "tr", "th" and "td" tags
+$dom = DOM::Tiny->new(<<EOF);
+<table>
+  <col id=morefail>
+  <col id=fail>
+  <colgroup>
+    <col id=foo />
+    <col class=foo>
+  <colgroup>
+    <col id=bar>
+  </colgroup>
+  <thead>
+    <tr>
+      <th>A</th>
+      <th>D
+  <tbody>
+    <tr>
+      <td>B
+  <tbody>
+    <tr>
+      <td>E
+</table>
+EOF
+is $dom->find('table > col')->[0]->attr->{id}, 'morefail', 'right attribute';
+is $dom->find('table > col')->[1]->attr->{id}, 'fail',     'right attribute';
+is $dom->find('table > colgroup > col')->[0]->attr->{id}, 'foo',
+  'right attribute';
+is $dom->find('table > colgroup > col')->[1]->attr->{class}, 'foo',
+  'right attribute';
+is $dom->find('table > colgroup > col')->[2]->attr->{id}, 'bar',
+  'right attribute';
+is $dom->at('table > thead > tr > th')->text, 'A', 'right text';
+is $dom->find('table > thead > tr > th')->[1]->text, 'D', 'right text';
+is $dom->at('table > tbody > tr > td')->text, 'B', 'right text';
+is $dom->find('table > tbody > tr > td')->map('text')->join("\n"), "B\nE",
+  'right text';
+
+# Optional "colgroup", "tbody", "tr", "th" and "td" tags
+$dom = DOM::Tiny->new(<<EOF);
+<table>
+  <colgroup>
+    <col id=foo />
+    <col class=foo>
+  <colgroup>
+    <col id=bar>
+  </colgroup>
+  <tbody>
+    <tr>
+      <td>B
+</table>
+EOF
+is $dom->find('table > colgroup > col')->[0]->attr->{id}, 'foo',
+  'right attribute';
+is $dom->find('table > colgroup > col')->[1]->attr->{class}, 'foo',
+  'right attribute';
+is $dom->find('table > colgroup > col')->[2]->attr->{id}, 'bar',
+  'right attribute';
+is $dom->at('table > tbody > tr > td')->text, 'B', 'right text';
+
+# Optional "tr" and "td" tags
+$dom = DOM::Tiny->new(<<EOF);
+<table>
+    <tr>
+      <td>A
+      <td>B</td>
+    <tr>
+      <td>C
+    </tr>
+    <tr>
+      <td>D
+</table>
+EOF
+is $dom->find('table > tr > td')->[0]->text, 'A', 'right text';
+is $dom->find('table > tr > td')->[1]->text, 'B', 'right text';
+is $dom->find('table > tr > td')->[2]->text, 'C', 'right text';
+is $dom->find('table > tr > td')->[3]->text, 'D', 'right text';
+
+# Real world table
+$dom = DOM::Tiny->new(<<EOF);
+<html>
+  <head>
+    <title>Real World!</title>
+  <body>
+    <p>Just a test
+    <table class=RealWorld>
+      <thead>
+        <tr>
+          <th class=one>One
+          <th class=two>Two
+          <th class=three>Three
+          <th class=four>Four
+      <tbody>
+        <tr>
+          <td class=alpha>Alpha
+          <td class=beta>Beta
+          <td class=gamma><a href="#gamma">Gamma</a>
+          <td class=delta>Delta
+        <tr>
+          <td class=alpha>Alpha Two
+          <td class=beta>Beta Two
+          <td class=gamma><a href="#gamma-two">Gamma Two</a>
+          <td class=delta>Delta Two
+    </table>
+EOF
+is $dom->find('html > head > title')->[0]->text, 'Real World!', 'right text';
+is $dom->find('html > body > p')->[0]->text,     'Just a test', 'right text';
+is $dom->find('p')->[0]->text,                   'Just a test', 'right text';
+is $dom->find('thead > tr > .three')->[0]->text, 'Three',       'right text';
+is $dom->find('thead > tr > .four')->[0]->text,  'Four',        'right text';
+is $dom->find('tbody > tr > .beta')->[0]->text,  'Beta',        'right text';
+is $dom->find('tbody > tr > .gamma')->[0]->text, '',            'no text';
+is $dom->find('tbody > tr > .gamma > a')->[0]->text, 'Gamma',     'right text';
+is $dom->find('tbody > tr > .alpha')->[1]->text,     'Alpha Two', 'right text';
+is $dom->find('tbody > tr > .gamma > a')->[1]->text, 'Gamma Two', 'right text';
+my @following
+  = $dom->find('tr > td:nth-child(1)')->map(following => ':nth-child(even)')
+  ->flatten->map('all_text')->each;
+is_deeply \@following, ['Beta', 'Delta', 'Beta Two', 'Delta Two'],
+  'right results';
+
+# Real world list
+$dom = DOM::Tiny->new(<<EOF);
+<html>
+  <head>
+    <title>Real World!</title>
+  <body>
+    <ul>
+      <li>
+        Test
+        <br>
+        123
+        <p>
+
+      <li>
+        Test
+        <br>
+        321
+        <p>
+      <li>
+        Test
+        3
+        2
+        1
+        <p>
+    </ul>
+EOF
+is $dom->find('html > head > title')->[0]->text,    'Real World!', 'right text';
+is $dom->find('body > ul > li')->[0]->text,         'Test 123',    'right text';
+is $dom->find('body > ul > li > p')->[0]->text,     '',            'no text';
+is $dom->find('body > ul > li')->[1]->text,         'Test 321',    'right text';
+is $dom->find('body > ul > li > p')->[1]->text,     '',            'no text';
+is $dom->find('body > ul > li')->[1]->all_text,     'Test 321',    'right text';
+is $dom->find('body > ul > li > p')->[1]->all_text, '',            'no text';
+is $dom->find('body > ul > li')->[2]->text,         'Test 3 2 1',  'right text';
+is $dom->find('body > ul > li > p')->[2]->text,     '',            'no text';
+is $dom->find('body > ul > li')->[2]->all_text,     'Test 3 2 1',  'right text';
+is $dom->find('body > ul > li > p')->[2]->all_text, '',            'no text';
+
+# Advanced whitespace trimming (punctuation)
+$dom = DOM::Tiny->new(<<EOF);
+<html>
+  <head>
+    <title>Real World!</title>
+  <body>
+    <div>foo <strong>bar</strong>.</div>
+    <div>foo<strong>, bar</strong>baz<strong>; yada</strong>.</div>
+    <div>foo<strong>: bar</strong>baz<strong>? yada</strong>!</div>
+EOF
+is $dom->find('html > head > title')->[0]->text, 'Real World!', 'right text';
+is $dom->find('body > div')->[0]->all_text,      'foo bar.',    'right text';
+is $dom->find('body > div')->[1]->all_text, 'foo, bar baz; yada.', 'right text';
+is $dom->find('body > div')->[1]->text,     'foo baz.',            'right text';
+is $dom->find('body > div')->[2]->all_text, 'foo: bar baz? yada!', 'right text';
+is $dom->find('body > div')->[2]->text,     'foo baz!',            'right text';
+
+# Real world JavaScript and CSS
+$dom = DOM::Tiny->new(<<EOF);
+<html>
+  <head>
+    <style test=works>#style { foo: style('<test>'); }</style>
+    <script>
+      if (a < b) {
+        alert('<123>');
+      }
+    </script>
+    < sCriPt two="23" >if (b > c) { alert('&<ohoh>') }< / scRiPt >
+  <body>Foo!</body>
+EOF
+is $dom->find('html > body')->[0]->text, 'Foo!', 'right text';
+is $dom->find('html > head > style')->[0]->text,
+  "#style { foo: style('<test>'); }", 'right text';
+is $dom->find('html > head > script')->[0]->text,
+  "\n      if (a < b) {\n        alert('<123>');\n      }\n    ", 'right text';
+is $dom->find('html > head > script')->[1]->text,
+  "if (b > c) { alert('&<ohoh>') }", 'right text';
+
+# More real world JavaScript
+$dom = DOM::Tiny->new(<<EOF);
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Foo</title>
+    <script src="/js/one.js"></script>
+    <script src="/js/two.js"></script>
+    <script src="/js/three.js"></script>
+  </head>
+  <body>Bar</body>
+</html>
+EOF
+is $dom->at('title')->text, 'Foo', 'right text';
+is $dom->find('html > head > script')->[0]->attr('src'), '/js/one.js',
+  'right attribute';
+is $dom->find('html > head > script')->[1]->attr('src'), '/js/two.js',
+  'right attribute';
+is $dom->find('html > head > script')->[2]->attr('src'), '/js/three.js',
+  'right attribute';
+is $dom->find('html > head > script')->[2]->text, '', 'no text';
+is $dom->at('html > body')->text, 'Bar', 'right text';
+
+# Even more real world JavaScript
+$dom = DOM::Tiny->new(<<EOF);
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Foo</title>
+    <script src="/js/one.js"></script>
+    <script src="/js/two.js"></script>
+    <script src="/js/three.js">
+  </head>
+  <body>Bar</body>
+</html>
+EOF
+is $dom->at('title')->text, 'Foo', 'right text';
+is $dom->find('html > head > script')->[0]->attr('src'), '/js/one.js',
+  'right attribute';
+is $dom->find('html > head > script')->[1]->attr('src'), '/js/two.js',
+  'right attribute';
+is $dom->find('html > head > script')->[2]->attr('src'), '/js/three.js',
+  'right attribute';
+is $dom->find('html > head > script')->[2]->text, '', 'no text';
+is $dom->at('html > body')->text, 'Bar', 'right text';
+
+# Inline DTD
+$dom = DOM::Tiny->new(<<EOF);
+<?xml version="1.0"?>
+<!-- This is a Test! -->
+<!DOCTYPE root [
+  <!ELEMENT root (#PCDATA)>
+  <!ATTLIST root att CDATA #REQUIRED>
+]>
+<root att="test">
+  <![CDATA[<hello>world</hello>]]>
+</root>
+EOF
+ok $dom->xml, 'XML mode detected';
+is $dom->at('root')->attr('att'), 'test', 'right attribute';
+is $dom->tree->[5][1], ' root [
+  <!ELEMENT root (#PCDATA)>
+  <!ATTLIST root att CDATA #REQUIRED>
+]', 'right doctype';
+is $dom->at('root')->text, '<hello>world</hello>', 'right text';
+$dom = DOM::Tiny->new(<<EOF);
+<!doctype book
+SYSTEM "usr.dtd"
+[
+  <!ENTITY test "yeah">
+]>
+<foo />
+EOF
+is $dom->tree->[1][1], ' book
+SYSTEM "usr.dtd"
+[
+  <!ENTITY test "yeah">
+]', 'right doctype';
+ok !$dom->xml, 'XML mode not detected';
+is $dom->at('foo'), '<foo></foo>', 'right element';
+$dom = DOM::Tiny->new(<<EOF);
+<?xml version="1.0" encoding = 'utf-8'?>
+<!DOCTYPE foo [
+  <!ELEMENT foo ANY>
+  <!ATTLIST foo xml:lang CDATA #IMPLIED>
+  <!ENTITY % e SYSTEM "myentities.ent">
+  %myentities;
+]  >
+<foo xml:lang="de">Check!</fOo>
+EOF
+ok $dom->xml, 'XML mode detected';
+is $dom->tree->[3][1], ' foo [
+  <!ELEMENT foo ANY>
+  <!ATTLIST foo xml:lang CDATA #IMPLIED>
+  <!ENTITY % e SYSTEM "myentities.ent">
+  %myentities;
+]  ', 'right doctype';
+is $dom->at('foo')->attr->{'xml:lang'}, 'de', 'right attribute';
+is $dom->at('foo')->text, 'Check!', 'right text';
+$dom = DOM::Tiny->new(<<EOF);
+<!DOCTYPE TESTSUITE PUBLIC "my.dtd" 'mhhh' [
+  <!ELEMENT foo ANY>
+  <!ATTLIST foo bar ENTITY 'true'>
+  <!ENTITY system_entities SYSTEM 'systems.xml'>
+  <!ENTITY leertaste '&#32;'>
+  <!-- This is a comment -->
+  <!NOTATION hmmm SYSTEM "hmmm">
+]   >
+<?check for-nothing?>
+<foo bar='false'>&leertaste;!!!</foo>
+EOF
+is $dom->tree->[1][1], ' TESTSUITE PUBLIC "my.dtd" \'mhhh\' [
+  <!ELEMENT foo ANY>
+  <!ATTLIST foo bar ENTITY \'true\'>
+  <!ENTITY system_entities SYSTEM \'systems.xml\'>
+  <!ENTITY leertaste \'&#32;\'>
+  <!-- This is a comment -->
+  <!NOTATION hmmm SYSTEM "hmmm">
+]   ', 'right doctype';
+is $dom->at('foo')->attr('bar'), 'false', 'right attribute';
+
+# Broken "font" block and useless end tags
+$dom = DOM::Tiny->new(<<EOF);
+<html>
+  <head><title>Test</title></head>
+  <body>
+    <table>
+      <tr><td><font>test</td></font></tr>
+      </tr>
+    </table>
+  </body>
+</html>
+EOF
+is $dom->at('html > head > title')->text,          'Test', 'right text';
+is $dom->at('html body table tr td > font')->text, 'test', 'right text';
+
+# Different broken "font" block
+$dom = DOM::Tiny->new(<<EOF);
+<html>
+  <head><title>Test</title></head>
+  <body>
+    <font>
+    <table>
+      <tr>
+        <td>test1<br></td></font>
+        <td>test2<br>
+    </table>
+  </body>
+</html>
+EOF
+is $dom->at('html > head > title')->text, 'Test', 'right text';
+is $dom->find('html > body > font > table > tr > td')->[0]->text, 'test1',
+  'right text';
+is $dom->find('html > body > font > table > tr > td')->[1]->text, 'test2',
+  'right text';
+
+# Broken "font" and "div" blocks
+$dom = DOM::Tiny->new(<<EOF);
+<html>
+  <head><title>Test</title></head>
+  <body>
+    <font>
+    <div>test1<br>
+      <div>test2<br></font>
+    </div>
+  </body>
+</html>
+EOF
+is $dom->at('html head title')->text,            'Test',  'right text';
+is $dom->at('html body font > div')->text,       'test1', 'right text';
+is $dom->at('html body font > div > div')->text, 'test2', 'right text';
+
+# Broken "div" blocks
+$dom = DOM::Tiny->new(<<EOF);
+<html>
+  <head><title>Test</title></head>
+  <body>
+    <div>
+    <table>
+      <tr><td><div>test</td></div></tr>
+      </div>
+    </table>
+  </body>
+</html>
+EOF
+is $dom->at('html head title')->text,                 'Test', 'right text';
+is $dom->at('html body div table tr td > div')->text, 'test', 'right text';
+
+# And another broken "font" block
+$dom = DOM::Tiny->new(<<EOF);
+<html>
+  <head><title>Test</title></head>
+  <body>
+    <table>
+      <tr>
+        <td><font><br>te<br>st<br>1</td></font>
+        <td>x1<td><img>tes<br>t2</td>
+        <td>x2<td><font>t<br>est3</font></td>
+      </tr>
+    </table>
+  </body>
+</html>
+EOF
+is $dom->at('html > head > title')->text, 'Test', 'right text';
+is $dom->find('html body table tr > td > font')->[0]->text, 'te st 1',
+  'right text';
+is $dom->find('html body table tr > td')->[1]->text, 'x1',     'right text';
+is $dom->find('html body table tr > td')->[2]->text, 'tes t2', 'right text';
+is $dom->find('html body table tr > td')->[3]->text, 'x2',     'right text';
+is $dom->find('html body table tr > td')->[5], undef, 'no result';
+is $dom->find('html body table tr > td')->size, 5, 'right number of elements';
+is $dom->find('html body table tr > td > font')->[1]->text, 't est3',
+  'right text';
+is $dom->find('html body table tr > td > font')->[2], undef, 'no result';
+is $dom->find('html body table tr > td > font')->size, 2,
+  'right number of elements';
+is $dom, <<EOF, 'right result';
+<html>
+  <head><title>Test</title></head>
+  <body>
+    <table>
+      <tr>
+        <td><font><br>te<br>st<br>1</font></td>
+        <td>x1</td><td><img>tes<br>t2</td>
+        <td>x2</td><td><font>t<br>est3</font></td>
+      </tr>
+    </table>
+  </body>
+</html>
+EOF
+
+# A collection of wonderful screwups
+$dom = DOM::Tiny->new(<<'EOF');
+<!DOCTYPE html>
+<html lang="en">
+  <head><title>Wonderful Screwups</title></head>
+  <body id="screw-up">
+    <div>
+      <div class="ewww">
+        <a href="/test" target='_blank'><img src="/test.png"></a>
+        <a href='/real bad' screwup: http://localhost/bad' target='_blank'>
+          <img src="/test2.png">
+      </div>
+      </mt:If>
+    </div>
+    <b>>la<>la<<>>la<</b>
+  </body>
+</html>
+EOF
+is $dom->at('#screw-up > b')->text, '>la<>la<<>>la<', 'right text';
+is $dom->at('#screw-up .ewww > a > img')->attr('src'), '/test.png',
+  'right attribute';
+is $dom->find('#screw-up .ewww > a > img')->[1]->attr('src'), '/test2.png',
+  'right attribute';
+is $dom->find('#screw-up .ewww > a > img')->[2], undef, 'no result';
+is $dom->find('#screw-up .ewww > a > img')->size, 2, 'right number of elements';
+
+# Broken "br" tag
+$dom = DOM::Tiny->new('<br< abc abc abc abc abc abc abc abc<p>Test</p>');
+is $dom->at('p')->text, 'Test', 'right text';
+
+# Modifying an XML document
+$dom = DOM::Tiny->new(<<'EOF');
+<?xml version='1.0' encoding='UTF-8'?>
+<XMLTest />
+EOF
+ok $dom->xml, 'XML mode detected';
+$dom->at('XMLTest')->content('<Element />');
+my $element = $dom->at('Element');
+is $element->tag, 'Element', 'right tag';
+ok $element->xml, 'XML mode active';
+$element = $dom->at('XMLTest')->children->[0];
+is $element->tag, 'Element', 'right child';
+is $element->parent->tag, 'XMLTest', 'right parent';
+ok $element->root->xml, 'XML mode active';
+$dom->replace('<XMLTest2 /><XMLTest3 just="works" />');
+ok $dom->xml, 'XML mode active';
+$dom->at('XMLTest2')->{foo} = undef;
+is $dom, '<XMLTest2 foo="foo" /><XMLTest3 just="works" />', 'right result';
+
+# Ensure HTML semantics
+ok !DOM::Tiny->new->xml(undef)->parse('<?xml version="1.0"?>')->xml,
+  'XML mode not detected';
+$dom
+  = DOM::Tiny->new->xml(0)->parse('<?xml version="1.0"?><br><div>Test</div>');
+is $dom->at('div:root')->text, 'Test', 'right text';
+
+# Ensure XML semantics
+ok !!DOM::Tiny->new->xml(1)->parse('<foo />')->xml, 'XML mode active';
+$dom = DOM::Tiny->new(<<'EOF');
+<?xml version='1.0' encoding='UTF-8'?>
+<script>
+  <table>
+    <td>
+      <tr><thead>foo<thead></tr>
+    </td>
+    <td>
+      <tr><thead>bar<thead></tr>
+    </td>
+  </table>
+</script>
+EOF
+is $dom->find('table > td > tr > thead')->[0]->text, 'foo', 'right text';
+is $dom->find('script > table > td > tr > thead')->[1]->text, 'bar',
+  'right text';
+is $dom->find('table > td > tr > thead')->[2], undef, 'no result';
+is $dom->find('table > td > tr > thead')->size, 2, 'right number of elements';
+
+# Ensure XML semantics again
+$dom = DOM::Tiny->new->xml(1)->parse(<<'EOF');
+<table>
+  <td>
+    <tr><thead>foo<thead></tr>
+  </td>
+  <td>
+    <tr><thead>bar<thead></tr>
+  </td>
+</table>
+EOF
+is $dom->find('table > td > tr > thead')->[0]->text, 'foo', 'right text';
+is $dom->find('table > td > tr > thead')->[1]->text, 'bar', 'right text';
+is $dom->find('table > td > tr > thead')->[2], undef, 'no result';
+is $dom->find('table > td > tr > thead')->size, 2, 'right number of elements';
+
+# Nested tables
+$dom = DOM::Tiny->new(<<'EOF');
+<table id="foo">
+  <tr>
+    <td>
+      <table id="bar">
+        <tr>
+          <td>baz</td>
+        </tr>
+      </table>
+    </td>
+  </tr>
+</table>
+EOF
+is $dom->find('#foo > tr > td > #bar > tr >td')->[0]->text, 'baz', 'right text';
+is $dom->find('table > tr > td > table > tr >td')->[0]->text, 'baz',
+  'right text';
+
+# Nested find
+$dom->parse(<<EOF);
+<c>
+  <a>foo</a>
+  <b>
+    <a>bar</a>
+    <c>
+      <a>baz</a>
+      <d>
+        <a>yada</a>
+      </d>
+    </c>
+  </b>
+</c>
+EOF
+my @results;
+$dom->find('b')->each(
+  sub {
+    $_->find('a')->each(sub { push @results, $_->text });
+  }
+);
+is_deeply \@results, [qw(bar baz yada)], 'right results';
+@results = ();
+$dom->find('a')->each(sub { push @results, $_->text });
+is_deeply \@results, [qw(foo bar baz yada)], 'right results';
+@results = ();
+$dom->find('b')->each(
+  sub {
+    $_->find('c a')->each(sub { push @results, $_->text });
+  }
+);
+is_deeply \@results, [qw(baz yada)], 'right results';
+is $dom->at('b')->at('a')->text, 'bar', 'right text';
+is $dom->at('c > b > a')->text, 'bar', 'right text';
+is $dom->at('b')->at('c > b > a'), undef, 'no result';
+
+# Direct hash access to attributes in XML mode
+$dom = DOM::Tiny->new->xml(1)->parse(<<EOF);
+<a id="one">
+  <B class="two" test>
+    foo
+    <c id="three">bar</c>
+    <c ID="four">baz</c>
+  </B>
+</a>
+EOF
+ok $dom->xml, 'XML mode active';
+is $dom->at('a')->{id}, 'one', 'right attribute';
+is_deeply [sort keys %{$dom->at('a')}], ['id'], 'right attributes';
+is $dom->at('a')->at('B')->text, 'foo', 'right text';
+is $dom->at('B')->{class}, 'two', 'right attribute';
+is_deeply [sort keys %{$dom->at('a B')}], [qw(class test)], 'right attributes';
+is $dom->find('a B c')->[0]->text, 'bar', 'right text';
+is $dom->find('a B c')->[0]{id}, 'three', 'right attribute';
+is_deeply [sort keys %{$dom->find('a B c')->[0]}], ['id'], 'right attributes';
+is $dom->find('a B c')->[1]->text, 'baz', 'right text';
+is $dom->find('a B c')->[1]{ID}, 'four', 'right attribute';
+is_deeply [sort keys %{$dom->find('a B c')->[1]}], ['ID'], 'right attributes';
+is $dom->find('a B c')->[2], undef, 'no result';
+is $dom->find('a B c')->size, 2, 'right number of elements';
+@results = ();
+$dom->find('a B c')->each(sub { push @results, $_->text });
+is_deeply \@results, [qw(bar baz)], 'right results';
+is $dom->find('a B c')->join("\n"),
+  qq{<c id="three">bar</c>\n<c ID="four">baz</c>}, 'right result';
+is_deeply [keys %$dom], [], 'root has no attributes';
+is $dom->find('#nothing')->join, '', 'no result';
+
+# Direct hash access to attributes in HTML mode
+$dom = DOM::Tiny->new(<<EOF);
+<a id="one">
+  <B class="two" test>
+    foo
+    <c id="three">bar</c>
+    <c ID="four">baz</c>
+  </B>
+</a>
+EOF
+ok !$dom->xml, 'XML mode not active';
+is $dom->at('a')->{id}, 'one', 'right attribute';
+is_deeply [sort keys %{$dom->at('a')}], ['id'], 'right attributes';
+is $dom->at('a')->at('b')->text, 'foo', 'right text';
+is $dom->at('b')->{class}, 'two', 'right attribute';
+is_deeply [sort keys %{$dom->at('a b')}], [qw(class test)], 'right attributes';
+is $dom->find('a b c')->[0]->text, 'bar', 'right text';
+is $dom->find('a b c')->[0]{id}, 'three', 'right attribute';
+is_deeply [sort keys %{$dom->find('a b c')->[0]}], ['id'], 'right attributes';
+is $dom->find('a b c')->[1]->text, 'baz', 'right text';
+is $dom->find('a b c')->[1]{id}, 'four', 'right attribute';
+is_deeply [sort keys %{$dom->find('a b c')->[1]}], ['id'], 'right attributes';
+is $dom->find('a b c')->[2], undef, 'no result';
+is $dom->find('a b c')->size, 2, 'right number of elements';
+@results = ();
+$dom->find('a b c')->each(sub { push @results, $_->text });
+is_deeply \@results, [qw(bar baz)], 'right results';
+is $dom->find('a b c')->join("\n"),
+  qq{<c id="three">bar</c>\n<c id="four">baz</c>}, 'right result';
+is_deeply [keys %$dom], [], 'root has no attributes';
+is $dom->find('#nothing')->join, '', 'no result';
+
+# Append and prepend content
+$dom = DOM::Tiny->new('<a><b>Test<c /></b></a>');
+$dom->at('b')->append_content('<d />');
+is $dom->children->[0]->tag, 'a', 'right tag';
+is $dom->all_text, 'Test', 'right text';
+is $dom->at('c')->parent->tag, 'b', 'right tag';
+is $dom->at('d')->parent->tag, 'b', 'right tag';
+$dom->at('b')->prepend_content('<e>DOM</e>');
+is $dom->at('e')->parent->tag, 'b', 'right tag';
+is $dom->all_text, 'DOM Test', 'right text';
+
+# Wrap elements
+$dom = DOM::Tiny->new('<a>Test</a>');
+is $dom->wrap('<b></b>')->type, 'root', 'right type';
+is "$dom", '<b><a>Test</a></b>', 'right result';
+is $dom->at('b')->strip->at('a')->wrap('A')->tag, 'a', 'right tag';
+is "$dom", '<a>Test</a>', 'right result';
+is $dom->at('a')->wrap('<b></b>')->tag, 'a', 'right tag';
+is "$dom", '<b><a>Test</a></b>', 'right result';
+is $dom->at('a')->wrap('C<c><d>D</d><e>E</e></c>F')->parent->tag, 'd',
+  'right tag';
+is "$dom", '<b>C<c><d>D<a>Test</a></d><e>E</e></c>F</b>', 'right result';
+
+# Wrap content
+$dom = DOM::Tiny->new('<a>Test</a>');
+is $dom->at('a')->wrap_content('A')->tag, 'a', 'right tag';
+is "$dom", '<a>Test</a>', 'right result';
+is $dom->wrap_content('<b></b>')->type, 'root', 'right type';
+is "$dom", '<b><a>Test</a></b>', 'right result';
+is $dom->at('b')->strip->at('a')->tag('e:a')->wrap_content('1<b c="d"></b>')
+  ->tag, 'e:a', 'right tag';
+is "$dom", '<e:a>1<b c="d">Test</b></e:a>', 'right result';
+is $dom->at('a')->wrap_content('C<c><d>D</d><e>E</e></c>F')->parent->type,
+  'root', 'right type';
+is "$dom", '<e:a>C<c><d>D1<b c="d">Test</b></d><e>E</e></c>F</e:a>',
+  'right result';
+
+# Broken "div" in "td"
+$dom = DOM::Tiny->new(<<EOF);
+<table>
+  <tr>
+    <td><div id="A"></td>
+    <td><div id="B"></td>
+  </tr>
+</table>
+EOF
+is $dom->find('table tr td')->[0]->at('div')->{id}, 'A', 'right attribute';
+is $dom->find('table tr td')->[1]->at('div')->{id}, 'B', 'right attribute';
+is $dom->find('table tr td')->[2], undef, 'no result';
+is $dom->find('table tr td')->size, 2, 'right number of elements';
+is "$dom", <<EOF, 'right result';
+<table>
+  <tr>
+    <td><div id="A"></div></td>
+    <td><div id="B"></div></td>
+  </tr>
+</table>
+EOF
+
+# Preformatted text
+$dom = DOM::Tiny->new(<<EOF);
+<div>
+  looks
+  <pre><code>like
+  it
+    really</code>
+  </pre>
+  works
+</div>
+EOF
+is $dom->text, '', 'no text';
+is $dom->text(0), "\n", 'right text';
+is $dom->all_text, "looks like\n  it\n    really\n  works", 'right text';
+is $dom->all_text(0), "\n  looks\n  like\n  it\n    really\n  \n  works\n\n",
+  'right text';
+is $dom->at('div')->text, 'looks works', 'right text';
+is $dom->at('div')->text(0), "\n  looks\n  \n  works\n", 'right text';
+is $dom->at('div')->all_text, "looks like\n  it\n    really\n  works",
+  'right text';
+is $dom->at('div')->all_text(0),
+  "\n  looks\n  like\n  it\n    really\n  \n  works\n", 'right text';
+is $dom->at('div pre')->text, "\n  ", 'right text';
+is $dom->at('div pre')->text(0), "\n  ", 'right text';
+is $dom->at('div pre')->all_text, "like\n  it\n    really\n  ", 'right text';
+is $dom->at('div pre')->all_text(0), "like\n  it\n    really\n  ", 'right text';
+is $dom->at('div pre code')->text, "like\n  it\n    really", 'right text';
+is $dom->at('div pre code')->text(0), "like\n  it\n    really", 'right text';
+is $dom->at('div pre code')->all_text, "like\n  it\n    really", 'right text';
+is $dom->at('div pre code')->all_text(0), "like\n  it\n    really",
+  'right text';
+
+# Form values
+$dom = DOM::Tiny->new(<<EOF);
+<form action="/foo">
+  <p>Test</p>
+  <input type="text" name="a" value="A" />
+  <input type="checkbox" checked name="b" value="B">
+  <input type="radio" checked name="c" value="C">
+  <select multiple name="f">
+    <option value="F">G</option>
+    <optgroup>
+      <option>H</option>
+      <option selected>I</option>
+    </optgroup>
+    <option value="J" selected>K</option>
+  </select>
+  <select name="n"><option>N</option></select>
+  <select multiple name="q"><option>Q</option></select>
+  <select name="d">
+    <option selected>R</option>
+    <option selected>D</option>
+  </select>
+  <textarea name="m">M</textarea>
+  <button name="o" value="O">No!</button>
+  <input type="submit" name="p" value="P" />
+</form>
+EOF
+is $dom->at('p')->val,                         undef, 'no value';
+is $dom->at('input')->val,                     'A',   'right value';
+is $dom->at('input:checked')->val,             'B',   'right value';
+is $dom->at('input:checked[type=radio]')->val, 'C',   'right value';
+is_deeply $dom->at('select')->val, ['I', 'J'], 'right values';
+is $dom->at('select option')->val,                          'F', 'right value';
+is $dom->at('select optgroup option:not([selected])')->val, 'H', 'right value';
+is $dom->find('select')->[1]->at('option')->val, 'N', 'right value';
+is $dom->find('select')->[1]->val,        undef, 'no value';
+is_deeply $dom->find('select')->[2]->val, undef, 'no value';
+is $dom->find('select')->[2]->at('option')->val, 'Q', 'right value';
+is_deeply $dom->find('select')->last->val, 'D', 'right value';
+is_deeply $dom->find('select')->last->at('option')->val, 'R', 'right value';
+is $dom->at('textarea')->val, 'M', 'right value';
+is $dom->at('button')->val,   'O', 'right value';
+is $dom->find('form input')->last->val, 'P', 'right value';
+
+# PoCo example with whitespace sensitive text
+$dom = DOM::Tiny->new(<<EOF);
+<?xml version="1.0" encoding="UTF-8"?>
+<response>
+  <entry>
+    <id>1286823</id>
+    <displayName>Homer Simpson</displayName>
+    <addresses>
+      <type>home</type>
+      <formatted><![CDATA[742 Evergreen Terrace
+Springfield, VT 12345 USA]]></formatted>
+    </addresses>
+  </entry>
+  <entry>
+    <id>1286822</id>
+    <displayName>Marge Simpson</displayName>
+    <addresses>
+      <type>home</type>
+      <formatted>742 Evergreen Terrace
+Springfield, VT 12345 USA</formatted>
+    </addresses>
+  </entry>
+</response>
+EOF
+is $dom->find('entry')->[0]->at('displayName')->text, 'Homer Simpson',
+  'right text';
+is $dom->find('entry')->[0]->at('id')->text, '1286823', 'right text';
+is $dom->find('entry')->[0]->at('addresses')->children('type')->[0]->text,
+  'home', 'right text';
+is $dom->find('entry')->[0]->at('addresses formatted')->text,
+  "742 Evergreen Terrace\nSpringfield, VT 12345 USA", 'right text';
+is $dom->find('entry')->[0]->at('addresses formatted')->text(0),
+  "742 Evergreen Terrace\nSpringfield, VT 12345 USA", 'right text';
+is $dom->find('entry')->[1]->at('displayName')->text, 'Marge Simpson',
+  'right text';
+is $dom->find('entry')->[1]->at('id')->text, '1286822', 'right text';
+is $dom->find('entry')->[1]->at('addresses')->children('type')->[0]->text,
+  'home', 'right text';
+is $dom->find('entry')->[1]->at('addresses formatted')->text,
+  '742 Evergreen Terrace Springfield, VT 12345 USA', 'right text';
+is $dom->find('entry')->[1]->at('addresses formatted')->text(0),
+  "742 Evergreen Terrace\nSpringfield, VT 12345 USA", 'right text';
+is $dom->find('entry')->[2], undef, 'no result';
+is $dom->find('entry')->size, 2, 'right number of elements';
+
+# Find attribute with hyphen in name and value
+$dom = DOM::Tiny->new(<<EOF);
+<html>
+  <head><meta http-equiv="content-type" content="text/html"></head>
+</html>
+EOF
+is $dom->find('[http-equiv]')->[0]{content}, 'text/html', 'right attribute';
+is $dom->find('[http-equiv]')->[1], undef, 'no result';
+is $dom->find('[http-equiv="content-type"]')->[0]{content}, 'text/html',
+  'right attribute';
+is $dom->find('[http-equiv="content-type"]')->[1], undef, 'no result';
+is $dom->find('[http-equiv^="content-"]')->[0]{content}, 'text/html',
+  'right attribute';
+is $dom->find('[http-equiv^="content-"]')->[1], undef, 'no result';
+is $dom->find('head > [http-equiv$="-type"]')->[0]{content}, 'text/html',
+  'right attribute';
+is $dom->find('head > [http-equiv$="-type"]')->[1], undef, 'no result';
+
+# Find "0" attribute value
+$dom = DOM::Tiny->new(<<EOF);
+<a accesskey="0">Zero</a>
+<a accesskey="1">O&gTn&gt;e</a>
+EOF
+is $dom->find('a[accesskey]')->[0]->text, 'Zero',    'right text';
+is $dom->find('a[accesskey]')->[1]->text, 'O&gTn>e', 'right text';
+is $dom->find('a[accesskey]')->[2], undef, 'no result';
+is $dom->find('a[accesskey=0]')->[0]->text, 'Zero', 'right text';
+is $dom->find('a[accesskey=0]')->[1], undef, 'no result';
+is $dom->find('a[accesskey^=0]')->[0]->text, 'Zero', 'right text';
+is $dom->find('a[accesskey^=0]')->[1], undef, 'no result';
+is $dom->find('a[accesskey$=0]')->[0]->text, 'Zero', 'right text';
+is $dom->find('a[accesskey$=0]')->[1], undef, 'no result';
+is $dom->find('a[accesskey~=0]')->[0]->text, 'Zero', 'right text';
+is $dom->find('a[accesskey~=0]')->[1], undef, 'no result';
+is $dom->find('a[accesskey*=0]')->[0]->text, 'Zero', 'right text';
+is $dom->find('a[accesskey*=0]')->[1], undef, 'no result';
+is $dom->find('a[accesskey=1]')->[0]->text, 'O&gTn>e', 'right text';
+is $dom->find('a[accesskey=1]')->[1], undef, 'no result';
+is $dom->find('a[accesskey^=1]')->[0]->text, 'O&gTn>e', 'right text';
+is $dom->find('a[accesskey^=1]')->[1], undef, 'no result';
+is $dom->find('a[accesskey$=1]')->[0]->text, 'O&gTn>e', 'right text';
+is $dom->find('a[accesskey$=1]')->[1], undef, 'no result';
+is $dom->find('a[accesskey~=1]')->[0]->text, 'O&gTn>e', 'right text';
+is $dom->find('a[accesskey~=1]')->[1], undef, 'no result';
+is $dom->find('a[accesskey*=1]')->[0]->text, 'O&gTn>e', 'right text';
+is $dom->find('a[accesskey*=1]')->[1], undef, 'no result';
+is $dom->at('a[accesskey*="."]'), undef, 'no result';
+
+# Empty attribute value
+$dom = DOM::Tiny->new(<<EOF);
+<foo bar=>
+  test
+</foo>
+<bar>after</bar>
+EOF
+is $dom->tree->[0], 'root', 'right type';
+is $dom->tree->[1][0], 'tag', 'right type';
+is $dom->tree->[1][1], 'foo', 'right tag';
+is_deeply $dom->tree->[1][2], {bar => ''}, 'right attributes';
+is $dom->tree->[1][4][0], 'text',       'right type';
+is $dom->tree->[1][4][1], "\n  test\n", 'right text';
+is $dom->tree->[3][0], 'tag', 'right type';
+is $dom->tree->[3][1], 'bar', 'right tag';
+is $dom->tree->[3][4][0], 'text',  'right type';
+is $dom->tree->[3][4][1], 'after', 'right text';
+is "$dom", <<EOF, 'right result';
+<foo bar="">
+  test
+</foo>
+<bar>after</bar>
+EOF
+
+# Case-insensitive attribute values
+$dom = DOM::Tiny->new(<<EOF);
+<p class="foo">A</p>
+<p class="foo bAr">B</p>
+<p class="FOO">C</p>
+EOF
+is $dom->find('.foo')->map('text')->join(','),            'A,B', 'right result';
+is $dom->find('.FOO')->map('text')->join(','),            'C',   'right result';
+is $dom->find('[class=foo]')->map('text')->join(','),     'A',   'right result';
+is $dom->find('[class=foo i]')->map('text')->join(','),   'A,C', 'right result';
+is $dom->find('[class="foo" i]')->map('text')->join(','), 'A,C', 'right result';
+is $dom->find('[class="foo bar"]')->size, 0, 'no results';
+is $dom->find('[class="foo bar" i]')->map('text')->join(','), 'B',
+  'right result';
+is $dom->find('[class~=foo]')->map('text')->join(','), 'A,B', 'right result';
+is $dom->find('[class~=foo i]')->map('text')->join(','), 'A,B,C',
+  'right result';
+is $dom->find('[class*=f]')->map('text')->join(','),   'A,B',   'right result';
+is $dom->find('[class*=f i]')->map('text')->join(','), 'A,B,C', 'right result';
+is $dom->find('[class^=F]')->map('text')->join(','),   'C',     'right result';
+is $dom->find('[class^=F i]')->map('text')->join(','), 'A,B,C', 'right result';
+is $dom->find('[class$=O]')->map('text')->join(','),   'C',     'right result';
+is $dom->find('[class$=O i]')->map('text')->join(','), 'A,C',   'right result';
+
+# Nested description lists
+$dom = DOM::Tiny->new(<<EOF);
+<dl>
+  <dt>A</dt>
+  <DD>
+    <dl>
+      <dt>B
+      <dd>C
+    </dl>
+  </dd>
+</dl>
+EOF
+is $dom->find('dl > dd > dl > dt')->[0]->text, 'B', 'right text';
+is $dom->find('dl > dd > dl > dd')->[0]->text, 'C', 'right text';
+is $dom->find('dl > dt')->[0]->text,           'A', 'right text';
+
+# Nested lists
+$dom = DOM::Tiny->new(<<EOF);
+<div>
+  <ul>
+    <li>
+      A
+      <ul>
+        <li>B</li>
+        C
+      </ul>
+    </li>
+  </ul>
+</div>
+EOF
+is $dom->find('div > ul > li')->[0]->text, 'A', 'right text';
+is $dom->find('div > ul > li')->[1], undef, 'no result';
+is $dom->find('div > ul li')->[0]->text, 'A', 'right text';
+is $dom->find('div > ul li')->[1]->text, 'B', 'right text';
+is $dom->find('div > ul li')->[2], undef, 'no result';
+is $dom->find('div > ul ul')->[0]->text, 'C', 'right text';
+is $dom->find('div > ul ul')->[1], undef, 'no result';
+
+# Unusual order
+$dom
+  = DOM::Tiny->new('<a href="http://example.com" id="foo" class="bar">Ok!</a>');
+is $dom->at('a:not([href$=foo])[href^=h]')->text, 'Ok!', 'right text';
+is $dom->at('a:not([href$=example.com])[href^=h]'), undef, 'no result';
+is $dom->at('a[href^=h]#foo.bar')->text, 'Ok!', 'right text';
+is $dom->at('a[href^=h]#foo.baz'), undef, 'no result';
+is $dom->at('a[href^=h]#foo:not(b)')->text, 'Ok!', 'right text';
+is $dom->at('a[href^=h]#foo:not(a)'), undef, 'no result';
+is $dom->at('[href^=h].bar:not(b)[href$=m]#foo')->text, 'Ok!', 'right text';
+is $dom->at('[href^=h].bar:not(b)[href$=m]#bar'), undef, 'no result';
+is $dom->at(':not(b)#foo#foo')->text, 'Ok!', 'right text';
+is $dom->at(':not(b)#foo#bar'), undef, 'no result';
+is $dom->at(':not([href^=h]#foo#bar)')->text, 'Ok!', 'right text';
+is $dom->at(':not([href^=h]#foo#foo)'), undef, 'no result';
+
+# Slash between attributes
+$dom = DOM::Tiny->new('<input /type=checkbox / value="/a/" checked/><br/>');
+is_deeply $dom->at('input')->attr,
+  {type => 'checkbox', value => '/a/', checked => undef}, 'right attributes';
+is "$dom", '<input checked type="checkbox" value="/a/"><br>', 'right result';
+
+# Dot and hash in class and id attributes
+$dom = DOM::Tiny->new('<p class="a#b.c">A</p><p id="a#b.c">B</p>');
+is $dom->at('p.a\#b\.c')->text,       'A', 'right text';
+is $dom->at(':not(p.a\#b\.c)')->text, 'B', 'right text';
+is $dom->at('p#a\#b\.c')->text,       'B', 'right text';
+is $dom->at(':not(p#a\#b\.c)')->text, 'A', 'right text';
+
+# Extra whitespace
+$dom = DOM::Tiny->new('< span>a< /span><b >b</b><span >c</ span>');
+is $dom->at('span')->text,     'a', 'right text';
+is $dom->at('span + b')->text, 'b', 'right text';
+is $dom->at('b + span')->text, 'c', 'right text';
+is "$dom", '<span>a</span><b>b</b><span>c</span>', 'right result';
+
+# Selectors with leading and trailing whitespace
+$dom = DOM::Tiny->new('<div id=foo><b>works</b></div>');
+is $dom->at(' div   b ')->text,          'works', 'right text';
+is $dom->at('  :not(  #foo  )  ')->text, 'works', 'right text';
+
+# "0"
+$dom = DOM::Tiny->new('0');
+is "$dom", '0', 'right result';
+$dom->append_content('☃');
+is "$dom", '0☃', 'right result';
+is $dom->parse('<!DOCTYPE 0>'),  '<!DOCTYPE 0>',  'successful roundtrip';
+is $dom->parse('<!--0-->'),      '<!--0-->',      'successful roundtrip';
+is $dom->parse('<![CDATA[0]]>'), '<![CDATA[0]]>', 'successful roundtrip';
+is $dom->parse('<?0?>'),         '<?0?>',         'successful roundtrip';
+
+# Not self-closing
+$dom = DOM::Tiny->new('<div />< div ><pre />test</div >123');
+is $dom->at('div > div > pre')->text, 'test', 'right text';
+is "$dom", '<div><div><pre>test</pre></div>123</div>', 'right result';
+$dom = DOM::Tiny->new('<p /><svg><circle /><circle /></svg>');
+is $dom->find('p > svg > circle')->size, 2, 'two circles';
+is "$dom", '<p><svg><circle></circle><circle></circle></svg></p>',
+  'right result';
+
+# "image"
+$dom = DOM::Tiny->new('<image src="foo.png">test');
+is $dom->at('img')->{src}, 'foo.png', 'right attribute';
+is "$dom", '<img src="foo.png">test', 'right result';
+
+# "title"
+$dom = DOM::Tiny->new('<title> <p>test&lt;</title>');
+is $dom->at('title')->text, ' <p>test<', 'right text';
+is "$dom", '<title> <p>test<</title>', 'right result';
+
+# "textarea"
+$dom = DOM::Tiny->new('<textarea id="a"> <p>test&lt;</textarea>');
+is $dom->at('textarea#a')->text, ' <p>test<', 'right text';
+is "$dom", '<textarea id="a"> <p>test<</textarea>', 'right result';
+
+# Comments
+$dom = DOM::Tiny->new(<<EOF);
+<!-- HTML5 -->
+<!-- bad idea -- HTML5 -->
+<!-- HTML4 -- >
+<!-- bad idea -- HTML4 -- >
+EOF
+is $dom->tree->[1][1], ' HTML5 ',             'right comment';
+is $dom->tree->[3][1], ' bad idea -- HTML5 ', 'right comment';
+is $dom->tree->[5][1], ' HTML4 ',             'right comment';
+is $dom->tree->[7][1], ' bad idea -- HTML4 ', 'right comment';
+
+# Huge number of attributes
+$dom = DOM::Tiny->new('<div ' . ('a=b ' x 32768) . '>Test</div>');
+is $dom->at('div[a=b]')->text, 'Test', 'right text';
+
+# Huge number of nested tags
+my $huge = ('<a>' x 100) . 'works' . ('</a>' x 100);
+$dom = DOM::Tiny->new($huge);
+is $dom->all_text, 'works', 'right text';
+is "$dom", $huge, 'right result';
+
+done_testing();
diff --git a/t/entities.t b/t/entities.t
new file mode 100644 (file)
index 0000000..79fa8cd
--- /dev/null
@@ -0,0 +1,43 @@
+use strict;
+use warnings;
+use utf8;
+use Test::More;
+use DOM::Tiny::Entities qw(html_unescape xml_escape);
+use Encode 'decode';
+
+# html_unescape
+is html_unescape('&#x3c;foo&#x3E;bar&lt;baz&gt;&#x0026;&#34;'),
+  "<foo>bar<baz>&\"", 'right HTML unescaped result';
+
+# html_unescape (special entities)
+is html_unescape('foo &#x2603; &CounterClockwiseContourIntegral; bar &sup1baz'),
+  "foo ☃ \x{2233} bar &sup1baz", 'right HTML unescaped result';
+
+# html_unescape (multi-character entity)
+is html_unescape(decode 'UTF-8', '&acE;'), "\x{223e}\x{0333}",
+  'right HTML unescaped result';
+
+# html_unescape (apos)
+is html_unescape('foobar&apos;&lt;baz&gt;&#x26;&#34;'), "foobar'<baz>&\"",
+  'right HTML unescaped result';
+
+# html_unescape (nothing to unescape)
+is html_unescape('foobar'), 'foobar', 'right HTML unescaped result';
+
+# html_unescape (UTF-8)
+is html_unescape(decode 'UTF-8', 'foo&lt;baz&gt;&#x26;&#34;&OElig;&Foo;'),
+  "foo<baz>&\"\x{152}&Foo;", 'right HTML unescaped result';
+
+# xml_escape
+is xml_escape(qq{la<f>\nbar"baz"'yada\n'&lt;la}),
+  "la&lt;f&gt;\nbar&quot;baz&quot;&#39;yada\n&#39;&amp;lt;la",
+  'right XML escaped result';
+
+# xml_escape (UTF-8 with nothing to escape)
+is xml_escape('привет'), 'привет', 'right XML escaped result';
+
+# xml_escape (UTF-8)
+is xml_escape('привет<foo>'), 'привет&lt;foo&gt;',
+  'right XML escaped result';
+
+done_testing;