fix(js): make setAttributeNS/getAttributeNS/removeAttributeNS namespace-aware

The three NS methods ignored the namespace: setAttributeNS stored the qualified
name against the non-namespaced store and getAttributeNS/removeAttributeNS
looked up by local name only, so a namespaced attribute was not addressable by
(namespace, localName) — getAttributeNS(xlink, 'href') returned null.

Record the namespace on the attribute and add namespace-aware DOM ops that match
by (namespace, localName), where the local name is the attribute name after any
prefix (so both setAttributeNS-created 'xlink:href' and parser-created 'href'
resolve). The non-namespaced getAttribute/serialization paths are untouched, and
getAttributeNS(null, name) still reads plain attributes.

Fixes #503
This commit is contained in:
Andrey Mnatsakanov committed 2026-07-26 13:29:43 +02:00
1 parent 8078cc3070
commit 6314ecbf88
4 files changed
+114 -3

No files matched your search

+30
View File
@@ -378,6 +378,36 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
});
"true".into()
}
// Namespace-aware attribute ops. arg2 packs the pieces with a NUL:
// get/remove: "<namespace>\0<localName>"
// set: "<namespace>\0<qualifiedName>\0<value>"
"get_attribute_ns" => {
let nid = arg1.parse::<u32>().unwrap_or(0);
let (ns, local) = arg2.split_once('\0').unwrap_or(("", arg2.as_str()));
let val = dom
.with_node(NodeId::new(nid), |n| n.get_attribute_ns(ns, local).map(|s| s.to_string()))
.flatten();
serde_json::to_string(&val).unwrap_or("null".into())
}
"set_attribute_ns" => {
let nid = arg1.parse::<u32>().unwrap_or(0);
let mut parts = arg2.splitn(3, '\0');
let ns = parts.next().unwrap_or("");
let qualified = parts.next().unwrap_or("");
let value = parts.next().unwrap_or("");
if !qualified.is_empty() {
dom.with_node_mut(NodeId::new(nid), |n| {
n.set_attribute_ns(ns, qualified, value.to_string())
});
}
"true".into()
}
"remove_attribute_ns" => {
let nid = arg1.parse::<u32>().unwrap_or(0);
let (ns, local) = arg2.split_once('\0').unwrap_or(("", arg2.as_str()));
dom.with_node_mut(NodeId::new(nid), |n| n.remove_attribute_ns(ns, local));
"true".into()
}
"set_inner_html" => {
let nid = match arg1.parse::<u32>() {
Ok(n) if n > 0 => n,
+28
View File
@@ -1317,6 +1317,34 @@ mod tests {
rt
}
#[test]
fn set_attribute_ns_is_retrievable_by_namespace_and_local_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.getAttributeNS('http://www.w3.org/1999/xlink','href');})()")
.unwrap();
assert_eq!(v, serde_json::json!("#g"));
}
#[test]
fn remove_attribute_ns_removes_by_namespace() {
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'); s.removeAttributeNS('http://www.w3.org/1999/xlink','href'); return s.getAttributeNS('http://www.w3.org/1999/xlink','href');})()")
.unwrap();
assert_eq!(v, serde_json::json!(null));
}
#[test]
fn get_attribute_ns_reads_plain_attributes_with_null_namespace() {
// Backward-compat: getAttributeNS(null, name) still reads a plain attr.
let mut rt = setup_runtime(r#"<html><body><div id="d" title="hi"></div></body></html>"#);
let v = rt
.evaluate("document.getElementById('d').getAttributeNS(null,'title')")
.unwrap();
assert_eq!(v, serde_json::json!("hi"));
}
#[test]
fn test_document_title() {
let mut rt = setup_runtime("<html><head><title>Test</title></head><body></body></html>");