module Nokogiri module CSS class XPathVisitor def visit_function node # note that nth-child and nth-last-child are preprocessed in css/node.rb. case node.value.first when /^text\(/ 'child::text()' when /^self\(/ "self::#{node.value[1]}" when /^(eq|nth|nth-of-type|nth-child)\(/ if node.value[1].is_a?(Nokogiri::CSS::Node) and node.value[1].type == :AN_PLUS_B an_plus_b(node.value[1]) else "position() = " + node.value[1] end when /^(first|first-of-type)\(/ "position() = 1" when /^(last|last-of-type)\(/ "position() = last()" when /^(nth-last-child|nth-last-of-type)\(/ "position() = last() - #{node.value[1]}" when /^contains\(/ "contains(., #{node.value[1]})" when /^gt\(/ "position() > #{node.value[1]}" when /^only-child\(/ "last() = 1" else node.value.first + ')' end end def visit_not node 'not(' + node.value.first.accept(self) + ')' end def visit_preceding_selector node node.value.last.accept(self) + '[preceding-sibling::' + node.value.first.accept(self) + ']' end def visit_direct_adjacent_selector node node.value.last.accept(self) + '[preceding-sibling::' + node.value.first.accept(self) + '][position()=1]' end def visit_id node node.value.first =~ /^#(.*)$/ "@id = '#{$1}'" end def visit_attribute_condition node attribute = if (node.value.first.type == :FUNCTION) or (node.value.first.value.first =~ /::/) '' else '@' end attribute += node.value.first.accept(self) # Support non-standard css attribute.gsub!(/^@@/, '@') return attribute unless node.value.length == 3 value = node.value.last value = "'#{value}'" if value !~ /^['"]/ case node.value[1] when '*=' "contains(#{attribute}, #{value})" when '^=' "starts-with(#{attribute}, #{value})" when '|=' "#{attribute} = #{value} or starts-with(#{attribute}, concat(#{value}, '-'))" when '~=' "contains(concat(\" \", #{attribute}, \" \"),concat(\" \", #{value}, \" \"))" when '$=' "substring(#{attribute}, string-length(#{attribute}) - " + "string-length(#{value}) + 1, string-length(#{value})) = #{value}" else attribute + " #{node.value[1]} " + "#{value}" end end def visit_pseudo_class node if node.value.first.is_a?(Nokogiri::CSS::Node) and node.value.first.type == :FUNCTION node.value.first.accept(self) else case node.value.first when "first" then "position() = 1" when "last" then "position() = last()" when "first-of-type" then "position() = 1" when "last-of-type" then "position() = last()" when "only-of-type" then "last() = 1" when "empty" then "not(node())" when "parent" then "node()" else '1 = 1' end end end def visit_class_condition node "contains(concat(' ', @class, ' '),concat(' ', '#{node.value.first}', ' '))" end def visit_combinator node node.value.first.accept(self) + ' and ' + node.value.last.accept(self) end def visit_conditional_selector node node.value.first.accept(self) + '[' + node.value.last.accept(self) + ']' end def visit_descendant_selector node node.value.first.accept(self) + '//' + node.value.last.accept(self) end def visit_child_selector node node.value.first.accept(self) + '/' + node.value.last.accept(self) end def visit_element_name node node.value.first end def accept node node.accept(self) end private def an_plus_b node raise ArgumentError, "expected an+b node to contain 4 tokens, but is #{node.value.inspect}" unless node.value.size == 4 a = node.value[0].to_i b = node.value[3].to_i if (b == 0) return "(position() mod #{a}) = 0" else compare = (a < 0) ? "<=" : ">=" return "(position() #{compare} #{b}) and (((position()-#{b}) mod #{a.abs}) = 0)" end end end end end