fix(dom): preserve namespaced attribute qualified names

This commit is contained in:
SGavrl committed 2026-08-01 16:00:52 +00:00
1 parent 1e305135e3
commit 76e5280c32
5 files changed
+96 -27

No files matched your search

+22 -1
View File
@@ -1402,7 +1402,12 @@ class Element extends Node {
if (popoverPrev !== undefined) this._popoverTypeMaybeChanged(popoverPrev);
if (globalThis.__mutationObservers?.length) globalThis.__notifyMutation('attributes', this._nid, [], [], n);
}
setAttributeNS(ns, n, v) { _dom("set_attribute_ns", this._nid, String(ns == null ? "" : ns) + "\0" + String(n) + "\0" + String(v)); } // n is the qualified name; stored with its namespace
setAttributeNS(ns, n, v) {
ns = ns == null || ns === '' ? '' : String(ns);
n = String(n);
_ns_validateQualifiedName(ns, n);
_dom("set_attribute_ns", this._nid, ns + "\0" + n + "\0" + String(v));
}
removeAttribute(n) { n = _htmlAttrName(this, n); const popoverPrev = (n === "popover") ? this.popover : undefined; _dom("remove_attribute", this._nid, n); if (popoverPrev !== undefined) this._popoverTypeMaybeChanged(popoverPrev); }
removeAttributeNS(ns, n) { _dom("remove_attribute_ns", this._nid, String(ns == null ? "" : ns) + "\0" + String(n)); }
hasAttribute(n) { return this.getAttribute(n) !== null; }
@@ -8423,6 +8428,22 @@ const _ns_isValidXmlName = (name) => {
return /^[A-Za-z_:][\w.\-:]*$/.test(name);
};
const _ns_validateQualifiedName = (namespaceURI, qualifiedName) => {
const parts = qualifiedName.split(':');
if (parts.length > 2 || parts.some((part) => !_ns_isValidXmlName(part))) {
throw new DOMException('Invalid attribute name', 'InvalidCharacterError');
}
const prefix = parts.length === 2 ? parts[0] : null;
const XML = 'http://www.w3.org/XML/1998/namespace';
const XMLNS = 'http://www.w3.org/2000/xmlns/';
if ((prefix && !namespaceURI)
|| (prefix === 'xml' && namespaceURI !== XML)
|| ((qualifiedName === 'xmlns' || prefix === 'xmlns') && namespaceURI !== XMLNS)
|| (namespaceURI === XMLNS && qualifiedName !== 'xmlns' && prefix !== 'xmlns')) {
throw new DOMException('The namespace is invalid', 'NamespaceError');
}
};
// Document.prototype.createAttribute: create a detached Attr node
if (!Document.prototype.createAttribute) {
Document.prototype.createAttribute = function(localName) {
+2 -2
View File
@@ -321,7 +321,7 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
let names: Vec<String> = dom
.with_node(NodeId::new(nid), |n| {
n.attrs()
.map(|a| a.iter().map(|x| x.name.local.as_ref().to_string()).collect())
.map(|a| a.iter().map(|x| x.qualified_name()).collect())
.unwrap_or_default()
})
.unwrap_or_default();
@@ -373,7 +373,7 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
let nid = arg1.parse::<u32>().unwrap_or(0);
dom.with_node_mut(NodeId::new(nid), |n| {
if let NodeData::Element { attrs, .. } = &mut n.data {
attrs.retain(|a| a.name.local.as_ref() != arg2.as_str());
attrs.retain(|a| !a.qualified_name_eq(&arg2));
}
});
"true".into()
+34
View File
@@ -1345,6 +1345,40 @@ mod tests {
assert_eq!(v, serde_json::json!("hi"));
}
#[test]
fn namespaced_attribute_keeps_its_qualified_name() {
let mut rt = setup_runtime("<html><body></body></html>");
let v = rt
.evaluate("(function(){var s=document.createElementNS('http://www.w3.org/2000/svg','svg');s.setAttributeNS('http://www.w3.org/1999/xlink','xlink:href','#g');return s.getAttribute('xlink:href')+'|'+s.getAttributeNames()[0]+'|'+s.outerHTML;})()")
.unwrap();
assert_eq!(v, serde_json::json!("#g|xlink:href|<svg xlink:href=\"#g\"></svg>"));
}
#[test]
fn parsed_xlink_attribute_is_available_through_both_apis() {
let mut rt = setup_runtime(
r##"<html><body><svg><use id="u" xlink:href="#icon"></use></svg></body></html>"##,
);
let v = rt
.evaluate("(function(){var u=document.getElementById('u');return u.getAttribute('xlink:href')+'|'+u.getAttributeNS('http://www.w3.org/1999/xlink','href')+'|'+u.getAttributeNames().join(',');})()")
.unwrap();
assert_eq!(v, serde_json::json!("#icon|#icon|id,xlink:href"));
}
#[test]
fn set_attribute_ns_validates_namespace_constraints() {
let mut rt = setup_runtime("<html><body></body></html>");
let v = rt
.evaluate("(function(){var e=document.createElement('div'),out=[];for(const args of [[null,'x:y'],['urn:test','a:b:c'],['urn:test','xml:lang'],['urn:test','xmlns:x']]){try{e.setAttributeNS(args[0],args[1],'v');out.push('none')}catch(err){out.push(err.name)}}return out.join('|');})()")
.unwrap();
assert_eq!(
v,
serde_json::json!(
"NamespaceError|InvalidCharacterError|NamespaceError|NamespaceError"
)
);
}
#[test]
fn test_document_title() {
let mut rt = setup_runtime("<html><head><title>Test</title></head><body></body></html>");