fix(js,dom): keep parsed SVG in the SVG namespace so href reflects an SVGAnimatedString

get namespaceURI() derived the namespace from the tag name and only ever
recognised the <svg> root, so every element parsed inside an <svg> subtree
reported the XHTML namespace. get href() gates the SVGAnimatedString
reflection on the SVG namespace, so for parsed content the gate never
opened: href returned a plain string and .animVal/.baseVal were undefined,
making new URL(el.href.animVal) throw. Only createElementNS-built elements
worked, because createElementNS records _ns explicitly.

The tree builder already applies the HTML5 foreign-content rules, so
QualName.ns is correct for parsed SVG; it just was not exposed to JS. Add a
namespace_uri DOM op and read it in get namespaceURI(), keeping _ns as the
override and the old tag-name heuristic as the fallback when no element name
is recorded. Namespaces are immutable, so the result is cached like _lname.

Fixes #484
This commit is contained in:
ulises2k committed 2026-07-25 22:30:38 -03:00
1 parent e314b9b0f3
commit 3a4e3f7d7d
2 files changed
+22 -4

No files matched your search

+14 -4
View File
@@ -1210,11 +1210,21 @@ class Element extends Node {
set className(v) { this.setAttribute("class", v); }
get namespaceURI() {
// createElementNS records the requested namespace on _ns; an empty string
// maps to the null namespace per spec. Elements made via createElement (or
// parsed) have no _ns: default to XHTML, except <svg> which is SVG.
// maps to the null namespace per spec.
if (this._ns !== undefined) return this._ns === "" ? null : this._ns;
if (this.localName === "svg") return "http://www.w3.org/2000/svg";
return "http://www.w3.org/1999/xhtml";
// Otherwise use the namespace the HTML tree builder assigned. Foreign
// content puts the WHOLE <svg>/<math> subtree in that namespace, not just
// the root, so deriving it from the tag name (the old `localName === "svg"`
// check) left every descendant looking like HTML and skipped the SVG-only
// reflections -- notably `get href()`, which then returned a plain string
// instead of an SVGAnimatedString. An element's namespace never changes,
// so cache it like _lname.
if (this._nsCache !== undefined) return this._nsCache;
let ns = _domParse("namespace_uri", this._nid) || "";
// Nodes with no element name recorded fall back to the previous heuristic.
if (!ns) ns = this.localName === "svg" ? "http://www.w3.org/2000/svg" : "http://www.w3.org/1999/xhtml";
this._nsCache = ns;
return ns;
}
// `inner_html` resolves a <template> to its contents document on the Rust
// side (issue #463), so this needs no template special case.
+8
View File
@@ -303,6 +303,14 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
let name = dom.with_node(NodeId::new(nid), |n| n.as_element().map(|name| name.local.as_ref().to_ascii_uppercase())).flatten().unwrap_or_default();
serde_json::to_string(&name).unwrap_or("\"\"".into())
}
// The tree builder already assigns foreign content (an <svg>/<math>
// subtree) its own namespace; expose it so JS does not have to guess
// the namespace from the tag name.
"namespace_uri" => {
let nid = arg1.parse::<u32>().unwrap_or(0);
let ns = dom.with_node(NodeId::new(nid), |n| n.as_element().map(|name| name.ns.as_ref().to_string())).flatten().unwrap_or_default();
serde_json::to_string(&ns).unwrap_or("\"\"".into())
}
"get_attribute" => {
let nid = arg1.parse::<u32>().unwrap_or(0);
let val = dom.with_node(NodeId::new(nid), |n| n.get_attribute(&arg2).map(|s| s.to_string())).flatten();