diff --git a/ext/java/nokogiri/XmlXpathContext.java b/ext/java/nokogiri/XmlXpathContext.java index 16939f3066b..8375385932a 100644 --- a/ext/java/nokogiri/XmlXpathContext.java +++ b/ext/java/nokogiri/XmlXpathContext.java @@ -1,6 +1,5 @@ package nokogiri; -import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -104,7 +103,7 @@ public class XmlXpathContext extends RubyObject { String query = rbQuery.convertToString().asJavaString(); - if (!handler.isNil() && !isContainsPrefix(query)) { + if (!isContainsPrefix(query)) { // // The user has passed in a handler, but isn't using the `nokogiri:` prefix as // instructed in JRuby land, so let's try to be clever and rewrite the query, inserting @@ -113,9 +112,6 @@ public class XmlXpathContext extends RubyObject StringBuilder namespacedQuery = new StringBuilder(); int jchar = 0; - // Find the methods on the handler object - Set methodNames = handler.getMetaClass().getMethods().keySet(); - // Find the function calls in the xpath query Matcher xpathFunctionCalls = XPathFunctionCaptureRE.matcher(query); @@ -123,7 +119,7 @@ public class XmlXpathContext extends RubyObject namespacedQuery.append(query.subSequence(jchar, xpathFunctionCalls.start())); jchar = xpathFunctionCalls.start(); - if (methodNames.contains(xpathFunctionCalls.group())) { + if (handler.respondsTo(xpathFunctionCalls.group())) { namespacedQuery.append(NokogiriNamespaceContext.NOKOGIRI_PREFIX); namespacedQuery.append(":"); } @@ -132,7 +128,7 @@ public class XmlXpathContext extends RubyObject jchar = xpathFunctionCalls.end(); } - if (jchar < query.length() - 1) { + if (jchar < query.length()) { namespacedQuery.append(query.subSequence(jchar, query.length())); } query = namespacedQuery.toString(); diff --git a/lib/nokogiri/xml.rb b/lib/nokogiri/xml.rb index 9d16fb14989..fa368c283ab 100644 --- a/lib/nokogiri/xml.rb +++ b/lib/nokogiri/xml.rb @@ -67,6 +67,7 @@ def fragment(string, options = ParseOptions::DEFAULT_XML, &block) require_relative "xml/syntax_error" require_relative "xml/xpath" require_relative "xml/xpath_context" +require_relative "xml/xpath_functions" require_relative "xml/builder" require_relative "xml/reader" require_relative "xml/notation" diff --git a/lib/nokogiri/xml/searchable.rb b/lib/nokogiri/xml/searchable.rb index cc493e56f47..639f9770f2e 100644 --- a/lib/nokogiri/xml/searchable.rb +++ b/lib/nokogiri/xml/searchable.rb @@ -253,6 +253,7 @@ def extract_params(params) # :nodoc: ![Hash, String, Symbol].include?(param.class) end params -= [handler] if handler + handler = XPathFunctions.wrap(handler) hashes = [] while Hash === params.last || params.last.nil? diff --git a/lib/nokogiri/xml/xpath_functions.rb b/lib/nokogiri/xml/xpath_functions.rb new file mode 100644 index 00000000000..2ad5f2c5e67 --- /dev/null +++ b/lib/nokogiri/xml/xpath_functions.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "delegate" + +module Nokogiri + module XML + class XPathFunctions < SimpleDelegator + class << self + def wrap(handler) + if handler.nil? + @wrap_nil ||= new(Object.new) + else + new(handler) + end + end + end + end + end +end diff --git a/nokogiri.gemspec b/nokogiri.gemspec index 77836a5560f..8ffd4096a3b 100644 --- a/nokogiri.gemspec +++ b/nokogiri.gemspec @@ -309,6 +309,7 @@ Gem::Specification.new do |spec| "lib/nokogiri/xml/xpath.rb", "lib/nokogiri/xml/xpath/syntax_error.rb", "lib/nokogiri/xml/xpath_context.rb", + "lib/nokogiri/xml/xpath_functions.rb", "lib/nokogiri/xslt.rb", "lib/nokogiri/xslt/stylesheet.rb", "lib/xsd/xmlparser/nokogiri.rb", diff --git a/test/xml/test_xpath.rb b/test/xml/test_xpath.rb index cb1ffd45186..acae48851a3 100644 --- a/test/xml/test_xpath.rb +++ b/test/xml/test_xpath.rb @@ -166,6 +166,40 @@ def test_search_with_xpath_query_using_namespaced_custom_function end end + def test_search_with_xpath_query_uses_global_custom_selectors_with_arguments + XPathFunctions.include(Module.new do + def our_filter(*args) + my_filter(*args) + end + end) + + set = if Nokogiri.uses_libxml? + @xml.search('//employee/address[our_filter(., "domestic", "Yes")]', @handler) + else + @xml.search('//employee/address[nokogiri:our_filter(., "domestic", "Yes")]', @handler) + end + refute_empty(set) + set.each do |node| + assert_equal("Yes", node["domestic"]) + end + end + + def test_search_with_xpath_query_uses_global_custom_selectors_with_arguments_without_namespace + skip("Testing fallback behavior in JRuby") unless Nokogiri.jruby? + + XPathFunctions.include(Module.new do + def our_filter(*args) + my_filter(*args) + end + end) + + set = @xml.search('//employee/address[our_filter(., "domestic", "Yes")]', @handler) + refute_empty(set) + set.each do |node| + assert_equal("Yes", node["domestic"]) + end + end + def test_pass_self_to_function set = if Nokogiri.uses_libxml? @xml.xpath('//employee/address[my_filter(., "domestic", "Yes")]', @handler)