Thanks to visit codestin.com
Credit goes to Github.com

Skip to content

Comments

fix: infer/generate prefixes for unprefixed namespaced attribute nodes.#903

Open
hungarian-notation wants to merge 15 commits intoxmldom:masterfrom
hungarian-notation:unprefixed-attributes-fix
Open

fix: infer/generate prefixes for unprefixed namespaced attribute nodes.#903
hungarian-notation wants to merge 15 commits intoxmldom:masterfrom
hungarian-notation:unprefixed-attributes-fix

Conversation

@hungarian-notation
Copy link

Small patch that implements the changes described in #901

The changes broke existing test XML Namespace Parse > should ignore default prefix xml attribute , which relied on the buggy behavior. I have amended that test accordingly.

@hungarian-notation hungarian-notation changed the title Assume prefixes for unprefixed namespaced attribute nodes (issue #901) Assume prefixes for unprefixed namespaced attribute nodes. Jun 22, 2025
@hungarian-notation hungarian-notation changed the title Assume prefixes for unprefixed namespaced attribute nodes. Infer/generate prefixes for unprefixed namespaced attribute nodes. Jun 22, 2025
const n1_test = doc.createElementNS(n1, 'test');
n1_test.setAttribute('xmlns', n1);
n1_test.setAttributeNS(n1, 'bar', 'valx');
expect(n1_test.toString()).toBe('<test xmlns="' + n1 + '" bar="valx"/>');
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in #901, the unprefixed bar="..." attribute the original test is looking for isn't actually in the n1 namespace, it's in no namespace at all.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting. Since we had a test for it, I think it count's as "expected behavior".
So changing that behavior should be considered a breaking change, right?

Copy link
Author

@hungarian-notation hungarian-notation Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xmldom is already parsing unprefixed attributes correctly, it's only mishandling them in XMLSerializer. What this means is that getAttributeNS and setAttributeNS are asymmetric for unprefixed attributes if you serialize/de-serialize the document in-between.

import { DOMParser, XMLSerializer } from "@xmldom/xmldom";

(() => {
  const document = new DOMParser().parseFromString("<root/>", "text/xml");

  const attributeNamespace = "uri:default";
  const attributeLocalName = "example";
  const attributeValue = "value";

  // setAttributeNS
  document.documentElement.setAttributeNS(
    attributeNamespace,
    attributeLocalName,
    attributeValue
  );

  const serialized = new XMLSerializer().serializeToString(document);
  const deserialized = new DOMParser().parseFromString(serialized, "text/xml");

  // getAttributeNS
  const deserializedAttributeValue = deserialized.documentElement.getAttributeNS(
    attributeNamespace,
    attributeLocalName
  );

  console.log(" expected:", attributeValue);
  console.log("   actual:", deserializedAttributeValue);
})();

Without this patch, xmldom gives null for deserializedAttributeValue, which I'd argue is a bug. This test codifies this bug.

Both Gecko and Chromium give the expected result of "value" instead.

Copy link
Author

@hungarian-notation hungarian-notation Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const n1_test = doc.createElementNS(n1, 'test');
n1_test.setAttribute('xmlns', n1);
n1_test.setAttributeNS(n1, 'bar', 'valx');
expect(n1_test.toString()).toBe('<test xmlns="' + n1 + '" bar="valx"/>');

What this code is doing is asking xmldom to create an unrepresentable DOM state and then to emit XML that describes a different state entirely. The first argument being passed to setAttributeNS could be literally anything here, since the attribute the serializer is emitting is not in whatever namespace the user has specified.

In fact, the bugginess becomes much more obvious if the attribute namespace doesn't match the default namespace.

(() => {
  const defaultNamespace = "uri:default-namespace";

  const document = new DOMParser().parseFromString(`<root xmlns="${defaultNamespace}"/>`, "text/xml");

  const attributeNamespace = "uri:default";
  const attributeLocalName = "example";
  const attributeValue = "value";

  document.documentElement.setAttributeNS(
    attributeNamespace,
    attributeLocalName,
    attributeValue
  );

  const serialized = new XMLSerializer({ }).serializeToString(document);
  console.log(serialized);
})();

Currently, this emits:

<root xmlns="uri:default-namespace" xmlns="uri:default" example="value" xmlns="uri:default-namespace"/>

Which is obviously incorrect. Not only is this not well-formed XML, and not only is example still not in either namespace, but now it's ambiguous as to what namespace the root element is in. It would be odd for an implementation to respect the second of the three xmlns attributes, but it would be equally valid as respecting either the first or third.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an alternative, the more rational behavior could be hidden behind an options flag on the XMLSerializer object so as not to break any real-world code that is relying on the buggy behavior.

063aa08

If that's preferable, you can reject this pull and I'll open a new one along those lines.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the decent explaination. Makes total sense that we should consider this a bug, and not a breaking change.

Are you willing to check if the same bug exists in the latest 0.8.* version, which we provide as "kind of an LTS if possible"? And if it's present how hard it would be to backport your additions there? This request is of course completely independent from this PR.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's almost directly applicable to 0.8.x as well, just needed a few tweaks. See #904

@codecov
Copy link

codecov bot commented Jun 22, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 95.18%. Comparing base (eb64722) to head (d4a3d79).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #903      +/-   ##
==========================================
+ Coverage   95.12%   95.18%   +0.05%     
==========================================
  Files           8        8              
  Lines        2196     2220      +24     
  Branches      577      585       +8     
==========================================
+ Hits         2089     2113      +24     
  Misses        107      107              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Member

@karfau karfau left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx for your contribution. So far I only had a first quick look and left some comments.
Please take care of them, I'll need some time to digest this PR in detail.

@karfau karfau changed the title Infer/generate prefixes for unprefixed namespaced attribute nodes. fix: infer/generate prefixes for unprefixed namespaced attribute nodes. Jun 23, 2025
hungarian-notation added a commit to hungarian-notation/xmldom that referenced this pull request Jun 23, 2025
hungarian-notation added a commit to hungarian-notation/xmldom that referenced this pull request Jun 23, 2025
this time without tracking a bunch of junk in the coverage folder
@karfau
Copy link
Member

karfau commented Jun 29, 2025

Sorry for the delay in processing this. I still have one very busy week in front of me, but afterwards I should be able to find the time for this topic.

@karfau
Copy link
Member

karfau commented Jul 24, 2025

Hey @hungarian-notation I finally managed to look into it closer and still have some doubts:

I'm still having a hard time wrapping my head around the topic, but I know that validateAndExtract, which is called as part of most if not all *AttributeNs menthos, prevents combinations of namespaceURI being defined but the qualified name not having a prefix. SO why doesn't it complain in this case?
I wonder if we can instead fix it in a way to make it impossible to produce the invalid DOM state?
Maybe we can have a call to help me understand all the details of it? If you are up for it, please reach out to me via e-mail from my GitHub profile.

PS: I'm not in reach of any computer and very likely also only with limited internet connection on the phone until 4th of August, so I will only be able to reply afterwards.


The rest of the message is only relevant, if those operations need to be possible and the fix can only ever be applied as part of serialization:

I think I detected some flaws in your implementation of assumePrefix:

  • prefix defaults to an empty string, so there is no need to check for null later
  • there is some redundancy in the early returns, I think some of them are not needed
  • there were some == comparisons, we should always use ===
  • let's not add module level variables that might never be used, I would prefer to define them inside the function and only once they are needed
  • the detection of existing generated namespaces with all the slicing and NaN detection is very verbose and not ideal, since it also takes place in the case where the last visible namespace is an exact match and returns existingPrefix
  • existing generated namespaces are only present when they have already been parsed, right? How do we ensure that we are not reusing generated prefixes across an element/node, since they are not added to visibelNamespaces, or are they?
  • (minor) I would prefer the name serializableNamespacePrefix, does it also make sense for you?

I have refactored the method accordingly, started adding some basic tests which left me puzzled how to reproduce the different cases.
It would be great of you could add the remaining test cases to cover the whole function directly, which should be simpler then covering it through serializeToString.

@karfau karfau added this to the 0.9.9 milestone Jul 24, 2025
@shunkica
Copy link
Collaborator

Here is a failing test

	test('should jump over existing prefixes when generating new ones', () => {
		const doc = new DOMParser().parseFromString('<doc xmlns:ns1="uri:existing"/>', MIME_TYPE.XML_TEXT);
		const element = doc.documentElement;

		// Add an attribute that would normally use ns1 prefix
		element.setAttributeNS('uri:new', 'name', 'value');
		element.setAttributeNS('uri:other', 'ns1:other', 'value');

		const serialized = new XMLSerializer().serializeToString(doc);
		expect(serialized).toContain('xmlns:ns1="uri:existing"');
		expect(serialized).toContain('xmlns:ns2="uri:new"');
		expect(serialized).toContain('ns2:name="value"');
		expect(serialized).toContain('xmlns:ns3="uri:other"');
		expect(serialized).toContain('ns3:other="value"');
	});

The serialized result is:

<doc xmlns:ns1="uri:existing" xmlns:ns2="uri:new" ns2:name="value" xmlns:ns1="uri:other" ns1:other="value"/>

Notice the duplicate attribute.

@shunkica
Copy link
Collaborator

I tried to work on the serializer but ended up completely rewriting it because I disliked the monolithic approach, and having it all squished into dom.js was quite cumbersome to work on.

This is what I ended up with

I had to fiddle with it quite a bit to match the expected behavior of the old serializer (attribute ordering, self closing tags etc) and managed to end up with only 1 failing test:

'should add local namespace as xmlns in HTML'
Expected: "<a:foo xmlns:a=\"AAA\"><child xmlns=\"AAA\"></child></a:foo>"
Received: "<a:foo xmlns:a=\"AAA\"><a:child></a:child></a:foo>"

But I would argue that what this test expects is wrong.

In any case I recognize this would be a big change so I am not going to push here or a new PR if this is not the direction you want to go in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Element.setAttributeNS behaves as if unprefixed attributes are in the active default namespace, in contravention of the specification.

3 participants