From 22e1a946aee9e9e2f64d267ac23a20093b5b0ab5 Mon Sep 17 00:00:00 2001 From: tompng Date: Sun, 28 Jun 2026 16:00:28 +0900 Subject: [PATCH 1/2] Fix xpath functions related to names Fix and simplify name(nodesets), local-name(nodesets) and namespace-uri(nodesets) node select logic. All functions that uses a single node should use the first document-ordered node, but these functions were wrongly skipping un-named nodes. --- lib/rexml/functions.rb | 36 ++++++++++--------------------- test/functions/test_local_name.rb | 18 ++++++++++++++-- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/rexml/functions.rb b/lib/rexml/functions.rb index 8881c3fad..1270f2f99 100644 --- a/lib/rexml/functions.rb +++ b/lib/rexml/functions.rb @@ -60,41 +60,27 @@ def id( object ) end def local_name(node_set=nil) - get_namespace(node_set) do |node| - return node.local_name - end - "" + target_named_node(node_set)&.local_name || "" end def namespace_uri( node_set=nil ) - get_namespace( node_set ) do |node| - return node.namespace - end - "" + target_named_node(node_set)&.namespace || "" end def name( node_set=nil ) - get_namespace( node_set ) do |node| - return node.expanded_name - end - "" + target_named_node(node_set)&.expanded_name || "" end # Helper method. - def get_namespace( node_set = nil ) - if node_set == nil - yield @context[:node] if @context[:node].respond_to?(:namespace) - else - if node_set.kind_of? Array - result = [] - XPathParser.sort(node_set).each do |node| - result << yield(node) if node.respond_to?(:namespace) - end - result - elsif node_set.respond_to? :namespace - yield node_set + def target_named_node(node_set = nil) + node = + case node_set + when nil + node = @context[:node] + when Array + node = XPathParser.sort(node_set).first end - end + node if node.respond_to?(:namespace) end # A node-set is converted to a string by returning the string-value of the diff --git a/test/functions/test_local_name.rb b/test/functions/test_local_name.rb index 97c9e7485..01c09eedd 100644 --- a/test/functions/test_local_name.rb +++ b/test/functions/test_local_name.rb @@ -16,7 +16,7 @@ def test_one XML - node_set = document.root.children + node_set = document.root.elements.to_a assert_equal("child", REXML::Functions.local_name(node_set)) end @@ -27,7 +27,7 @@ def test_multiple XML - node_set = document.root.children + node_set = document.root.elements.to_a assert_equal("child1", REXML::Functions.local_name(node_set)) end @@ -35,6 +35,20 @@ def test_nonexistent assert_equal("", REXML::Functions.local_name([])) end + def test_attribute + document = REXML::Document.new("") + node_set = [document.root.attributes.to_a.last] + assert_equal("attr", REXML::Functions.local_name(node_set)) + end + + def test_non_named + document = REXML::Document.new("text") + children = document.root.children + assert_equal("", REXML::Functions.local_name([children[0]])) + assert_equal("", REXML::Functions.local_name([children[1]])) + assert_equal("", REXML::Functions.local_name(children)) + end + def test_context document = REXML::Document.new("") REXML::Functions.context = {node: document.root} From f280f78725fbac203cb6f559283c8fb24506ac52 Mon Sep 17 00:00:00 2001 From: tompng Date: Sun, 28 Jun 2026 18:17:19 +0900 Subject: [PATCH 2/2] Update XPath internal function names --- lib/rexml/functions.rb | 4 +++- test/functions/test_base.rb | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/rexml/functions.rb b/lib/rexml/functions.rb index 1270f2f99..281d38096 100644 --- a/lib/rexml/functions.rb +++ b/lib/rexml/functions.rb @@ -22,8 +22,10 @@ def initialize :variables, :variables=, :context=, - :get_namespace, + :target_named_node, :send, + :compare_language, + :string_value, ] class << self def method_added(name) diff --git a/test/functions/test_base.rb b/test/functions/test_base.rb index eb67fa3f8..36f2ea168 100644 --- a/test/functions/test_base.rb +++ b/test/functions/test_base.rb @@ -13,6 +13,16 @@ def setup REXML::Functions.context = nil end + def test_available_functions + expected_functions = %w[ + boolean ceiling concat contains count false floor id lang last local-name + name namespace-uri normalize-space not number position round starts-with + string string-length substring substring-after substring-before sum translate true + ] + methods = REXML::FunctionsClass.class_variable_get(:@@available_functions).keys.sort + assert_equal expected_functions, methods.map { |m| m.to_s.tr('_', '-') }.sort + end + def test_functions # trivial text() test # confuse-a-function