123456789_123456789_123456789_123456789_123456789_

Class: Nokogiri::CSS::XPathVisitor

Relationships & Source Files
Namespace Children
Modules:
Inherits: Object
Defined in: lib/nokogiri/css/xpath_visitor.rb

Overview

When translating ::Nokogiri::CSS selectors to XPath queries with xpath_for, the XPathVisitor class allows for changing some of the behaviors related to builtin xpath functions and quirks of ::Nokogiri::HTML5.

Constant Summary

Class Method Summary

Instance Attribute Summary

  • #builtins readonly

    The visitor configuration set via the builtins: keyword argument to .new.

  • #doctype readonly

    The visitor configuration set via the doctype: keyword argument to .new.

  • #namespaces readonly

    The visitor configuration set via the namespaces: keyword argument to .new.

  • #prefix readonly

    The visitor configuration set via the prefix: keyword argument to .new.

Instance Method Summary

Constructor Details

.new() → XPathVisitor) ⇒ XPathVisitor .new(builtins:, doctype:) → XPathVisitor) ⇒ XPathVisitor

[Parameters]

  • builtins: (BuiltinsConfig) Determine when to use Nokogiri's built-in xpath functions for performance improvements.
  • doctype: (DoctypeConfig) Make document-type-specific accommodations for ::Nokogiri::CSS queries.

[Returns] XPathVisitor

