port CSS fixes from Mojolicious 6.31 and 6.32
Dan Book [Sun, 22 Nov 2015 22:11:22 +0000 (17:11 -0500)]
Changes
README.pod
lib/DOM/Tiny.pm
lib/DOM/Tiny/_CSS.pm
t/dom.t

diff --git a/Changes b/Changes
index 729ea59..4f1671b 100644 (file)
--- a/Changes
+++ b/Changes
@@ -1,4 +1,5 @@
 {{$NEXT}}
+  - Merge several CSS bugfixes and improvements from Mojolicious 6.31 and 6.32.
 
 0.002     2015-11-09 19:28:42 EST
   - Support perl 5.8 (mst)
index 35efe2d..29e6d47 100644 (file)
@@ -77,8 +77,8 @@ names are lowercased and selectors need to be lowercase as well.
   my $dom = DOM::Tiny->new('<P ID="greeting">Hi!</P>');
   say $dom->at('p[id]')->text;
 
-If XML processing instructions are found, the parser will automatically switch
-into XML mode and everything becomes case-sensitive.
+If an XML declaration is found, the parser will automatically switch into XML
+mode and everything becomes case-sensitive.
 
   # XML semantics
   my $dom = DOM::Tiny->new('<?xml version="1.0"?><P ID="greeting">Hi!</P>');
@@ -427,8 +427,8 @@ node's content.
   my $result = $dom->at('div ~ p');
 
 Find first descendant element of this element matching the CSS selector and
-return it as a L<DOM::Tiny> object or return C<undef> if none could be found.
-All selectors listed in L</"SELECTORS"> are supported.
+return it as a L<DOM::Tiny> object, or C<undef> if none could be found. All
+selectors listed in L</"SELECTORS"> are supported.
 
   # Find first element with "svg" namespace definition
   my $namespace = $dom->at('[xmlns\:svg]')->{'xmlns:svg'};
@@ -584,7 +584,7 @@ L</"SELECTORS"> are supported.
 
   my $namespace = $dom->namespace;
 
-Find this element's namespace or return C<undef> if none could be found.
+Find this element's namespace, or return C<undef> if none could be found.
 
   # Find namespace for an element with namespace prefix
   my $namespace = $dom->at('svg > svg\:circle')->namespace;
@@ -596,8 +596,8 @@ Find this element's namespace or return C<undef> if none could be found.
 
   my $sibling = $dom->next;
 
-Return L<DOM::Tiny> object for next sibling element or C<undef> if there are no
-more siblings.
+Return L<DOM::Tiny> object for next sibling element, or C<undef> if there are
+no more siblings.
 
   # "<h2>123</h2>"
   $dom->parse('<div><h1>Test</h1><h2>123</h2></div>')->at('h1')->next;
@@ -606,7 +606,7 @@ more siblings.
 
   my $sibling = $dom->next_node;
 
-Return L<DOM::Tiny> object for next sibling node or C<undef> if there are no
+Return L<DOM::Tiny> object for next sibling node, or C<undef> if there are no
 more siblings.
 
   # "456"
@@ -621,8 +621,11 @@ more siblings.
 
   my $parent = $dom->parent;
 
-Return L<DOM::Tiny> object for parent of this node or C<undef> if this node has
-no parent.
+Return L<DOM::Tiny> object for parent of this node, or C<undef> if this node
+has no parent.
+
+  # "<b><i>Test</i></b>"
+  $dom->parse('<p><b><i>Test</i></b></p>')->at('i')->parent;
 
 =head2 parse
 
@@ -631,7 +634,7 @@ no parent.
 Parse HTML/XML fragment.
 
   # Parse XML
-  my $dom = DOM::Tiny->new->xml(1)->parse($xml);
+  my $dom = DOM::Tiny->new->xml(1)->parse('<foo>I ♥ DOM::Tiny!</foo>');
 
 =head2 preceding
 
@@ -691,7 +694,7 @@ node's content.
 
   my $sibling = $dom->previous;
 
-Return L<DOM::Tiny> object for previous sibling element or C<undef> if there
+Return L<DOM::Tiny> object for previous sibling element, or C<undef> if there
 are no more siblings.
 
   # "<h1>Test</h1>"
