From c8bcb1cc99bcbef72dd076856bd1d4924cf9c63c Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 1 Sep 2026 11:23:22 +0800 Subject: [PATCH] fix(svg): implement live point lists --- .../src/context_bootstrap/geometry_runtime.rs | 36 ++++- .../src/context_bootstrap/runtime_state.rs | 1 + .../src/context_bootstrap/specs/registry.rs | 4 + .../context_bootstrap/svg_runtime/bindings.rs | 92 +++++++++++ .../context_bootstrap/svg_runtime/builders.rs | 101 +++++++++++- .../svg_runtime/callbacks.rs | 97 ++++++++++++ .../src/context_bootstrap/svg_runtime/mod.rs | 19 ++- .../dom_interfaces_and_collections.rs | 148 ++++++++++++++++++ moli-renderer-v8/src/web_api_interfaces.rs | 1 + moli-svg/src/length.rs | 13 ++ moli-svg/src/lib.rs | 22 ++- 11 files changed, 522 insertions(+), 12 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap/geometry_runtime.rs b/moli-renderer-v8/src/context_bootstrap/geometry_runtime.rs index c086d764f7..403efceddd 100644 --- a/moli-renderer-v8/src/context_bootstrap/geometry_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/geometry_runtime.rs @@ -14,6 +14,7 @@ const DOM_POINT_Y_SLOT: &str = "__moliDomPointY"; const DOM_POINT_Z_SLOT: &str = "__moliDomPointZ"; const DOM_POINT_W_SLOT: &str = "__moliDomPointW"; const DOM_POINT_RESTRICTED_NUMBER_SLOT: &str = "__moliDomPointRestrictedNumber"; +const DOM_POINT_READ_ONLY_SLOT: &str = "__moliDomPointReadOnly"; const DOM_MATRIX_M11_SLOT: &str = "__moliDomMatrixM11"; const DOM_MATRIX_M12_SLOT: &str = "__moliDomMatrixM12"; @@ -788,7 +789,15 @@ pub(in crate::context_bootstrap) fn build_dom_point_object<'s>( pub(in crate::context_bootstrap) fn build_svg_point_object<'s>( scope: &mut v8::PinScope<'s, '_>, ) -> v8::Local<'s, v8::Object> { - let object = build_dom_point_object(scope, 0.0, 0.0, 0.0, 1.0); + build_svg_point_object_with_values(scope, 0.0, 0.0) +} + +pub(in crate::context_bootstrap) fn build_svg_point_object_with_values<'s>( + scope: &mut v8::PinScope<'s, '_>, + x: f64, + y: f64, +) -> v8::Local<'s, v8::Object> { + let object = build_dom_point_object(scope, x, y, 0.0, 1.0); let restricted = v8::Boolean::new(scope, true); set_private_value( scope, @@ -799,6 +808,19 @@ pub(in crate::context_bootstrap) fn build_svg_point_object<'s>( object } +pub(in crate::context_bootstrap) fn set_svg_point_read_only<'s>( + scope: &mut v8::PinScope<'s, '_>, + point: v8::Local<'s, v8::Object>, + read_only: bool, +) { + set_private_value( + scope, + point, + DOM_POINT_READ_ONLY_SLOT, + v8::Boolean::new(scope, read_only).into(), + ); +} + fn build_dom_point_readonly_object<'s>( scope: &mut v8::PinScope<'s, '_>, x: f64, @@ -910,6 +932,17 @@ fn dom_point_setter_callback<'s>( throw_type_error(scope, "Illegal invocation"); return; } + if get_private_value(scope, args.this(), DOM_POINT_READ_ONLY_SLOT) + .is_some_and(|value| value.boolean_value(scope)) + { + throw_dom_exception( + scope, + "NoModificationAllowedError", + 7, + "The SVG point is read-only.", + ); + return; + } let context = webidl::Context::member("DOMPoint", slot); let value = if get_private_value(scope, args.this(), DOM_POINT_RESTRICTED_NUMBER_SLOT) .is_some_and(|value| value.boolean_value(scope)) @@ -933,6 +966,7 @@ fn dom_point_setter_callback<'s>( slot, v8::Number::new(scope, value).into(), ); + super::svg_runtime::reflect_svg_point_mutation(scope, args.this()); rv.set_undefined(); } diff --git a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs index 8dd549e788..373d2ccb2c 100644 --- a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs +++ b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs @@ -1907,6 +1907,7 @@ pub(crate) fn finish_context_bootstrap( ("SVGAnimatedNumber", "SVGAnimatedNumber"), ("SVGAnimatedInteger", "SVGAnimatedInteger"), ("SVGNumberList", "SVGNumberList"), + ("SVGPointList", "SVGPointList"), ("SVGStringList", "SVGStringList"), ("SVGAnimatedNumberList", "SVGAnimatedNumberList"), ("SVGAnimatedBoolean", "SVGAnimatedBoolean"), diff --git a/moli-renderer-v8/src/context_bootstrap/specs/registry.rs b/moli-renderer-v8/src/context_bootstrap/specs/registry.rs index 1fbb3e9765..32cd6342a9 100644 --- a/moli-renderer-v8/src/context_bootstrap/specs/registry.rs +++ b/moli-renderer-v8/src/context_bootstrap/specs/registry.rs @@ -148,6 +148,10 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[ interface: web_api_interfaces::SVGNumberList::DESCRIPTOR, kind: ConstructorKind::Illegal, }, + ConstructorSpec { + interface: web_api_interfaces::SVGPointList::DESCRIPTOR, + kind: ConstructorKind::Illegal, + }, ConstructorSpec { interface: web_api_interfaces::SVGStringList::DESCRIPTOR, kind: ConstructorKind::Illegal, 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 db4602befc..8e6384145a 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs @@ -184,6 +184,51 @@ struct SvgNumberListTemplateMethodsDeclaration { append_item: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGPointList, enumerable)] +struct SvgPointListTemplateMethodsDeclaration { + #[webapi(method = "clear", length = 0, callback = svg_point_list_clear_callback)] + clear: (), + + #[webapi( + method = "initialize", + length = 1, + callback = svg_point_list_initialize_callback + )] + initialize: (), + + #[webapi(method = "getItem", length = 1, callback = svg_point_list_get_item_callback)] + get_item: (), + + #[webapi( + method = "insertItemBefore", + length = 2, + callback = svg_point_list_insert_item_before_callback + )] + insert_item_before: (), + + #[webapi( + method = "replaceItem", + length = 2, + callback = svg_point_list_replace_item_callback + )] + replace_item: (), + + #[webapi( + method = "removeItem", + length = 1, + callback = svg_point_list_remove_item_callback + )] + remove_item: (), + + #[webapi( + method = "appendItem", + length = 1, + callback = svg_point_list_append_item_callback + )] + append_item: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::SVGStringList, enumerable)] struct SvgStringListTemplateMethodsDeclaration { @@ -1180,6 +1225,19 @@ struct SvgNumberListTemplateAccessorsDeclaration { number_of_items: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGPointList, enumerable)] +struct SvgPointListTemplateAccessorsDeclaration { + #[webapi(accessor_property = "length", getter = svg_point_list_length_getter)] + length: (), + + #[webapi( + accessor_property = "numberOfItems", + getter = svg_point_list_length_getter + )] + number_of_items: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::SVGStringList, enumerable)] struct SvgStringListTemplateAccessorsDeclaration { @@ -1485,6 +1543,24 @@ struct SvgGeometryElementPrototypeAccessorsDeclaration { path_length: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(name = "SVGAnimatedPoints", enumerable)] +struct SvgAnimatedPointsPrototypeAccessorsDeclaration { + #[webapi( + accessor_property = "points", + getter = svg_animated_points_getter, + data = callback_data_index_value(scope, 0) + )] + points: (), + + #[webapi( + accessor_property = "animatedPoints", + getter = svg_animated_points_getter, + data = callback_data_index_value(scope, 1) + )] + animated_points: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(name = "SVGFitToViewBox", enumerable)] struct SvgFitToViewBoxPrototypeAccessorsDeclaration { @@ -2189,6 +2265,13 @@ pub(super) fn install_svg_number_list_bindings<'s>( install_svg_value_list_bindings(scope, template, SvgListKind::Number); } +pub(super) fn install_svg_point_list_bindings<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, +) { + install_svg_value_list_bindings(scope, template, SvgListKind::Point); +} + pub(super) fn install_svg_string_list_bindings<'s>( scope: &mut v8::PinScope<'s, '_, ()>, template: v8::Local<'s, v8::FunctionTemplate>, @@ -2283,6 +2366,10 @@ pub(super) fn install_svg_value_list_bindings<'s>( SvgNumberListTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto); SvgNumberListTemplateMethodsDeclaration::initialize_prototype_template(scope, proto); } + SvgListKind::Point => { + SvgPointListTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto); + SvgPointListTemplateMethodsDeclaration::initialize_prototype_template(scope, proto); + } } } @@ -2607,6 +2694,11 @@ pub(super) fn install_svg_element_accessor_bindings<'s>( scope, prototype, ); } + "SVGPolygonElement" | "SVGPolylineElement" => { + SvgAnimatedPointsPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } "SVGMarkerElement" => { SvgMarkerElementPrototypeAccessorsDeclaration::initialize_prototype_template( scope, prototype, 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 9bc8dfccac..24f0e87e51 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs @@ -298,6 +298,33 @@ struct SvgNumberListObjectDeclaration<'scope> { append_item: (), } +#[derive(WebApiObject)] +#[webapi(interface = web_api_interfaces::SVGPointList, own_to_string_tag = "SVGPointList")] +struct SvgPointListObjectDeclaration<'scope> { + #[webapi(slot = SVG_POINT_LIST_ITEMS_SLOT)] + items: Vec>, + #[webapi(slot = SVG_VALUE_LIST_READ_ONLY_SLOT)] + read_only: bool, + #[webapi(method, callback = svg_point_list_clear_callback, length = 0)] + clear: (), + #[webapi(method, callback = svg_point_list_initialize_callback, length = 1)] + initialize: (), + #[webapi(method, callback = svg_point_list_get_item_callback, length = 1)] + get_item: (), + #[webapi( + method, + callback = svg_point_list_insert_item_before_callback, + length = 2 + )] + insert_item_before: (), + #[webapi(method, callback = svg_point_list_replace_item_callback, length = 2)] + replace_item: (), + #[webapi(method, callback = svg_point_list_remove_item_callback, length = 1)] + remove_item: (), + #[webapi(method, callback = svg_point_list_append_item_callback, length = 1)] + append_item: (), +} + #[derive(WebApiObject)] #[webapi(interface = web_api_interfaces::SVGStringList, own_to_string_tag = "SVGStringList")] struct SvgStringListObjectDeclaration<'scope> { @@ -521,6 +548,7 @@ pub(super) fn build_svg_animated_value_list_for_attribute<'s>( let object = match kind { SvgListKind::Length => build_svg_animated_length_list(scope), SvgListKind::Number => build_svg_animated_number_list(scope), + SvgListKind::Point => unreachable!("SVGPointList is not an animated wrapper"), }; sync_svg_animated_value_list_from_owner_attribute(scope, object, owner, attribute, kind); if let Some(base_val) = svg_animated_value_list_member(scope, object, "baseVal", kind) { @@ -951,6 +979,32 @@ pub(super) fn build_svg_number_list<'s>( object } +pub(super) fn build_svg_point_list<'s>( + scope: &mut v8::PinScope<'s, '_>, + read_only: bool, +) -> v8::Local<'s, v8::Object> { + let template = v8::ObjectTemplate::new(scope); + configure_svg_value_list_indexed_property_handler(template); + let object = template + .new_instance(scope) + .expect("SVGPointList object template should instantiate"); + SvgPointListObjectDeclaration::new(Vec::new(), read_only) + .bind_into(scope, object) + .expect("SVGPointList declaration should bind"); + object +} + +pub(super) fn build_svg_point_list_for_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + read_only: bool, +) -> v8::Local<'s, v8::Object> { + let list = build_svg_point_list(scope, read_only); + set_svg_value_list_owner_attribute(scope, list, owner, "points"); + sync_svg_value_list_from_owner_attribute(scope, list, SvgListKind::Point); + list +} + pub(super) fn build_svg_string_list_for_attribute<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, @@ -1834,16 +1888,22 @@ pub(super) fn svg_value_list_item_or_throw<'s>( value: v8::Local<'s, v8::Value>, kind: SvgListKind, ) -> Option> { - let (brand_slot, interface) = match kind { - SvgListKind::Length => (SVG_LENGTH_VALUE_SLOT, "SVGLength"), - SvgListKind::Number => (SVG_NUMBER_VALUE_SLOT, "SVGNumber"), - }; let object = v8::Local::::try_from(value).ok(); - if let Some(object) = object - && get_private_value(scope, object, brand_slot).is_some() - { - return Some(object); + let valid = object.is_some_and(|object| match kind { + SvgListKind::Length => get_private_value(scope, object, SVG_LENGTH_VALUE_SLOT).is_some(), + SvgListKind::Number => get_private_value(scope, object, SVG_NUMBER_VALUE_SLOT).is_some(), + SvgListKind::Point => { + dom_point_clone_data(scope, object).is_some_and(|(mutable, _)| mutable) + } + }); + if valid { + return object; } + let interface = match kind { + SvgListKind::Length => "SVGLength", + SvgListKind::Number => "SVGNumber", + SvgListKind::Point => "DOMPoint", + }; webidl::throw_type_error( scope, &format!("Argument 1 can not be converted to {interface}"), @@ -1867,6 +1927,7 @@ pub(super) fn svg_value_list_items<'s>( let slot = match kind { SvgListKind::Length => SVG_LENGTH_LIST_ITEMS_SLOT, SvgListKind::Number => SVG_NUMBER_LIST_ITEMS_SLOT, + SvgListKind::Point => SVG_POINT_LIST_ITEMS_SLOT, }; get_private_value(scope, object, slot) .and_then(|value| v8::Local::::try_from(value).ok()) @@ -1882,12 +1943,23 @@ pub(super) fn set_svg_value_list_items<'s>( let slot = match kind { SvgListKind::Length => SVG_LENGTH_LIST_ITEMS_SLOT, SvgListKind::Number => SVG_NUMBER_LIST_ITEMS_SLOT, + SvgListKind::Point => SVG_POINT_LIST_ITEMS_SLOT, }; if let Some(current) = get_private_value(scope, object, slot) .and_then(|value| v8::Local::::try_from(value).ok()) { detach_svg_value_list_items(scope, current); } + if matches!(kind, SvgListKind::Point) && svg_value_list_is_read_only(scope, object) { + for index in 0..items.length() { + if let Some(point) = items + .get_index(scope, index) + .and_then(|value| v8::Local::::try_from(value).ok()) + { + set_svg_point_read_only(scope, point, true); + } + } + } attach_svg_value_list_items(scope, object, items); set_private_value(scope, object, slot, items.into()); } @@ -2060,6 +2132,11 @@ pub(super) fn build_svg_value_list_item_values_from_attribute<'s>( .into_iter() .map(|value| build_svg_number(scope, value)) .collect(), + SvgListKind::Point => svg_geometry::parse_point_list(raw) + .unwrap_or_default() + .into_iter() + .map(|(x, y)| build_svg_point_object_with_values(scope, x, y)) + .collect(), } } @@ -2160,6 +2237,14 @@ pub(super) fn serialize_svg_value_list_item<'s>( let value = svg_number_slot(scope, item, SVG_NUMBER_VALUE_SLOT)?; Some(svg_geometry::serialize_number(value)) } + SvgListKind::Point => { + let point = dom_point_init_from_object(scope, item); + Some(format!( + "{} {}", + svg_geometry::serialize_number(point.x), + svg_geometry::serialize_number(point.y) + )) + } } } 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 4b0bf1ed9d..0cd4621e98 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs @@ -119,6 +119,11 @@ const SVG_TEXT_POSITIONING_LIST_ATTRIBUTES: &[(&str, &str, SvgListKind)] = &[ ), ]; +const SVG_ANIMATED_POINTS_PROPERTIES: &[(&str, &str, bool)] = &[ + ("points", SVG_POINTS_SLOT, false), + ("animatedPoints", SVG_ANIMATED_POINTS_SLOT, true), +]; + fn require_svg_receiver<'s>( scope: &mut v8::PinScope<'s, '_>, receiver: v8::Local<'s, v8::Object>, @@ -954,6 +959,56 @@ pub(super) fn svg_geometry_path_length_getter<'s>( rv.set(value.into()); } +pub(super) fn svg_animated_points_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some((name, cache_slot, read_only)) = callback_data_item( + scope, + &args, + SVG_ANIMATED_POINTS_PROPERTIES, + "SVGAnimatedPoints properties", + ) else { + rv.set_undefined(); + return; + }; + let owner = args.this(); + let Ok((runtime_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner) + else { + webidl::throw_type_error( + scope, + &format!("SVGAnimatedPoints.{name} called on incompatible receiver."), + ); + return; + }; + let is_animated_points = unsafe { &*runtime_ptr } + .dom_host() + .node(handle) + .and_then(|node| node.as_element()) + .is_some_and(|element| { + element.is_svg_element("polygon") || element.is_svg_element("polyline") + }); + if !is_animated_points { + webidl::throw_type_error( + scope, + &format!("SVGAnimatedPoints.{name} called on incompatible receiver."), + ); + return; + } + if let Some(value) = get_private_value(scope, owner, cache_slot) { + if let Ok(list) = v8::Local::::try_from(value) { + sync_svg_value_list_from_owner_attribute(scope, list, SvgListKind::Point); + } + rv.set(value); + return; + } + let list = build_svg_point_list_for_attribute(scope, owner, read_only); + set_private_value(scope, owner, cache_slot, list.into()); + rv.set(list.into()); +} + pub(super) fn svg_text_content_text_length_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -2458,6 +2513,7 @@ fn svg_value_list_metadata(kind: SvgListKind) -> (&'static str, &'static str) { match kind { SvgListKind::Length => (SVG_LENGTH_LIST_ITEMS_SLOT, "SVGLengthList"), SvgListKind::Number => (SVG_NUMBER_LIST_ITEMS_SLOT, "SVGNumberList"), + SvgListKind::Point => (SVG_POINT_LIST_ITEMS_SLOT, "SVGPointList"), } } @@ -2469,6 +2525,8 @@ fn svg_value_list_kind<'s>( Some(SvgListKind::Length) } else if web_api_interfaces::SVGNumberList::is_instance(scope, list) { Some(SvgListKind::Number) + } else if get_private_value(scope, list, SVG_POINT_LIST_ITEMS_SLOT).is_some() { + Some(SvgListKind::Point) } else { None } @@ -2498,6 +2556,45 @@ fn require_svg_value_list_items<'s>( Some(svg_value_list_items(scope, list, kind)) } +macro_rules! define_svg_point_list_callback { + ($name:ident, $callback:ident) => { + pub(super) fn $name<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, + ) { + $callback(scope, args, rv, SvgListKind::Point); + } + }; +} + +define_svg_point_list_callback!(svg_point_list_length_getter, svg_value_list_length_getter); +define_svg_point_list_callback!(svg_point_list_clear_callback, svg_value_list_clear_callback); +define_svg_point_list_callback!( + svg_point_list_initialize_callback, + svg_value_list_initialize_callback +); +define_svg_point_list_callback!( + svg_point_list_get_item_callback, + svg_value_list_get_item_callback +); +define_svg_point_list_callback!( + svg_point_list_insert_item_before_callback, + svg_value_list_insert_item_before_callback +); +define_svg_point_list_callback!( + svg_point_list_replace_item_callback, + svg_value_list_replace_item_callback +); +define_svg_point_list_callback!( + svg_point_list_remove_item_callback, + svg_value_list_remove_item_callback +); +define_svg_point_list_callback!( + svg_point_list_append_item_callback, + svg_value_list_append_item_callback +); + pub(super) fn svg_length_list_length_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs index 51ddc5cc48..cd4cc47086 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs @@ -1,5 +1,10 @@ use super::{ - build_dom_point_object, build_dom_rect_object, optional_dom_point_init_arg, + build_dom_point_object, build_dom_rect_object, + geometry_runtime::{ + build_svg_point_object_with_values, dom_point_clone_data, dom_point_init_from_object, + set_svg_point_read_only, + }, + optional_dom_point_init_arg, selection::{selection_clear, selection_dispatch_change, selection_has_range}, selection_value_for_window, }; @@ -85,6 +90,9 @@ const SVG_ANIMATED_INTEGER_PROPERTY_INDEX_SLOT: &str = "__moliSvgAnimatedInteger const SVG_ANIMATED_NUMBER_LIST_BASE_VAL_SLOT: &str = "__moliSvgAnimatedNumberListBaseVal"; const SVG_ANIMATED_NUMBER_LIST_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedNumberListAnimVal"; const SVG_NUMBER_LIST_ITEMS_SLOT: &str = "__moliSvgNumberListItems"; +const SVG_POINT_LIST_ITEMS_SLOT: &str = "__moliSvgPointListItems"; +const SVG_POINTS_SLOT: &str = "__moliSvgPoints"; +const SVG_ANIMATED_POINTS_SLOT: &str = "__moliSvgAnimatedPoints"; const SVG_STRING_LIST_ITEMS_SLOT: &str = "__moliSvgStringListItems"; const SVG_STRING_LIST_OWNER_ELEMENT_SLOT: &str = "__moliSvgStringListOwnerElement"; const SVG_STRING_LIST_OWNER_ATTRIBUTE_SLOT: &str = "__moliSvgStringListOwnerAttribute"; @@ -163,6 +171,7 @@ const SVG_TEST_STRING_LIST_ATTRIBUTES: &[(&str, &str)] = &[ enum SvgListKind { Length, Number, + Point, } #[derive(Clone, Copy)] @@ -1289,6 +1298,7 @@ pub(in crate::context_bootstrap) fn install_svg_template_bindings<'s>( "SVGAnimatedNumber" => bindings::install_svg_animated_number_bindings(scope, template), "SVGAnimatedInteger" => bindings::install_svg_animated_integer_bindings(scope, template), "SVGNumberList" => bindings::install_svg_number_list_bindings(scope, template), + "SVGPointList" => bindings::install_svg_point_list_bindings(scope, template), "SVGStringList" => bindings::install_svg_string_list_bindings(scope, template), "SVGAnimatedNumberList" => { bindings::install_svg_animated_number_list_bindings(scope, template) @@ -1316,3 +1326,10 @@ pub(in crate::context_bootstrap) fn install_svg_template_bindings<'s>( _ => {} } } + +pub(in crate::context_bootstrap) fn reflect_svg_point_mutation<'s>( + scope: &mut v8::PinScope<'s, '_>, + point: v8::Local<'s, v8::Object>, +) { + builders::reflect_svg_value_list_item_to_owner_list(scope, point, SvgListKind::Point); +} 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 c22e8496f4..2a8b4160aa 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 @@ -3102,3 +3102,151 @@ fn svg_value_lists_enforce_item_types_indices_and_read_only_anim_values() { assert_eq!(result, "ok"); } + +#[test] +fn svg_point_lists_are_live_mutable_and_clear_invalid_content() { + let mut vm = new_storage_test_vm("https://svg-point-list-semantics.test/"); + + let result = vm + .eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const errorName = callback => { + try { + callback(); + return "none"; + } catch (error) { + return error.name; + } + }; + const ns = "http://www.w3.org/2000/svg"; + const polygon = document.createElementNS(ns, "polygon"); + const polyline = document.createElementNS(ns, "polyline"); + const svg = document.createElementNS(ns, "svg"); + + assert(typeof SVGPointList === "function", "constructor exposed"); + assert(errorName(() => new SVGPointList()) === "TypeError", + "illegal constructor"); + for (const [constructor, element] of [ + [SVGPolygonElement, polygon], + [SVGPolylineElement, polyline], + ]) { + const pointsDescriptor = Object.getOwnPropertyDescriptor( + constructor.prototype, + "points", + ); + const animatedDescriptor = Object.getOwnPropertyDescriptor( + constructor.prototype, + "animatedPoints", + ); + assert(typeof pointsDescriptor.get === "function" && + pointsDescriptor.set === undefined, `${constructor.name}.points descriptor`); + assert(typeof animatedDescriptor.get === "function" && + animatedDescriptor.set === undefined, + `${constructor.name}.animatedPoints descriptor`); + assert(pointsDescriptor.enumerable && pointsDescriptor.configurable && + animatedDescriptor.enumerable && animatedDescriptor.configurable, + `${constructor.name} descriptor flags`); + assert(element.points instanceof SVGPointList && + element.animatedPoints instanceof SVGPointList, + `${constructor.name} point list interfaces`); + assert(element.points === element.points && + element.animatedPoints === element.animatedPoints && + element.points !== element.animatedPoints, + `${constructor.name} SameObject lists`); + } + + polygon.setAttribute("points", "0,0 100,0 100,100 0,100"); + const points = polygon.points; + const animatedPoints = polygon.animatedPoints; + assert(Object.prototype.toString.call(points) === "[object SVGPointList]", + "point list tag"); + assert(points.length === 4 && points.numberOfItems === 4, + "valid content points"); + assert(points.getItem(1) === points[1] && points[1] instanceof DOMPoint && + points[1] instanceof SVGPoint && points[1].x === 100 && points[1].y === 0, + "indexed point identity and values"); + + polygon.setAttribute("points", "0,0 100,0 INVALID"); + assert(points.numberOfItems === 0, + "invalid token clears the whole point list"); + polygon.setAttribute("points", "0,0 100,0 20"); + assert(points.numberOfItems === 2, + "missing final y coordinate truncates the point list"); + polygon.setAttribute("points", "0,0 100,0 20,"); + assert(points.numberOfItems === 2, + "trailing comma with missing y truncates the point list"); + + polygon.setAttribute("points", "0,0 10,20"); + const first = points[0]; + first.x = 2; + assert(polygon.getAttribute("points") === "2 0 10 20", + "point coordinate mutation reflects to content"); + + const point = svg.createSVGPoint(); + point.x = 5; + point.y = 6; + points.clear(); + assert(points.length === 0 && polygon.getAttribute("points") === "", + "clear reflects an empty list"); + assert(points.initialize(point) === point && points[0] === point, + "initialize keeps point identity"); + assert(polygon.getAttribute("points") === "5 6", "initialize reflection"); + point.x = 7; + assert(polygon.getAttribute("points") === "7 6", "owned point stays live"); + + const second = svg.createSVGPoint(); + second.x = 8; + second.y = 9; + assert(points.appendItem(second) === second && points.length === 2, + "appendItem"); + const third = svg.createSVGPoint(); + third.x = 10; + third.y = 11; + assert(points.insertItemBefore(third, 1) === third && points.length === 3, + "insertItemBefore"); + const replacement = svg.createSVGPoint(); + replacement.x = 12; + replacement.y = 13; + assert(points.replaceItem(replacement, 0) === replacement && + points[0] === replacement, + "replaceItem"); + assert(points.removeItem(1) === third && points.length === 2, + "removeItem"); + points[0] = point; + assert(points[0] === point, "indexed setter"); + + for (const invalid of [1, "point", polygon, null]) { + assert(errorName(() => points.appendItem(invalid)) === "TypeError", + "point item type enforcement"); + } + assert(errorName(() => points.getItem(99)) === "IndexSizeError", + "getItem bounds"); + + polygon.setAttribute("points", "1,2 3,4"); + assert(animatedPoints.length === 2 && animatedPoints[1].x === 3, + "animatedPoints live synchronization"); + assert(errorName(() => animatedPoints.clear()) === "NoModificationAllowedError", + "animatedPoints list is read-only"); + assert(errorName(() => { animatedPoints[0].x = 9; }) === + "NoModificationAllowedError", "animated point is read-only"); + assert(errorName(() => SVGPointList.prototype.clear.call({})) === "TypeError", + "point list receiver brand"); + + const pointsDescriptor = Object.getOwnPropertyDescriptor( + SVGPolygonElement.prototype, + "points", + ); + assert(errorName(() => pointsDescriptor.get.call(svg)) === "TypeError", + "animated points receiver brand"); + return "ok"; + })() + "#, + ) + .expect("SVG point list semantics probe should evaluate"); + + assert_eq!(result, "ok"); +} diff --git a/moli-renderer-v8/src/web_api_interfaces.rs b/moli-renderer-v8/src/web_api_interfaces.rs index 7dad6ae39c..b95c50766e 100644 --- a/moli-renderer-v8/src/web_api_interfaces.rs +++ b/moli-renderer-v8/src/web_api_interfaces.rs @@ -459,6 +459,7 @@ interfaces! { SVGTextPositioningElement: SVGTextContentElement; SVGTitleElement: SVGElement; SVGUnitTypes; + SVGPointList; SVGStringList; SVGTransform; SVGTransformList; diff --git a/moli-svg/src/length.rs b/moli-svg/src/length.rs index d5b60eea0b..4f2dad9f75 100644 --- a/moli-svg/src/length.rs +++ b/moli-svg/src/length.rs @@ -82,6 +82,19 @@ pub fn parse_number_list(raw: &str) -> Option> { .collect() } +pub fn parse_point_list(raw: &str) -> Option> { + let values = parse_number_list(raw).or_else(|| { + let raw = raw.trim_end(); + parse_number_list(raw.strip_suffix(',')?) + })?; + Some( + values + .chunks_exact(2) + .map(|point| (point[0], point[1])) + .collect(), + ) +} + fn svg_length(length: SvgTypesLength) -> SvgLength { SvgLength { value: length.number, diff --git a/moli-svg/src/lib.rs b/moli-svg/src/lib.rs index 0407c40cb8..4b15cc0d45 100644 --- a/moli-svg/src/lib.rs +++ b/moli-svg/src/lib.rs @@ -12,6 +12,7 @@ pub use geometry::{ }; pub use length::{ SvgLength, SvgLengthUnit, parse_length, parse_length_list, parse_number, parse_number_list, + parse_point_list, }; pub use matrix::{SvgMatrixComponents, serialize_number}; pub use transform::{ @@ -26,14 +27,31 @@ mod tests { SvgMatrixComponents, SvgTransform, SvgTransformKind, bounding_box_for_element, bounding_box_for_segments, bounding_box_for_transformed_element, consolidate_transform_matrices, is_point_in_fill, parse_length, parse_length_list, - parse_number, parse_number_list, parse_transform_attribute, point_at_length, - segments_for_element, serialize_number, serialize_transform_list, + parse_number, parse_number_list, parse_point_list, parse_transform_attribute, + point_at_length, segments_for_element, serialize_number, serialize_transform_list, }; fn path_segments(raw: &str) -> Vec { segments_for_element(SvgGeometryElement::Path { d: raw.to_owned() }) } + #[test] + fn point_list_parser_rejects_invalid_tokens_and_truncates_incomplete_pairs() { + assert_eq!( + parse_point_list("0,0 100,0 100,100 0,100"), + Some(vec![(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)]) + ); + assert_eq!( + parse_point_list("0,0 100,0 20"), + Some(vec![(0.0, 0.0), (100.0, 0.0)]) + ); + assert_eq!( + parse_point_list("0,0 100,0 20,"), + Some(vec![(0.0, 0.0), (100.0, 0.0)]) + ); + assert_eq!(parse_point_list("0,0 100,0 INVALID"), None); + } + fn polyline_segments(raw: &str) -> Vec { segments_for_element(SvgGeometryElement::Polyline { points: raw.to_owned(),