Skip to content

Commit 762f2fb

Browse files
committed
fix: reconciliation of ns on unparented nodes
Closes #3459
1 parent 3ff6b8e commit 762f2fb

File tree

4 files changed

+59
-11
lines changed

4 files changed

+59
-11
lines changed

ext/java/nokogiri/XmlNode.java

+19-11
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ public class XmlNode extends RubyObject
421421
}
422422

423423
Element e = (Element) node;
424+
Node parent = e.getParentNode();
424425

425426
// disable error checking to prevent lines like the following
426427
// from throwing a `NAMESPACE_ERR' exception:
@@ -439,22 +440,27 @@ public class XmlNode extends RubyObject
439440
set_namespace(context, ((XmlNode)parent(context)).namespace(context));
440441
}
441442
} else {
442-
String currentPrefix = e.getParentNode().lookupPrefix(nsURI);
443-
String currentURI = e.getParentNode().lookupNamespaceURI(prefix);
444-
boolean isDefault = e.getParentNode().isDefaultNamespace(nsURI);
445-
446443
// add xmlns attribute if this is a new root node or if the node's
447444
// namespace isn't a default namespace in the new document
448-
if (e.getParentNode().getNodeType() == Node.DOCUMENT_NODE) {
445+
if (parent == null || parent.getNodeType() == Node.DOCUMENT_NODE) {
449446
// this is the root node, so we must set the namespaces attributes anyway
450447
e.setAttribute(prefix == null ? "xmlns" : "xmlns:" + prefix, nsURI);
451448
} else if (prefix == null) {
452449
// this is a default namespace but isn't the default where this node is being added
453-
if (!isDefault) { e.setAttribute("xmlns", nsURI); }
454-
} else if (!prefix.equals(currentPrefix) || !nsURI.equals(currentURI)) {
455-
// this is a prefixed namespace
456-
// but doesn't have the same prefix or the prefix is set to a different URI
457-
e.setAttribute("xmlns:" + prefix, nsURI);
450+
boolean isDefault = parent.isDefaultNamespace(nsURI);
451+
452+
if (!isDefault) {
453+
e.setAttribute("xmlns", nsURI);
454+
}
455+
} else if (parent.getNodeType() == Node.ELEMENT_NODE) {
456+
String currentPrefix = parent.lookupPrefix(nsURI);
457+
String currentURI = parent.lookupNamespaceURI(prefix);
458+
459+
if (!prefix.equals(currentPrefix) || !nsURI.equals(currentURI)) {
460+
// this is a prefixed namespace
461+
// but doesn't have the same prefix or the prefix is set to a different URI
462+
e.setAttribute("xmlns:" + prefix, nsURI);
463+
}
458464
}
459465
}
460466

@@ -492,7 +498,7 @@ public class XmlNode extends RubyObject
492498

493499
// if this namespace is a duplicate of what's already in the document, remove it.
494500
// a "duplicate" here is if the prefix and the URI both match what resolves in the parent.
495-
if (e.getParentNode().getNodeType() == Node.ELEMENT_NODE) {
501+
if (parent != null && parent.getNodeType() == Node.ELEMENT_NODE) {
496502
RubyArray<?> nsdefs = this.namespace_definitions(context);
497503
for (int j = 0 ; j < nsdefs.getLength() ; j++) {
498504
XmlNamespace ns = (XmlNamespace)nsdefs.get(j);
@@ -1524,6 +1530,8 @@ public class XmlNode extends RubyObject
15241530
// and keep the return value. -mbklein
15251531
String new_name = NokogiriHelpers.newQName(ns.getPrefix(), node);
15261532
this.node = NokogiriHelpers.renameNode(node, ns.getHref(), new_name);
1533+
1534+
this.relink_namespace(context);
15271535
}
15281536

15291537
clearXpathContext(getNode());

ext/nokogiri/xml_node.c

+4
Original file line numberDiff line numberDiff line change
@@ -1334,6 +1334,10 @@ set_namespace(VALUE self, VALUE namespace)
13341334

13351335
xmlSetNs(node, ns);
13361336

1337+
if (ns && ns->prefix) {
1338+
xmlReconciliateNs(node->doc, node);
1339+
}
1340+
13371341
return self;
13381342
}
13391343

test/xml/test_node.rb

+13
Original file line numberDiff line numberDiff line change
@@ -1336,6 +1336,19 @@ def test_namespace_in_rendered_xml
13361336
assert_match(/xmlns="bar"/, subject.to_xml)
13371337
end
13381338

1339+
# https://github.com/sparklemotion/nokogiri/issues/3459
1340+
def test_namespace_set_on_unparented_node_also_sets_a_definition
1341+
doc = Nokogiri::XML(%{<root xmlns:foo="http://example.com/foo"></root>})
1342+
ns = doc.root.namespace_definitions.first
1343+
1344+
node = doc.create_element("other")
1345+
node.namespace = ns
1346+
1347+
assert_pattern do
1348+
node.namespace_definitions => [ { prefix: "foo", href: "http://example.com/foo" } ]
1349+
end
1350+
end
1351+
13391352
# issue 771
13401353
def test_format_noblank
13411354
content = <<~XML

test/xml/test_node_reparenting.rb

+23
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,29 @@ def coerce(data)
845845
end
846846
end
847847
end
848+
849+
# https://github.com/sparklemotion/nokogiri/issues/3459
850+
describe "reparenting with duplicate namespace prefixes" do
851+
it "stitches together ok" do
852+
doc = Nokogiri::XML(<<~XML)
853+
<dnd:adventure xmlns:dnd="http://www.w3.org/dungeons#">
854+
<dnd:party xmlns:dnd="http://www.w3.org/dragons#">
855+
<dnd:members>
856+
</dnd:members>
857+
</dnd:party>
858+
</dnd:adventure>
859+
XML
860+
861+
dungeons_ns = doc.root.namespace_definitions.find { |ns| ns.prefix == "dnd" }
862+
parent = doc.xpath("//ns:members", ns: "http://www.w3.org/dragons#").first
863+
864+
node = doc.create_element("character")
865+
node.namespace = dungeons_ns
866+
parent.add_child(node)
867+
868+
assert_includes(doc.to_xml, %{<dnd:character xmlns:dnd="http://www.w3.org/dungeons#"/>})
869+
end
870+
end
848871
end
849872
end
850873
end

0 commit comments

Comments
 (0)