make DOM::Tiny::HTML and DOM::Tiny::CSS private also
Dan Book [Mon, 9 Nov 2015 06:03:18 +0000 (01:03 -0500)]
META.json
README.pod
lib/DOM/Tiny.pm
lib/DOM/Tiny/CSS.pm [deleted file]
lib/DOM/Tiny/_CSS.pm [new file with mode: 0644]
lib/DOM/Tiny/_Collection.pm
lib/DOM/Tiny/_HTML.pm [moved from lib/DOM/Tiny/HTML.pm with 82% similarity]

index 2c6a671..d532ec5 100644 (file)
--- a/META.json
+++ b/META.json
          "file" : "lib/DOM/Tiny.pm",
          "version" : "0.001"
       },
-      "DOM::Tiny::CSS" : {
-         "file" : "lib/DOM/Tiny/CSS.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",
index dc7e059..a2937c4 100644 (file)
@@ -2,8 +2,6 @@
 
 =encoding utf8
 
-=for Pod::Coverage TO_JSON
-
 =head1 NAME
 
 DOM::Tiny - Minimalistic HTML/XML DOM parser with CSS selectors
@@ -37,10 +35,11 @@ DOM::Tiny - Minimalistic HTML/XML DOM parser with CSS selectors
 
 =head1 DESCRIPTION
 
-L<DOM::Tiny> is a minimalistic and relaxed pure-perl HTML/XML DOM parser with
-support for the L<HTML Living Standard|https://html.spec.whatwg.org/> and
-L<CSS3 selectors|http://www.w3.org/TR/selectors/> based on L<Mojo::DOM>. It
-will even try to interpret broken HTML and XML, so you should not use it for
+L<DOM::Tiny> is a minimalistic and relaxed pure-perl HTML/XML DOM parser based
+on L<Mojo::DOM>. It supports the L<HTML Living Standard|https://html.spec.whatwg.org/>
+and L<Extensible Markup Language (XML) 1.0|http://www.w3.org/TR/xml/>, and
+matching based on L<CSS3 selectors|http://www.w3.org/TR/selectors/>. It will
+even try to interpret broken HTML and XML, so you should not use it for
 validation.
 
 =head1 NODES AND ELEMENTS
@@ -95,6 +94,40 @@ XML detection can also be disabled with the L</"xml"> method.
   my $dom = DOM::Tiny->new->xml(0)->parse('<P ID="greeting">Hi!</P>');
   say $dom->at('p[id]')->text;
 
+=head1 OPERATORS
+
+L<DOM::Tiny> overloads the following operators.
+
+=head2 array
+
+  my @nodes = @$dom;
+
+Alias for L</"child_nodes">.
+
+  # "<!-- Test -->"
+  $dom->parse('<!-- Test --><b>123</b>')->[0];
+
+=head2 bool
+
+  my $bool = !!$dom;
+
+Always true.
+
+=head2 hash
+
+  my %attrs = %$dom;
+
+Alias for L</"attr">.
+
+  # "test"
+  $dom->parse('<div id="test">Test</div>')->at('div')->{id};
+
+=head2 stringify
+
+  my $str = "$dom";
+
+Alias for L</"to_string">.
+
 =head1 METHODS
 
 L<DOM::Tiny> implements the following methods.
@@ -128,7 +161,7 @@ whitespace trimming is enabled by default.
 
 Find all ancestor elements of this node matching the CSS selector and return a
 L<collection|/"COLLECTION METHODS"> containing these elements as L<DOM::Tiny>
-objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+objects. All selectors listed in L</"SELECTORS"> are supported.
 
   # List tag names of ancestor elements
   say $dom->ancestors->map('tag')->join("\n");
@@ -171,7 +204,7 @@ node's content.
 
 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.
+All selectors listed in L</"SELECTORS"> are supported.
 
   # Find first element with "svg" namespace definition
   my $namespace = $dom->at('[xmlns\:svg]')->{'xmlns:svg'};
@@ -217,7 +250,7 @@ element as L<DOM::Tiny> objects.
 
 Find all child elements of this element matching the CSS selector and return a
 L<collection|/"COLLECTION METHODS"> containing these elements as L<DOM::Tiny>
-objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+objects. All selectors listed in L</"SELECTORS"> are supported.
 
   # Show tag name of random child element
   say $dom->children->shuffle->first->tag;
@@ -272,8 +305,7 @@ this element as L<DOM::Tiny> objects.
 
 Find all descendant elements of this element matching the CSS selector and
 return a L<collection|/"COLLECTION METHODS"> containing these elements as
-L<DOM::Tiny> objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are
-supported.
+L<DOM::Tiny> objects. All selectors listed in L</"SELECTORS"> are supported.
 
   # Find a specific element and extract information
   my $id = $dom->find('div')->[23]{id};
@@ -294,7 +326,7 @@ supported.
 
 Find all sibling elements after this node matching the CSS selector and return
 a L<collection|/"COLLECTION METHODS"> containing these elements as L<DOM::Tiny>
-objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+objects. All selectors listen in L</"SELECTORS"> are supported.
 
   # List tags of sibling elements after this node
   say $dom->following->map('tag')->join("\n");
@@ -313,8 +345,8 @@ this node as L<DOM::Tiny> objects.
 
   my $bool = $dom->matches('div ~ p');
 
-Check if this element matches the CSS selector. All selectors from
-L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+Check if this element matches the CSS selector. All selectors listed in
+L</"SELECTORS"> are supported.
 
   # True
   $dom->parse('<p class="a">A</p>')->at('p')->matches('.a');
@@ -372,7 +404,7 @@ no parent.
 
   $dom = $dom->parse('<foo bar="baz">I ♥ DOM::Tiny!</foo>');
 
-Parse HTML/XML fragment with L<DOM::Tiny::HTML>.
+Parse HTML/XML fragment.
 
   # Parse XML
   my $dom = DOM::Tiny->new->xml(1)->parse($xml);
@@ -384,7 +416,7 @@ Parse HTML/XML fragment with L<DOM::Tiny::HTML>.
 
 Find all sibling elements before this node matching the CSS selector and return
 a L<collection|/"COLLECTION METHODS"> containing these elements as L<DOM::Tiny>
-objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+objects. All selectors listed in L</"SELECTORS"> are supported.
 
   # List tags of sibling elements before this node
   say $dom->preceding->map('tag')->join("\n");
@@ -643,45 +675,11 @@ children of the first innermost element.
 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 COLLECTION METHODS
 
-Some L<DOM::Tiny> methods return an array-based collection object, which can
-either be accessed directly as an array reference, or with the following
-methods.
+Some L<DOM::Tiny> methods return an array-based collection object based on
+L<Mojo::Collection>, which can either be accessed directly as an array
+reference, or with the following methods.
 
   # Chain methods
   $collection->map(sub { ucfirst })->shuffle->each(sub {
@@ -888,6 +886,226 @@ callback/method.
   # $collection contains ([1, 2], [2, 1], [3, 2])
   $collection->uniq(sub{ $_->[1] })->to_array; # "[[1, 2], [2, 1]]"
 
+=head1 SELECTORS
+
+L<DOM::Tiny> uses a CSS selector engine based on L<Mojo::DOM::CSS>. All CSS
+selectors that make sense for a standalone parser are supported.
+
+=head2 *
+
+Any element.
+
+  my $all = $dom->find('*');
+
+=head2 E
+
+An element of type C<E>.
+
+  my $title = $dom->at('title');
+
+=head2 E[foo]
+
+An C<E> element with a C<foo> attribute.
+
+  my $links = $dom->find('a[href]');
+
+=head2 E[foo="bar"]
+
+An C<E> element whose C<foo> attribute value is exactly equal to C<bar>.
+
+  my $case_sensitive = $dom->find('input[type="hidden"]');
+  my $case_sensitive = $dom->find('input[type=hidden]');
+
+=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 = $dom->find('input[type="hidden" i]');
+  my $case_insensitive = $dom->find('input[type=hidden i]');
+  my $case_insensitive = $dom->find('input[class~="foo" i]');
+
+This selector is part of
+L<Selectors Level 4|http://dev.w3.org/csswg/selectors-4>, which is still a work
+in progress.
+
+=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 = $dom->find('input[class~="foo"]');
+  my $foo = $dom->find('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 = $dom->find('input[name^="f"]');
+  my $begins_with = $dom->find('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 = $dom->find('input[name$="o"]');
+  my $ends_with = $dom->find('input[name$=o]');
+
+=head2 E[foo*="bar"]
+
+An C<E> element whose C<foo> attribute value contains the substring C<bar>.
+
+  my $contains = $dom->find('input[name*="fo"]');
+  my $contains = $dom->find('input[name*=fo]');
+
+=head2 E:root
+
+An C<E> element, root of the document.
+
+  my $root = $dom->at(':root');
+
+=head2 E:nth-child(n)
+
+An C<E> element, the C<n-th> child of its parent.
+
+  my $third = $dom->find('div:nth-child(3)');
+  my $odd   = $dom->find('div:nth-child(odd)');
+  my $even  = $dom->find('div:nth-child(even)');
+  my $top3  = $dom->find('div:nth-child(-n+3)');
+
+=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    = $dom->find('div:nth-last-child(3)');
+  my $odd      = $dom->find('div:nth-last-child(odd)');
+  my $even     = $dom->find('div:nth-last-child(even)');
+  my $bottom3  = $dom->find('div:nth-last-child(-n+3)');
+
+=head2 E:nth-of-type(n)
+
+An C<E> element, the C<n-th> sibling of its type.
+
+  my $third = $dom->find('div:nth-of-type(3)');
+  my $odd   = $dom->find('div:nth-of-type(odd)');
+  my $even  = $dom->find('div:nth-of-type(even)');
+  my $top3  = $dom->find('div:nth-of-type(-n+3)');
+
+=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    = $dom->find('div:nth-last-of-type(3)');
+  my $odd      = $dom->find('div:nth-last-of-type(odd)');
+  my $even     = $dom->find('div:nth-last-of-type(even)');
+  my $bottom3  = $dom->find('div:nth-last-of-type(-n+3)');
+
+=head2 E:first-child
+
+An C<E> element, first child of its parent.
+
+  my $first = $dom->find('div p:first-child');
+
+=head2 E:last-child
+
+An C<E> element, last child of its parent.
+
+  my $last = $dom->find('div p:last-child');
+
+=head2 E:first-of-type
+
+An C<E> element, first sibling of its type.
+
+  my $first = $dom->find('div p:first-of-type');
+
+=head2 E:last-of-type
+
+An C<E> element, last sibling of its type.
+
+  my $last = $dom->find('div p:last-of-type');
+
+=head2 E:only-child
+
+An C<E> element, only child of its parent.
+
+  my $lonely = $dom->find('div p:only-child');
+
+=head2 E:only-of-type
+
+An C<E> element, only sibling of its type.
+
+  my $lonely = $dom->find('div p:only-of-type');
+
+=head2 E:empty
+
+An C<E> element that has no children (including text nodes).
+
+  my $empty = $dom->find(':empty');
+
+=head2 E:checked
+
+A user interface element C<E> which is checked (for instance a radio-button or
+checkbox).
+
+  my $input = $dom->find(':checked');
+
+=head2 E.warning
+
+An C<E> element whose class is "warning".
+
+  my $warning = $dom->find('div.warning');
+
+=head2 E#myid
+
+An C<E> element with C<ID> equal to "myid".
+
+  my $foo = $dom->at('div#foo');
+
+=head2 E:not(s)
+
+An C<E> element that does not match simple selector C<s>.
+
+  my $others = $dom->find('div p:not(:first-child)');
+
+=head2 E F
+
+An C<F> element descendant of an C<E> element.
+
+  my $headlines = $dom->find('div h1');
+
+=head2 E E<gt> F
+
+An C<F> element child of an C<E> element.
+
+  my $headlines = $dom->find('html > body > div > h1');
+
+=head2 E + F
+
+An C<F> element immediately preceded by an C<E> element.
+
+  my $second = $dom->find('h1 + h2');
+
+=head2 E ~ F
+
+An C<F> element preceded by an C<E> element.
+
+  my $second = $dom->find('h1 ~ h2');
+
+=head2 E, F, G
+
+Elements of type C<E>, C<F> and C<G>.
+
+  my $headlines = $dom->find('h1, h2, h3');
+
+=head2 E[foo=bar][bar=baz]
+
+An C<E> element whose attributes match all following attribute selectors.
+
+  my $links = $dom->find('a[foo^=b][foo$=ar]');
+
 =head1 BUGS
 
 Report any issues on the public bugtracker.
@@ -908,4 +1126,6 @@ This is free software, licensed under:
 
 L<Mojo::DOM>, L<XML::LibXML>, L<XML::Twig>, L<HTML::TreeBuilder>, L<XML::Smart>
 
+=for Pod::Coverage TO_JSON
+
 =cut
index cf6cddc..dd7a826 100644 (file)
@@ -12,15 +12,15 @@ use overload
 
 use Carp 'croak';
 use DOM::Tiny::_Collection;
-use DOM::Tiny::CSS;
-use DOM::Tiny::HTML;
+use DOM::Tiny::_CSS;
+use DOM::Tiny::_HTML;
 use Scalar::Util qw(blessed weaken);
 
 our $VERSION = '0.001';
 
 sub new {
   my $class = shift;
-  my $self = bless \DOM::Tiny::HTML->new, ref $class || $class;
+  my $self = bless \DOM::Tiny::_HTML->new, ref $class || $class;
   return @_ ? $self->parse(@_) : $self;
 }
 
@@ -67,7 +67,7 @@ sub content {
   my $type = $self->type;
   if ($type eq 'root' || $type eq 'tag') {
     return $self->_content(0, 1, @_) if @_;
-    my $html = DOM::Tiny::HTML->new(xml => $self->xml);
+    my $html = DOM::Tiny::_HTML->new(xml => $self->xml);
     return join '', map { $html->tree($_)->render } _nodes($self->tree);
   }
 
@@ -244,7 +244,7 @@ sub _content {
   return $self;
 }
 
-sub _css { DOM::Tiny::CSS->new(tree => shift->tree) }
+sub _css { DOM::Tiny::_CSS->new(tree => shift->tree) }
 
 sub _delegate {
   my ($self, $method) = (shift, shift);
@@ -283,7 +283,7 @@ sub _offset {
 
 sub _parent { $_[0]->tree->[$_[0]->type eq 'tag' ? 3 : 2] }
 
-sub _parse { DOM::Tiny::HTML->new(xml => shift->xml)->parse(shift)->tree }
+sub _parse { DOM::Tiny::_HTML->new(xml => shift->xml)->parse(shift)->tree }
 
 sub _replace {
   my ($self, $parent, $child, @nodes) = @_;
@@ -389,8 +389,6 @@ sub _wrap {
 
 =encoding utf8
 
-=for Pod::Coverage TO_JSON
-
 =head1 NAME
 
 DOM::Tiny - Minimalistic HTML/XML DOM parser with CSS selectors
@@ -424,10 +422,11 @@ DOM::Tiny - Minimalistic HTML/XML DOM parser with CSS selectors
 
 =head1 DESCRIPTION
 
-L<DOM::Tiny> is a minimalistic and relaxed pure-perl HTML/XML DOM parser with
-support for the L<HTML Living Standard|https://html.spec.whatwg.org/> and
-L<CSS3 selectors|http://www.w3.org/TR/selectors/> based on L<Mojo::DOM>. It
-will even try to interpret broken HTML and XML, so you should not use it for
+L<DOM::Tiny> is a minimalistic and relaxed pure-perl HTML/XML DOM parser based
+on L<Mojo::DOM>. It supports the L<HTML Living Standard|https://html.spec.whatwg.org/>
+and L<Extensible Markup Language (XML) 1.0|http://www.w3.org/TR/xml/>, and
+matching based on L<CSS3 selectors|http://www.w3.org/TR/selectors/>. It will
+even try to interpret broken HTML and XML, so you should not use it for
 validation.
 
 =head1 NODES AND ELEMENTS
@@ -482,6 +481,40 @@ XML detection can also be disabled with the L</"xml"> method.
   my $dom = DOM::Tiny->new->xml(0)->parse('<P ID="greeting">Hi!</P>');
   say $dom->at('p[id]')->text;
 
+=head1 OPERATORS
+
+L<DOM::Tiny> overloads the following operators.
+
+=head2 array
+
+  my @nodes = @$dom;
+
+Alias for L</"child_nodes">.
+
+  # "<!-- Test -->"
+  $dom->parse('<!-- Test --><b>123</b>')->[0];
+
+=head2 bool
+
+  my $bool = !!$dom;
+
+Always true.
+
+=head2 hash
+
+  my %attrs = %$dom;
+
+Alias for L</"attr">.
+
+  # "test"
+  $dom->parse('<div id="test">Test</div>')->at('div')->{id};
+
+=head2 stringify
+
+  my $str = "$dom";
+
+Alias for L</"to_string">.
+
 =head1 METHODS
 
 L<DOM::Tiny> implements the following methods.
@@ -515,7 +548,7 @@ whitespace trimming is enabled by default.
 
 Find all ancestor elements of this node matching the CSS selector and return a
 L<collection|/"COLLECTION METHODS"> containing these elements as L<DOM::Tiny>
-objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+objects. All selectors listed in L</"SELECTORS"> are supported.
 
   # List tag names of ancestor elements
   say $dom->ancestors->map('tag')->join("\n");
@@ -558,7 +591,7 @@ node's content.
 
 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.
+All selectors listed in L</"SELECTORS"> are supported.
 
   # Find first element with "svg" namespace definition
   my $namespace = $dom->at('[xmlns\:svg]')->{'xmlns:svg'};
@@ -604,7 +637,7 @@ element as L<DOM::Tiny> objects.
 
 Find all child elements of this element matching the CSS selector and return a
 L<collection|/"COLLECTION METHODS"> containing these elements as L<DOM::Tiny>
-objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+objects. All selectors listed in L</"SELECTORS"> are supported.
 
   # Show tag name of random child element
   say $dom->children->shuffle->first->tag;
@@ -659,8 +692,7 @@ this element as L<DOM::Tiny> objects.
 
 Find all descendant elements of this element matching the CSS selector and
 return a L<collection|/"COLLECTION METHODS"> containing these elements as
-L<DOM::Tiny> objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are
-supported.
+L<DOM::Tiny> objects. All selectors listed in L</"SELECTORS"> are supported.
 
   # Find a specific element and extract information
   my $id = $dom->find('div')->[23]{id};
@@ -681,7 +713,7 @@ supported.
 
 Find all sibling elements after this node matching the CSS selector and return
 a L<collection|/"COLLECTION METHODS"> containing these elements as L<DOM::Tiny>
-objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+objects. All selectors listen in L</"SELECTORS"> are supported.
 
   # List tags of sibling elements after this node
   say $dom->following->map('tag')->join("\n");
@@ -700,8 +732,8 @@ this node as L<DOM::Tiny> objects.
 
   my $bool = $dom->matches('div ~ p');
 
-Check if this element matches the CSS selector. All selectors from
-L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+Check if this element matches the CSS selector. All selectors listed in
+L</"SELECTORS"> are supported.
 
   # True
   $dom->parse('<p class="a">A</p>')->at('p')->matches('.a');
@@ -759,7 +791,7 @@ no parent.
 
   $dom = $dom->parse('<foo bar="baz">I ♥ DOM::Tiny!</foo>');
 
-Parse HTML/XML fragment with L<DOM::Tiny::HTML>.
+Parse HTML/XML fragment.
 
   # Parse XML
   my $dom = DOM::Tiny->new->xml(1)->parse($xml);
@@ -771,7 +803,7 @@ Parse HTML/XML fragment with L<DOM::Tiny::HTML>.
 
 Find all sibling elements before this node matching the CSS selector and return
 a L<collection|/"COLLECTION METHODS"> containing these elements as L<DOM::Tiny>
-objects. All selectors from L<DOM::Tiny::CSS/"SELECTORS"> are supported.
+objects. All selectors listed in L</"SELECTORS"> are supported.
 
   # List tags of sibling elements before this node
   say $dom->preceding->map('tag')->join("\n");
@@ -1030,45 +1062,11 @@ children of the first innermost element.
 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 COLLECTION METHODS
 
-Some L<DOM::Tiny> methods return an array-based collection object, which can
-either be accessed directly as an array reference, or with the following
-methods.
+Some L<DOM::Tiny> methods return an array-based collection object based on
+L<Mojo::Collection>, which can either be accessed directly as an array
+reference, or with the following methods.
 
   # Chain methods
   $collection->map(sub { ucfirst })->shuffle->each(sub {
@@ -1275,6 +1273,226 @@ callback/method.
   # $collection contains ([1, 2], [2, 1], [3, 2])
   $collection->uniq(sub{ $_->[1] })->to_array; # "[[1, 2], [2, 1]]"
 
+=head1 SELECTORS
+
+L<DOM::Tiny> uses a CSS selector engine based on L<Mojo::DOM::CSS>. All CSS
+selectors that make sense for a standalone parser are supported.
+
+=head2 *
+
+Any element.
+
+  my $all = $dom->find('*');
+
+=head2 E
+
+An element of type C<E>.
+
+  my $title = $dom->at('title');
+
+=head2 E[foo]
+
+An C<E> element with a C<foo> attribute.
+
+  my $links = $dom->find('a[href]');
+
+=head2 E[foo="bar"]
+
+An C<E> element whose C<foo> attribute value is exactly equal to C<bar>.
+
+  my $case_sensitive = $dom->find('input[type="hidden"]');
+  my $case_sensitive = $dom->find('input[type=hidden]');
+
+=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 = $dom->find('input[type="hidden" i]');
+  my $case_insensitive = $dom->find('input[type=hidden i]');
+  my $case_insensitive = $dom->find('input[class~="foo" i]');
+
+This selector is part of
+L<Selectors Level 4|http://dev.w3.org/csswg/selectors-4>, which is still a work
+in progress.
+
+=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 = $dom->find('input[class~="foo"]');
+  my $foo = $dom->find('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 = $dom->find('input[name^="f"]');
+  my $begins_with = $dom->find('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 = $dom->find('input[name$="o"]');
+  my $ends_with = $dom->find('input[name$=o]');
+
+=head2 E[foo*="bar"]
+
+An C<E> element whose C<foo> attribute value contains the substring C<bar>.
+
+  my $contains = $dom->find('input[name*="fo"]');
+  my $contains = $dom->find('input[name*=fo]');
+
+=head2 E:root
+
+An C<E> element, root of the document.
+
+  my $root = $dom->at(':root');
+
+=head2 E:nth-child(n)
+
+An C<E> element, the C<n-th> child of its parent.
+
+  my $third = $dom->find('div:nth-child(3)');
+  my $odd   = $dom->find('div:nth-child(odd)');
+  my $even  = $dom->find('div:nth-child(even)');
+  my $top3  = $dom->find('div:nth-child(-n+3)');
+
+=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    = $dom->find('div:nth-last-child(3)');
+  my $odd      = $dom->find('div:nth-last-child(odd)');
+  my $even     = $dom->find('div:nth-last-child(even)');
+  my $bottom3  = $dom->find('div:nth-last-child(-n+3)');
+
+=head2 E:nth-of-type(n)
+
+An C<E> element, the C<n-th> sibling of its type.
+
+  my $third = $dom->find('div:nth-of-type(3)');
+  my $odd   = $dom->find('div:nth-of-type(odd)');
+  my $even  = $dom->find('div:nth-of-type(even)');
+  my $top3  = $dom->find('div:nth-of-type(-n+3)');
+
+=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    = $dom->find('div:nth-last-of-type(3)');
+  my $odd      = $dom->find('div:nth-last-of-type(odd)');
+  my $even     = $dom->find('div:nth-last-of-type(even)');
+  my $bottom3  = $dom->find('div:nth-last-of-type(-n+3)');
+
+=head2 E:first-child
+
+An C<E> element, first child of its parent.
+
+  my $first = $dom->find('div p:first-child');
+
+=head2 E:last-child
+
+An C<E> element, last child of its parent.
+
+  my $last = $dom->find('div p:last-child');
+
+=head2 E:first-of-type
+
+An C<E> element, first sibling of its type.
+
+  my $first = $dom->find('div p:first-of-type');
+
+=head2 E:last-of-type
+
+An C<E> element, last sibling of its type.
+
+  my $last = $dom->find('div p:last-of-type');
+
+=head2 E:only-child
+
+An C<E> element, only child of its parent.
+
+  my $lonely = $dom->find('div p:only-child');
+
+=head2 E:only-of-type
+
+An C<E> element, only sibling of its type.
+
+  my $lonely = $dom->find('div p:only-of-type');
+
+=head2 E:empty
+
+An C<E> element that has no children (including text nodes).
+
+  my $empty = $dom->find(':empty');
+
+=head2 E:checked
+
+A user interface element C<E> which is checked (for instance a radio-button or
+checkbox).
+
+  my $input = $dom->find(':checked');
+
+=head2 E.warning
+
+An C<E> element whose class is "warning".
+
+  my $warning = $dom->find('div.warning');
+
+=head2 E#myid
+
+An C<E> element with C<ID> equal to "myid".
+
+  my $foo = $dom->at('div#foo');
+
+=head2 E:not(s)
+
+An C<E> element that does not match simple selector C<s>.
+
+  my $others = $dom->find('div p:not(:first-child)');
+
+=head2 E F
+
+An C<F> element descendant of an C<E> element.
+
+  my $headlines = $dom->find('div h1');
+
+=head2 E E<gt> F
+
+An C<F> element child of an C<E> element.
+
+  my $headlines = $dom->find('html > body > div > h1');
+
+=head2 E + F
+
+An C<F> element immediately preceded by an C<E> element.
+
+  my $second = $dom->find('h1 + h2');
+
+=head2 E ~ F
+
+An C<F> element preceded by an C<E> element.
+
+  my $second = $dom->find('h1 ~ h2');
+
+=head2 E, F, G
+
+Elements of type C<E>, C<F> and C<G>.
+
+  my $headlines = $dom->find('h1, h2, h3');
+
+=head2 E[foo=bar][bar=baz]
+
+An C<E> element whose attributes match all following attribute selectors.
+
+  my $links = $dom->find('a[foo^=b][foo$=ar]');
+
 =head1 BUGS
 
 Report any issues on the public bugtracker.
@@ -1294,3 +1512,7 @@ This is free software, licensed under:
 =head1 SEE ALSO
 
 L<Mojo::DOM>, L<XML::LibXML>, L<XML::Twig>, L<HTML::TreeBuilder>, L<XML::Smart>
+
+=for Pod::Coverage TO_JSON
+
+=cut
diff --git a/lib/DOM/Tiny/CSS.pm b/lib/DOM/Tiny/CSS.pm
deleted file mode 100644 (file)
index 3970dfd..0000000
+++ /dev/null
@@ -1,604 +0,0 @@
-package DOM::Tiny::CSS;
-
-use strict;
-use warnings;
-
-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 new {
-  my $class = shift;
-  bless @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {}, ref $class || $class;
-}
-
-sub tree {
-  my $self = shift;
-  return $self->{tree} unless @_;
-  $self->{tree} = shift;
-  return $self;
-}
-
-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 new
-
-  my $css = DOM::Tiny::CSS->new;
-  my $css = DOM::Tiny::CSS->new(tree => $tree);
-  my $css = DOM::Tiny::CSS->new({tree => $tree});
-
-Construct a new hash-based L<DOM::Tiny::CSS> object.
-
-=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/_CSS.pm b/lib/DOM/Tiny/_CSS.pm
new file mode 100644 (file)
index 0000000..e3c254f
--- /dev/null
@@ -0,0 +1,308 @@
+package DOM::Tiny::_CSS;
+
+use strict;
+use warnings;
+
+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 new {
+  my $class = shift;
+  bless @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {}, ref $class || $class;
+}
+
+sub tree {
+  my $self = shift;
+  return $self->{tree} unless @_;
+  $self->{tree} = shift;
+  return $self;
+}
+
+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;
+
+=for Pod::Coverage *EVERYTHING*
+
+=cut
index a053563..b115b13 100644 (file)
@@ -8,10 +8,6 @@ use Scalar::Util 'blessed';
 
 our $VERSION = '0.001';
 
-=for Pod::Coverage *EVERYTHING*
-
-=cut
-
 sub new {
   my $class = shift;
   return bless [@_], ref $class || $class;
@@ -111,3 +107,7 @@ sub _flatten {
 sub _ref { ref $_[0] eq 'ARRAY' || blessed $_[0] && $_[0]->isa(__PACKAGE__) }
 
 1;
+
+=for Pod::Coverage *EVERYTHING*
+
+=cut
similarity index 82%
rename from lib/DOM/Tiny/HTML.pm
rename to lib/DOM/Tiny/_HTML.pm
index dfcb190..9049d9b 100644 (file)
@@ -1,4 +1,4 @@
-package DOM::Tiny::HTML;
+package DOM::Tiny::_HTML;
 
 use strict;
 use warnings;
@@ -285,88 +285,6 @@ sub _start {
 
 1;
 
-=encoding utf8
+=for Pod::Coverage *EVERYTHING*
 
-=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 new
-
-  my $html = DOM::Tiny::HTML->new;
-  my $html = DOM::Tiny::HTML->new(xml => 1);
-  my $html = DOM::Tiny::HTML->new({xml => 1});
-
-Construct a new hash-based L<DOM::Tiny::HTML> object.
-
-=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>
+=cut