@@ -701,7 +704,7 @@ are no more siblings.
 
   my $sibling = $dom->previous_node;
 
-Return L<DOM::Tiny> object for previous sibling node or C<undef> if there are
+Return L<DOM::Tiny> object for previous sibling node, or C<undef> if there are
 no more siblings.
 
   # "123"
@@ -837,10 +840,10 @@ C<root>, C<tag> or C<text>.
   my $value = $dom->val;
 
 Extract value from form element (such as C<button>, C<input>, C<option>,
-C<select> and C<textarea>) or return C<undef> if this element has no value. In
+C<select> and C<textarea>), or return C<undef> if this element has no value. In
 the case of C<select> with C<multiple> attribute, find C<option> elements with
-C<selected> attribute and return an array reference with all values or C<undef>
-if none could be found.
+C<selected> attribute and return an array reference with all values, or
+C<undef> if none could be found.
 
   # "a"
   $dom->parse('<input name="test" value="a">')->at('input')->val;
@@ -897,7 +900,7 @@ children of the first innermost element.
   $dom     = $dom->xml($bool);
 
 Disable HTML semantics in parser and activate case-sensitivity, defaults to
-auto detection based on processing instructions.
+auto detection based on XML declarations.
 
 =head1 COLLECTION METHODS
 
index 1686642..febc2a5 100644 (file)
@@ -465,8 +465,8 @@ names are lowercased and selectors need to be lowercase as well.
   my $dom = DOM::Tiny->new('<P ID="greeting">Hi!</P>');
   say $dom->at('p[id]')->text;
 
-If XML processing instructions are found, the parser will automatically switch
-into XML mode and everything becomes case-sensitive.
+If an XML declaration is found, the parser will automatically switch into XML
+mode and everything becomes case-sensitive.
 
   # XML semantics
   my $dom = DOM::Tiny->new('<?xml version="1.0"?><P ID="greeting">Hi!</P>');
@@ -815,8 +815,8 @@ node's content.
   my $result = $dom->at('div ~ p');
 
 Find first descendant element of this element matching the CSS selector and
-return it as a L<DOM::Tiny> object or return C<undef> if none could be found.
-All selectors listed in L</"SELECTORS"> are supported.
+return it as a L<DOM::Tiny> object, or C<undef> if none could be found. All
+selectors listed in L</"SELECTORS"> are supported.
 
   # Find first element with "svg" namespace definition
   my $namespace = $dom->at('[xmlns\:svg]')->{'xmlns:svg'};
@@ -972,7 +972,7 @@ L</"SELECTORS"> are supported.
 
   my $namespace = $dom->namespace;
 
-Find this element's namespace or return C<undef> if none could be found.
+Find this element's namespace, or return C<undef> if none could be found.
 
   # Find namespace for an element with namespace prefix
   my $namespace = $dom->at('svg > svg\:circle')->namespace;
@@ -984,8 +984,8 @@ Find this element's namespace or return C<undef> if none could be found.
 
   my $sibling = $dom->next;
 
-Return L<DOM::Tiny> object for next sibling element or C<undef> if there are no
-more siblings.
+Return L<DOM::Tiny> object for next sibling element, or C<undef> if there are
+no more siblings.
 
   # "<h2>123</h2>"
   $dom->parse('<div><h1>Test</h1><h2>123</h2></div>')->at('h1')->next;
@@ -994,7 +994,7 @@ more siblings.
 
   my $sibling = $dom->next_node;
 
-Return L<DOM::Tiny> object for next sibling node or C<undef> if there are no
+Return L<DOM::Tiny> object for next sibling node, or C<undef> if there are no
 more siblings.
 
   # "456"
@@ -1009,8 +1009,11 @@ more siblings.
 
   my $parent = $dom->parent;
 
-Return L<DOM::Tiny> object for parent of this node or C<undef> if this node has
-no parent.
+Return L<DOM::Tiny> object for parent of this node, or C<undef> if this node
+has no parent.
+
+  # "<b><i>Test</i></b>"
+  $dom->parse('<p><b><i>Test</i></b></p>')->at('i')->parent;
 
 =head2 parse
 
@@ -1019,7 +1022,7 @@ no parent.
 Parse HTML/XML fragment.
 
   # Parse XML
