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
-
WILDCARD_NAMESPACES =
Internal use only
# File 'lib/nokogiri/css/xpath_visitor.rb', line 10Nokogiri.libxml2_patches.include?("0009-allow-wildcard-namespaces.patch")
-
XPATH_NAME =
private
# File 'lib/nokogiri/css/xpath_visitor.rb', line 14
The ASCII characters allowed in an
::Nokogiri::XMLNCName. Every XPath delimiter and operator is ASCII, so non-ASCII characters are left for the XPath parser to accept or reject./\A(?:[A-Za-z_]|[^\x00-\x7F])(?:[A-Za-z0-9_.-]|[^\x00-\x7F])*\z/
Class Method Summary
-
.new() → XPathVisitor) ⇒ XPathVisitor
constructor
[Parameters] -
builtins:(BuiltinsConfig) Determine when to use Nokogiri's built-in xpath functions for performance improvements.
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
-
#config() → Hash)
[Returns].
- #accept(node) Internal use only
- #visit_attrib_name(node) Internal use only
- #visit_attribute_condition(node) Internal use only
- #visit_class_condition(node) Internal use only
- #visit_combinator(node) Internal use only
- #visit_conditional_selector(node) Internal use only
- #visit_element_name(node) Internal use only
- #visit_function(node) Internal use only
- #visit_id(node) Internal use only
- #visit_not(node) Internal use only
- #visit_pseudo_class(node) Internal use only
- #css_class(hay, needle) private Internal use only
- #html5_element_name_needs_namespace_handling(node) private Internal use only
- #is_of_type_pseudo_class?(node) ⇒ Boolean private Internal use only
- #nth(node, options = {}) private Internal use only
- #read_a_and_positive_b(values) private Internal use only
- #validate_xpath_element_name(name) private Internal use only
- #validate_xpath_function_name(name) private Internal use only
-
#xpath_function_argument(arg)
private
Internal use only
Function arguments reach the visitor as raw tokens, and only a STRING token starts with a quote.
- #xpath_name?(name) ⇒ Boolean private Internal use only
-
#xpath_string_literal(string)
private
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().
Constructor Details
.new() → XPathVisitor) ⇒ XPathVisitor
.new(builtins:, doctype:) → XPathVisitor) ⇒ 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::CSSqueries.
[Returns] XPathVisitor
# 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.
# 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.
# 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.
# 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.
# File 'lib/nokogiri/css/xpath_visitor.rb', line 59
attr_reader :prefix
Instance Method Details
#accept(node)
# 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.
# 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)
# 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)
# 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)
#nth(node, options = {}) (private)
# File 'lib/nokogiri/css/xpath_visitor.rb', line 346
def nth(node, = {}) 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 [:child] [:last] ? "(count(following-sibling::*)+1)" : "(count(preceding-sibling::*)+1)" else [: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)
# 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)
# 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)
# 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)
# 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)
# 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)
# File 'lib/nokogiri/css/xpath_visitor.rb', line 230
def visit_class_condition(node) css_class("@class", node.value.first) end
#visit_combinator(node)
# 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)
#visit_element_name(node)
# 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)
# 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)
# 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)
#visit_pseudo_class(node)
# 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)
Function arguments reach the visitor as raw tokens, and only a STRING token starts with a quote.
# 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)
# File 'lib/nokogiri/css/xpath_visitor.rb', line 324
def xpath_name?(name) XPATH_NAME.match?(name) end
#xpath_string_literal(string) (private)
XPath 1.0 string literals have no escape mechanism, so a string containing both quote characters has to be assembled with concat().
# 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