[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 74

def initialize(
  builtins: BuiltinsConfig::NEVER,
  doctype: DoctypeConfig::XML,
  prefix: Nokogiri::XML::XPath::GLOBAL_SEARCH_PREFIX,
  namespaces: nil
)
  unless BuiltinsConfig::VALUES.include?(builtins)
    raise(ArgumentError, "Invalid values #{builtins.inspect} for builtins: keyword parameter")
  end
  unless DoctypeConfig::VALUES.include?(doctype)
    raise(ArgumentError, "Invalid values #{doctype.inspect} for doctype: keyword parameter")
  end

  @builtins = builtins
  @doctype = doctype
  @prefix = prefix
  @namespaces = namespaces
end

Instance Attribute Details

#builtins (readonly)

The visitor configuration set via the builtins: keyword argument to .new.

[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 53

attr_reader :builtins

#doctype (readonly)

The visitor configuration set via the doctype: keyword argument to .new.

[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 56

attr_reader :doctype

#namespaces (readonly)

The visitor configuration set via the namespaces: keyword argument to .new.

[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 62

attr_reader :namespaces

#prefix (readonly)

The visitor configuration set via the prefix: keyword argument to .new.

[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 59

attr_reader :prefix

Instance Method Details

#accept(node)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 306

def accept(node)
  node.accept(self)
end

#config() → Hash)

[Returns] a Hash representing the configuration of the XPathVisitor, suitable for use as part of the CSS cache key.

[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 98

def config
  { builtins: @builtins, doctype: @doctype, prefix: @prefix, namespaces: @namespaces }
end

#css_class(hay, needle) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 396

def css_class(hay, needle)
  if @builtins == BuiltinsConfig::ALWAYS || (@builtins == BuiltinsConfig::OPTIMAL && Nokogiri.uses_libxml?)
    # use the builtin implementation
    "nokogiri-builtin:css-class(#{hay},#{xpath_string_literal(needle)})"
  else
    # use only ordinary xpath functions
    "contains(concat(' ',normalize-space(#{hay}),' '),#{xpath_string_literal(" #{needle} ")})"
  end
end

#html5_element_name_needs_namespace_handling(node) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 339

def html5_element_name_needs_namespace_handling(node)
  # if there is already a namespace (i.e., it is a prefixed QName), use it as normal
  node.value.length == 1 &&
    # if this is the wildcard selector "*", use it as normal
    node.value.first != "*"
end

#is_of_type_pseudo_class?(node) ⇒ Boolean (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 386

def is_of_type_pseudo_class?(node)
  if node.type == :PSEUDO_CLASS
    if node.value[0].is_a?(Nokogiri::CSS::Node) && (node.value[0].type == :FUNCTION)
      node.value[0].value[0]
    else
      node.value[0]
    end =~ /(nth|first|last|only)-of-type(\()?/
  end
end

#nth(node, options = {}) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 346

def nth(node, options = {})
  unless node.value.size == 4
    raise(ArgumentError, "expected an+b node to contain 4 tokens, but is #{node.value.inspect}")
  end

  a, b = read_a_and_positive_b(node.value)
  position = if options[:child]
    options[:last] ? "(count(following-sibling::*)+1)" : "(count(preceding-sibling::*)+1)"
  else
    options[:last] ? "(last()-position()+1)" : "position()"
  end

  if a.zero?
    "#{position}=#{b}"
  elsif b.zero? && a.positive?
    "(#{position} mod #{a})=0"
  else
    compare = a < 0 ? "<=" : ">="
    if a.abs == 1
      "#{position}#{compare}#{b}"
    else
      "(#{position}#{compare}#{b}) and (((#{position}-#{b}) mod #{a.abs})=0)"
    end
  end
end

#read_a_and_positive_b(values) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 372

def read_a_and_positive_b(values)
  op = values[2].strip
  if op == "+"
    a = values[0].to_i
    b = values[3].to_i
  elsif op == "-"
    a = values[0].to_i
    b = a.zero? ? -values[3].to_i : a - (values[3].to_i % a)
  else
    raise ArgumentError, "expected an+b node to have either + or - as the operator, but is #{op.inspect}"
  end
  [a, b]
end

#validate_xpath_element_name(name) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 318

def validate_xpath_element_name(name)
  unless name.nil? || name == "*" || xpath_name?(name)
    raise Nokogiri::CSS::SyntaxError, "Invalid element name '#{name}'"
  end
end

#validate_xpath_function_name(name) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 312

def validate_xpath_function_name(name)
  if name.start_with?("-")
    raise Nokogiri::CSS::SyntaxError, "Invalid XPath function name '#{name}'"
  end
end

#visit_attrib_name(node)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 291

def visit_attrib_name(node)
  if node.value.length == 2 # has a namespace prefix
    unless node.value.all? { |name| xpath_name?(name) }
      raise Nokogiri::CSS::SyntaxError, "Invalid attribute name '#{node.value.join("|")}'"
    end

    "@#{node.value.join(":")}"
  elsif xpath_name?(node.value.first)
    "@#{node.value.first}"
  else
    # HTML parsers allow attribute names like "@click" and ":class". On JRuby, local-name() of ":class" is "class".
    "@*[namespace-uri()='' and name()=#{xpath_string_literal(node.value.first)}]"
  end
end

#visit_attribute_condition(node)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 177

def visit_attribute_condition(node)
  attribute = node.value.first.accept(self)
  return attribute if node.value.length == 1

  str_value = node.value.last
  value = xpath_string_literal(str_value)

  case node.value[1]
  when :equal
    attribute + "=" + value.to_s
  when :not_equal
    attribute + "!=" + value.to_s
  when :substring_match
    "contains(#{attribute},#{value})"
  when :prefix_match
    "starts-with(#{attribute},#{value})"
  when :dash_match
    "#{attribute}=#{value} or starts-with(#{attribute},concat(#{value},'-'))"
  when :includes
    css_class(attribute, str_value)
  when :suffix_match
    "substring(#{attribute},string-length(#{attribute})-string-length(#{value})+1,string-length(#{value}))=#{value}"
  else
    attribute + " #{node.value[1]} " + value.to_s
  end
end

#visit_class_condition(node)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 230

def visit_class_condition(node)
  css_class("@class", node.value.first)
end

#visit_combinator(node)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 234

def visit_combinator(node)
  if is_of_type_pseudo_class?(node.value.last)
    "#{node.value.first&.accept(self)}][#{node.value.last.accept(self)}"
  else
    "#{node.value.first&.accept(self)} and #{node.value.last.accept(self)}"
  end
end

#visit_conditional_selector(node)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 255

def visit_conditional_selector(node)
  node.value.first.accept(self) + "[" +
    node.value.last.accept(self) + "]"
end

#visit_element_name(node)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 260

def visit_element_name(node)
  html5_local_name = @doctype == DoctypeConfig::HTML5 && html5_element_name_needs_namespace_handling(node)
  node.value.each { |name| validate_xpath_element_name(name) } unless html5_local_name

  if html5_local_name
    # HTML5 has namespaces that should be ignored in CSS queries
    # https://github.com/sparklemotion/nokogiri/issues/2376
    name = node.value.first
    if @builtins == BuiltinsConfig::ALWAYS || (@builtins == BuiltinsConfig::OPTIMAL && Nokogiri.uses_libxml?)
      if WILDCARD_NAMESPACES && xpath_name?(name)
        "*:#{name}"
      else
        "*[nokogiri-builtin:local-name-is(#{xpath_string_literal(name)})]"
      end
    else
      "*[local-name()=#{xpath_string_literal(name)}]"
    end
  elsif node.value.length == 2 # has a namespace prefix
    if node.value.first.nil? # namespace prefix is empty
      node.value.last
    else
      node.value.join(":")
    end
  elsif node.value.first != "*" && @namespaces&.key?("xmlns")
    # apply the default namespace (if one is present) to a non-wildcard selector
    "xmlns:#{node.value.first}"
  else
    node.value.first
  end
end

#visit_function(node)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 103

def visit_function(node)
  msg = :"visit_function_#{node.value.first.gsub(/[(]/, "")}"
  return send(msg, node) if respond_to?(msg)

  case node.value.first
  when /^text\(/
    "child::text()"
  when /^self\(/
    "self::#{node.value[1]}"
  when /^eq\(/
    "position()=#{node.value[1]}"
  when /^(nth|nth-of-type)\(/
    if node.value[1].is_a?(Nokogiri::CSS::Node) && (node.value[1].type == :NTH)
      nth(node.value[1])
    else
      "position()=#{node.value[1]}"
    end
  when /^nth-child\(/
    if node.value[1].is_a?(Nokogiri::CSS::Node) && (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) && (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) && (node.value[1].type == :NTH)
      nth(node.value[1], last: true, child: true)
    else
      "count(following-sibling::*)=#{node.value[1].to_i - 1}"
    end
  when /^(first|first-of-type)\(/
    "position()=1"
  when /^(last|last-of-type)\(/
    "position()=last()"
  when /^contains\(/
    "contains(.,#{xpath_function_argument(node.value[1])})"
  when /^gt\(/
    "position()>#{node.value[1]}"
  when /^only-child\(/
    "last()=1"
  when /^comment\(/
    "comment()"
  when /^has\(/
    is_direct = node.value[1].value[0].nil? # e.g. "has(> a)", "has(~ a)", "has(+ a)"
    ".#{"//" unless is_direct}#{node.value[1].accept(self)}"
  else
    validate_xpath_function_name(node.value.first)

    # xpath function call, let's marshal those arguments
    args = ["."]
    args += node.value[1..-1].map { |arg| xpath_function_argument(arg) }
    "nokogiri:#{node.value.first}#{args.join(",")})"
  end
end

#visit_id(node)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 173

def visit_id(node)
  "@id=#{xpath_string_literal(node.value.first.delete_prefix("#"))}"
end

#visit_not(node)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 164

def visit_not(node)
  child = node.value.first
  if :ELEMENT_NAME == child.type
    "not(self::#{child.accept(self)})"
  else
    "not(#{child.accept(self)})"
  end
end

#visit_pseudo_class(node)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 204

def visit_pseudo_class(node)
  if node.value.first.is_a?(Nokogiri::CSS::Node) && (node.value.first.type == :FUNCTION)
    node.value.first.accept(self)
  else
    msg = :"visit_pseudo_class_#{node.value.first.gsub(/[(]/, "")}"
    return send(msg, node) if respond_to?(msg)

    case node.value.first
    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 "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()"
    when "root" then "not(parent::*)"
    else
      validate_xpath_function_name(node.value.first)
      "nokogiri:#{node.value.first}(.)"
    end
  end
end

#xpath_function_argument(arg) (private)

This method is for internal use only.

Function arguments reach the visitor as raw tokens, and only a STRING token starts with a quote.

[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 329

def xpath_function_argument(arg)
  if arg.is_a?(Nokogiri::CSS::Node)
    arg.accept(self)
  elsif arg.is_a?(String) && arg.start_with?("'", '"')
    xpath_string_literal(Parser.unescape_css_string(arg[1..-2]))
  else
    arg
  end
end

#xpath_name?(name) ⇒ Boolean (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 324

def xpath_name?(name)
  XPATH_NAME.match?(name)
end

#xpath_string_literal(string) (private)

This method is for internal use only.

XPath 1.0 string literals have no escape mechanism, so a string containing both quote characters has to be assembled with concat().

[ GitHub ]

  
# File 'lib/nokogiri/css/xpath_visitor.rb', line 408

def xpath_string_literal(string)
  if !string.include?("'")
    "'#{string}'"
  elsif !string.include?('"')
    %("#{string}")
  else
    "concat('" + string.split("'", -1).join(%q{',"'",'}) + "')"
  end
end