-  my $dom = DOM::Tiny->new->xml(1)->parse($xml);
+  my $dom = DOM::Tiny->new->xml(1)->parse('<foo>I ♥ DOM::Tiny!</foo>');
 
 =head2 preceding
 
@@ -1079,7 +1082,7 @@ node's content.
 
   my $sibling = $dom->previous;
 
-Return L<DOM::Tiny> object for previous sibling element or C<undef> if there
+Return L<DOM::Tiny> object for previous sibling element, or C<undef> if there
 are no more siblings.
 
   # "<h1>Test</h1>"
@@ -1089,7 +1092,7 @@ are no more siblings.
 
   my $sibling = $dom->previous_node;
 
-Return L<DOM::Tiny> object for previous sibling node or C<undef> if there are
+Return L<DOM::Tiny> object for previous sibling node, or C<undef> if there are
 no more siblings.
 
   # "123"
@@ -1225,10 +1228,10 @@ C<root>, C<tag> or C<text>.
   my $value = $dom->val;
 
 Extract value from form element (such as C<button>, C<input>, C<option>,
-C<select> and C<textarea>) or return C<undef> if this element has no value. In
+C<select> and C<textarea>), or return C<undef> if this element has no value. In
 the case of C<select> with C<multiple> attribute, find C<option> elements with
-C<selected> attribute and return an array reference with all values or C<undef>
-if none could be found.
+C<selected> attribute and return an array reference with all values, or
+C<undef> if none could be found.
 
   # "a"
   $dom->parse('<input name="test" value="a">')->at('input')->val;
@@ -1285,7 +1288,7 @@ children of the first innermost element.
   $dom     = $dom->xml($bool);
 
 Disable HTML semantics in parser and activate case-sensitivity, defaults to
-auto detection based on processing instructions.
+auto detection based on XML declarations.
 
 =head1 COLLECTION METHODS
 
index 2817fce..1e890dc 100644 (file)
@@ -119,9 +119,23 @@ sub _compile {
       ];
     }
 
-    # Pseudo-class (":not" contains more selectors)
+    # Pseudo-class
     elsif ($css =~ /\G:([\w\-]+)(?:\(((?:\([^)]+\)|[^)])+)\))?/gcs) {
-      push @$last, ['pc', lc $1, $1 eq 'not' ? _compile($2) : _equation($2)];
+      my ($name, $args) = (lc $1, $2);
+
+      # ":not" (contains more selectors)
+      $args = _compile($args) if $name eq 'not';
+
+      # ":nth-*" (with An+B notation)
+      $args = _equation($args) if $name =~ /^nth-/;
+
+      # ":first-*" (rewrite to ":nth-*")
+      ($name, $args) = ("nth-$1", [0, 1]) if $name =~ /^first-(.+)$/;
+
+      # ":last-*" (rewrite to ":nth-*")
+      ($name, $args) = ("nth-$name", [-1, 1]) if $name =~ /^last-/;
+
+      push @$last, ['pc', $name, $args];
     }
 
     # Tag
