From e08c77b5cf00a5b5d914779c92b8c5c7cbba38f7 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sun, 30 Aug 2026 18:57:17 +0800 Subject: [PATCH] fix(svg): implement ownerSVGElement --- .../context_bootstrap/svg_runtime/bindings.rs | 6 ++ .../svg_runtime/callbacks.rs | 57 +++++++++++++++ .../live_document/geometry_and_svg.rs | 71 +++++++++++++++++++ 3 files changed, 134 insertions(+) diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs index e3e5549401..17891d233e 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs @@ -968,6 +968,12 @@ struct SvgElementPrototypeAccessorsDeclaration { getter = svg_element_class_name_getter )] class_name: (), + + #[webapi( + accessor_property = "ownerSVGElement", + getter = svg_element_owner_svg_element_getter + )] + owner_svg_element: (), } #[derive(WebApiFunctionTemplate)] diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs index cb3183503a..b9b50904f0 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs @@ -136,6 +136,63 @@ pub(super) fn svg_element_class_name_getter<'s>( svg_animated_string_attribute_getter(scope, args, rv, SVG_ELEMENT_CLASS_NAME_SLOT, "class"); } +pub(super) fn svg_element_owner_svg_element_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + let Ok((runtime_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, args.this()) + else { + webidl::throw_type_error( + scope, + "SVGElement.ownerSVGElement called on incompatible receiver.", + ); + return; + }; + let owner_handle = { + let runtime = unsafe { &*runtime_ptr }; + let Some(node) = runtime.dom_host().node(handle) else { + rv.set_null(); + return; + }; + if node.namespace() != Some(crate::native_bridge::document::SVG_NS) { + webidl::throw_type_error( + scope, + "SVGElement.ownerSVGElement called on incompatible receiver.", + ); + return; + } + + let mut current = node.parent_node_id(); + let mut owner = None; + while let Some(candidate) = current { + let Some(ancestor) = runtime.dom_host().node(candidate) else { + break; + }; + if ancestor.namespace() != Some(crate::native_bridge::document::SVG_NS) + || ancestor.local_name() == Some("foreignObject") + { + break; + } + if ancestor.local_name() == Some("svg") { + owner = Some(candidate); + break; + } + current = ancestor.parent_node_id(); + } + owner + }; + + let Some(owner) = owner_handle.and_then(|owner| { + crate::native_bridge::document::detached_native_object_for_handle(scope, runtime_ptr, owner) + }) else { + rv.set_null(); + return; + }; + rv.set(owner.into()); +} + pub(super) fn svg_uri_href_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/geometry_and_svg.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/geometry_and_svg.rs index 40ccb20cb3..153764eafa 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/geometry_and_svg.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/geometry_and_svg.rs @@ -284,6 +284,77 @@ fn svg_historical_interfaces_keep_current_element_constructors_only() { assert_eq!(result, "ok"); } +#[test] +fn svg_owner_svg_element_tracks_the_nearest_svg_fragment_root() { + let mut vm = new_parsed_test_vm( + "https://svg-owner-element.test/", + r#"
+ + + + +
"#, + ); + + let result = vm + .eval( + r##" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const ns = "http://www.w3.org/2000/svg"; + const outer = document.querySelector("#outer"); + const circle = document.querySelector("#circle"); + const inner = document.querySelector("#inner"); + const rect = document.querySelector("#rect"); + const foreignSvg = document.querySelector("#foreign-svg"); + const foreignInner = document.querySelector("#foreign-inner"); + + assert(outer.ownerSVGElement === null, "outer SVG"); + assert(circle.ownerSVGElement === outer, "descendant of outer SVG"); + assert(inner.ownerSVGElement === outer, "nested SVG"); + assert(rect.ownerSVGElement === inner, "descendant of nested SVG"); + assert(foreignSvg.ownerSVGElement === null, "foreignObject starts a new SVG fragment"); + assert(foreignInner.ownerSVGElement === foreignSvg, "nested foreignObject SVG"); + + document.querySelector("#container").remove(); + assert(circle.ownerSVGElement === outer, "detached outer SVG descendant"); + assert(inner.ownerSVGElement === outer, "detached nested SVG"); + assert(rect.ownerSVGElement === inner, "detached nested SVG descendant"); + + const standalone = document.createElementNS(ns, "ellipse"); + assert(standalone.ownerSVGElement === null, "standalone SVG element"); + + const svgDocument = document.implementation.createDocument(ns, "svg", null); + const documentRoot = svgDocument.documentElement; + const documentRect = svgDocument.createElementNS(ns, "rect"); + documentRoot.append(documentRect); + assert(documentRoot.ownerSVGElement === null, "SVG document root"); + assert(documentRect.ownerSVGElement === documentRoot, "SVG document child"); + + const descriptor = Object.getOwnPropertyDescriptor( + SVGElement.prototype, + "ownerSVGElement", + ); + assert(typeof descriptor.get === "function", "prototype getter"); + assert(descriptor.enumerable && descriptor.configurable, "getter flags"); + let incompatibleReceiver = false; + try { + descriptor.get.call(document.body); + } catch (error) { + incompatibleReceiver = error instanceof TypeError; + } + assert(incompatibleReceiver, "incompatible receiver"); + return "ok"; + })() + "##, + ) + .expect("SVG ownerSVGElement probe should evaluate"); + + assert_eq!(result, "ok"); +} + #[test] fn svg_geometry_queries_use_computed_paths_live_tree_and_kurbo_bounds() { let mut vm = new_parsed_test_vm(