fix(svg): honor xlink href fallback

This commit is contained in:
ldm0
2026-09-27 19:23:03 +08:00
parent c71610375d
commit de795b5bfe
4 changed files with 143 additions and 7 deletions
@@ -2151,9 +2151,20 @@ pub(super) fn svg_owner_attribute_value<'s>(
attribute: &str,
) -> Option<String> {
let (runtime_ptr, handle) =
crate::native_bridge::node_runtime_and_handle_from_object(scope, owner).ok()?;
let runtime = unsafe { &mut *runtime_ptr };
runtime.dom_host().get_attribute(handle, attribute)
crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner).ok()?;
let runtime = unsafe { &*runtime_ptr };
if attribute != "href" {
return runtime.dom_host().get_attribute(handle, attribute);
}
let value = runtime.dom_host().get_attribute_ns(handle, None, attribute);
if value.is_some() {
return value;
}
runtime.dom_host().get_attribute_ns(
handle,
Some(crate::native_bridge::document::XLINK_NS),
attribute,
)
}
pub(super) fn parse_svg_length_value(raw: &str) -> Option<SvgParsedLength> {
@@ -300,6 +300,7 @@ pub(in crate::native_bridge::document) use structure::set_document_body_for_nati
pub(crate) const XHTML_NS: &str = "http://www.w3.org/1999/xhtml";
pub(crate) const SVG_NS: &str = "http://www.w3.org/2000/svg";
pub(crate) const XLINK_NS: &str = "http://www.w3.org/1999/xlink";
#[derive(Clone, Copy)]
#[repr(u32)]
@@ -344,6 +344,21 @@ pub(in crate::native_bridge) fn trusted_attribute_string_value<'s>(
)
}
fn svg_animated_string_attribute_namespace(
runtime: &JsContextHost,
handle: DomHandle,
attribute: &str,
) -> Option<&'static str> {
(attribute == "href"
&& !runtime.dom_host().has_attribute_ns(handle, None, attribute)
&& runtime.dom_host().has_attribute_ns(
handle,
Some(crate::native_bridge::document::XLINK_NS),
attribute,
))
.then_some(crate::native_bridge::document::XLINK_NS)
}
pub(crate) fn set_svg_animated_string_base_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner: v8::Local<'s, v8::Object>,
@@ -351,17 +366,43 @@ pub(crate) fn set_svg_animated_string_base_value<'s>(
value: v8::Local<'s, v8::Value>,
) -> Option<String> {
let (runtime_ptr, handle) =
crate::native_bridge::node_runtime_and_handle_from_object(scope, owner).ok()?;
crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner).ok()?;
let namespace =
svg_animated_string_attribute_namespace(unsafe { &*runtime_ptr }, handle, attribute);
let value = trusted_attribute_value_string(
scope,
Some((runtime_ptr, handle)),
None,
namespace,
attribute,
value,
TrustedAttributeSetter::SvgAnimatedStringBaseVal,
)?;
let _ =
unsafe { &mut *runtime_ptr }.set_attribute(scope, runtime_ptr, handle, attribute, &value);
let namespace =
svg_animated_string_attribute_namespace(unsafe { &*runtime_ptr }, handle, attribute);
if attribute == "href" {
let _ = unsafe { &mut *runtime_ptr }.set_attribute_ns(
scope,
runtime_ptr,
handle,
namespace,
namespace.map(|_| "xlink"),
attribute,
if namespace.is_some() {
"xlink:href"
} else {
attribute
},
&value,
);
} else {
let _ = unsafe { &mut *runtime_ptr }.set_attribute(
scope,
runtime_ptr,
handle,
attribute,
&value,
);
}
Some(value)
}
@@ -2934,3 +2934,86 @@ fn svg_svg_element_deselect_all_clears_the_owner_document_selection() {
assert_eq!(result, "ok");
}
#[test]
fn svg_href_animated_string_prefers_href_and_falls_back_to_xlink_href() {
let mut vm = new_storage_test_vm("https://svg-href-reflection.test/");
let result = vm
.eval(
r#"
(() => {
const assert = (condition, message) => {
if (!condition) throw new Error(message);
};
const svg = "http://www.w3.org/2000/svg";
const xlink = "http://www.w3.org/1999/xlink";
const unrelated = "https://namespaced-href.test/";
const exercise = ownerDocument => {
const anchor = ownerDocument.createElementNS(svg, "a");
const href = anchor.href;
assert(href === anchor.href, "href is SameObject");
anchor.setAttributeNS(xlink, "href", "xlink-unprefixed");
assert(href.baseVal === "xlink-unprefixed", "unprefixed XLink fallback");
assert(href.animVal === "xlink-unprefixed", "XLink animVal fallback");
href.baseVal = "xlink-updated";
assert(anchor.getAttributeNS(xlink, "href") === "xlink-updated",
"baseVal updates the XLink attribute");
assert(!anchor.hasAttributeNS(null, "href"),
"XLink update does not create href");
anchor.setAttributeNS(null, "href", "preferred");
assert(href.baseVal === "preferred", "href wins regardless of insertion order");
href.baseVal = "preferred-updated";
assert(anchor.getAttributeNS(null, "href") === "preferred-updated",
"baseVal updates preferred href");
assert(anchor.getAttributeNS(xlink, "href") === "xlink-updated",
"preferred href leaves XLink unchanged");
anchor.removeAttributeNS(null, "href");
assert(href.baseVal === "xlink-updated", "removing href restores fallback");
anchor.removeAttributeNS(xlink, "href");
assert(href.baseVal === "" && href.animVal === "", "removing fallback resets values");
anchor.setAttributeNS(xlink, "xlink:href", "xlink-prefixed");
assert(href.baseVal === "xlink-prefixed", "prefixed XLink fallback");
href.baseVal = "xlink-prefixed-updated";
assert(anchor.getAttributeNS(xlink, "href") === "xlink-prefixed-updated",
"baseVal updates prefixed XLink attribute");
assert(anchor.getAttributeNames().includes("xlink:href"),
"baseVal preserves the XLink prefix");
anchor.removeAttributeNS(xlink, "href");
anchor.setAttributeNS(unrelated, "href", "unrelated");
assert(href.baseVal === "", "unrelated namespaced href is ignored");
href.baseVal = "created";
assert(anchor.getAttributeNS(null, "href") === "created",
"baseVal creates an unnamespaced href");
assert(anchor.getAttributeNS(unrelated, "href") === "unrelated",
"baseVal leaves unrelated namespaced href unchanged");
anchor.removeAttributeNS(null, "href");
anchor.setAttributeNS(xlink, "xlink:href", "side-effect-fallback");
href.baseVal = {
toString() {
anchor.setAttributeNS(null, "href", "created-during-conversion");
return "converted";
}
};
assert(anchor.getAttributeNS(null, "href") === "converted",
"baseVal chooses its backing attribute after value conversion");
assert(anchor.getAttributeNS(xlink, "href") === "side-effect-fallback",
"conversion-created href leaves XLink fallback unchanged");
};
exercise(document);
exercise(document.implementation.createHTMLDocument(""));
return "ok";
})()
"#,
)
.expect("SVG href reflection should evaluate");
assert_eq!(result, "ok");
}