@@ -138,7 +152,7 @@ sub _compile {
 sub _empty { $_[0][0] eq 'comment' || $_[0][0] eq 'pi' }
 
 sub _equation {
-  return [] unless my $equation = shift;
+  return [0, 0] unless my $equation = shift;
 
   # "even"
   return [2, 2] if $equation =~ /^\s*even\s*$/i;
@@ -146,14 +160,13 @@ sub _equation {
   # "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] = defined($3) ? $3 : 0;
-  $num->[1] =~ s/\s+//g;
-  return $num;
+  # "4", "+4" or "-4"
+  return [0, $1] if $equation =~ /^\s*((?:\+|-)?\d+)\s*$/;
+
+  # "n", "4n", "+4n", "-4n", "n+1", "4n-1", "+4n-1" (and other variations)
+  return [0, 0]
+    unless $equation =~ /^\s*((?:\+|-)?(?:\d+)?)?n\s*((?:\+|-)\s*\d+)?\s*$/i;
+  return [$1 eq '-' ? -1 : $1 eq '' ? 1 : $1, join('', split(' ', $2 || 0))];
 }
 
 sub _match {
@@ -167,29 +180,23 @@ 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';
+  # ":checked"
+  return exists $current->[2]{checked} || exists $current->[2]{selected}
+    if $class eq 'checked';
 
   # ":not"
   return !_match($args, $current, $current) if $class eq 'not';
 
-  # ":checked"
-  return exists $current->[2]{checked} || exists $current->[2]{selected}
-    if $class eq 'checked';
+  # ":empty"
+  return !grep { !_empty($_) } @$current[4 .. $#$current] if $class eq 'empty';
 
-  # ":first-*" or ":last-*" (rewrite with equation)
-  ($class, $args) = $1 ? ("nth-$class", [0, 1]) : ("nth-last-$class", [-1, 1])
-    if $class =~ s/^(?:(first)|last)-//;
+  # ":root"
+  return $current->[3] && $current->[3][0] eq 'root' if $class eq 'root';
 
-  # ":nth-*"
-  if ($class =~ /^nth-/) {
+  # ":nth-child", ":nth-last-child", ":nth-of-type" or ":nth-last-of-type"
+  if (ref $args) {
     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) {
@@ -199,10 +206,10 @@ sub _pc {
     }
   }
 
-  # ":only-*"
-  elsif ($class =~ /^only-(?:child|(of-type))$/) {
-    $_ ne $current and return undef
-      for @{_siblings($current, $1 ? $current->[1] : undef)};
+  # ":only-child" or ":only-of-type"
+  elsif ($class eq 'only-child' || $class eq 'only-of-type') {
+    my $type = $class eq 'only-of-type' ? $current->[1] : undef;
+    $_ ne $current and return undef for @{_siblings($current, $type)};
     return 1;
   }
 
diff --git a/t/dom.t b/t/dom.t
index e345d27..a6b8a72 100644 (file)
--- a/t/dom.t
+++ b/t/dom.t
@@ -979,28 +979,28 @@ $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';
+is_deeply \@li, [qw(D H)], 'found the right li elements';
 @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';
+is_deeply \@li, [qw(A E)], 'found the right li elements';
 @li = ();
 $dom->find('li:nth-child(4n)')->each(sub { push @li, shift->text });
-is_deeply \@li, [qw(D H)], 'found the right li element';
+is_deeply \@li, [qw(D H)], 'found the right li elements';
 @li = ();
 $dom->find('li:nth-child( 4n )')->each(sub { push @li, shift->text });
-is_deeply \@li, [qw(D H)], 'found the right li element';
+is_deeply \@li, [qw(D H)], 'found the right li elements';
 @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';
+is_deeply \@li, [qw(A E)], 'found the right li elements';
 @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';
+is_deeply \@li, [qw(C H)], 'found the right li elements';
 @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';
+is_deeply \@li, [qw(C H)], 'found the right li elements';
 @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';
+is_deeply \@li, [qw(A F)], 'found the right 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';
@@ -1029,26 +1029,43 @@ is_deeply \@li, [qw(C F)], 'found every third li elements';
 $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 });
+$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 });
+$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';
+is_deeply \@li, [qw(A B C D E F G)], 'found all li elements';
+@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 all 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';
+is_deeply \@li, [qw(A B C D E F G)], 'found all 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 all 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 all 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';
+is_deeply \@li, [qw(A B C D E F G)], 'found all 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';
+is_deeply \@li, [qw(A B C D E F G)], 'found all 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';
+is_deeply \@li, [qw(A B C D E F G)], 'found all li elements';
+@li = ();
+$dom->find('li:nth-child(0n+1)')->each(sub { push @li, shift->text });
+is_deeply \@li, [qw(A)], 'found first li element';
+is $dom->find('li:nth-child(0n+0)')->size,     0, 'no results';
+is $dom->find('li:nth-child(0)')->size,        0, 'no results';
+is $dom->find('li:nth-child()')->size,         0, 'no results';
+is $dom->find('li:nth-child(whatever)')->size, 0, 'no results';
+is $dom->find('li:whatever(whatever)')->size,  0, 'no results';
 
 # Even more pseudo-classes
 $dom = DOM::Tiny->new(<<EOF);
@@ -1127,6 +1144,9 @@ is_deeply \@e, ['I'], 'found last child';
 $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(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';