Skip to content

Commit

Permalink
fix all *-child pseudo selectors when used with a class and remove ne…
Browse files Browse the repository at this point in the history
…ed for preprocessing the node tree
  • Loading branch information
twalpole committed Sep 7, 2013
1 parent 99b32d2 commit 044bc91
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 67 deletions.
50 changes: 0 additions & 50 deletions lib/nokogiri/css/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
32 changes: 25 additions & 7 deletions lib/nokogiri/css/xpath_visitor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)\(/
Expand Down Expand Up @@ -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()"
Expand Down Expand Up @@ -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"
Expand Down
16 changes: 8 additions & 8 deletions test/css/test_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions test/css/test_xpath_visitor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 044bc91

Please sign in to comment.