From 044bc9105ba3432cf54fef5d8cc81ea02e5c6e44 Mon Sep 17 00:00:00 2001 From: Thomas Walpole Date: Fri, 6 Sep 2013 21:40:21 -0700 Subject: [PATCH] fix all *-child pseudo selectors when used with a class and remove need for preprocessing the node tree --- lib/nokogiri/css/node.rb | 50 ------------------------------- lib/nokogiri/css/xpath_visitor.rb | 32 +++++++++++++++----- test/css/test_parser.rb | 16 +++++----- test/css/test_xpath_visitor.rb | 4 +-- 4 files changed, 35 insertions(+), 67 deletions(-) diff --git a/lib/nokogiri/css/node.rb b/lib/nokogiri/css/node.rb index a650b557040..37ee56b7862 100644 --- a/lib/nokogiri/css/node.rb +++ b/lib/nokogiri/css/node.rb @@ -22,60 +22,10 @@ def accept visitor ### # Convert this CSS node to xpath with +prefix+ using +visitor+ def to_xpath prefix = '//', visitor = XPathVisitor.new - self.preprocess! prefix = '.' if ALLOW_COMBINATOR_ON_SELF.include?(type) && value.first.nil? prefix + visitor.accept(self) end - # Preprocess this node tree - def preprocess! - ### Deal with nth-child - matches = find_by_type( - [:CONDITIONAL_SELECTOR, - [:ELEMENT_NAME], - [:PSEUDO_CLASS, - [:FUNCTION] - ] - ] - ) - matches.each do |match| - if match.value[1].value[0].value[0] =~ /^nth-(last-)?child/ - tag_name = match.value[0].value.first - match.value[0].value = ['*'] - match.value[1] = Node.new(:COMBINATOR, [ - match.value[1].value[0], - Node.new(:FUNCTION, ['self(', tag_name]) - ]) - end - end - - ### Deal with first-child, last-child - matches = find_by_type( - [:CONDITIONAL_SELECTOR, - [:ELEMENT_NAME], [:PSEUDO_CLASS] - ]) - matches.each do |match| - if ['first-child', 'last-child'].include?(match.value[1].value.first) - which = match.value[1].value.first.gsub(/-\w*$/, '') - tag_name = match.value[0].value.first - match.value[0].value = ['*'] - match.value[1] = Node.new(:COMBINATOR, [ - Node.new(:FUNCTION, ["#{which}("]), - Node.new(:FUNCTION, ['self(', tag_name]) - ]) - elsif 'only-child' == match.value[1].value.first - tag_name = match.value[0].value.first - match.value[0].value = ['*'] - match.value[1] = Node.new(:COMBINATOR, [ - Node.new(:FUNCTION, ["#{match.value[1].value.first}("]), - Node.new(:FUNCTION, ['self(', tag_name]) - ]) - end - end - - self - end - # Find a node by type using +types+ def find_by_type types matches = [] diff --git a/lib/nokogiri/css/xpath_visitor.rb b/lib/nokogiri/css/xpath_visitor.rb index a41570d292a..d81b28ea735 100644 --- a/lib/nokogiri/css/xpath_visitor.rb +++ b/lib/nokogiri/css/xpath_visitor.rb @@ -2,7 +2,7 @@ module Nokogiri module CSS class XPathVisitor # :nodoc: def visit_function node - # note that nth-child and nth-last-child are preprocessed in css/node.rb. + msg = :"visit_function_#{node.value.first.gsub(/[(]/, '')}" return self.send(msg, node) if self.respond_to?(msg) @@ -13,19 +13,31 @@ def visit_function node "self::#{node.value[1]}" when /^eq\(/ "position() = #{node.value[1]}" - when /^(nth|nth-of-type|nth-child)\(/ + when /^(nth|nth-of-type)\(/ if node.value[1].is_a?(Nokogiri::CSS::Node) and node.value[1].type == :NTH nth(node.value[1]) else "position() = #{node.value[1]}" end - when /^(nth-last-child|nth-last-of-type)\(/ + when /^nth-child\(/ + if node.value[1].is_a?(Nokogiri::CSS::Node) and node.value[1].type == :NTH + nth(node.value[1], :child => true) + else + "count(preceding-sibling::*) = #{node.value[1].to_i-1}" + end + when /^nth-last-of-type\(/ if node.value[1].is_a?(Nokogiri::CSS::Node) and node.value[1].type == :NTH nth(node.value[1], :last => true) else index = node.value[1].to_i - 1 index == 0 ? "position() = last()" : "position() = last() - #{index}" end + when /^nth-last-child\(/ + if node.value[1].is_a?(Nokogiri::CSS::Node) and node.value[1].type == :NTH + nth(node.value[1], :last => true, :child => true) + else + "count(following-sbiling::*) = #{node.value[1].to_i-1}" + end when /^(first|first-of-type)\(/ "position() = 1" when /^(last|last-of-type)\(/ @@ -105,11 +117,13 @@ def visit_pseudo_class node return self.send(msg, node) if self.respond_to?(msg) case node.value.first - when "first", "first-child" then "position() = 1" - when "last", "last-child" then "position() = last()" + when "first" then "position() = 1" + when "first-child" then "count(preceding-sibling::*) = 0" + when "last" then "position() = last()" + when "last-child" then "count(following-sibling::*) = 0" when "first-of-type" then "position() = 1" when "last-of-type" then "position() = last()" - when "only-child" then "last() = 1" + when "only-child" then "count(preceding-sibling::*) = 0 and count(following-sibling::*) = 0" when "only-of-type" then "last() = 1" when "empty" then "not(node())" when "parent" then "node()" @@ -163,7 +177,11 @@ def nth node, options={} raise ArgumentError, "expected an+b node to contain 4 tokens, but is #{node.value.inspect}" unless node.value.size == 4 a, b = read_a_and_positive_b node.value - position = options[:last] ? "(last()-position()+1)" : "position()" + position = if options[:child] + options[:last] ? "(count(following-sibling::*) + 1)" : "(count(preceding-sibling::*) + 1)" + else + options[:last] ? "(last()-position()+1)" : "position()" + end if (b == 0) return "(#{position} mod #{a}) = 0" diff --git a/test/css/test_parser.rb b/test/css/test_parser.rb index 1b3f7146808..08f8be0724c 100644 --- a/test/css/test_parser.rb +++ b/test/css/test_parser.rb @@ -82,9 +82,9 @@ def test_includes end def test_function_with_arguments - assert_xpath "//*[position() = 2 and self::a]", + assert_xpath "//a[count(preceding-sibling::*) = 1]", @parser.parse("a[2]") - assert_xpath "//*[position() = 2 and self::a]", + assert_xpath "//a[count(preceding-sibling::*) = 1]", @parser.parse("a:nth-child(2)") end @@ -174,15 +174,15 @@ def test_standard_nth_selectors end def test_nth_child_selectors - assert_xpath '//*[position() = 1 and self::a]', @parser.parse('a:first-child') - assert_xpath '//*[position() = 99 and self::a]', @parser.parse('a:nth-child(99)') - assert_xpath '//*[position() = last() and self::a]', @parser.parse('a:last-child') - assert_xpath '//*[position() = last() and self::a]', @parser.parse('a:nth-last-child(1)') - assert_xpath '//*[position() = last() - 98 and self::a]', @parser.parse('a:nth-last-child(99)') + assert_xpath '//a[count(preceding-sibling::*) = 0]', @parser.parse('a:first-child') + assert_xpath '//a[count(preceding-sibling::*) = 98]', @parser.parse('a:nth-child(99)') + assert_xpath '//a[count(following-sibling::*) = 0]', @parser.parse('a:last-child') + assert_xpath '//a[count(following-sbiling::*) = 0]', @parser.parse('a:nth-last-child(1)') + assert_xpath '//a[count(following-sbiling::*) = 98]', @parser.parse('a:nth-last-child(99)') end def test_miscellaneous_selectors - assert_xpath '//*[last() = 1 and self::a]', @parser.parse('a:only-child') + assert_xpath '//a[count(preceding-sibling::*) = 0 and count(following-sibling::*) = 0]', @parser.parse('a:only-child') assert_xpath '//a[last() = 1]', @parser.parse('a:only-of-type') assert_xpath '//a[not(node())]', @parser.parse('a:empty') end diff --git a/test/css/test_xpath_visitor.rb b/test/css/test_xpath_visitor.rb index fbbb677fe23..3163b42acb6 100644 --- a/test/css/test_xpath_visitor.rb +++ b/test/css/test_xpath_visitor.rb @@ -13,12 +13,12 @@ def test_not_simple_selector end def test_not_last_child - assert_xpath('//ol/*[not(position() = last())]', + assert_xpath('//ol/*[not(count(following-sibling::*) = 0)]', @parser.parse('ol > *:not(:last-child)')) end def test_not_only_child - assert_xpath('//ol/*[not(last() = 1)]', + assert_xpath('//ol/*[not(count(preceding-sibling::*) = 0 and count(following-sibling::*) = 0)]', @parser.parse('ol > *:not(:only-child)')) end