diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs index a3361f6600..d661cb2564 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs @@ -2151,9 +2151,20 @@ pub(super) fn svg_owner_attribute_value<'s>( attribute: &str, ) -> Option { 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 { diff --git a/moli-renderer-v8/src/native_bridge/document.rs b/moli-renderer-v8/src/native_bridge/document.rs index 84ffd81ea5..0f660a477f 100644 --- a/moli-renderer-v8/src/native_bridge/document.rs +++ b/moli-renderer-v8/src/native_bridge/document.rs @@ -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)] diff --git a/moli-renderer-v8/src/native_bridge/element/trusted_types.rs b/moli-renderer-v8/src/native_bridge/element/trusted_types.rs index 2f1b46464b..59cb15dd4c 100644 --- a/moli-renderer-v8/src/native_bridge/element/trusted_types.rs +++ b/moli-renderer-v8/src/native_bridge/element/trusted_types.rs @@ -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 { 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) } diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/dom_interfaces_and_collections.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/dom_interfaces_and_collections.rs index d8c89c0faa..4df8ce801d 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/dom_interfaces_and_collections.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/dom_interfaces_and_collections.rs @@ -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"); +}