Skip to content

Commit 3ff6b8e

Browse files
authored
fix: XML::Builder namespace stitching (#3461)
**What problem is this PR intended to solve?** Simplify Builder to do late binding on namespaces, so that nodes can be built that rely on namespaces they define. Fixes #3458 **Have you included adequate test coverage?** Existing pending coverage (now resolved) is sufficient. **Does this change affect the behavior of either the C or the Java implementations?** This really only affects the CRuby implementation, which now behaves like the JRuby impl does.
2 parents db88e25 + 2c0c85d commit 3ff6b8e

File tree

4 files changed

+53
-41
lines changed

4 files changed

+53
-41
lines changed

lib/nokogiri/xml/builder.rb

+24-32
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ def initialize(options = {}, root = nil, &block)
314314

315315
@context = nil
316316
@arity = nil
317-
@ns = nil
317+
@ns_prefix = nil
318318

319319
options = DEFAULT_DOCUMENT_OPTIONS.merge(options)
320320
options.each do |k, v|
@@ -355,20 +355,8 @@ def comment(string)
355355
###
356356
# Build a tag that is associated with namespace +ns+. Raises an
357357
# ArgumentError if +ns+ has not been defined higher in the tree.
358-
def [](ns)
359-
if @parent != @doc
360-
@ns = @parent.namespace_definitions.find { |x| x.prefix == ns.to_s }
361-
end
362-
return self if @ns
363-
364-
@parent.ancestors.each do |a|
365-
next if a == doc
366-
367-
@ns = a.namespace_definitions.find { |x| x.prefix == ns.to_s }
368-
return self if @ns
369-
end
370-
371-
@ns = { pending: ns.to_s }
358+
def [](ns_prefix)
359+
@ns_prefix = ns_prefix.to_s
372360
self
373361
end
374362

@@ -395,29 +383,33 @@ def method_missing(method, *args, &block) # :nodoc:
395383
if @context&.respond_to?(method)
396384
@context.send(method, *args, &block)
397385
else
398-
node = @doc.create_element(method.to_s.sub(/[_!]$/, ""), *args) do |n|
399-
# Set up the namespace
400-
if @ns.is_a?(Nokogiri::XML::Namespace)
401-
n.namespace = @ns
402-
@ns = nil
403-
end
404-
end
405-
406-
if @ns.is_a?(Hash)
407-
node.namespace = node.namespace_definitions.find { |x| x.prefix == @ns[:pending] }
408-
if node.namespace.nil?
409-
raise ArgumentError, "Namespace #{@ns[:pending]} has not been defined"
410-
end
411-
412-
@ns = nil
413-
end
414-
386+
node = @doc.create_element(method.to_s.sub(/[_!]$/, ""), *args)
387+
bind_ns(node)
415388
insert(node, &block)
416389
end
417390
end
418391

419392
private
420393

394+
def bind_ns(node)
395+
return if @ns_prefix.nil?
396+
397+
ancestors = [node, parent, parent.ancestors].flatten
398+
ancestors.each do |ancestor|
399+
break if ancestor.nil? || ancestor == @doc
400+
401+
if (ns = ancestor.namespace_definitions.find { |x| x.prefix == @ns_prefix })
402+
@ns_prefix = nil
403+
node.namespace = ns
404+
break
405+
end
406+
end
407+
408+
return if @ns_prefix.nil?
409+
410+
raise ArgumentError, "Namespace prefix #{@ns_prefix.inspect} has not been defined"
411+
end
412+
421413
###
422414
# Insert +node+ as a child of the current Node
423415
def insert(node, &block)

test/helper.rb

+4-4
Original file line numberDiff line numberDiff line change
@@ -266,19 +266,19 @@ def util_decorate(document, decorator_module)
266266
document.decorate!
267267
end
268268

269-
def pending(msg)
269+
def pending(msg, extra_uplevel = 0)
270270
begin
271271
yield
272272
rescue Minitest::Assertion
273-
skip("pending #{msg} [#{caller(2..2).first}]")
273+
skip("pending #{msg} [#{caller(2 + extra_uplevel, 1).first}]")
274274
end
275-
flunk("pending test unexpectedly passed: #{msg} [#{caller(1..1).first}]")
275+
flunk("pending test unexpectedly passed: #{msg} [#{caller(1 + extra_uplevel, 1).first}]")
276276
end
277277

278278
def pending_if(msg, pend_eh, &block)
279279
return yield unless pend_eh
280280

281-
pending(msg, &block)
281+
pending(msg, 1, &block)
282282
end
283283

284284
# returns the page size in bytes

test/namespaces/test_serializing_namespaces.rb

+1-5
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,7 @@
4747
assert_includes(doc, '<dnd:adventure xmlns:dnd="http://www.w3.org/dungeons#">')
4848
assert_includes(doc, '<dnd:party xmlns:dnd="http://www.w3.org/dragons#">')
4949
assert_includes(doc, "<dnd:members>")
50-
pending_if("https://github.com/sparklemotion/nokogiri/issues/3458", !Nokogiri.jruby?) do
51-
# Here MRI Ruby is incorrectly omitting the xmlns namespace declaration, i.e.:
52-
# '<dnd:character>'
53-
assert_includes(doc, '<dnd:character xmlns:dnd="http://www.w3.org/dungeons#">')
54-
end
50+
assert_includes(doc, '<dnd:character xmlns:dnd="http://www.w3.org/dungeons#">')
5551
assert_includes(doc, "<dnd:name>Nigel</dnd:name>")
5652
end
5753

test/xml/test_builder.rb

+24
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,30 @@ def test_builder_namespace_part_deux
7878
assert_equal({ "xmlns:a" => "x", "xmlns:c" => "z" }, namespaces_defined_on(b))
7979
end
8080

81+
def test_builder_namespaces_part_three
82+
doc = Nokogiri::XML::Builder.new do |xml|
83+
xml["dnd"].adventure("xmlns:dnd" => "http://www.w3.org/dungeons#") do
84+
xml["dnd"].party("xmlns:dnd" => "http://www.w3.org/dragons#") do
85+
xml["dnd"].character("xmlns:dnd" => "http://www.w3.org/dungeons#") do
86+
xml["dnd"].name("Nigel", "xmlns:dnd" => "http://www.w3.org/dungeons#")
87+
end
88+
end
89+
end
90+
end.doc
91+
92+
adventure_node = doc.at_xpath("//*[local-name()='adventure']")
93+
assert_equal("http://www.w3.org/dungeons#", adventure_node.namespace.href)
94+
95+
party_node = doc.at_xpath("//*[local-name()='party']")
96+
assert_equal("http://www.w3.org/dragons#", party_node.namespace.href)
97+
98+
character_node = doc.at_xpath("//*[local-name()='character']")
99+
assert_equal("http://www.w3.org/dungeons#", character_node.namespace.href)
100+
101+
name_node = doc.at_xpath("//*[local-name()='name']")
102+
assert_equal("http://www.w3.org/dungeons#", name_node.namespace.href)
103+
end
104+
81105
def test_builder_with_unlink
82106
b = Nokogiri::XML::Builder.new do |xml|
83107
xml.foo do

0 commit comments

Comments
 (0)