From 111afae3bb6504b67fd941b051bca9fb4f64ebae Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 30 Sep 2026 21:30:25 +0800 Subject: [PATCH] feat(svg): expose native value and element interfaces Completes native SVG value and element interface exposure in one independently mergeable PR against main, refreshing the former SVG constructor PR. Animated scalar values, angles, preserve-aspect-ratio values and live point/string/number/length/transform lists provide the shared native DOM foundation for the added elements. Adds the remaining component-transfer, flood, image, merge, merge-node and tile filter interfaces; SVGSwitchElement and SVGMPathElement; and the animation, animate, set, animate-motion and animate-transform interfaces. Native factories, inheritance, SVGTests lists, attribute reflection, receiver validation and animation event handlers use native objects and the existing DOM backend. Tests cover main/iframe realms, live and windowless documents, native identity, value/list mutation, units, Trusted Types, animation target resolution, handlers and timing argument validation. All internal SVG dependencies are included here, with no external PR merge order. Filter raster rendering and SMIL scheduling remain unimplemented. Timing controls reject NotSupportedError, getStartTime reports no interval, and getCurrentTime remains zero. The separate SVG path parser changes and animated-string UTF-16 storage migration are excluded. The retained length synchronization is included; a separate ignored regression records the lone-surrogate limitation in the current DOM attribute storage. SVG event-handler properties reuse main's existing shared event backend. Tests verify replacement, clearing, canonical event types, cancellation and realm/receiver identity; complete handler/listener registration-order semantics remain part of the separate event-handler backend migration. --- .../src/native/element/interface_names.rs | 30 +- moli-dom/src/native/element/tests.rs | 65 + .../bridge_descriptor/descriptors.rs | 128 +- .../src/context_bootstrap/geometry_runtime.rs | 175 +- .../src/context_bootstrap/runtime_state.rs | 13 + .../src/context_bootstrap/specs/registry.rs | 40 + .../svg_runtime/animation.rs | 197 ++ .../context_bootstrap/svg_runtime/bindings.rs | 1546 ++++++++++- .../context_bootstrap/svg_runtime/builders.rs | 2268 +++++++++++++++- .../svg_runtime/callbacks.rs | 2301 ++++++++++++++++- .../context_bootstrap/svg_runtime/filters.rs | 129 + .../src/context_bootstrap/svg_runtime/mod.rs | 1054 +++++++- .../src/context_bootstrap/svg_runtime/rect.rs | 78 +- moli-renderer-v8/src/host/events.rs | 6 +- .../src/native_bridge/document.rs | 1 + moli-renderer-v8/src/native_bridge/element.rs | 4 +- .../element/event_handlers/generic.rs | 17 + .../element/event_handlers/mod.rs | 6 +- .../native_bridge/element/trusted_types.rs | 85 + .../tests/browser_api/trusted_types.rs | 111 + .../dom_interfaces_and_collections.rs | 233 ++ .../live_document/geometry_and_svg.rs | 1825 +++++++++++++ .../live_document/svg_animation_interfaces.js | 174 ++ .../live_document/svg_filter_primitives.js | 130 + .../live_document/svg_switch_mpath.js | 119 + moli-renderer-v8/src/web_api_interfaces.rs | 53 +- moli-svg/src/length.rs | 163 +- moli-svg/src/lib.rs | 34 +- 28 files changed, 10785 insertions(+), 200 deletions(-) create mode 100644 moli-renderer-v8/src/context_bootstrap/svg_runtime/animation.rs create mode 100644 moli-renderer-v8/src/context_bootstrap/svg_runtime/filters.rs create mode 100644 moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/svg_animation_interfaces.js create mode 100644 moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/svg_filter_primitives.js create mode 100644 moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/svg_switch_mpath.js diff --git a/moli-dom/src/native/element/interface_names.rs b/moli-dom/src/native/element/interface_names.rs index 2ed4f5b1f2..7706ab6cb8 100644 --- a/moli-dom/src/native/element/interface_names.rs +++ b/moli-dom/src/native/element/interface_names.rs @@ -94,23 +94,46 @@ pub fn svg_element_interface_name(local_name: &str) -> &'static str { "animate" => "SVGAnimateElement", "animateMotion" => "SVGAnimateMotionElement", "animateTransform" => "SVGAnimateTransformElement", - "set" => "SVGSetElement", "circle" => "SVGCircleElement", + "clipPath" => "SVGClipPathElement", "defs" => "SVGDefsElement", "desc" => "SVGDescElement", "ellipse" => "SVGEllipseElement", + "feBlend" => "SVGFEBlendElement", + "feColorMatrix" => "SVGFEColorMatrixElement", "feComponentTransfer" => "SVGFEComponentTransferElement", + "feComposite" => "SVGFECompositeElement", + "feConvolveMatrix" => "SVGFEConvolveMatrixElement", + "feDiffuseLighting" => "SVGFEDiffuseLightingElement", + "feDisplacementMap" => "SVGFEDisplacementMapElement", + "feDistantLight" => "SVGFEDistantLightElement", + "feDropShadow" => "SVGFEDropShadowElement", "feFlood" => "SVGFEFloodElement", + "feFuncA" => "SVGFEFuncAElement", + "feFuncB" => "SVGFEFuncBElement", + "feFuncG" => "SVGFEFuncGElement", + "feFuncR" => "SVGFEFuncRElement", + "feGaussianBlur" => "SVGFEGaussianBlurElement", "feImage" => "SVGFEImageElement", "feMerge" => "SVGFEMergeElement", "feMergeNode" => "SVGFEMergeNodeElement", + "feMorphology" => "SVGFEMorphologyElement", + "feOffset" => "SVGFEOffsetElement", + "fePointLight" => "SVGFEPointLightElement", + "feSpecularLighting" => "SVGFESpecularLightingElement", + "feSpotLight" => "SVGFESpotLightElement", "feTile" => "SVGFETileElement", + "feTurbulence" => "SVGFETurbulenceElement", + "filter" => "SVGFilterElement", "foreignObject" => "SVGForeignObjectElement", "g" => "SVGGElement", "image" => "SVGImageElement", "line" => "SVGLineElement", "linearGradient" => "SVGLinearGradientElement", + "marker" => "SVGMarkerElement", + "mask" => "SVGMaskElement", "metadata" => "SVGMetadataElement", + "mpath" => "SVGMPathElement", "path" => "SVGPathElement", "pattern" => "SVGPatternElement", "polygon" => "SVGPolygonElement", @@ -118,15 +141,18 @@ pub fn svg_element_interface_name(local_name: &str) -> &'static str { "radialGradient" => "SVGRadialGradientElement", "rect" => "SVGRectElement", "script" => "SVGScriptElement", + "set" => "SVGSetElement", + "stop" => "SVGStopElement", "svg" => "SVGSVGElement", "style" => "SVGStyleElement", "switch" => "SVGSwitchElement", - "mpath" => "SVGMPathElement", "symbol" => "SVGSymbolElement", "text" => "SVGTextElement", + "textPath" => "SVGTextPathElement", "title" => "SVGTitleElement", "tspan" => "SVGTSpanElement", "use" => "SVGUseElement", + "view" => "SVGViewElement", _ => "SVGElement", } } diff --git a/moli-dom/src/native/element/tests.rs b/moli-dom/src/native/element/tests.rs index f6b0e6ba0a..27561d42c7 100644 --- a/moli-dom/src/native/element/tests.rs +++ b/moli-dom/src/native/element/tests.rs @@ -221,9 +221,69 @@ fn html_element_interface_name_covers_replay_tags() { fn svg_element_interface_name_specializes_standard_elements() { assert_eq!(svg_element_interface_name("a"), "SVGAElement"); assert_eq!(svg_element_interface_name("circle"), "SVGCircleElement"); + assert_eq!(svg_element_interface_name("clipPath"), "SVGClipPathElement"); assert_eq!(svg_element_interface_name("defs"), "SVGDefsElement"); assert_eq!(svg_element_interface_name("desc"), "SVGDescElement"); assert_eq!(svg_element_interface_name("ellipse"), "SVGEllipseElement"); + assert_eq!(svg_element_interface_name("feBlend"), "SVGFEBlendElement"); + assert_eq!( + svg_element_interface_name("feColorMatrix"), + "SVGFEColorMatrixElement" + ); + assert_eq!( + svg_element_interface_name("feComposite"), + "SVGFECompositeElement" + ); + assert_eq!( + svg_element_interface_name("feConvolveMatrix"), + "SVGFEConvolveMatrixElement" + ); + assert_eq!( + svg_element_interface_name("feDiffuseLighting"), + "SVGFEDiffuseLightingElement" + ); + assert_eq!( + svg_element_interface_name("feDisplacementMap"), + "SVGFEDisplacementMapElement" + ); + assert_eq!( + svg_element_interface_name("feDistantLight"), + "SVGFEDistantLightElement" + ); + assert_eq!( + svg_element_interface_name("feDropShadow"), + "SVGFEDropShadowElement" + ); + assert_eq!(svg_element_interface_name("feFuncA"), "SVGFEFuncAElement"); + assert_eq!(svg_element_interface_name("feFuncB"), "SVGFEFuncBElement"); + assert_eq!(svg_element_interface_name("feFuncG"), "SVGFEFuncGElement"); + assert_eq!(svg_element_interface_name("feFuncR"), "SVGFEFuncRElement"); + assert_eq!( + svg_element_interface_name("feGaussianBlur"), + "SVGFEGaussianBlurElement" + ); + assert_eq!( + svg_element_interface_name("feMorphology"), + "SVGFEMorphologyElement" + ); + assert_eq!(svg_element_interface_name("feOffset"), "SVGFEOffsetElement"); + assert_eq!( + svg_element_interface_name("fePointLight"), + "SVGFEPointLightElement" + ); + assert_eq!( + svg_element_interface_name("feSpecularLighting"), + "SVGFESpecularLightingElement" + ); + assert_eq!( + svg_element_interface_name("feSpotLight"), + "SVGFESpotLightElement" + ); + assert_eq!( + svg_element_interface_name("feTurbulence"), + "SVGFETurbulenceElement" + ); + assert_eq!(svg_element_interface_name("filter"), "SVGFilterElement"); assert_eq!( svg_element_interface_name("foreignObject"), "SVGForeignObjectElement" @@ -236,6 +296,8 @@ fn svg_element_interface_name_specializes_standard_elements() { "SVGLinearGradientElement" ); assert_eq!(svg_element_interface_name("metadata"), "SVGMetadataElement"); + assert_eq!(svg_element_interface_name("marker"), "SVGMarkerElement"); + assert_eq!(svg_element_interface_name("mask"), "SVGMaskElement"); assert_eq!(svg_element_interface_name("path"), "SVGPathElement"); assert_eq!(svg_element_interface_name("pattern"), "SVGPatternElement"); assert_eq!(svg_element_interface_name("polygon"), "SVGPolygonElement"); @@ -246,12 +308,15 @@ fn svg_element_interface_name_specializes_standard_elements() { ); assert_eq!(svg_element_interface_name("rect"), "SVGRectElement"); assert_eq!(svg_element_interface_name("script"), "SVGScriptElement"); + assert_eq!(svg_element_interface_name("stop"), "SVGStopElement"); assert_eq!(svg_element_interface_name("svg"), "SVGSVGElement"); assert_eq!(svg_element_interface_name("symbol"), "SVGSymbolElement"); assert_eq!(svg_element_interface_name("text"), "SVGTextElement"); + assert_eq!(svg_element_interface_name("textPath"), "SVGTextPathElement"); assert_eq!(svg_element_interface_name("title"), "SVGTitleElement"); assert_eq!(svg_element_interface_name("tspan"), "SVGTSpanElement"); assert_eq!(svg_element_interface_name("use"), "SVGUseElement"); + assert_eq!(svg_element_interface_name("view"), "SVGViewElement"); assert_eq!(svg_element_interface_name("style"), "SVGStyleElement"); assert_eq!(svg_element_interface_name("custom"), "SVGElement"); } diff --git a/moli-renderer-v8/src/context_bootstrap/bridge_descriptor/descriptors.rs b/moli-renderer-v8/src/context_bootstrap/bridge_descriptor/descriptors.rs index 996bd6a4af..1ae68d8ed5 100644 --- a/moli-renderer-v8/src/context_bootstrap/bridge_descriptor/descriptors.rs +++ b/moli-renderer-v8/src/context_bootstrap/bridge_descriptor/descriptors.rs @@ -100,14 +100,6 @@ const NODE_BRIDGE_DESCRIPTORS: &[BridgeDescriptor] = &[ ), descriptor(web_api_interfaces::Element::DESCRIPTOR, ELEMENT_GROUPS), descriptor(web_api_interfaces::SVGElement::DESCRIPTOR, ELEMENT_GROUPS), - descriptor( - web_api_interfaces::SVGGraphicsElement::DESCRIPTOR, - ELEMENT_GROUPS, - ), - descriptor( - web_api_interfaces::SVGGeometryElement::DESCRIPTOR, - ELEMENT_GROUPS, - ), descriptor( web_api_interfaces::SVGAnimationElement::DESCRIPTOR, ELEMENT_GROUPS, @@ -128,7 +120,31 @@ const NODE_BRIDGE_DESCRIPTORS: &[BridgeDescriptor] = &[ web_api_interfaces::SVGSetElement::DESCRIPTOR, ELEMENT_GROUPS, ), + descriptor( + web_api_interfaces::SVGGraphicsElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGGeometryElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), descriptor(web_api_interfaces::SVGAElement::DESCRIPTOR, ELEMENT_GROUPS), + descriptor( + web_api_interfaces::SVGClipPathElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFilterElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGMaskElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGViewElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), specialized_descriptor( web_api_interfaces::SVGCircleElement::DESCRIPTOR, ELEMENT_GROUPS, @@ -149,6 +165,34 @@ const NODE_BRIDGE_DESCRIPTORS: &[BridgeDescriptor] = &[ SpecializedTemplateInstaller::None, SVG_GEOMETRY_RUNTIME_INSTALL_GROUPS, ), + descriptor( + web_api_interfaces::SVGComponentTransferFunctionElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFEFuncAElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFEFuncBElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFEFuncGElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFEFuncRElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFEBlendElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFEColorMatrixElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), descriptor( web_api_interfaces::SVGFEComponentTransferElement::DESCRIPTOR, ELEMENT_GROUPS, @@ -173,6 +217,58 @@ const NODE_BRIDGE_DESCRIPTORS: &[BridgeDescriptor] = &[ web_api_interfaces::SVGFETileElement::DESCRIPTOR, ELEMENT_GROUPS, ), + descriptor( + web_api_interfaces::SVGFECompositeElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFEConvolveMatrixElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFEDiffuseLightingElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFEDisplacementMapElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFEDistantLightElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFEDropShadowElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFEGaussianBlurElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFEMorphologyElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFEOffsetElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFEPointLightElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFESpecularLightingElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFESpotLightElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), + descriptor( + web_api_interfaces::SVGFETurbulenceElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), descriptor( web_api_interfaces::SVGForeignObjectElement::DESCRIPTOR, ELEMENT_GROUPS, @@ -204,6 +300,10 @@ const NODE_BRIDGE_DESCRIPTORS: &[BridgeDescriptor] = &[ svg_gradient_transform: true, }, ), + descriptor( + web_api_interfaces::SVGMarkerElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), descriptor( web_api_interfaces::SVGMetadataElement::DESCRIPTOR, ELEMENT_GROUPS, @@ -212,6 +312,10 @@ const NODE_BRIDGE_DESCRIPTORS: &[BridgeDescriptor] = &[ web_api_interfaces::SVGScriptElement::DESCRIPTOR, ELEMENT_GROUPS, ), + descriptor( + web_api_interfaces::SVGStopElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), descriptor( web_api_interfaces::SVGStyleElement::DESCRIPTOR, ELEMENT_GROUPS, @@ -278,6 +382,10 @@ const NODE_BRIDGE_DESCRIPTORS: &[BridgeDescriptor] = &[ web_api_interfaces::SVGTextContentElement::DESCRIPTOR, ELEMENT_GROUPS, ), + descriptor( + web_api_interfaces::SVGTextPathElement::DESCRIPTOR, + ELEMENT_GROUPS, + ), descriptor( web_api_interfaces::SVGTextPositioningElement::DESCRIPTOR, ELEMENT_GROUPS, @@ -715,10 +823,6 @@ const NODE_BRIDGE_DESCRIPTORS: &[BridgeDescriptor] = &[ SpecializedTemplateInstaller::HtmlTableSectionElement, NONE_RUNTIME_INSTALL_GROUPS, ), - descriptor( - web_api_interfaces::HTMLTitleElement::DESCRIPTOR, - HTML_ELEMENT_GROUPS, - ), descriptor( web_api_interfaces::HTMLUListElement::DESCRIPTOR, HTML_ELEMENT_GROUPS, diff --git a/moli-renderer-v8/src/context_bootstrap/geometry_runtime.rs b/moli-renderer-v8/src/context_bootstrap/geometry_runtime.rs index cee092b3da..caade8b622 100644 --- a/moli-renderer-v8/src/context_bootstrap/geometry_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/geometry_runtime.rs @@ -13,6 +13,8 @@ const DOM_POINT_X_SLOT: &str = "__moliDomPointX"; 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"; @@ -561,7 +563,73 @@ pub(in crate::context_bootstrap) fn build_dom_point_object<'s>( .expect("DOMPoint declaration should bind") } -pub(in crate::context_bootstrap) fn build_dom_matrix_identity_object<'s>( +pub(super) fn dom_point_init_from_object<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> DomPointInit { + DomPointInit { + x: dom_point_slot(scope, object, DOM_POINT_X_SLOT, 0.0), + y: dom_point_slot(scope, object, DOM_POINT_Y_SLOT, 0.0), + z: dom_point_slot(scope, object, DOM_POINT_Z_SLOT, 0.0), + w: dom_point_slot(scope, object, DOM_POINT_W_SLOT, 1.0), + } +} + +pub(super) fn dom_point_clone_data<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> Option<(bool, [f64; 4])> { + if !dom_point_receiver_branded(scope, object) { + return None; + } + let mutable = web_api_interfaces::DOMPoint::is_instance(scope, object); + Some(( + mutable, + [ + dom_point_slot(scope, object, DOM_POINT_X_SLOT, 0.0), + dom_point_slot(scope, object, DOM_POINT_Y_SLOT, 0.0), + dom_point_slot(scope, object, DOM_POINT_Z_SLOT, 0.0), + dom_point_slot(scope, object, DOM_POINT_W_SLOT, 1.0), + ], + )) +} + +pub(in crate::context_bootstrap) fn build_svg_point_object<'s>( + scope: &mut v8::PinScope<'s, '_>, +) -> v8::Local<'s, v8::Object> { + 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, + object, + DOM_POINT_RESTRICTED_NUMBER_SLOT, + restricted.into(), + ); + 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_matrix_identity_object<'s>( scope: &mut v8::PinScope<'s, '_>, ) -> v8::Local<'s, v8::Object> { build_dom_matrix_object(scope, DomMatrixComponents::identity()) @@ -647,12 +715,33 @@ fn dom_point_setter_callback<'s>( throw_type_error(scope, "Illegal invocation"); return; } - let Some(value) = geometry_number_value( - scope, - args.get(0), - webidl::Context::member("DOMPoint", slot), - ) else { + 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)) + { + match webidl::convert::(scope, args.get(0), context) { + Ok(value) => value.0, + Err(error) => { + webidl::throw_error(scope, &error); + return; + } + } + } else { + let Some(value) = geometry_number_value(scope, args.get(0), context) else { + return; + }; + value }; set_private_value( scope, @@ -660,6 +749,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(); } @@ -2193,3 +2283,76 @@ const DOM_MATRIX_ATTRIBUTES: &[DomMatrixAttribute] = &[ DOM_MATRIX_READONLY_ATTRIBUTES[0], DOM_MATRIX_READONLY_ATTRIBUTES[1], ]; + +#[derive(Default, webidl::WebIdlDictionary)] +#[webidl(prefix = "DOMMatrix2DInit")] +struct DomMatrix2DInit { + a: Option, + b: Option, + c: Option, + d: Option, + e: Option, + f: Option, + m11: Option, + m12: Option, + m21: Option, + m22: Option, + m41: Option, + m42: Option, +} + +pub(in crate::context_bootstrap) fn dom_matrix_2d_init_arg<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: &v8::FunctionCallbackArguments<'s>, + index: i32, + prefix: &'static str, +) -> Option<[f64; 6]> { + if index >= args.length() || args.get(index).is_undefined() { + return Some([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]); + } + let init = match webidl::parse_dictionary::( + scope, + args.get(index), + webidl::Context::argument(prefix, (index + 1) as usize), + ) { + Ok(Some(init)) => init, + Ok(None) => DomMatrix2DInit::default(), + Err(error) => { + webidl::throw_error(scope, &error); + return None; + } + }; + Some([ + validated_dom_matrix_2d_alias(scope, "DOMMatrix2DInit", "a", init.a, "m11", init.m11, 1.0)?, + validated_dom_matrix_2d_alias(scope, "DOMMatrix2DInit", "b", init.b, "m12", init.m12, 0.0)?, + validated_dom_matrix_2d_alias(scope, "DOMMatrix2DInit", "c", init.c, "m21", init.m21, 0.0)?, + validated_dom_matrix_2d_alias(scope, "DOMMatrix2DInit", "d", init.d, "m22", init.m22, 1.0)?, + validated_dom_matrix_2d_alias(scope, "DOMMatrix2DInit", "e", init.e, "m41", init.m41, 0.0)?, + validated_dom_matrix_2d_alias(scope, "DOMMatrix2DInit", "f", init.f, "m42", init.m42, 0.0)?, + ]) +} + +fn validated_dom_matrix_2d_alias( + scope: &mut v8::PinScope<'_, '_>, + dictionary_name: &'static str, + alias_name: &'static str, + alias: Option, + matrix_name: &'static str, + matrix: Option, + default: f64, +) -> Option { + if let (Some(alias), Some(matrix)) = (alias, matrix) + && !same_dom_matrix_2d_number(alias, matrix) + { + throw_type_error( + scope, + &format!("{dictionary_name} {alias_name} and {matrix_name} values differ."), + ); + return None; + } + Some(matrix.or(alias).unwrap_or(default)) +} + +fn same_dom_matrix_2d_number(lhs: f64, rhs: f64) -> bool { + lhs == rhs || (lhs.is_nan() && rhs.is_nan()) +} diff --git a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs index 0573a0d1d0..e68766dee8 100644 --- a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs +++ b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs @@ -1787,14 +1787,27 @@ pub(crate) fn finish_context_bootstrap( ("XPathExpression", "XPathExpression"), ("XPathResult", "XPathResult"), ("SVGLength", "SVGLength"), + ("SVGAngle", "SVGAngle"), ("SVGNumber", "SVGNumber"), ("SVGRect", "SVGRect"), + ("SVGAnimatedString", "SVGAnimatedString"), ("SVGAnimatedLength", "SVGAnimatedLength"), + ("SVGAnimatedAngle", "SVGAnimatedAngle"), + ("SVGAnimatedRect", "SVGAnimatedRect"), + ("SVGPreserveAspectRatio", "SVGPreserveAspectRatio"), + ( + "SVGAnimatedPreserveAspectRatio", + "SVGAnimatedPreserveAspectRatio", + ), ("SVGLengthList", "SVGLengthList"), ("SVGAnimatedLengthList", "SVGAnimatedLengthList"), ("SVGAnimatedNumber", "SVGAnimatedNumber"), + ("SVGAnimatedInteger", "SVGAnimatedInteger"), ("SVGNumberList", "SVGNumberList"), + ("SVGPointList", "SVGPointList"), + ("SVGStringList", "SVGStringList"), ("SVGAnimatedNumberList", "SVGAnimatedNumberList"), + ("SVGAnimatedBoolean", "SVGAnimatedBoolean"), ("SVGAnimatedEnumeration", "SVGAnimatedEnumeration"), ("SVGUnitTypes", "SVGUnitTypes"), ("SVGAnimatedTransformList", "SVGAnimatedTransformList"), diff --git a/moli-renderer-v8/src/context_bootstrap/specs/registry.rs b/moli-renderer-v8/src/context_bootstrap/specs/registry.rs index 1490e7d66b..57bafd0543 100644 --- a/moli-renderer-v8/src/context_bootstrap/specs/registry.rs +++ b/moli-renderer-v8/src/context_bootstrap/specs/registry.rs @@ -100,6 +100,10 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[ interface: web_api_interfaces::SVGLength::DESCRIPTOR, kind: ConstructorKind::Illegal, }, + ConstructorSpec { + interface: web_api_interfaces::SVGAngle::DESCRIPTOR, + kind: ConstructorKind::Illegal, + }, ConstructorSpec { interface: web_api_interfaces::SVGNumber::DESCRIPTOR, kind: ConstructorKind::Illegal, @@ -108,10 +112,30 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[ interface: web_api_interfaces::SVGRect::DESCRIPTOR, kind: ConstructorKind::Illegal, }, + ConstructorSpec { + interface: web_api_interfaces::SVGAnimatedString::DESCRIPTOR, + kind: ConstructorKind::Illegal, + }, ConstructorSpec { interface: web_api_interfaces::SVGAnimatedLength::DESCRIPTOR, kind: ConstructorKind::Illegal, }, + ConstructorSpec { + interface: web_api_interfaces::SVGAnimatedAngle::DESCRIPTOR, + kind: ConstructorKind::Illegal, + }, + ConstructorSpec { + interface: web_api_interfaces::SVGAnimatedRect::DESCRIPTOR, + kind: ConstructorKind::Illegal, + }, + ConstructorSpec { + interface: web_api_interfaces::SVGPreserveAspectRatio::DESCRIPTOR, + kind: ConstructorKind::Illegal, + }, + ConstructorSpec { + interface: web_api_interfaces::SVGAnimatedPreserveAspectRatio::DESCRIPTOR, + kind: ConstructorKind::Illegal, + }, ConstructorSpec { interface: web_api_interfaces::SVGLengthList::DESCRIPTOR, kind: ConstructorKind::Illegal, @@ -124,14 +148,30 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[ interface: web_api_interfaces::SVGAnimatedNumber::DESCRIPTOR, kind: ConstructorKind::Illegal, }, + ConstructorSpec { + interface: web_api_interfaces::SVGAnimatedInteger::DESCRIPTOR, + kind: ConstructorKind::Illegal, + }, 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, + }, ConstructorSpec { interface: web_api_interfaces::SVGAnimatedNumberList::DESCRIPTOR, kind: ConstructorKind::Illegal, }, + ConstructorSpec { + interface: web_api_interfaces::SVGAnimatedBoolean::DESCRIPTOR, + kind: ConstructorKind::Illegal, + }, ConstructorSpec { interface: web_api_interfaces::SVGAnimatedEnumeration::DESCRIPTOR, kind: ConstructorKind::Illegal, diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/animation.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/animation.rs new file mode 100644 index 0000000000..fe55b5d249 --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/animation.rs @@ -0,0 +1,197 @@ +use moli_webapi_declare::WebApiFunctionTemplate; + +use super::callbacks::svg_test_string_list_getter; +use crate::{ + document_runtime::DomHandle, + native_bridge::{ + JsContextHost, + document::{SVG_NS, detached_native_object_for_handle}, + element::{node_event_handler_getter_function, node_event_handler_setter_function}, + node_runtime_and_handle_from_object_or_detached, throw_dom_exception, + }, + util::{callback_data_index_value, v8str}, + web_api_interfaces, webidl, +}; + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGAnimationElement, enumerable, receiver)] +struct SvgAnimationDeclaration { + #[webapi(accessor_property = "targetElement", getter = target_element_getter)] + target_element: (), + #[webapi(accessor_property = "onbegin", getter = node_event_handler_getter_function, + setter = node_event_handler_setter_function, data = v8str(scope, "onbegin"))] + onbegin: (), + #[webapi(accessor_property = "onend", getter = node_event_handler_getter_function, + setter = node_event_handler_setter_function, data = v8str(scope, "onend"))] + onend: (), + #[webapi(accessor_property = "onrepeat", getter = node_event_handler_getter_function, + setter = node_event_handler_setter_function, data = v8str(scope, "onrepeat"))] + onrepeat: (), + #[webapi(accessor_property = "requiredExtensions", getter = svg_test_string_list_getter, + data = callback_data_index_value(scope, 0))] + required_extensions: (), + #[webapi(accessor_property = "systemLanguage", getter = svg_test_string_list_getter, + data = callback_data_index_value(scope, 1))] + system_language: (), + #[webapi(method = "getStartTime", length = 0, callback = get_start_time)] + get_start_time: (), + #[webapi(method = "getCurrentTime", length = 0, callback = get_current_time)] + get_current_time: (), + #[webapi(method = "getSimpleDuration", length = 0, callback = get_simple_duration)] + get_simple_duration: (), + #[webapi(method = "beginElement", length = 0, callback = unsupported_timing)] + begin_element: (), + #[webapi(method = "beginElementAt", length = 1, callback = unsupported_timing_at)] + begin_element_at: (), + #[webapi(method = "endElement", length = 0, callback = unsupported_timing)] + end_element: (), + #[webapi(method = "endElementAt", length = 1, callback = unsupported_timing_at)] + end_element_at: (), +} + +pub(super) fn install_bindings<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, + name: &str, +) { + if name == "SVGAnimationElement" { + let prototype = template.prototype_template(scope); + SvgAnimationDeclaration::initialize_prototype_template(scope, prototype); + } +} + +fn target_element(runtime: &JsContextHost, animation: DomHandle) -> Option { + let dom = runtime.dom_host(); + if !dom.is_connected_to_document(animation) { + return None; + } + let href = dom + .get_attribute_ns(animation, None, "href") + .or_else(|| dom.get_attribute_ns(animation, Some("http://www.w3.org/1999/xlink"), "href")) + .unwrap_or_default(); + let target = if href.is_empty() { + dom.node(animation)?.parent_node()? + } else { + let fragment = if let Some(fragment) = href.strip_prefix('#') { + fragment.to_owned() + } else { + let document = dom.owner_document_handle(animation)?; + let mut url = runtime + .document_base_url_for_handle(document) + .join(&href) + .ok()?; + let fragment = url.fragment()?.to_owned(); + url.set_fragment(None); + let mut document_url = runtime.document_url_for_handle(document); + document_url.set_fragment(None); + if url != document_url { + return None; + } + fragment + }; + if fragment.is_empty() { + return None; + } + let id = percent_encoding::percent_decode_str(&fragment).decode_utf8_lossy(); + let root = dom.root_node_handle(animation)?; + dom.element_handle_by_id_in_subtree(root, &id)? + }; + dom.node(target) + .is_some_and(|node| node.namespace() == Some(SVG_NS)) + .then_some(target) +} + +fn target_element_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + let Ok((runtime_ptr, handle)) = + node_runtime_and_handle_from_object_or_detached(scope, args.this()) + else { + return; + }; + let target = target_element(unsafe { &*runtime_ptr }, handle); + let Some(target) = + target.and_then(|handle| detached_native_object_for_handle(scope, runtime_ptr, handle)) + else { + rv.set_null(); + return; + }; + rv.set(target.into()); +} + +// Native DOM, interface identity and handler dispatch are supported. A SMIL +// time container is not yet scheduled, so no current interval exists. Timing +// controls fail explicitly instead of pretending to start an animation. +fn get_start_time<'s>( + scope: &mut v8::PinScope<'s, '_>, + _args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'s, v8::Value>, +) { + throw_dom_exception( + scope, + "InvalidStateError", + 11, + "The animation has no current interval.", + ); +} + +fn get_current_time<'s>( + _scope: &mut v8::PinScope<'s, '_>, + _args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + rv.set_double(0.0); +} + +fn get_simple_duration<'s>( + scope: &mut v8::PinScope<'s, '_>, + _args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'s, v8::Value>, +) { + throw_dom_exception( + scope, + "NotSupportedError", + 9, + "SVG animation durations are not implemented.", + ); +} + +fn unsupported_timing<'s>( + scope: &mut v8::PinScope<'s, '_>, + _args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'s, v8::Value>, +) { + throw_dom_exception( + scope, + "NotSupportedError", + 9, + "SVG animation timing is not implemented.", + ); +} + +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "SVGAnimationElement")] +struct TimingOffsetArgs { + #[webidl(required, converter = "double")] + offset: f64, +} + +fn unsupported_timing_at<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'s, v8::Value>, +) { + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + if !(parsed.offset as f32).is_finite() { + webidl::throw_type_error( + scope, + "SVG animation offset is outside the finite float range.", + ); + return; + } + unsupported_timing(scope, args, rv); +} 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 84460bea7b..9223a069df 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs @@ -58,6 +58,42 @@ struct SvgLengthTemplateMethodsDeclaration { convert_to_specified_units: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGAngle, enumerable)] +struct SvgAngleTemplateMethodsDeclaration { + #[webapi(constant = "SVG_ANGLETYPE_UNKNOWN", value = SVG_ANGLE_TYPE_UNKNOWN)] + angle_type_unknown: (), + + #[webapi( + constant = "SVG_ANGLETYPE_UNSPECIFIED", + value = SVG_ANGLE_TYPE_UNSPECIFIED + )] + angle_type_unspecified: (), + + #[webapi(constant = "SVG_ANGLETYPE_DEG", value = SVG_ANGLE_TYPE_DEG)] + angle_type_deg: (), + + #[webapi(constant = "SVG_ANGLETYPE_RAD", value = SVG_ANGLE_TYPE_RAD)] + angle_type_rad: (), + + #[webapi(constant = "SVG_ANGLETYPE_GRAD", value = SVG_ANGLE_TYPE_GRAD)] + angle_type_grad: (), + + #[webapi( + method = "newValueSpecifiedUnits", + length = 2, + callback = svg_angle_new_value_specified_units_callback + )] + new_value_specified_units: (), + + #[webapi( + method = "convertToSpecifiedUnits", + length = 1, + callback = svg_angle_convert_to_specified_units_callback + )] + convert_to_specified_units: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::SVGLengthList, enumerable)] struct SvgLengthListTemplateMethodsDeclaration { @@ -148,6 +184,96 @@ 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 { + #[webapi(method = "clear", length = 0, callback = svg_string_list_clear_callback)] + clear: (), + + #[webapi( + method = "initialize", + length = 1, + callback = svg_string_list_initialize_callback + )] + initialize: (), + + #[webapi(method = "getItem", length = 1, callback = svg_string_list_get_item_callback)] + get_item: (), + + #[webapi( + method = "insertItemBefore", + length = 2, + callback = svg_string_list_insert_item_before_callback + )] + insert_item_before: (), + + #[webapi( + method = "replaceItem", + length = 2, + callback = svg_string_list_replace_item_callback + )] + replace_item: (), + + #[webapi( + method = "removeItem", + length = 1, + callback = svg_string_list_remove_item_callback + )] + remove_item: (), + + #[webapi( + method = "appendItem", + length = 1, + callback = svg_string_list_append_item_callback + )] + append_item: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::SVGTransformList, enumerable)] struct SvgTransformListTemplateMethodsDeclaration { @@ -194,7 +320,7 @@ struct SvgTransformListTemplateMethodsDeclaration { #[webapi( method = "createSVGTransformFromMatrix", - length = 1, + length = 0, callback = svg_transform_list_create_transform_from_matrix_callback )] create_svg_transform_from_matrix: (), @@ -481,6 +607,228 @@ struct SvgGradientElementTemplateConstantsDeclaration { spread_method_repeat: (), } +macro_rules! define_svg_enumeration_constants { + ($declaration:ident, $interface:literal, { + $($field:ident => $constant:literal = $value:expr),+ $(,)? + }) => { + #[derive(WebApiFunctionTemplate)] + #[webapi(name = $interface, enumerable)] + struct $declaration { + $( + #[webapi(constant = $constant, value = $value)] + $field: (), + )+ + } + }; +} + +define_svg_enumeration_constants!( + SvgPreserveAspectRatioTemplateConstantsDeclaration, + "SVGPreserveAspectRatio", + { + preserve_aspect_ratio_unknown => "SVG_PRESERVEASPECTRATIO_UNKNOWN" = SVG_PRESERVE_ASPECT_RATIO_UNKNOWN, + preserve_aspect_ratio_none => "SVG_PRESERVEASPECTRATIO_NONE" = SVG_PRESERVE_ASPECT_RATIO_NONE, + preserve_aspect_ratio_x_min_y_min => "SVG_PRESERVEASPECTRATIO_XMINYMIN" = SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MIN, + preserve_aspect_ratio_x_mid_y_min => "SVG_PRESERVEASPECTRATIO_XMIDYMIN" = SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MIN, + preserve_aspect_ratio_x_max_y_min => "SVG_PRESERVEASPECTRATIO_XMAXYMIN" = SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MIN, + preserve_aspect_ratio_x_min_y_mid => "SVG_PRESERVEASPECTRATIO_XMINYMID" = SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MID, + preserve_aspect_ratio_x_mid_y_mid => "SVG_PRESERVEASPECTRATIO_XMIDYMID" = SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MID, + preserve_aspect_ratio_x_max_y_mid => "SVG_PRESERVEASPECTRATIO_XMAXYMID" = SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MID, + preserve_aspect_ratio_x_min_y_max => "SVG_PRESERVEASPECTRATIO_XMINYMAX" = SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MAX, + preserve_aspect_ratio_x_mid_y_max => "SVG_PRESERVEASPECTRATIO_XMIDYMAX" = SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MAX, + preserve_aspect_ratio_x_max_y_max => "SVG_PRESERVEASPECTRATIO_XMAXYMAX" = SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MAX, + meet_or_slice_unknown => "SVG_MEETORSLICE_UNKNOWN" = SVG_MEET_OR_SLICE_UNKNOWN, + meet_or_slice_meet => "SVG_MEETORSLICE_MEET" = SVG_MEET_OR_SLICE_MEET, + meet_or_slice_slice => "SVG_MEETORSLICE_SLICE" = SVG_MEET_OR_SLICE_SLICE, + } +); + +define_svg_enumeration_constants!( + SvgComponentTransferFunctionElementTemplateConstantsDeclaration, + "SVGComponentTransferFunctionElement", + { + unknown => "SVG_FECOMPONENTTRANSFER_TYPE_UNKNOWN" = SVG_COMPONENT_TRANSFER_TYPE_UNKNOWN, + identity => "SVG_FECOMPONENTTRANSFER_TYPE_IDENTITY" = SVG_COMPONENT_TRANSFER_TYPE_IDENTITY, + table => "SVG_FECOMPONENTTRANSFER_TYPE_TABLE" = SVG_COMPONENT_TRANSFER_TYPE_TABLE, + discrete => "SVG_FECOMPONENTTRANSFER_TYPE_DISCRETE" = SVG_COMPONENT_TRANSFER_TYPE_DISCRETE, + linear => "SVG_FECOMPONENTTRANSFER_TYPE_LINEAR" = SVG_COMPONENT_TRANSFER_TYPE_LINEAR, + gamma => "SVG_FECOMPONENTTRANSFER_TYPE_GAMMA" = SVG_COMPONENT_TRANSFER_TYPE_GAMMA, + } +); + +define_svg_enumeration_constants!( + SvgFeBlendElementTemplateConstantsDeclaration, + "SVGFEBlendElement", + { + unknown => "SVG_FEBLEND_MODE_UNKNOWN" = SVG_FE_BLEND_MODE_UNKNOWN, + normal => "SVG_FEBLEND_MODE_NORMAL" = SVG_FE_BLEND_MODE_NORMAL, + multiply => "SVG_FEBLEND_MODE_MULTIPLY" = SVG_FE_BLEND_MODE_MULTIPLY, + screen => "SVG_FEBLEND_MODE_SCREEN" = SVG_FE_BLEND_MODE_SCREEN, + darken => "SVG_FEBLEND_MODE_DARKEN" = SVG_FE_BLEND_MODE_DARKEN, + lighten => "SVG_FEBLEND_MODE_LIGHTEN" = SVG_FE_BLEND_MODE_LIGHTEN, + overlay => "SVG_FEBLEND_MODE_OVERLAY" = SVG_FE_BLEND_MODE_OVERLAY, + color_dodge => "SVG_FEBLEND_MODE_COLOR_DODGE" = SVG_FE_BLEND_MODE_COLOR_DODGE, + color_burn => "SVG_FEBLEND_MODE_COLOR_BURN" = SVG_FE_BLEND_MODE_COLOR_BURN, + hard_light => "SVG_FEBLEND_MODE_HARD_LIGHT" = SVG_FE_BLEND_MODE_HARD_LIGHT, + soft_light => "SVG_FEBLEND_MODE_SOFT_LIGHT" = SVG_FE_BLEND_MODE_SOFT_LIGHT, + difference => "SVG_FEBLEND_MODE_DIFFERENCE" = SVG_FE_BLEND_MODE_DIFFERENCE, + exclusion => "SVG_FEBLEND_MODE_EXCLUSION" = SVG_FE_BLEND_MODE_EXCLUSION, + hue => "SVG_FEBLEND_MODE_HUE" = SVG_FE_BLEND_MODE_HUE, + saturation => "SVG_FEBLEND_MODE_SATURATION" = SVG_FE_BLEND_MODE_SATURATION, + color => "SVG_FEBLEND_MODE_COLOR" = SVG_FE_BLEND_MODE_COLOR, + luminosity => "SVG_FEBLEND_MODE_LUMINOSITY" = SVG_FE_BLEND_MODE_LUMINOSITY, + } +); + +define_svg_enumeration_constants!( + SvgFeColorMatrixElementTemplateConstantsDeclaration, + "SVGFEColorMatrixElement", + { + unknown => "SVG_FECOLORMATRIX_TYPE_UNKNOWN" = SVG_FE_COLOR_MATRIX_TYPE_UNKNOWN, + matrix => "SVG_FECOLORMATRIX_TYPE_MATRIX" = SVG_FE_COLOR_MATRIX_TYPE_MATRIX, + saturate => "SVG_FECOLORMATRIX_TYPE_SATURATE" = SVG_FE_COLOR_MATRIX_TYPE_SATURATE, + hue_rotate => "SVG_FECOLORMATRIX_TYPE_HUEROTATE" = SVG_FE_COLOR_MATRIX_TYPE_HUE_ROTATE, + luminance_to_alpha => "SVG_FECOLORMATRIX_TYPE_LUMINANCETOALPHA" = SVG_FE_COLOR_MATRIX_TYPE_LUMINANCE_TO_ALPHA, + } +); + +define_svg_enumeration_constants!( + SvgFeCompositeElementTemplateConstantsDeclaration, + "SVGFECompositeElement", + { + unknown => "SVG_FECOMPOSITE_OPERATOR_UNKNOWN" = SVG_FE_COMPOSITE_OPERATOR_UNKNOWN, + over => "SVG_FECOMPOSITE_OPERATOR_OVER" = SVG_FE_COMPOSITE_OPERATOR_OVER, + input => "SVG_FECOMPOSITE_OPERATOR_IN" = SVG_FE_COMPOSITE_OPERATOR_IN, + out => "SVG_FECOMPOSITE_OPERATOR_OUT" = SVG_FE_COMPOSITE_OPERATOR_OUT, + atop => "SVG_FECOMPOSITE_OPERATOR_ATOP" = SVG_FE_COMPOSITE_OPERATOR_ATOP, + xor => "SVG_FECOMPOSITE_OPERATOR_XOR" = SVG_FE_COMPOSITE_OPERATOR_XOR, + lighter => "SVG_FECOMPOSITE_OPERATOR_LIGHTER" = SVG_FE_COMPOSITE_OPERATOR_LIGHTER, + arithmetic => "SVG_FECOMPOSITE_OPERATOR_ARITHMETIC" = SVG_FE_COMPOSITE_OPERATOR_ARITHMETIC, + } +); + +define_svg_enumeration_constants!( + SvgFeConvolveMatrixElementTemplateConstantsDeclaration, + "SVGFEConvolveMatrixElement", + { + unknown => "SVG_EDGEMODE_UNKNOWN" = SVG_EDGE_MODE_UNKNOWN, + duplicate => "SVG_EDGEMODE_DUPLICATE" = SVG_EDGE_MODE_DUPLICATE, + wrap => "SVG_EDGEMODE_WRAP" = SVG_EDGE_MODE_WRAP, + none => "SVG_EDGEMODE_NONE" = SVG_EDGE_MODE_NONE, + } +); + +define_svg_enumeration_constants!( + SvgFeDisplacementMapElementTemplateConstantsDeclaration, + "SVGFEDisplacementMapElement", + { + unknown => "SVG_CHANNEL_UNKNOWN" = SVG_CHANNEL_UNKNOWN, + red => "SVG_CHANNEL_R" = SVG_CHANNEL_R, + green => "SVG_CHANNEL_G" = SVG_CHANNEL_G, + blue => "SVG_CHANNEL_B" = SVG_CHANNEL_B, + alpha => "SVG_CHANNEL_A" = SVG_CHANNEL_A, + } +); + +define_svg_enumeration_constants!( + SvgFeMorphologyElementTemplateConstantsDeclaration, + "SVGFEMorphologyElement", + { + unknown => "SVG_MORPHOLOGY_OPERATOR_UNKNOWN" = SVG_MORPHOLOGY_OPERATOR_UNKNOWN, + erode => "SVG_MORPHOLOGY_OPERATOR_ERODE" = SVG_MORPHOLOGY_OPERATOR_ERODE, + dilate => "SVG_MORPHOLOGY_OPERATOR_DILATE" = SVG_MORPHOLOGY_OPERATOR_DILATE, + } +); + +define_svg_enumeration_constants!( + SvgFeTurbulenceElementTemplateConstantsDeclaration, + "SVGFETurbulenceElement", + { + turbulence_unknown => "SVG_TURBULENCE_TYPE_UNKNOWN" = SVG_TURBULENCE_TYPE_UNKNOWN, + fractal_noise => "SVG_TURBULENCE_TYPE_FRACTALNOISE" = SVG_TURBULENCE_TYPE_FRACTAL_NOISE, + turbulence => "SVG_TURBULENCE_TYPE_TURBULENCE" = SVG_TURBULENCE_TYPE_TURBULENCE, + stitch_unknown => "SVG_STITCHTYPE_UNKNOWN" = SVG_STITCH_TYPE_UNKNOWN, + stitch => "SVG_STITCHTYPE_STITCH" = SVG_STITCH_TYPE_STITCH, + no_stitch => "SVG_STITCHTYPE_NOSTITCH" = SVG_STITCH_TYPE_NO_STITCH, + } +); + +define_svg_enumeration_constants!( + SvgTextPathElementTemplateConstantsDeclaration, + "SVGTextPathElement", + { + method_unknown => "TEXTPATH_METHODTYPE_UNKNOWN" = SVG_TEXT_PATH_METHOD_TYPE_UNKNOWN, + method_align => "TEXTPATH_METHODTYPE_ALIGN" = SVG_TEXT_PATH_METHOD_TYPE_ALIGN, + method_stretch => "TEXTPATH_METHODTYPE_STRETCH" = SVG_TEXT_PATH_METHOD_TYPE_STRETCH, + spacing_unknown => "TEXTPATH_SPACINGTYPE_UNKNOWN" = SVG_TEXT_PATH_SPACING_TYPE_UNKNOWN, + spacing_auto => "TEXTPATH_SPACINGTYPE_AUTO" = SVG_TEXT_PATH_SPACING_TYPE_AUTO, + spacing_exact => "TEXTPATH_SPACINGTYPE_EXACT" = SVG_TEXT_PATH_SPACING_TYPE_EXACT, + side_unknown => "TEXTPATH_SIDETYPE_UNKNOWN" = SVG_TEXT_PATH_SIDE_TYPE_UNKNOWN, + side_left => "TEXTPATH_SIDETYPE_LEFT" = SVG_TEXT_PATH_SIDE_TYPE_LEFT, + side_right => "TEXTPATH_SIDETYPE_RIGHT" = SVG_TEXT_PATH_SIDE_TYPE_RIGHT, + } +); + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGMarkerElement, enumerable)] +struct SvgMarkerElementTemplateMethodsDeclaration { + #[webapi( + constant = "SVG_MARKERUNITS_UNKNOWN", + value = SVG_MARKER_UNITS_UNKNOWN + )] + marker_units_unknown: (), + + #[webapi( + constant = "SVG_MARKERUNITS_USERSPACEONUSE", + value = SVG_MARKER_UNITS_USER_SPACE_ON_USE + )] + marker_units_user_space_on_use: (), + + #[webapi( + constant = "SVG_MARKERUNITS_STROKEWIDTH", + value = SVG_MARKER_UNITS_STROKE_WIDTH + )] + marker_units_stroke_width: (), + + #[webapi( + constant = "SVG_MARKER_ORIENT_UNKNOWN", + value = SVG_MARKER_ORIENT_UNKNOWN + )] + marker_orient_unknown: (), + + #[webapi( + constant = "SVG_MARKER_ORIENT_AUTO", + value = SVG_MARKER_ORIENT_AUTO + )] + marker_orient_auto: (), + + #[webapi( + constant = "SVG_MARKER_ORIENT_ANGLE", + value = SVG_MARKER_ORIENT_ANGLE + )] + marker_orient_angle: (), + + #[webapi( + constant = "SVG_MARKER_ORIENT_AUTO_START_REVERSE", + value = SVG_MARKER_ORIENT_AUTO_START_REVERSE + )] + marker_orient_auto_start_reverse: (), + + #[webapi( + method = "setOrientToAuto", + length = 0, + callback = svg_marker_set_orient_to_auto_callback + )] + set_orient_to_auto: (), + + #[webapi( + method = "setOrientToAngle", + length = 1, + callback = svg_marker_set_orient_to_angle_callback + )] + set_orient_to_angle: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::SVGSVGElement, enumerable, receiver)] struct SvgSvgElementTemplateMethodsDeclaration { @@ -498,6 +846,34 @@ struct SvgSvgElementTemplateMethodsDeclaration { )] deselect_all: (), + #[webapi( + method = "createSVGNumber", + length = 0, + callback = svg_svg_element_create_number_callback + )] + create_svg_number: (), + + #[webapi( + method = "createSVGLength", + length = 0, + callback = svg_svg_element_create_length_callback + )] + create_svg_length: (), + + #[webapi( + method = "createSVGAngle", + length = 0, + callback = svg_svg_element_create_angle_callback + )] + create_svg_angle: (), + + #[webapi( + method = "createSVGPoint", + length = 0, + callback = svg_svg_element_create_point_callback + )] + create_svg_point: (), + #[webapi( method = "createSVGMatrix", length = 0, @@ -514,7 +890,7 @@ struct SvgSvgElementTemplateMethodsDeclaration { #[webapi( method = "createSVGTransformFromMatrix", - length = 1, + length = 0, callback = svg_svg_element_create_transform_from_matrix_callback )] create_svg_transform_from_matrix: (), @@ -555,6 +931,79 @@ struct SvgLengthTemplateAccessorsDeclaration { value_as_string: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGAngle, enumerable)] +struct SvgAngleTemplateAccessorsDeclaration { + #[webapi( + accessor_property = "unitType", + getter = svg_angle_getter, + data = callback_data_index_value(scope, 0) + )] + unit_type: (), + + #[webapi( + accessor_property = "value", + getter = svg_angle_getter, + setter = svg_angle_setter, + data = callback_data_index_value(scope, 1) + )] + value: (), + + #[webapi( + accessor_property = "valueInSpecifiedUnits", + getter = svg_angle_getter, + setter = svg_angle_setter, + data = callback_data_index_value(scope, 2) + )] + value_in_specified_units: (), + + #[webapi( + accessor_property = "valueAsString", + getter = svg_angle_getter, + setter = svg_angle_setter, + data = callback_data_index_value(scope, 3) + )] + value_as_string: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGAnimatedString, enumerable)] +struct SvgAnimatedStringTemplateAccessorsDeclaration { + #[webapi( + accessor_property = "baseVal", + getter = svg_animated_string_getter, + setter = svg_animated_string_setter, + data = callback_data_index_value(scope, 0) + )] + base_val: (), + + #[webapi( + accessor_property = "animVal", + getter = svg_animated_string_getter, + data = callback_data_index_value(scope, 1) + )] + anim_val: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGAnimatedBoolean, enumerable)] +struct SvgAnimatedBooleanTemplateAccessorsDeclaration { + #[webapi( + accessor_property = "baseVal", + getter = svg_animated_boolean_getter, + setter = svg_animated_boolean_setter, + data = callback_data_index_value(scope, 0) + )] + base_val: (), + + #[webapi( + accessor_property = "animVal", + getter = svg_animated_boolean_getter, + data = callback_data_index_value(scope, 1) + )] + anim_val: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::SVGAnimatedLength, enumerable)] struct SvgAnimatedLengthTemplateAccessorsDeclaration { @@ -573,6 +1022,80 @@ struct SvgAnimatedLengthTemplateAccessorsDeclaration { anim_val: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGAnimatedAngle, enumerable)] +struct SvgAnimatedAngleTemplateAccessorsDeclaration { + #[webapi( + accessor_property = "baseVal", + getter = svg_animated_angle_getter, + data = callback_data_index_value(scope, 0) + )] + base_val: (), + + #[webapi( + accessor_property = "animVal", + getter = svg_animated_angle_getter, + data = callback_data_index_value(scope, 1) + )] + anim_val: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGAnimatedRect, enumerable)] +struct SvgAnimatedRectTemplateAccessorsDeclaration { + #[webapi( + accessor_property = "baseVal", + getter = svg_animated_rect_getter, + data = callback_data_index_value(scope, 0) + )] + base_val: (), + + #[webapi( + accessor_property = "animVal", + getter = svg_animated_rect_getter, + data = callback_data_index_value(scope, 1) + )] + anim_val: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGPreserveAspectRatio, enumerable)] +struct SvgPreserveAspectRatioTemplateAccessorsDeclaration { + #[webapi( + accessor_property = "align", + getter = svg_preserve_aspect_ratio_getter, + setter = svg_preserve_aspect_ratio_setter, + data = callback_data_index_value(scope, 0) + )] + align: (), + + #[webapi( + accessor_property = "meetOrSlice", + getter = svg_preserve_aspect_ratio_getter, + setter = svg_preserve_aspect_ratio_setter, + data = callback_data_index_value(scope, 1) + )] + meet_or_slice: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGAnimatedPreserveAspectRatio, enumerable)] +struct SvgAnimatedPreserveAspectRatioTemplateAccessorsDeclaration { + #[webapi( + accessor_property = "baseVal", + getter = svg_animated_preserve_aspect_ratio_getter, + data = callback_data_index_value(scope, 0) + )] + base_val: (), + + #[webapi( + accessor_property = "animVal", + getter = svg_animated_preserve_aspect_ratio_getter, + data = callback_data_index_value(scope, 1) + )] + anim_val: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::SVGNumber, enumerable)] struct SvgNumberTemplateAccessorsDeclaration { @@ -621,6 +1144,25 @@ struct SvgAnimatedNumberTemplateAccessorsDeclaration { anim_val: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGAnimatedInteger, enumerable)] +struct SvgAnimatedIntegerTemplateAccessorsDeclaration { + #[webapi( + accessor_property = "baseVal", + getter = svg_animated_integer_getter, + setter = svg_animated_integer_setter, + data = callback_data_index_value(scope, 0) + )] + base_val: (), + + #[webapi( + accessor_property = "animVal", + getter = svg_animated_integer_getter, + data = callback_data_index_value(scope, 1) + )] + anim_val: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::SVGAnimatedNumberList, enumerable)] struct SvgAnimatedNumberListTemplateAccessorsDeclaration { @@ -684,6 +1226,32 @@ 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 { + #[webapi(accessor_property = "length", getter = svg_string_list_length_getter)] + length: (), + + #[webapi( + accessor_property = "numberOfItems", + getter = svg_string_list_length_getter + )] + number_of_items: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::SVGAnimatedTransformList, enumerable)] struct SvgAnimatedTransformListTemplateAccessorsDeclaration { @@ -792,6 +1360,152 @@ struct SvgMatrixTemplateAccessorsDeclaration { f: (), } +macro_rules! define_svg_animated_number_accessors { + ( + $declaration:ident, + $interface:literal, + $(($field:ident, $name:literal, $index:literal)),+ $(,)? + ) => { + #[derive(WebApiFunctionTemplate)] + #[webapi(name = $interface, enumerable)] + struct $declaration { + $( + #[webapi( + accessor_property = $name, + getter = svg_element_animated_number_getter, + data = callback_data_index_value(scope, $index) + )] + $field: (), + )+ + } + }; +} + +define_svg_animated_number_accessors!( + SvgComponentTransferFunctionAnimatedNumberAccessorsDeclaration, + "SVGComponentTransferFunctionElement", + (slope, "slope", 0), + (intercept, "intercept", 1), + (amplitude, "amplitude", 2), + (exponent, "exponent", 3), + (offset, "offset", 4), +); + +define_svg_animated_number_accessors!( + SvgFeCompositeAnimatedNumberAccessorsDeclaration, + "SVGFECompositeElement", + (k1, "k1", 5), + (k2, "k2", 6), + (k3, "k3", 7), + (k4, "k4", 8), +); + +define_svg_animated_number_accessors!( + SvgFeConvolveMatrixAnimatedNumberAccessorsDeclaration, + "SVGFEConvolveMatrixElement", + (divisor, "divisor", 9), + (bias, "bias", 10), + (kernel_unit_length_x, "kernelUnitLengthX", 11), + (kernel_unit_length_y, "kernelUnitLengthY", 12), +); + +define_svg_animated_number_accessors!( + SvgFeDiffuseLightingAnimatedNumberAccessorsDeclaration, + "SVGFEDiffuseLightingElement", + (surface_scale, "surfaceScale", 13), + (diffuse_constant, "diffuseConstant", 14), + (kernel_unit_length_x, "kernelUnitLengthX", 15), + (kernel_unit_length_y, "kernelUnitLengthY", 16), +); + +define_svg_animated_number_accessors!( + SvgFeDisplacementMapAnimatedNumberAccessorsDeclaration, + "SVGFEDisplacementMapElement", + (scale, "scale", 17), +); + +define_svg_animated_number_accessors!( + SvgFeDistantLightAnimatedNumberAccessorsDeclaration, + "SVGFEDistantLightElement", + (azimuth, "azimuth", 18), + (elevation, "elevation", 19), +); + +define_svg_animated_number_accessors!( + SvgFeDropShadowAnimatedNumberAccessorsDeclaration, + "SVGFEDropShadowElement", + (dx, "dx", 20), + (dy, "dy", 21), + (std_deviation_x, "stdDeviationX", 22), + (std_deviation_y, "stdDeviationY", 23), +); + +define_svg_animated_number_accessors!( + SvgFeGaussianBlurAnimatedNumberAccessorsDeclaration, + "SVGFEGaussianBlurElement", + (std_deviation_x, "stdDeviationX", 24), + (std_deviation_y, "stdDeviationY", 25), +); + +define_svg_animated_number_accessors!( + SvgFeMorphologyAnimatedNumberAccessorsDeclaration, + "SVGFEMorphologyElement", + (radius_x, "radiusX", 26), + (radius_y, "radiusY", 27), +); + +define_svg_animated_number_accessors!( + SvgFeOffsetAnimatedNumberAccessorsDeclaration, + "SVGFEOffsetElement", + (dx, "dx", 28), + (dy, "dy", 29), +); + +define_svg_animated_number_accessors!( + SvgFePointLightAnimatedNumberAccessorsDeclaration, + "SVGFEPointLightElement", + (x, "x", 30), + (y, "y", 31), + (z, "z", 32), +); + +define_svg_animated_number_accessors!( + SvgFeSpecularLightingAnimatedNumberAccessorsDeclaration, + "SVGFESpecularLightingElement", + (surface_scale, "surfaceScale", 33), + (specular_constant, "specularConstant", 34), + (specular_exponent, "specularExponent", 35), + (kernel_unit_length_x, "kernelUnitLengthX", 36), + (kernel_unit_length_y, "kernelUnitLengthY", 37), +); + +define_svg_animated_number_accessors!( + SvgFeSpotLightAnimatedNumberAccessorsDeclaration, + "SVGFESpotLightElement", + (x, "x", 38), + (y, "y", 39), + (z, "z", 40), + (points_at_x, "pointsAtX", 41), + (points_at_y, "pointsAtY", 42), + (points_at_z, "pointsAtZ", 43), + (specular_exponent, "specularExponent", 44), + (limiting_cone_angle, "limitingConeAngle", 45), +); + +define_svg_animated_number_accessors!( + SvgFeTurbulenceAnimatedNumberAccessorsDeclaration, + "SVGFETurbulenceElement", + (base_frequency_x, "baseFrequencyX", 46), + (base_frequency_y, "baseFrequencyY", 47), + (seed, "seed", 48), +); + +define_svg_animated_number_accessors!( + SvgStopAnimatedNumberAccessorsDeclaration, + "SVGStopElement", + (offset, "offset", 49), +); + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::SVGElement, enumerable, receiver)] struct SvgElementPrototypeAccessorsDeclaration { @@ -800,6 +1514,8 @@ struct SvgElementPrototypeAccessorsDeclaration { getter = svg_element_owner_svg_element_getter )] owner_svg_element: (), + #[webapi(accessor_property = "className", getter = svg_element_class_name_getter)] + class_name: (), } #[derive(WebApiFunctionTemplate)] @@ -807,6 +1523,20 @@ struct SvgElementPrototypeAccessorsDeclaration { struct SvgGraphicsElementPrototypeAccessorsDeclaration { #[webapi(accessor_property = "transform", getter = svg_graphics_transform_getter)] transform: (), + + #[webapi( + accessor_property = "requiredExtensions", + getter = svg_graphics_test_string_list_getter, + data = callback_data_index_value(scope, 0) + )] + required_extensions: (), + + #[webapi( + accessor_property = "systemLanguage", + getter = svg_graphics_test_string_list_getter, + data = callback_data_index_value(scope, 1) + )] + system_language: (), } #[derive(WebApiFunctionTemplate)] @@ -816,13 +1546,64 @@ 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 { + #[webapi( + accessor_property = "viewBox", + getter = svg_fit_to_view_box_getter, + data = callback_data_index_value(scope, 0) + )] + view_box: (), + + #[webapi( + accessor_property = "preserveAspectRatio", + getter = svg_fit_to_view_box_getter, + data = callback_data_index_value(scope, 1) + )] + preserve_aspect_ratio: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGImageElement, enumerable)] +struct SvgImagePreserveAspectRatioPrototypeAccessorsDeclaration { + #[webapi( + accessor_property = "preserveAspectRatio", + getter = svg_fit_to_view_box_getter, + data = callback_data_index_value(scope, 1) + )] + preserve_aspect_ratio: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::SVGTextContentElement, enumerable)] struct SvgTextContentElementPrototypeAccessorsDeclaration { #[webapi(accessor_property = "textLength", getter = svg_text_content_text_length_getter)] text_length: (), - #[webapi(accessor_property = "lengthAdjust", getter = svg_text_content_length_adjust_getter)] + #[webapi( + accessor_property = "lengthAdjust", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 9) + )] length_adjust: (), } @@ -848,6 +1629,32 @@ struct SvgTextPositioningElementPrototypeAccessorsDeclaration { #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::SVGPatternElement, enumerable)] struct SvgPatternElementPrototypeAccessorsDeclaration { + #[webapi(accessor_property = "x", getter = svg_pattern_animated_length_getter, data = callback_data_index_value(scope, 0))] + x: (), + + #[webapi(accessor_property = "y", getter = svg_pattern_animated_length_getter, data = callback_data_index_value(scope, 1))] + y: (), + + #[webapi(accessor_property = "width", getter = svg_pattern_animated_length_getter, data = callback_data_index_value(scope, 2))] + width: (), + + #[webapi(accessor_property = "height", getter = svg_pattern_animated_length_getter, data = callback_data_index_value(scope, 3))] + height: (), + + #[webapi( + accessor_property = "patternUnits", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 7) + )] + pattern_units: (), + + #[webapi( + accessor_property = "patternContentUnits", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 8) + )] + pattern_content_units: (), + #[webapi(accessor_property = "patternTransform", getter = svg_pattern_transform_getter)] pattern_transform: (), } @@ -855,10 +1662,336 @@ struct SvgPatternElementPrototypeAccessorsDeclaration { #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::SVGGradientElement, enumerable)] struct SvgGradientElementPrototypeAccessorsDeclaration { + #[webapi( + accessor_property = "gradientUnits", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 3) + )] + gradient_units: (), + + #[webapi( + accessor_property = "spreadMethod", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 4) + )] + spread_method: (), + #[webapi(accessor_property = "gradientTransform", getter = svg_gradient_transform_getter)] gradient_transform: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGLinearGradientElement, enumerable)] +struct SvgLinearGradientElementPrototypeAccessorsDeclaration { + #[webapi(accessor_property = "x1", getter = svg_linear_gradient_animated_length_getter, data = callback_data_index_value(scope, 0))] + x1: (), + + #[webapi(accessor_property = "y1", getter = svg_linear_gradient_animated_length_getter, data = callback_data_index_value(scope, 1))] + y1: (), + + #[webapi(accessor_property = "x2", getter = svg_linear_gradient_animated_length_getter, data = callback_data_index_value(scope, 2))] + x2: (), + + #[webapi(accessor_property = "y2", getter = svg_linear_gradient_animated_length_getter, data = callback_data_index_value(scope, 3))] + y2: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGRadialGradientElement, enumerable)] +struct SvgRadialGradientElementPrototypeAccessorsDeclaration { + #[webapi(accessor_property = "cx", getter = svg_radial_gradient_animated_length_getter, data = callback_data_index_value(scope, 0))] + cx: (), + + #[webapi(accessor_property = "cy", getter = svg_radial_gradient_animated_length_getter, data = callback_data_index_value(scope, 1))] + cy: (), + + #[webapi(accessor_property = "r", getter = svg_radial_gradient_animated_length_getter, data = callback_data_index_value(scope, 2))] + r: (), + + #[webapi(accessor_property = "fx", getter = svg_radial_gradient_animated_length_getter, data = callback_data_index_value(scope, 3))] + fx: (), + + #[webapi(accessor_property = "fy", getter = svg_radial_gradient_animated_length_getter, data = callback_data_index_value(scope, 4))] + fy: (), + + #[webapi(accessor_property = "fr", getter = svg_radial_gradient_animated_length_getter, data = callback_data_index_value(scope, 5))] + fr: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGSVGElement, enumerable)] +struct SvgSvgElementPrototypeAccessorsDeclaration { + #[webapi(accessor_property = "x", getter = svg_svg_animated_length_getter, data = callback_data_index_value(scope, 0))] + x: (), + + #[webapi(accessor_property = "y", getter = svg_svg_animated_length_getter, data = callback_data_index_value(scope, 1))] + y: (), + + #[webapi(accessor_property = "width", getter = svg_svg_animated_length_getter, data = callback_data_index_value(scope, 2))] + width: (), + + #[webapi(accessor_property = "height", getter = svg_svg_animated_length_getter, data = callback_data_index_value(scope, 3))] + height: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGClipPathElement, enumerable)] +struct SvgClipPathElementPrototypeAccessorsDeclaration { + #[webapi( + accessor_property = "clipPathUnits", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 0) + )] + clip_path_units: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGFilterElement, enumerable)] +struct SvgFilterElementPrototypeAccessorsDeclaration { + #[webapi(accessor_property = "x", getter = svg_filter_animated_length_getter, data = callback_data_index_value(scope, 0))] + x: (), + + #[webapi(accessor_property = "y", getter = svg_filter_animated_length_getter, data = callback_data_index_value(scope, 1))] + y: (), + + #[webapi(accessor_property = "width", getter = svg_filter_animated_length_getter, data = callback_data_index_value(scope, 2))] + width: (), + + #[webapi(accessor_property = "height", getter = svg_filter_animated_length_getter, data = callback_data_index_value(scope, 3))] + height: (), + + #[webapi( + accessor_property = "filterUnits", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 1) + )] + filter_units: (), + + #[webapi( + accessor_property = "primitiveUnits", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 2) + )] + primitive_units: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(name = "SVGFilterPrimitiveStandardAttributes", enumerable)] +struct SvgFilterPrimitiveStandardAttributesPrototypeAccessorsDeclaration { + #[webapi(accessor_property = "x", getter = svg_filter_primitive_animated_length_getter, data = callback_data_index_value(scope, 0))] + x: (), + + #[webapi(accessor_property = "y", getter = svg_filter_primitive_animated_length_getter, data = callback_data_index_value(scope, 1))] + y: (), + + #[webapi(accessor_property = "width", getter = svg_filter_primitive_animated_length_getter, data = callback_data_index_value(scope, 2))] + width: (), + + #[webapi(accessor_property = "height", getter = svg_filter_primitive_animated_length_getter, data = callback_data_index_value(scope, 3))] + height: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGComponentTransferFunctionElement, enumerable)] +struct SvgComponentTransferFunctionElementPrototypeAccessorsDeclaration { + #[webapi( + accessor_property = "type", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 12) + )] + transfer_type: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGFEBlendElement, enumerable)] +struct SvgFeBlendElementPrototypeAccessorsDeclaration { + #[webapi( + accessor_property = "mode", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 13) + )] + mode: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGFEColorMatrixElement, enumerable)] +struct SvgFeColorMatrixElementPrototypeAccessorsDeclaration { + #[webapi( + accessor_property = "type", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 14) + )] + color_matrix_type: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGFECompositeElement, enumerable)] +struct SvgFeCompositeElementPrototypeAccessorsDeclaration { + #[webapi( + accessor_property = "operator", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 15) + )] + operator: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGFEConvolveMatrixElement, enumerable)] +struct SvgFeConvolveMatrixElementPrototypeAccessorsDeclaration { + #[webapi( + accessor_property = "orderX", + getter = svg_element_animated_integer_getter, + data = callback_data_index_value(scope, 0) + )] + order_x: (), + + #[webapi( + accessor_property = "orderY", + getter = svg_element_animated_integer_getter, + data = callback_data_index_value(scope, 1) + )] + order_y: (), + + #[webapi( + accessor_property = "targetX", + getter = svg_element_animated_integer_getter, + data = callback_data_index_value(scope, 2) + )] + target_x: (), + + #[webapi( + accessor_property = "targetY", + getter = svg_element_animated_integer_getter, + data = callback_data_index_value(scope, 3) + )] + target_y: (), + + #[webapi( + accessor_property = "edgeMode", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 16) + )] + edge_mode: (), + + #[webapi( + accessor_property = "preserveAlpha", + getter = svg_fe_convolve_matrix_preserve_alpha_getter + )] + preserve_alpha: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGFEDisplacementMapElement, enumerable)] +struct SvgFeDisplacementMapElementPrototypeAccessorsDeclaration { + #[webapi( + accessor_property = "xChannelSelector", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 17) + )] + x_channel_selector: (), + + #[webapi( + accessor_property = "yChannelSelector", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 18) + )] + y_channel_selector: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGFEMorphologyElement, enumerable)] +struct SvgFeMorphologyElementPrototypeAccessorsDeclaration { + #[webapi( + accessor_property = "operator", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 19) + )] + operator: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGFETurbulenceElement, enumerable)] +struct SvgFeTurbulenceElementPrototypeAccessorsDeclaration { + #[webapi( + accessor_property = "numOctaves", + getter = svg_element_animated_integer_getter, + data = callback_data_index_value(scope, 4) + )] + num_octaves: (), + + #[webapi( + accessor_property = "stitchTiles", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 20) + )] + stitch_tiles: (), + + #[webapi( + accessor_property = "type", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 21) + )] + turbulence_type: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGTextPathElement, enumerable)] +struct SvgTextPathElementPrototypeAccessorsDeclaration { + #[webapi(accessor_property = "startOffset", getter = svg_text_path_animated_length_getter, data = callback_data_index_value(scope, 0))] + start_offset: (), + + #[webapi( + accessor_property = "method", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 22) + )] + method: (), + + #[webapi( + accessor_property = "spacing", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 23) + )] + spacing: (), + + #[webapi( + accessor_property = "side", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 24) + )] + side: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGMaskElement, enumerable)] +struct SvgMaskElementPrototypeAccessorsDeclaration { + #[webapi(accessor_property = "x", getter = svg_mask_animated_length_getter, data = callback_data_index_value(scope, 0))] + x: (), + + #[webapi(accessor_property = "y", getter = svg_mask_animated_length_getter, data = callback_data_index_value(scope, 1))] + y: (), + + #[webapi(accessor_property = "width", getter = svg_mask_animated_length_getter, data = callback_data_index_value(scope, 2))] + width: (), + + #[webapi(accessor_property = "height", getter = svg_mask_animated_length_getter, data = callback_data_index_value(scope, 3))] + height: (), + + #[webapi( + accessor_property = "maskUnits", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 5) + )] + mask_units: (), + + #[webapi( + accessor_property = "maskContentUnits", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 6) + )] + mask_content_units: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::SVGRectElement, enumerable)] struct SvgRectElementPrototypeAccessorsDeclaration { @@ -942,6 +2075,56 @@ struct SvgGraphicsBoxElementPrototypeAccessorsDeclaration { height: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGMarkerElement, enumerable)] +struct SvgMarkerElementPrototypeAccessorsDeclaration { + #[webapi(accessor_property = "refX", getter = svg_marker_animated_length_getter, data = callback_data_index_value(scope, 0))] + ref_x: (), + + #[webapi(accessor_property = "refY", getter = svg_marker_animated_length_getter, data = callback_data_index_value(scope, 1))] + ref_y: (), + + #[webapi(accessor_property = "markerWidth", getter = svg_marker_animated_length_getter, data = callback_data_index_value(scope, 2))] + marker_width: (), + + #[webapi(accessor_property = "markerHeight", getter = svg_marker_animated_length_getter, data = callback_data_index_value(scope, 3))] + marker_height: (), + + #[webapi( + accessor_property = "markerUnits", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 10) + )] + marker_units: (), + + #[webapi( + accessor_property = "orientType", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 11) + )] + orient_type: (), + + #[webapi( + accessor_property = "orientAngle", + getter = svg_marker_orient_angle_getter + )] + orient_angle: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(name = "SVGURIReference", enumerable)] +struct SvgUriReferencePrototypeAccessorsDeclaration { + #[webapi(accessor_property = "href", getter = svg_uri_href_getter)] + href: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGMPathElement, enumerable, receiver)] +struct SvgMPathElementPrototypeAccessorsDeclaration { + #[webapi(accessor_property = "href", getter = svg_mpath_href_getter)] + href: (), +} + pub(super) fn install_svg_length_bindings<'s>( scope: &mut v8::PinScope<'s, '_, ()>, template: v8::Local<'s, v8::FunctionTemplate>, @@ -952,6 +2135,16 @@ pub(super) fn install_svg_length_bindings<'s>( SvgLengthTemplateMethodsDeclaration::initialize_prototype_template(scope, proto); } +pub(super) fn install_svg_angle_bindings<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, +) { + let proto = template.prototype_template(scope); + SvgAngleTemplateMethodsDeclaration::initialize_template(scope, template); + SvgAngleTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto); + SvgAngleTemplateMethodsDeclaration::initialize_prototype_template(scope, proto); +} + pub(super) fn install_svg_animated_length_bindings<'s>( scope: &mut v8::PinScope<'s, '_, ()>, template: v8::Local<'s, v8::FunctionTemplate>, @@ -960,6 +2153,42 @@ pub(super) fn install_svg_animated_length_bindings<'s>( SvgAnimatedLengthTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto); } +pub(super) fn install_svg_animated_angle_bindings<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, +) { + let proto = template.prototype_template(scope); + SvgAnimatedAngleTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto); +} + +pub(super) fn install_svg_animated_rect_bindings<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, +) { + let proto = template.prototype_template(scope); + SvgAnimatedRectTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto); +} + +pub(super) fn install_svg_preserve_aspect_ratio_bindings<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, +) { + let proto = template.prototype_template(scope); + SvgPreserveAspectRatioTemplateConstantsDeclaration::initialize_template(scope, template); + SvgPreserveAspectRatioTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto); + SvgPreserveAspectRatioTemplateConstantsDeclaration::initialize_prototype_template(scope, proto); +} + +pub(super) fn install_svg_animated_preserve_aspect_ratio_bindings<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, +) { + let proto = template.prototype_template(scope); + SvgAnimatedPreserveAspectRatioTemplateAccessorsDeclaration::initialize_prototype_template( + scope, proto, + ); +} + pub(super) fn install_svg_number_bindings<'s>( scope: &mut v8::PinScope<'s, '_, ()>, template: v8::Local<'s, v8::FunctionTemplate>, @@ -968,6 +2197,22 @@ pub(super) fn install_svg_number_bindings<'s>( SvgNumberTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto); } +pub(super) fn install_svg_animated_string_bindings<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, +) { + let proto = template.prototype_template(scope); + SvgAnimatedStringTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto); +} + +pub(super) fn install_svg_animated_boolean_bindings<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, +) { + let proto = template.prototype_template(scope); + SvgAnimatedBooleanTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto); +} + pub(super) fn install_svg_animated_length_list_bindings<'s>( scope: &mut v8::PinScope<'s, '_, ()>, template: v8::Local<'s, v8::FunctionTemplate>, @@ -991,6 +2236,14 @@ pub(super) fn install_svg_animated_number_bindings<'s>( SvgAnimatedNumberTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto); } +pub(super) fn install_svg_animated_integer_bindings<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, +) { + let proto = template.prototype_template(scope); + SvgAnimatedIntegerTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto); +} + pub(super) fn install_svg_animated_number_list_bindings<'s>( scope: &mut v8::PinScope<'s, '_, ()>, template: v8::Local<'s, v8::FunctionTemplate>, @@ -1006,6 +2259,22 @@ 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>, +) { + let proto = template.prototype_template(scope); + SvgStringListTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto); + SvgStringListTemplateMethodsDeclaration::initialize_prototype_template(scope, proto); +} + pub(super) fn install_svg_animated_enumeration_bindings<'s>( scope: &mut v8::PinScope<'s, '_, ()>, template: v8::Local<'s, v8::FunctionTemplate>, @@ -1032,6 +2301,50 @@ pub(super) fn install_svg_gradient_element_bindings<'s>( SvgGradientElementTemplateConstantsDeclaration::initialize_prototype_template(scope, prototype); } +pub(super) fn install_svg_enumeration_constant_bindings<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, + interface_name: &str, +) { + let prototype = template.prototype_template(scope); + macro_rules! install_constants { + ($declaration:ty) => {{ + <$declaration>::initialize_template(scope, template); + <$declaration>::initialize_prototype_template(scope, prototype); + }}; + } + match interface_name { + "SVGComponentTransferFunctionElement" => { + install_constants!(SvgComponentTransferFunctionElementTemplateConstantsDeclaration); + } + "SVGFEBlendElement" => { + install_constants!(SvgFeBlendElementTemplateConstantsDeclaration); + } + "SVGFEColorMatrixElement" => { + install_constants!(SvgFeColorMatrixElementTemplateConstantsDeclaration); + } + "SVGFECompositeElement" => { + install_constants!(SvgFeCompositeElementTemplateConstantsDeclaration); + } + "SVGFEConvolveMatrixElement" => { + install_constants!(SvgFeConvolveMatrixElementTemplateConstantsDeclaration); + } + "SVGFEDisplacementMapElement" => { + install_constants!(SvgFeDisplacementMapElementTemplateConstantsDeclaration); + } + "SVGFEMorphologyElement" => { + install_constants!(SvgFeMorphologyElementTemplateConstantsDeclaration); + } + "SVGFETurbulenceElement" => { + install_constants!(SvgFeTurbulenceElementTemplateConstantsDeclaration); + } + "SVGTextPathElement" => { + install_constants!(SvgTextPathElementTemplateConstantsDeclaration); + } + _ => {} + } +} + pub(super) fn install_svg_value_list_bindings<'s>( scope: &mut v8::PinScope<'s, '_, ()>, template: v8::Local<'s, v8::FunctionTemplate>, @@ -1047,6 +2360,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); + } } } @@ -1113,6 +2430,15 @@ pub(super) fn install_svg_text_content_element_bindings<'s>( SvgTextContentElementTemplateMethodsDeclaration::initialize_prototype_template(scope, proto); } +pub(super) fn install_svg_marker_element_bindings<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, +) { + let proto = template.prototype_template(scope); + SvgMarkerElementTemplateMethodsDeclaration::initialize_template(scope, template); + SvgMarkerElementTemplateMethodsDeclaration::initialize_prototype_template(scope, proto); +} + pub(super) fn install_svg_svg_element_bindings( scope: &mut v8::PinScope<'_, '_, ()>, template: v8::Local<'_, v8::FunctionTemplate>, @@ -1121,12 +2447,104 @@ pub(super) fn install_svg_svg_element_bindings( SvgSvgElementTemplateMethodsDeclaration::initialize_prototype_template(scope, proto); } +fn install_svg_animated_number_element_bindings<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + prototype: v8::Local<'s, v8::ObjectTemplate>, + interface_name: &str, +) { + macro_rules! install_accessors { + ($declaration:ty) => { + <$declaration>::initialize_prototype_template(scope, prototype) + }; + } + match interface_name { + "SVGComponentTransferFunctionElement" => { + install_accessors!(SvgComponentTransferFunctionAnimatedNumberAccessorsDeclaration); + } + "SVGFECompositeElement" => { + install_accessors!(SvgFeCompositeAnimatedNumberAccessorsDeclaration); + } + "SVGFEConvolveMatrixElement" => { + install_accessors!(SvgFeConvolveMatrixAnimatedNumberAccessorsDeclaration); + } + "SVGFEDiffuseLightingElement" => { + install_accessors!(SvgFeDiffuseLightingAnimatedNumberAccessorsDeclaration); + } + "SVGFEDisplacementMapElement" => { + install_accessors!(SvgFeDisplacementMapAnimatedNumberAccessorsDeclaration); + } + "SVGFEDistantLightElement" => { + install_accessors!(SvgFeDistantLightAnimatedNumberAccessorsDeclaration); + } + "SVGFEDropShadowElement" => { + install_accessors!(SvgFeDropShadowAnimatedNumberAccessorsDeclaration); + } + "SVGFEGaussianBlurElement" => { + install_accessors!(SvgFeGaussianBlurAnimatedNumberAccessorsDeclaration); + } + "SVGFEMorphologyElement" => { + install_accessors!(SvgFeMorphologyAnimatedNumberAccessorsDeclaration); + } + "SVGFEOffsetElement" => { + install_accessors!(SvgFeOffsetAnimatedNumberAccessorsDeclaration); + } + "SVGFEPointLightElement" => { + install_accessors!(SvgFePointLightAnimatedNumberAccessorsDeclaration); + } + "SVGFESpecularLightingElement" => { + install_accessors!(SvgFeSpecularLightingAnimatedNumberAccessorsDeclaration); + } + "SVGFESpotLightElement" => { + install_accessors!(SvgFeSpotLightAnimatedNumberAccessorsDeclaration); + } + "SVGFETurbulenceElement" => { + install_accessors!(SvgFeTurbulenceAnimatedNumberAccessorsDeclaration); + } + "SVGStopElement" => { + install_accessors!(SvgStopAnimatedNumberAccessorsDeclaration); + } + _ => {} + } +} + pub(super) fn install_svg_element_accessor_bindings<'s>( scope: &mut v8::PinScope<'s, '_, ()>, template: v8::Local<'s, v8::FunctionTemplate>, interface_name: &str, ) { let prototype = template.prototype_template(scope); + install_svg_animated_number_element_bindings(scope, prototype, interface_name); + if matches!( + interface_name, + "SVGSVGElement" + | "SVGSymbolElement" + | "SVGMarkerElement" + | "SVGPatternElement" + | "SVGViewElement" + ) { + SvgFitToViewBoxPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + if matches!( + interface_name, + "SVGFEBlendElement" + | "SVGFEColorMatrixElement" + | "SVGFECompositeElement" + | "SVGFEConvolveMatrixElement" + | "SVGFEDiffuseLightingElement" + | "SVGFEDisplacementMapElement" + | "SVGFEDropShadowElement" + | "SVGFEGaussianBlurElement" + | "SVGFEMorphologyElement" + | "SVGFEOffsetElement" + | "SVGFESpecularLightingElement" + | "SVGFETurbulenceElement" + ) { + SvgFilterPrimitiveStandardAttributesPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } match interface_name { "SVGElement" => { SvgElementPrototypeAccessorsDeclaration::initialize_prototype_template( @@ -1148,21 +2566,108 @@ pub(super) fn install_svg_element_accessor_bindings<'s>( scope, prototype, ); } + "SVGClipPathElement" => { + SvgClipPathElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + "SVGFilterElement" => { + SvgFilterElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + "SVGComponentTransferFunctionElement" => { + SvgComponentTransferFunctionElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + "SVGFEBlendElement" => { + SvgFeBlendElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + "SVGFEColorMatrixElement" => { + SvgFeColorMatrixElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + "SVGFECompositeElement" => { + SvgFeCompositeElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + "SVGFEConvolveMatrixElement" => { + SvgFeConvolveMatrixElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + "SVGFEDisplacementMapElement" => { + SvgFeDisplacementMapElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + "SVGFEMorphologyElement" => { + SvgFeMorphologyElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + "SVGFETurbulenceElement" => { + SvgFeTurbulenceElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + "SVGMaskElement" => { + SvgMaskElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } "SVGTextPositioningElement" => { SvgTextPositioningElementPrototypeAccessorsDeclaration::initialize_prototype_template( scope, prototype, ); } + "SVGTextPathElement" => { + SvgTextPathElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + SvgUriReferencePrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } "SVGPatternElement" => { SvgPatternElementPrototypeAccessorsDeclaration::initialize_prototype_template( scope, prototype, ); + SvgUriReferencePrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); } "SVGGradientElement" => { SvgGradientElementPrototypeAccessorsDeclaration::initialize_prototype_template( scope, prototype, ); } + "SVGLinearGradientElement" => { + SvgLinearGradientElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + SvgUriReferencePrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + "SVGRadialGradientElement" => { + SvgRadialGradientElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + SvgUriReferencePrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + "SVGSVGElement" => { + SvgSvgElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } "SVGRectElement" => { SvgRectElementPrototypeAccessorsDeclaration::initialize_prototype_template( scope, prototype, @@ -1183,7 +2688,40 @@ pub(super) fn install_svg_element_accessor_bindings<'s>( scope, prototype, ); } - "SVGImageElement" | "SVGUseElement" | "SVGForeignObjectElement" => { + "SVGPolygonElement" | "SVGPolylineElement" => { + SvgAnimatedPointsPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + "SVGMarkerElement" => { + SvgMarkerElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + "SVGAElement" | "SVGScriptElement" => { + SvgUriReferencePrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + "SVGMPathElement" => { + SvgMPathElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + "SVGImageElement" | "SVGUseElement" => { + SvgGraphicsBoxElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + SvgUriReferencePrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + if interface_name == "SVGImageElement" { + SvgImagePreserveAspectRatioPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + } + "SVGForeignObjectElement" => { SvgGraphicsBoxElementPrototypeAccessorsDeclaration::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 1b8a74a5f1..567b4191c2 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs @@ -4,6 +4,30 @@ use crate::util::serialize_v8_iter_array; use crate::web_api_interfaces; use moli_webapi_declare::WebApiObject; +#[derive(WebApiObject)] +#[webapi( + interface = web_api_interfaces::SVGAnimatedString, + own_to_string_tag = "SVGAnimatedString" +)] +struct SvgAnimatedStringObjectDeclaration { + #[webapi(slot = SVG_ANIMATED_STRING_BASE_VAL_SLOT)] + base_val: String, + #[webapi(slot = SVG_ANIMATED_STRING_ANIM_VAL_SLOT)] + anim_val: String, +} + +#[derive(WebApiObject)] +#[webapi( + interface = web_api_interfaces::SVGAnimatedBoolean, + own_to_string_tag = "SVGAnimatedBoolean" +)] +struct SvgAnimatedBooleanObjectDeclaration { + #[webapi(slot = SVG_ANIMATED_BOOLEAN_BASE_VAL_SLOT)] + base_val: bool, + #[webapi(slot = SVG_ANIMATED_BOOLEAN_ANIM_VAL_SLOT)] + anim_val: bool, +} + #[derive(WebApiObject)] #[webapi( interface = web_api_interfaces::SVGAnimatedNumber, @@ -16,6 +40,18 @@ struct SvgAnimatedNumberObjectDeclaration { anim_val: f64, } +#[derive(WebApiObject)] +#[webapi( + interface = web_api_interfaces::SVGAnimatedInteger, + own_to_string_tag = "SVGAnimatedInteger" +)] +struct SvgAnimatedIntegerObjectDeclaration { + #[webapi(slot = SVG_ANIMATED_INTEGER_BASE_VAL_SLOT)] + base_val: i32, + #[webapi(slot = SVG_ANIMATED_INTEGER_ANIM_VAL_SLOT)] + anim_val: i32, +} + #[derive(WebApiObject)] #[webapi(interface = web_api_interfaces::SVGNumber, own_to_string_tag = "SVGNumber")] struct SvgNumberObjectDeclaration { @@ -94,17 +130,88 @@ struct SvgAnimatedLengthObjectDeclaration<'scope> { anim_val: v8::Local<'scope, v8::Object>, } +#[derive(WebApiObject)] +#[webapi( + interface = web_api_interfaces::SVGAnimatedAngle, + fallback_to_string_tag = "SVGAnimatedAngle" +)] +struct SvgAnimatedAngleObjectDeclaration<'scope> { + #[webapi(slot = SVG_ANIMATED_ANGLE_BASE_VAL_SLOT)] + base_val: v8::Local<'scope, v8::Object>, + #[webapi(slot = SVG_ANIMATED_ANGLE_ANIM_VAL_SLOT)] + anim_val: v8::Local<'scope, v8::Object>, +} + +#[derive(WebApiObject)] +#[webapi( + interface = web_api_interfaces::SVGAnimatedRect, + fallback_to_string_tag = "SVGAnimatedRect" +)] +struct SvgAnimatedRectObjectDeclaration<'scope> { + #[webapi(slot = SVG_ANIMATED_RECT_BASE_VAL_SLOT)] + base_val: v8::Local<'scope, v8::Object>, + #[webapi(slot = SVG_ANIMATED_RECT_ANIM_VAL_SLOT)] + anim_val: v8::Local<'scope, v8::Object>, +} + +#[derive(WebApiObject)] +#[webapi( + interface = web_api_interfaces::SVGPreserveAspectRatio, + fallback_to_string_tag = "SVGPreserveAspectRatio" +)] +struct SvgPreserveAspectRatioObjectDeclaration<'scope> { + #[webapi(slot = SVG_PRESERVE_ASPECT_RATIO_ALIGN_SLOT)] + align: u32, + #[webapi(slot = SVG_PRESERVE_ASPECT_RATIO_MEET_OR_SLICE_SLOT)] + meet_or_slice: u32, + #[webapi(slot = SVG_PRESERVE_ASPECT_RATIO_OWNER_ELEMENT_SLOT)] + owner: v8::Local<'scope, v8::Object>, + #[webapi(slot = SVG_PRESERVE_ASPECT_RATIO_READ_ONLY_SLOT)] + read_only: bool, +} + +#[derive(WebApiObject)] +#[webapi( + interface = web_api_interfaces::SVGAnimatedPreserveAspectRatio, + fallback_to_string_tag = "SVGAnimatedPreserveAspectRatio" +)] +struct SvgAnimatedPreserveAspectRatioObjectDeclaration<'scope> { + #[webapi(slot = SVG_ANIMATED_PRESERVE_ASPECT_RATIO_BASE_VAL_SLOT)] + base_val: v8::Local<'scope, v8::Object>, + #[webapi(slot = SVG_ANIMATED_PRESERVE_ASPECT_RATIO_ANIM_VAL_SLOT)] + anim_val: v8::Local<'scope, v8::Object>, +} + #[derive(WebApiObject)] #[webapi(interface = web_api_interfaces::SVGLength, fallback_to_string_tag = "SVGLength")] struct SvgLengthObjectDeclaration { #[webapi(slot = SVG_LENGTH_UNIT_TYPE_SLOT)] unit_type: u32, + #[webapi(slot = SVG_LENGTH_UNIT_SUFFIX_SLOT)] + unit_suffix: String, #[webapi(slot = SVG_LENGTH_VALUE_SLOT)] value: f64, + #[webapi(slot = SVG_LENGTH_VALUE_IN_SPECIFIED_UNITS_SLOT)] + value_in_specified_units: f64, #[webapi(slot = SVG_LENGTH_VALUE_AS_STRING_SLOT)] value_as_string: String, } +#[derive(WebApiObject)] +#[webapi(interface = web_api_interfaces::SVGAngle, fallback_to_string_tag = "SVGAngle")] +struct SvgAngleObjectDeclaration { + #[webapi(slot = SVG_ANGLE_UNIT_TYPE_SLOT)] + unit_type: u32, + #[webapi(slot = SVG_ANGLE_VALUE_SLOT)] + value: f64, + #[webapi(slot = SVG_ANGLE_VALUE_IN_SPECIFIED_UNITS_SLOT)] + value_in_specified_units: f64, + #[webapi(slot = SVG_ANGLE_VALUE_AS_STRING_SLOT)] + value_as_string: String, + #[webapi(slot = SVG_ANGLE_READ_ONLY_SLOT)] + read_only: bool, +} + #[derive(WebApiObject)] #[webapi( interface = web_api_interfaces::SVGAnimatedLengthList, @@ -146,6 +253,8 @@ struct SvgAnimatedTransformListObjectDeclaration<'scope> { struct SvgLengthListObjectDeclaration<'scope> { #[webapi(slot = SVG_LENGTH_LIST_ITEMS_SLOT)] items: Vec>, + #[webapi(slot = SVG_VALUE_LIST_READ_ONLY_SLOT)] + read_only: bool, #[webapi(method, callback = svg_length_list_clear_callback, length = 0)] clear: (), #[webapi(method, callback = svg_length_list_initialize_callback, length = 1)] @@ -171,6 +280,8 @@ struct SvgLengthListObjectDeclaration<'scope> { struct SvgNumberListObjectDeclaration<'scope> { #[webapi(slot = SVG_NUMBER_LIST_ITEMS_SLOT)] items: Vec>, + #[webapi(slot = SVG_VALUE_LIST_READ_ONLY_SLOT)] + read_only: bool, #[webapi(method, callback = svg_number_list_clear_callback, length = 0)] clear: (), #[webapi(method, callback = svg_number_list_initialize_callback, length = 1)] @@ -191,6 +302,40 @@ 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> { + #[webapi(slot = SVG_STRING_LIST_ITEMS_SLOT)] + items: Vec>, +} + #[derive(WebApiObject)] #[webapi(interface = web_api_interfaces::SVGTransformList, own_to_string_tag = "SVGTransformList")] struct SvgTransformListObjectDeclaration<'scope> { @@ -251,11 +396,138 @@ struct SvgTransformObjectDeclaration<'scope> { set_skew_y: (), } +pub(super) fn build_svg_animated_string_for_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + attribute: &str, +) -> v8::Local<'s, v8::Object> { + let value = svg_owner_attribute_value(scope, owner, attribute).unwrap_or_default(); + let object = SvgAnimatedStringObjectDeclaration::new(value.clone(), value) + .bind(scope) + .expect("SVGAnimatedString declaration should bind"); + set_svg_animated_string_owner_attribute(scope, object, owner, attribute); + object +} + +pub(super) fn build_svg_animated_boolean_for_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + attribute: &str, + initial_value: bool, +) -> v8::Local<'s, v8::Object> { + let object = SvgAnimatedBooleanObjectDeclaration::new(initial_value, initial_value) + .bind(scope) + .expect("SVGAnimatedBoolean declaration should bind"); + set_private_value( + scope, + object, + SVG_ANIMATED_BOOLEAN_OWNER_ELEMENT_SLOT, + owner.into(), + ); + set_private_value( + scope, + object, + SVG_ANIMATED_BOOLEAN_OWNER_ATTRIBUTE_SLOT, + v8_string(scope, attribute) + .unwrap_or_else(|| v8str(scope, "")) + .into(), + ); + set_private_value( + scope, + object, + SVG_ANIMATED_BOOLEAN_INITIAL_VALUE_SLOT, + v8::Boolean::new(scope, initial_value).into(), + ); + sync_svg_animated_boolean_from_owner_attribute(scope, object); + object +} + +pub(super) fn sync_svg_animated_boolean_from_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + animated: v8::Local<'s, v8::Object>, +) { + let Some(owner) = get_private_value(scope, animated, SVG_ANIMATED_BOOLEAN_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let Some(attribute) = + get_private_value(scope, animated, SVG_ANIMATED_BOOLEAN_OWNER_ATTRIBUTE_SLOT) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + .filter(|value| !value.is_empty()) + else { + return; + }; + let initial_value = get_private_value(scope, animated, SVG_ANIMATED_BOOLEAN_INITIAL_VALUE_SLOT) + .is_some_and(|value| value.is_true()); + let value = match svg_owner_attribute_value(scope, owner, &attribute).as_deref() { + Some("true") => true, + Some("false") => false, + _ => initial_value, + }; + set_svg_animated_boolean_values(scope, animated, value); +} + +pub(super) fn set_svg_animated_boolean_values( + scope: &mut v8::PinScope<'_, '_>, + animated: v8::Local<'_, v8::Object>, + value: bool, +) { + let value = v8::Boolean::new(scope, value); + set_private_value( + scope, + animated, + SVG_ANIMATED_BOOLEAN_BASE_VAL_SLOT, + value.into(), + ); + set_private_value( + scope, + animated, + SVG_ANIMATED_BOOLEAN_ANIM_VAL_SLOT, + value.into(), + ); +} + +pub(super) fn reflect_svg_animated_boolean_to_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + animated: v8::Local<'s, v8::Object>, +) { + let Some(owner) = get_private_value(scope, animated, SVG_ANIMATED_BOOLEAN_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let Some(attribute) = + get_private_value(scope, animated, SVG_ANIMATED_BOOLEAN_OWNER_ATTRIBUTE_SLOT) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + .filter(|value| !value.is_empty()) + else { + return; + }; + let value = get_private_value(scope, animated, SVG_ANIMATED_BOOLEAN_BASE_VAL_SLOT) + .is_some_and(|value| value.is_true()); + let Ok((runtime_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner) + else { + return; + }; + let runtime = unsafe { &mut *runtime_ptr }; + let _ = runtime.set_attribute( + scope, + runtime_ptr, + handle, + &attribute, + if value { "true" } else { "false" }, + ); +} + pub(super) fn build_svg_animated_length_list<'s>( scope: &mut v8::PinScope<'s, '_>, ) -> v8::Local<'s, v8::Object> { - let base_val = build_svg_length_list(scope); - let anim_val = build_svg_length_list(scope); + let base_val = build_svg_length_list(scope, false); + let anim_val = build_svg_length_list(scope, true); SvgAnimatedLengthListObjectDeclaration::new(base_val, anim_val) .bind(scope) .expect("SVGAnimatedLengthList declaration should bind") @@ -264,8 +536,8 @@ pub(super) fn build_svg_animated_length_list<'s>( pub(super) fn build_svg_animated_number_list<'s>( scope: &mut v8::PinScope<'s, '_>, ) -> v8::Local<'s, v8::Object> { - let base_val = build_svg_number_list(scope); - let anim_val = build_svg_number_list(scope); + let base_val = build_svg_number_list(scope, false); + let anim_val = build_svg_number_list(scope, true); SvgAnimatedNumberListObjectDeclaration::new(base_val, anim_val) .bind(scope) .expect("SVGAnimatedNumberList declaration should bind") @@ -280,6 +552,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) { @@ -331,6 +604,195 @@ pub(super) fn build_svg_animated_number_for_attribute<'s>( object } +pub(super) fn build_svg_animated_number_for_property<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + property: SvgAnimatedNumberProperty, +) -> v8::Local<'s, v8::Object> { + let object = build_svg_animated_number(scope, property.initial_value); + set_svg_animated_number_owner_attribute(scope, object, owner, property.attribute); + let index = u32::try_from(property.index).expect("SVG number property index exceeds u32"); + let index = v8::Integer::new_from_unsigned(scope, index); + set_private_value( + scope, + object, + SVG_ANIMATED_NUMBER_PROPERTY_INDEX_SLOT, + index.into(), + ); + sync_svg_animated_number_from_property(scope, object); + object +} + +pub(super) fn svg_animated_number_property_for_object<'s>( + scope: &mut v8::PinScope<'s, '_>, + animated: v8::Local<'s, v8::Object>, +) -> Option { + let index = get_private_value(scope, animated, SVG_ANIMATED_NUMBER_PROPERTY_INDEX_SLOT)? + .uint32_value(scope)?; + SVG_ANIMATED_NUMBER_PROPERTIES.get(index as usize).copied() +} + +pub(super) fn svg_animated_number_cache_slot(property: SvgAnimatedNumberProperty) -> String { + format!("__moliSvgAnimatedNumberProperty{}", property.index) +} + +pub(super) fn build_svg_animated_integer<'s>( + scope: &mut v8::PinScope<'s, '_>, + value: i32, +) -> v8::Local<'s, v8::Object> { + SvgAnimatedIntegerObjectDeclaration::new(value, value) + .bind(scope) + .expect("SVGAnimatedInteger declaration should bind") +} + +pub(super) fn build_svg_animated_integer_for_property<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + property: SvgAnimatedIntegerProperty, +) -> v8::Local<'s, v8::Object> { + let object = build_svg_animated_integer(scope, property.initial_value); + set_private_value( + scope, + object, + SVG_ANIMATED_INTEGER_OWNER_ELEMENT_SLOT, + owner.into(), + ); + let index = u32::try_from(property.index).expect("SVG integer property index exceeds u32"); + let index = v8::Integer::new_from_unsigned(scope, index); + set_private_value( + scope, + object, + SVG_ANIMATED_INTEGER_PROPERTY_INDEX_SLOT, + index.into(), + ); + sync_svg_animated_integer_from_owner_attribute(scope, object); + object +} + +pub(super) fn svg_animated_integer_property<'s>( + scope: &mut v8::PinScope<'s, '_>, + animated: v8::Local<'s, v8::Object>, +) -> Option { + let index = get_private_value(scope, animated, SVG_ANIMATED_INTEGER_PROPERTY_INDEX_SLOT)? + .uint32_value(scope)?; + SVG_ANIMATED_INTEGER_PROPERTIES.get(index as usize).copied() +} + +pub(super) fn sync_svg_animated_integer_from_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + animated: v8::Local<'s, v8::Object>, +) { + let Some(property) = svg_animated_integer_property(scope, animated) else { + return; + }; + let Some(owner) = get_private_value(scope, animated, SVG_ANIMATED_INTEGER_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let value = svg_owner_attribute_value(scope, owner, property.attribute) + .as_deref() + .and_then(|raw| parse_svg_animated_integer(property, raw)) + .unwrap_or(property.initial_value); + set_svg_animated_integer_values(scope, animated, value); +} + +pub(super) fn reflect_svg_animated_integer_to_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + animated: v8::Local<'s, v8::Object>, +) { + let Some(property) = svg_animated_integer_property(scope, animated) else { + return; + }; + let Some(owner) = get_private_value(scope, animated, SVG_ANIMATED_INTEGER_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let value = get_private_value(scope, animated, SVG_ANIMATED_INTEGER_BASE_VAL_SLOT) + .and_then(|value| value.int32_value(scope)) + .unwrap_or(property.initial_value); + let serialized = match property.component { + SvgAnimatedIntegerComponent::Scalar => value.to_string(), + SvgAnimatedIntegerComponent::PairFirst | SvgAnimatedIntegerComponent::PairSecondOrFirst => { + let (mut first, mut second) = + svg_owner_attribute_value(scope, owner, property.attribute) + .as_deref() + .and_then(parse_svg_integer_pair) + .unwrap_or((property.initial_value, property.initial_value)); + match property.component { + SvgAnimatedIntegerComponent::PairFirst => first = value, + SvgAnimatedIntegerComponent::PairSecondOrFirst => second = value, + SvgAnimatedIntegerComponent::Scalar => unreachable!(), + } + format!("{first} {second}") + } + }; + let Ok((runtime_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner) + else { + return; + }; + let runtime = unsafe { &mut *runtime_ptr }; + let _ = runtime.set_attribute(scope, runtime_ptr, handle, property.attribute, &serialized); +} + +pub(super) fn set_svg_animated_integer_values( + scope: &mut v8::PinScope<'_, '_>, + animated: v8::Local<'_, v8::Object>, + value: i32, +) { + let value = v8::Integer::new(scope, value); + set_private_value( + scope, + animated, + SVG_ANIMATED_INTEGER_BASE_VAL_SLOT, + value.into(), + ); + set_private_value( + scope, + animated, + SVG_ANIMATED_INTEGER_ANIM_VAL_SLOT, + value.into(), + ); +} + +pub(super) fn parse_svg_animated_integer( + property: SvgAnimatedIntegerProperty, + value: &str, +) -> Option { + match property.component { + SvgAnimatedIntegerComponent::Scalar => parse_svg_integer(value), + SvgAnimatedIntegerComponent::PairFirst => { + parse_svg_integer_pair(value).map(|(first, _)| first) + } + SvgAnimatedIntegerComponent::PairSecondOrFirst => { + parse_svg_integer_pair(value).map(|(_, second)| second) + } + } +} + +fn parse_svg_integer(value: &str) -> Option { + value.trim().parse().ok() +} + +fn parse_svg_integer_pair(value: &str) -> Option<(i32, i32)> { + let value = value.trim(); + if let Some((first, second)) = value.split_once(',') { + if second.contains(',') { + return None; + } + return Some((parse_svg_integer(first)?, parse_svg_integer(second)?)); + } + let mut values = value.split_ascii_whitespace(); + let first = parse_svg_integer(values.next()?)?; + let second = match values.next() { + Some(value) => parse_svg_integer(value)?, + None => first, + }; + values.next().is_none().then_some((first, second)) +} + pub(super) fn build_svg_number<'s>( scope: &mut v8::PinScope<'s, '_>, value: f64, @@ -349,20 +811,378 @@ pub(super) fn build_svg_animated_enumeration<'s>( .expect("SVGAnimatedEnumeration declaration should bind") } +pub(super) fn build_svg_animated_enumeration_for_property<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + property: SvgAnimatedEnumerationProperty, +) -> v8::Local<'s, v8::Object> { + let object = build_svg_animated_enumeration(scope, property.initial_value); + set_private_value( + scope, + object, + SVG_ANIMATED_ENUMERATION_OWNER_ELEMENT_SLOT, + owner.into(), + ); + let index = u32::try_from(property.index).expect("SVG enumeration property index exceeds u32"); + let index = v8::Integer::new_from_unsigned(scope, index); + set_private_value( + scope, + object, + SVG_ANIMATED_ENUMERATION_PROPERTY_INDEX_SLOT, + index.into(), + ); + sync_svg_animated_enumeration_from_owner_attribute(scope, object); + object +} + +pub(super) fn svg_animated_enumeration_property<'s>( + scope: &mut v8::PinScope<'s, '_>, + animated: v8::Local<'s, v8::Object>, +) -> Option { + let index = get_private_value( + scope, + animated, + SVG_ANIMATED_ENUMERATION_PROPERTY_INDEX_SLOT, + )? + .uint32_value(scope)?; + SVG_ANIMATED_ENUMERATION_PROPERTIES + .get(index as usize) + .copied() +} + +pub(super) fn sync_svg_animated_enumeration_from_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + animated: v8::Local<'s, v8::Object>, +) { + let Some(property) = svg_animated_enumeration_property(scope, animated) else { + return; + }; + let Some(owner) = + get_private_value(scope, animated, SVG_ANIMATED_ENUMERATION_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let value = svg_owner_attribute_value(scope, owner, property.attribute) + .as_deref() + .and_then(|raw| parse_svg_animated_enumeration(property.kind, raw)) + .unwrap_or(property.initial_value); + set_svg_animated_enumeration_values(scope, animated, value); +} + +pub(super) fn reflect_svg_animated_enumeration_to_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + animated: v8::Local<'s, v8::Object>, +) { + let Some(property) = svg_animated_enumeration_property(scope, animated) else { + return; + }; + let Some(owner) = + get_private_value(scope, animated, SVG_ANIMATED_ENUMERATION_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let Some(value) = + svg_length_number_slot(scope, animated, SVG_ANIMATED_ENUMERATION_BASE_VAL_SLOT) + .map(|value| value as u32) + .and_then(|value| serialize_svg_animated_enumeration(property.kind, value)) + else { + return; + }; + let Ok((runtime_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner) + else { + return; + }; + let runtime = unsafe { &mut *runtime_ptr }; + let _ = runtime.set_attribute(scope, runtime_ptr, handle, property.attribute, value); +} + +pub(super) fn set_svg_animated_enumeration_values( + scope: &mut v8::PinScope<'_, '_>, + animated: v8::Local<'_, v8::Object>, + value: u32, +) { + let value = v8::Integer::new_from_unsigned(scope, value); + set_private_value( + scope, + animated, + SVG_ANIMATED_ENUMERATION_BASE_VAL_SLOT, + value.into(), + ); + set_private_value( + scope, + animated, + SVG_ANIMATED_ENUMERATION_ANIM_VAL_SLOT, + value.into(), + ); +} + +pub(super) fn parse_svg_animated_enumeration( + kind: SvgAnimatedEnumerationKind, + value: &str, +) -> Option { + match kind { + SvgAnimatedEnumerationKind::Keywords(values) => values + .iter() + .find_map(|(keyword, enumerated)| (*keyword == value).then_some(*enumerated)), + SvgAnimatedEnumerationKind::MarkerOrient => match value { + "auto" => Some(SVG_MARKER_ORIENT_AUTO), + "auto-start-reverse" => Some(SVG_MARKER_ORIENT_AUTO_START_REVERSE), + value if parse_svg_angle_value(value).is_some() => Some(SVG_MARKER_ORIENT_ANGLE), + _ => None, + }, + } +} + +pub(super) fn serialize_svg_animated_enumeration( + kind: SvgAnimatedEnumerationKind, + value: u32, +) -> Option<&'static str> { + match kind { + SvgAnimatedEnumerationKind::Keywords(values) => values + .iter() + .find_map(|(keyword, enumerated)| (*enumerated == value).then_some(*keyword)), + SvgAnimatedEnumerationKind::MarkerOrient => match value { + SVG_MARKER_ORIENT_AUTO => Some("auto"), + SVG_MARKER_ORIENT_ANGLE => Some("0"), + SVG_MARKER_ORIENT_AUTO_START_REVERSE => Some("auto-start-reverse"), + _ => None, + }, + } +} + pub(super) fn build_svg_length_list<'s>( scope: &mut v8::PinScope<'s, '_>, + read_only: bool, ) -> v8::Local<'s, v8::Object> { - SvgLengthListObjectDeclaration::new(Vec::new()) - .bind(scope) - .expect("SVGLengthList declaration should bind") + let template = v8::ObjectTemplate::new(scope); + configure_svg_value_list_indexed_property_handler(template); + let object = template + .new_instance(scope) + .expect("SVGLengthList object template should instantiate"); + SvgLengthListObjectDeclaration::new(Vec::new(), read_only) + .bind_into(scope, object) + .expect("SVGLengthList declaration should bind"); + object } pub(super) fn build_svg_number_list<'s>( scope: &mut v8::PinScope<'s, '_>, + read_only: bool, ) -> v8::Local<'s, v8::Object> { - SvgNumberListObjectDeclaration::new(Vec::new()) - .bind(scope) - .expect("SVGNumberList declaration should bind") + let template = v8::ObjectTemplate::new(scope); + configure_svg_value_list_indexed_property_handler(template); + let object = template + .new_instance(scope) + .expect("SVGNumberList object template should instantiate"); + SvgNumberListObjectDeclaration::new(Vec::new(), read_only) + .bind_into(scope, object) + .expect("SVGNumberList declaration should bind"); + 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>, + attribute: &str, +) -> v8::Local<'s, v8::Object> { + let template = v8::ObjectTemplate::new(scope); + configure_svg_string_list_indexed_property_handler(template); + let object = template + .new_instance(scope) + .expect("SVGStringList object template should instantiate"); + SvgStringListObjectDeclaration::new(Vec::new()) + .bind_into(scope, object) + .expect("SVGStringList declaration should bind"); + set_private_value( + scope, + object, + SVG_STRING_LIST_OWNER_ELEMENT_SLOT, + owner.into(), + ); + set_private_value( + scope, + object, + SVG_STRING_LIST_OWNER_ATTRIBUTE_SLOT, + v8_string(scope, attribute) + .unwrap_or_else(|| v8str(scope, "")) + .into(), + ); + sync_svg_string_list_from_owner_attribute(scope, object); + object +} + +pub(super) fn svg_string_list_items<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, +) -> Option> { + get_private_value(scope, list, SVG_STRING_LIST_ITEMS_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) +} + +pub(super) fn set_svg_string_list_items<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, + items: v8::Local<'s, v8::Array>, +) { + set_private_value(scope, list, SVG_STRING_LIST_ITEMS_SLOT, items.into()); +} + +pub(super) fn sync_svg_string_list_from_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, +) { + let Some((owner, attribute)) = svg_string_list_owner_attribute(scope, list) else { + return; + }; + let raw = svg_owner_attribute_value(scope, owner, &attribute); + let synced_value = get_private_value(scope, list, SVG_STRING_LIST_SYNCED_ATTRIBUTE_VALUE_SLOT) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)); + let synced_present = + get_private_value(scope, list, SVG_STRING_LIST_SYNCED_ATTRIBUTE_PRESENT_SLOT) + .map(|value| value.is_true()); + if synced_present == Some(raw.is_some()) + && synced_value.as_deref() == Some(raw.as_deref().unwrap_or_default()) + { + return; + } + + let values = parse_svg_string_list_attribute(&attribute, raw.as_deref()); + let length = i32::try_from(values.len()).unwrap_or(i32::MAX); + let items = v8::Array::new(scope, length); + for (index, value) in values.into_iter().enumerate() { + let Ok(index) = u32::try_from(index) else { + break; + }; + if let Some(value) = v8_string(scope, &value) { + let _ = items.set_index(scope, index, value.into()); + } + } + set_svg_string_list_items(scope, list, items); + set_svg_string_list_synced_attribute(scope, list, raw.as_deref()); +} + +pub(super) fn reflect_svg_string_list_to_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, +) { + let Some((owner, attribute)) = svg_string_list_owner_attribute(scope, list) else { + return; + }; + let value = serialize_svg_string_list(scope, list, &attribute); + let Ok((runtime_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner) + else { + return; + }; + let runtime = unsafe { &mut *runtime_ptr }; + let _ = runtime.set_attribute(scope, runtime_ptr, handle, &attribute, &value); + set_svg_string_list_synced_attribute(scope, list, Some(&value)); +} + +fn svg_string_list_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, +) -> Option<(v8::Local<'s, v8::Object>, String)> { + let owner = get_private_value(scope, list, SVG_STRING_LIST_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok())?; + let attribute = get_private_value(scope, list, SVG_STRING_LIST_OWNER_ATTRIBUTE_SLOT)? + .to_string(scope)? + .to_rust_string_lossy(scope); + (!attribute.is_empty()).then_some((owner, attribute)) +} + +fn set_svg_string_list_synced_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, + raw: Option<&str>, +) { + set_private_value( + scope, + list, + SVG_STRING_LIST_SYNCED_ATTRIBUTE_VALUE_SLOT, + v8_string(scope, raw.unwrap_or_default()) + .unwrap_or_else(|| v8str(scope, "")) + .into(), + ); + set_private_value( + scope, + list, + SVG_STRING_LIST_SYNCED_ATTRIBUTE_PRESENT_SLOT, + v8::Boolean::new(scope, raw.is_some()).into(), + ); +} + +fn parse_svg_string_list_attribute(attribute: &str, raw: Option<&str>) -> Vec { + let Some(raw) = raw else { + return Vec::new(); + }; + if attribute == "systemLanguage" { + return raw + .split(',') + .map(|token| token.trim_matches(is_html_ascii_whitespace).to_owned()) + .collect(); + } + raw.split(is_html_ascii_whitespace) + .filter(|token| !token.is_empty()) + .map(str::to_owned) + .collect() +} + +fn serialize_svg_string_list<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, + attribute: &str, +) -> String { + let Some(items) = svg_string_list_items(scope, list) else { + return String::new(); + }; + let values = (0..items.length()) + .filter_map(|index| items.get_index(scope, index)) + .filter_map(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + .collect::>(); + values.join(if attribute == "systemLanguage" { + "," + } else { + " " + }) +} + +fn is_html_ascii_whitespace(character: char) -> bool { + matches!( + character, + '\u{0009}' | '\u{000A}' | '\u{000C}' | '\u{000D}' | '\u{0020}' + ) } pub(super) fn build_svg_transform_list<'s>( @@ -403,32 +1223,101 @@ pub(super) fn build_svg_matrix<'s>( .expect("SVGMatrix declaration should bind") } -pub(super) fn build_svg_animated_length<'s>( +pub(super) fn build_svg_animated_length_for_attribute<'s>( scope: &mut v8::PinScope<'s, '_>, - value: f64, + owner: v8::Local<'s, v8::Object>, + attribute: &str, + initial_value: &str, ) -> v8::Local<'s, v8::Object> { - let base_val = build_svg_length(scope, value); - let anim_val = build_svg_length(scope, value); + let parsed = svg_animated_length_attribute_value(scope, owner, attribute, initial_value); + let base_val = build_svg_length_from_parsed(scope, parsed); + let anim_val = build_svg_length_from_parsed(scope, parsed); + for (value, read_only) in [(base_val, false), (anim_val, true)] { + set_svg_length_owner_attribute(scope, value, owner, attribute); + set_private_value( + scope, + value, + SVG_LENGTH_INITIAL_VALUE_SLOT, + v8_string(scope, initial_value) + .unwrap_or_else(|| v8str(scope, "0")) + .into(), + ); + set_private_value( + scope, + value, + SVG_LENGTH_READ_ONLY_SLOT, + v8::Boolean::new(scope, read_only).into(), + ); + } SvgAnimatedLengthObjectDeclaration::new(base_val, anim_val) .bind(scope) .expect("SVGAnimatedLength declaration should bind") } -pub(super) fn build_svg_animated_length_for_attribute<'s>( +pub(super) fn build_svg_animated_angle_for_attribute<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, attribute: &str, ) -> v8::Local<'s, v8::Object> { let parsed = svg_owner_attribute_value(scope, owner, attribute) .as_deref() - .and_then(parse_svg_length_value) + .and_then(parse_svg_orient_angle_value) .unwrap_or_default(); - let base_val = build_svg_length_from_parsed(scope, parsed); - set_svg_length_owner_attribute(scope, base_val, owner, attribute); - let anim_val = build_svg_length_from_parsed(scope, parsed); - SvgAnimatedLengthObjectDeclaration::new(base_val, anim_val) + let base_val = build_svg_angle_from_parsed(scope, &parsed, false); + set_svg_angle_owner_attribute(scope, base_val, owner, attribute); + let anim_val = build_svg_angle_from_parsed(scope, &parsed, true); + set_svg_angle_owner_attribute(scope, anim_val, owner, attribute); + SvgAnimatedAngleObjectDeclaration::new(base_val, anim_val) .bind(scope) - .expect("SVGAnimatedLength declaration should bind") + .expect("SVGAnimatedAngle declaration should bind") +} + +pub(super) fn build_svg_animated_rect_for_view_box<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, +) -> v8::Local<'s, v8::Object> { + let values = svg_view_box_attribute_value(scope, owner); + let base_val = super::rect::build_svg_view_box_rect(scope, owner, values, false); + let anim_val = super::rect::build_svg_view_box_rect(scope, owner, values, true); + SvgAnimatedRectObjectDeclaration::new(base_val, anim_val) + .bind(scope) + .expect("SVGAnimatedRect declaration should bind") +} + +pub(super) fn build_svg_animated_preserve_aspect_ratio<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, +) -> v8::Local<'s, v8::Object> { + let [align, meet_or_slice] = svg_preserve_aspect_ratio_attribute_value(scope, owner); + let base_val = SvgPreserveAspectRatioObjectDeclaration::new(align, meet_or_slice, owner, false) + .bind(scope) + .expect("SVGPreserveAspectRatio base value declaration should bind"); + let anim_val = SvgPreserveAspectRatioObjectDeclaration::new(align, meet_or_slice, owner, true) + .bind(scope) + .expect("SVGPreserveAspectRatio animated value declaration should bind"); + SvgAnimatedPreserveAspectRatioObjectDeclaration::new(base_val, anim_val) + .bind(scope) + .expect("SVGAnimatedPreserveAspectRatio declaration should bind") +} + +pub(super) fn build_svg_angle<'s>(scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Object> { + build_svg_angle_from_parsed(scope, &SvgParsedAngle::default(), false) +} + +pub(super) fn build_svg_angle_from_parsed<'s>( + scope: &mut v8::PinScope<'s, '_>, + parsed: &SvgParsedAngle, + read_only: bool, +) -> v8::Local<'s, v8::Object> { + SvgAngleObjectDeclaration::new( + parsed.unit_type, + parsed.value, + parsed.value_in_specified_units, + parsed.value_as_string.clone(), + read_only, + ) + .bind(scope) + .expect("SVGAngle declaration should bind") } pub(super) fn build_svg_length<'s>( @@ -440,6 +1329,7 @@ pub(super) fn build_svg_length<'s>( SvgParsedLength { value, unit_type: SVG_LENGTH_TYPE_NUMBER, + unit: SvgLengthUnit::Number, raw: None, }, ) @@ -452,11 +1342,19 @@ pub(super) fn build_svg_length_from_parsed<'s>( let value_as_string = parsed .raw .map(str::to_owned) - .or_else(|| Some(serialize_svg_length_value(parsed.value, parsed.unit_type))) + .or_else(|| Some(SvgLength::new(parsed.value, parsed.unit).serialize())) .unwrap_or_else(|| "0".to_owned()); - SvgLengthObjectDeclaration::new(parsed.unit_type, parsed.value, value_as_string) - .bind(scope) - .expect("SVGLength declaration should bind") + let value = + resolve_svg_length_without_context(parsed.value, parsed.unit).unwrap_or(parsed.value); + SvgLengthObjectDeclaration::new( + parsed.unit_type, + parsed.unit.suffix().to_owned(), + value, + parsed.value, + value_as_string, + ) + .bind(scope) + .expect("SVGLength declaration should bind") } pub(super) fn build_dom_point_like<'s>( @@ -1022,15 +1920,40 @@ pub(super) fn cloned_svg_matrix_value_or_throw<'s>( Some(build_svg_matrix(scope, components)) } -pub(super) fn svg_value_list_item_or_default<'s>( +pub(super) fn svg_value_list_item_or_throw<'s>( scope: &mut v8::PinScope<'s, '_>, value: v8::Local<'s, v8::Value>, kind: SvgListKind, -) -> v8::Local<'s, v8::Object> { - v8::Local::::try_from(value).unwrap_or_else(|_| match kind { - SvgListKind::Length => build_svg_length(scope, 0.0), - SvgListKind::Number => build_svg_number(scope, 0.0), - }) +) -> Option> { + let object = v8::Local::::try_from(value).ok(); + 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}"), + ); + None +} + +pub(super) fn svg_value_list_is_read_only<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> bool { + get_private_value(scope, object, SVG_VALUE_LIST_READ_ONLY_SLOT) + .is_some_and(|value| value.is_true()) } pub(super) fn svg_value_list_items<'s>( @@ -1041,6 +1964,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()) @@ -1056,12 +1980,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()); } @@ -1163,27 +2098,47 @@ pub(super) fn sync_svg_animated_value_list_from_owner_attribute<'s>( ) { let raw = svg_owner_attribute_value(scope, owner, attribute); let raw_value = raw.clone().unwrap_or_default(); - if let Some(base_val) = svg_animated_value_list_member(scope, animated, "baseVal", kind) - && svg_value_list_synced_attribute_value(scope, base_val) - .as_deref() - .is_some_and(|synced| synced == raw_value) - { - set_svg_value_list_owner_attribute(scope, base_val, owner, attribute); - return; - } - let base_items = build_svg_value_list_items_from_attribute(scope, raw.as_deref(), kind); - let anim_items = build_svg_value_list_items_from_attribute(scope, raw.as_deref(), kind); if let Some(base_val) = svg_animated_value_list_member(scope, animated, "baseVal", kind) { - set_svg_value_list_items(scope, base_val, base_items, kind); set_svg_value_list_owner_attribute(scope, base_val, owner, attribute); - set_svg_value_list_synced_attribute_value(scope, base_val, &raw_value); + sync_svg_value_list_from_owner_attribute(scope, base_val, kind); } - if let Some(anim_val) = svg_animated_value_list_member(scope, animated, "animVal", kind) { + if let Some(anim_val) = svg_animated_value_list_member(scope, animated, "animVal", kind) + && svg_value_list_synced_attribute_value(scope, anim_val).as_deref() + != Some(raw_value.as_str()) + { + let anim_items = build_svg_value_list_items_from_attribute(scope, raw.as_deref(), kind); set_svg_value_list_items(scope, anim_val, anim_items, kind); set_svg_value_list_synced_attribute_value(scope, anim_val, &raw_value); } } +pub(super) fn sync_svg_value_list_from_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, + kind: SvgListKind, +) { + let Some(owner) = get_private_value(scope, list, SVG_VALUE_LIST_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let Some(attribute) = get_private_value(scope, list, SVG_VALUE_LIST_OWNER_ATTRIBUTE_SLOT) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + .filter(|value| !value.is_empty()) + else { + return; + }; + let raw = svg_owner_attribute_value(scope, owner, &attribute); + let raw_value = raw.clone().unwrap_or_default(); + if svg_value_list_synced_attribute_value(scope, list).as_deref() == Some(raw_value.as_str()) { + return; + } + let items = build_svg_value_list_items_from_attribute(scope, raw.as_deref(), kind); + set_svg_value_list_items(scope, list, items, kind); + set_svg_value_list_synced_attribute_value(scope, list, &raw_value); +} + pub(super) fn build_svg_value_list_items_from_attribute<'s>( scope: &mut v8::PinScope<'s, '_>, raw: Option<&str>, @@ -1214,6 +2169,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(), } } @@ -1236,7 +2196,7 @@ pub(super) fn reflect_svg_value_list_to_owner_attribute<'s>( }; let value = serialize_svg_value_list_items(scope, list, kind); let Ok((runtime_ptr, handle)) = - crate::native_bridge::node_runtime_and_handle_from_object(scope, owner) + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner) else { return; }; @@ -1314,6 +2274,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) + )) + } } } @@ -1663,14 +2631,327 @@ pub(super) fn svg_animated_length_attribute_slot(name: &str) -> &'static str { "y1" => "__moliSvgAnimatedY1", "x2" => "__moliSvgAnimatedX2", "y2" => "__moliSvgAnimatedY2", + "fx" => "__moliSvgAnimatedFx", + "fy" => "__moliSvgAnimatedFy", + "fr" => "__moliSvgAnimatedFr", + "refX" => "__moliSvgAnimatedRefX", + "refY" => "__moliSvgAnimatedRefY", + "markerWidth" => "__moliSvgAnimatedMarkerWidth", + "markerHeight" => "__moliSvgAnimatedMarkerHeight", + "textLength" => SVG_TEXT_CONTENT_TEXT_LENGTH_SLOT, + "startOffset" => "__moliSvgAnimatedStartOffset", _ => "__moliSvgAnimatedUnknown", } } +#[derive(Clone, Debug)] +pub(super) struct SvgParsedAngle { + value: f64, + value_in_specified_units: f64, + unit_type: u32, + value_as_string: String, +} + +impl Default for SvgParsedAngle { + fn default() -> Self { + Self { + value: 0.0, + value_in_specified_units: 0.0, + unit_type: SVG_ANGLE_TYPE_UNSPECIFIED, + value_as_string: "0".to_owned(), + } + } +} + +pub(super) fn parse_svg_orient_angle_value(raw: &str) -> Option { + if matches!( + raw.trim().to_ascii_lowercase().as_str(), + "auto" | "auto-start-reverse" + ) { + return Some(SvgParsedAngle::default()); + } + parse_svg_angle_value(raw) +} + +pub(super) fn parse_svg_angle_value(raw: &str) -> Option { + let raw = raw.trim(); + if raw.is_empty() { + return None; + } + let lowercase = raw.to_ascii_lowercase(); + let (number_raw, unit_type, degrees_per_unit) = if lowercase.ends_with("deg") { + (&raw[..raw.len() - 3], SVG_ANGLE_TYPE_DEG, 1.0) + } else if lowercase.ends_with("grad") { + (&raw[..raw.len() - 4], SVG_ANGLE_TYPE_GRAD, 0.9) + } else if lowercase.ends_with("rad") { + ( + &raw[..raw.len() - 3], + SVG_ANGLE_TYPE_RAD, + 180.0 / std::f64::consts::PI, + ) + } else if lowercase.ends_with("turn") { + (&raw[..raw.len() - 4], SVG_ANGLE_TYPE_UNKNOWN, 360.0) + } else { + (raw, SVG_ANGLE_TYPE_UNSPECIFIED, 1.0) + }; + let value_in_specified_units = moli_css_parse::parse_number(number_raw.trim())?; + Some(SvgParsedAngle { + value: value_in_specified_units * degrees_per_unit, + value_in_specified_units, + unit_type, + value_as_string: raw.to_owned(), + }) +} + +pub(super) fn svg_angle_number_slot<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + slot: &str, +) -> Option { + get_private_value(scope, object, slot)?.number_value(scope) +} + +pub(super) fn svg_angle_string_slot<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + slot: &str, +) -> Option { + get_private_value(scope, object, slot) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) +} + +pub(super) fn svg_angle_is_read_only<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> bool { + get_private_value(scope, object, SVG_ANGLE_READ_ONLY_SLOT).is_some_and(|value| value.is_true()) +} + +pub(super) fn set_svg_angle_parsed_value( + scope: &mut v8::PinScope<'_, '_>, + object: v8::Local<'_, v8::Object>, + parsed: &SvgParsedAngle, +) { + set_private_value( + scope, + object, + SVG_ANGLE_UNIT_TYPE_SLOT, + v8::Integer::new_from_unsigned(scope, parsed.unit_type).into(), + ); + set_private_value( + scope, + object, + SVG_ANGLE_VALUE_SLOT, + v8::Number::new(scope, parsed.value).into(), + ); + set_private_value( + scope, + object, + SVG_ANGLE_VALUE_IN_SPECIFIED_UNITS_SLOT, + v8::Number::new(scope, parsed.value_in_specified_units).into(), + ); + set_private_value( + scope, + object, + SVG_ANGLE_VALUE_AS_STRING_SLOT, + v8_string(scope, &parsed.value_as_string) + .unwrap_or_else(|| v8str(scope, "0")) + .into(), + ); +} + +fn svg_angle_current_unit<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> (u32, bool) { + let unit_type = svg_angle_number_slot(scope, object, SVG_ANGLE_UNIT_TYPE_SLOT) + .unwrap_or(SVG_ANGLE_TYPE_UNSPECIFIED as f64) as u32; + let is_turn = unit_type == SVG_ANGLE_TYPE_UNKNOWN + && svg_angle_string_slot(scope, object, SVG_ANGLE_VALUE_AS_STRING_SLOT) + .is_some_and(|value| value.trim().to_ascii_lowercase().ends_with("turn")); + (unit_type, is_turn) +} + +fn svg_angle_degrees_per_unit(unit_type: u32, is_turn: bool) -> f64 { + match unit_type { + SVG_ANGLE_TYPE_RAD => 180.0 / std::f64::consts::PI, + SVG_ANGLE_TYPE_GRAD => 0.9, + SVG_ANGLE_TYPE_UNKNOWN if is_turn => 360.0, + _ => 1.0, + } +} + +fn svg_angle_unit_suffix(unit_type: u32, is_turn: bool) -> &'static str { + match unit_type { + SVG_ANGLE_TYPE_DEG => "deg", + SVG_ANGLE_TYPE_RAD => "rad", + SVG_ANGLE_TYPE_GRAD => "grad", + SVG_ANGLE_TYPE_UNKNOWN if is_turn => "turn", + _ => "", + } +} + +fn svg_angle_from_specified_value( + value_in_specified_units: f64, + unit_type: u32, + is_turn: bool, +) -> SvgParsedAngle { + SvgParsedAngle { + value: value_in_specified_units * svg_angle_degrees_per_unit(unit_type, is_turn), + value_in_specified_units, + unit_type, + value_as_string: format!( + "{}{}", + svg_geometry::serialize_number(value_in_specified_units), + svg_angle_unit_suffix(unit_type, is_turn) + ), + } +} + +pub(super) fn set_svg_angle_value_degrees<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + value: f64, +) { + let (unit_type, is_turn) = svg_angle_current_unit(scope, object); + let specified = value / svg_angle_degrees_per_unit(unit_type, is_turn); + let parsed = svg_angle_from_specified_value(specified, unit_type, is_turn); + set_svg_angle_parsed_value(scope, object, &parsed); +} + +pub(super) fn set_svg_angle_value_in_specified_units<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + value: f64, +) { + let (unit_type, is_turn) = svg_angle_current_unit(scope, object); + let parsed = svg_angle_from_specified_value(value, unit_type, is_turn); + set_svg_angle_parsed_value(scope, object, &parsed); +} + +pub(super) fn set_svg_angle_new_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + unit_type: u32, + value: f64, +) -> bool { + if !(SVG_ANGLE_TYPE_UNSPECIFIED..=SVG_ANGLE_TYPE_GRAD).contains(&unit_type) { + return false; + } + let parsed = svg_angle_from_specified_value(value, unit_type, false); + set_svg_angle_parsed_value(scope, object, &parsed); + true +} + +pub(super) fn convert_svg_angle_to_unit<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + unit_type: u32, +) -> bool { + if !(SVG_ANGLE_TYPE_UNSPECIFIED..=SVG_ANGLE_TYPE_GRAD).contains(&unit_type) { + return false; + } + let value = svg_angle_number_slot(scope, object, SVG_ANGLE_VALUE_SLOT).unwrap_or(0.0); + let specified = value / svg_angle_degrees_per_unit(unit_type, false); + let parsed = svg_angle_from_specified_value(specified, unit_type, false); + set_svg_angle_parsed_value(scope, object, &parsed); + true +} + +pub(super) fn set_svg_angle_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + angle: v8::Local<'s, v8::Object>, + owner: v8::Local<'s, v8::Object>, + attribute: &str, +) { + set_private_value(scope, angle, SVG_ANGLE_OWNER_ELEMENT_SLOT, owner.into()); + set_private_value( + scope, + angle, + SVG_ANGLE_OWNER_ATTRIBUTE_SLOT, + v8_string(scope, attribute) + .unwrap_or_else(|| v8str(scope, "")) + .into(), + ); +} + +pub(super) fn sync_svg_angle_from_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + angle: v8::Local<'s, v8::Object>, +) { + let Some(owner) = get_private_value(scope, angle, SVG_ANGLE_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let Some(attribute) = svg_angle_string_slot(scope, angle, SVG_ANGLE_OWNER_ATTRIBUTE_SLOT) + .filter(|value| !value.is_empty()) + else { + return; + }; + let parsed = svg_owner_attribute_value(scope, owner, &attribute) + .as_deref() + .and_then(parse_svg_orient_angle_value) + .unwrap_or_default(); + set_svg_angle_parsed_value(scope, angle, &parsed); +} + +pub(super) fn sync_svg_animated_angle_from_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + animated: v8::Local<'s, v8::Object>, + owner: v8::Local<'s, v8::Object>, + attribute: &str, +) { + let parsed = svg_owner_attribute_value(scope, owner, attribute) + .as_deref() + .and_then(parse_svg_orient_angle_value) + .unwrap_or_default(); + if let Some(base_val) = get_private_value(scope, animated, SVG_ANIMATED_ANGLE_BASE_VAL_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + { + set_svg_angle_parsed_value(scope, base_val, &parsed); + set_svg_angle_owner_attribute(scope, base_val, owner, attribute); + } + if let Some(anim_val) = get_private_value(scope, animated, SVG_ANIMATED_ANGLE_ANIM_VAL_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + { + set_svg_angle_parsed_value(scope, anim_val, &parsed); + set_svg_angle_owner_attribute(scope, anim_val, owner, attribute); + } +} + +pub(super) fn reflect_svg_angle_to_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + angle: v8::Local<'s, v8::Object>, +) { + let Some(owner) = get_private_value(scope, angle, SVG_ANGLE_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let Some(attribute) = svg_angle_string_slot(scope, angle, SVG_ANGLE_OWNER_ATTRIBUTE_SLOT) + .filter(|value| !value.is_empty()) + else { + return; + }; + let Some(value) = svg_angle_string_slot(scope, angle, SVG_ANGLE_VALUE_AS_STRING_SLOT) else { + return; + }; + let Ok((runtime_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner) + else { + return; + }; + let runtime = unsafe { &mut *runtime_ptr }; + let _ = runtime.set_attribute(scope, runtime_ptr, handle, &attribute, &value); +} + #[derive(Clone, Copy)] pub(super) struct SvgParsedLength { value: f64, unit_type: u32, + unit: SvgLengthUnit, raw: Option<&'static str>, } @@ -1679,6 +2960,7 @@ impl Default for SvgParsedLength { Self { value: 0.0, unit_type: SVG_LENGTH_TYPE_NUMBER, + unit: SvgLengthUnit::Number, raw: Some("0"), } } @@ -1693,9 +2975,9 @@ pub(super) fn svg_length_number_slot<'s>( value.number_value(scope) } -pub(super) fn set_svg_length_numeric_value( - scope: &mut v8::PinScope<'_, '_>, - object: v8::Local<'_, v8::Object>, +pub(super) fn set_svg_length_numeric_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, value: f64, unit_type: u32, ) { @@ -1706,15 +2988,16 @@ pub(super) fn set_svg_length_numeric_value( SvgParsedLength { value, unit_type, + unit: svg_length_unit_from_type(unit_type), raw: None, }, ); set_svg_length_value_string(scope, object, &value_as_string); } -pub(super) fn set_svg_length_parsed_value( - scope: &mut v8::PinScope<'_, '_>, - object: v8::Local<'_, v8::Object>, +pub(super) fn set_svg_length_parsed_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, parsed: SvgParsedLength, ) { set_private_value( @@ -1723,12 +3006,71 @@ pub(super) fn set_svg_length_parsed_value( SVG_LENGTH_UNIT_TYPE_SLOT, v8::Number::new(scope, parsed.unit_type as f64).into(), ); + set_private_value( + scope, + object, + SVG_LENGTH_UNIT_SUFFIX_SLOT, + v8_string(scope, parsed.unit.suffix()) + .unwrap_or_else(|| v8str(scope, "")) + .into(), + ); + set_private_value( + scope, + object, + SVG_LENGTH_VALUE_IN_SPECIFIED_UNITS_SLOT, + v8::Number::new(scope, parsed.value).into(), + ); let raw = parsed .raw .map(str::to_owned) - .or_else(|| Some(serialize_svg_length_value(parsed.value, parsed.unit_type))) + .or_else(|| Some(SvgLength::new(parsed.value, parsed.unit).serialize())) .unwrap_or_else(|| "0".to_owned()); - set_svg_length_value(scope, object, parsed.value, &raw); + let value = resolve_svg_length_user_value_for_unit(scope, object, parsed.value, parsed.unit) + .or_else(|| resolve_svg_length_without_context(parsed.value, parsed.unit)) + .unwrap_or(parsed.value); + set_svg_length_value(scope, object, value, &raw); +} + +pub(super) fn set_svg_length_value_in_user_units<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + value: f64, +) { + let unit = svg_length_unit_slot(scope, object); + let specified_value = svg_length_unit_factor(scope, object, unit) + .filter(|factor| *factor != 0.0) + .map(|factor| value / factor) + .unwrap_or(value); + set_svg_length_specified_value(scope, object, specified_value, value, unit); +} + +pub(super) fn set_svg_length_value_in_specified_units<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + specified_value: f64, +) { + let unit = svg_length_unit_slot(scope, object); + let value = resolve_svg_length_user_value_for_unit(scope, object, specified_value, unit) + .or_else(|| resolve_svg_length_without_context(specified_value, unit)) + .unwrap_or(specified_value); + set_svg_length_specified_value(scope, object, specified_value, value, unit); +} + +fn set_svg_length_specified_value( + scope: &mut v8::PinScope<'_, '_>, + object: v8::Local<'_, v8::Object>, + specified_value: f64, + value: f64, + unit: SvgLengthUnit, +) { + set_private_value( + scope, + object, + SVG_LENGTH_VALUE_IN_SPECIFIED_UNITS_SLOT, + v8::Number::new(scope, specified_value).into(), + ); + let value_as_string = SvgLength::new(specified_value, unit).serialize(); + set_svg_length_value(scope, object, value, &value_as_string); } pub(super) fn set_svg_length_value( @@ -1766,11 +3108,9 @@ pub(super) fn sync_svg_animated_length_from_owner_attribute<'s>( animated: v8::Local<'s, v8::Object>, owner: v8::Local<'s, v8::Object>, attribute: &str, + initial_value: &str, ) { - let parsed = svg_owner_attribute_value(scope, owner, attribute) - .as_deref() - .and_then(parse_svg_length_value) - .unwrap_or_default(); + let parsed = svg_animated_length_attribute_value(scope, owner, attribute, initial_value); if let Some(base_val) = get_private_value(scope, animated, SVG_ANIMATED_LENGTH_BASE_VAL_SLOT) .and_then(|value| v8::Local::::try_from(value).ok()) { @@ -1784,6 +3124,351 @@ pub(super) fn sync_svg_animated_length_from_owner_attribute<'s>( } } +pub(super) fn sync_svg_length_from_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + length: v8::Local<'s, v8::Object>, +) { + // Only an individual animated attribute has a default. List items retain + // their separate list ownership and synchronization rules. + let Some(initial) = get_private_value(scope, length, SVG_LENGTH_INITIAL_VALUE_SLOT) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + else { + return; + }; + let Some(owner) = get_private_value(scope, length, SVG_LENGTH_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let Some(attribute) = get_private_value(scope, length, SVG_LENGTH_OWNER_ATTRIBUTE_SLOT) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + else { + return; + }; + let parsed = svg_animated_length_attribute_value(scope, owner, &attribute, &initial); + set_svg_length_parsed_value(scope, length, parsed); +} + +fn svg_animated_length_attribute_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + attribute: &str, + initial_value: &str, +) -> SvgParsedLength { + svg_owner_attribute_value(scope, owner, attribute) + .as_deref() + .and_then(parse_svg_length_value) + .or_else(|| parse_svg_length_value(initial_value)) + .unwrap_or_default() +} + +pub(super) fn sync_svg_animated_rect_from_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + animated: v8::Local<'s, v8::Object>, + owner: v8::Local<'s, v8::Object>, +) { + let values = svg_view_box_attribute_value(scope, owner); + for slot in [ + SVG_ANIMATED_RECT_BASE_VAL_SLOT, + SVG_ANIMATED_RECT_ANIM_VAL_SLOT, + ] { + if let Some(rect) = get_private_value(scope, animated, slot) + .and_then(|value| v8::Local::::try_from(value).ok()) + { + super::rect::set_svg_view_box_rect_values(scope, rect, values); + } + } +} + +pub(super) fn sync_svg_view_box_rect_from_owner<'s>( + scope: &mut v8::PinScope<'s, '_>, + rect: v8::Local<'s, v8::Object>, +) { + let Some(owner) = super::rect::svg_view_box_rect_owner(scope, rect) else { + return; + }; + if let Some(animated) = get_private_value(scope, owner, SVG_FIT_VIEW_BOX_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + { + sync_svg_animated_rect_from_owner_attribute(scope, animated, owner); + } else { + let values = svg_view_box_attribute_value(scope, owner); + super::rect::set_svg_view_box_rect_values(scope, rect, values); + } +} + +pub(super) fn reflect_svg_view_box_rect_mutation<'s>( + scope: &mut v8::PinScope<'s, '_>, + rect: v8::Local<'s, v8::Object>, +) { + let Some(owner) = super::rect::svg_view_box_rect_owner(scope, rect) else { + return; + }; + let values = super::rect::svg_view_box_rect_values(scope, rect); + // Keep float round trips, including integral values outside the i64 range. + let serialized = values + .map(|value| { + if value == 0.0 { + "0".to_owned() + } else { + (value as f32).to_string() + } + }) + .join(" "); + set_svg_owner_attribute_value(scope, owner, "viewBox", &serialized); + if let Some(animated) = get_private_value(scope, owner, SVG_FIT_VIEW_BOX_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + { + sync_svg_animated_rect_from_owner_attribute(scope, animated, owner); + } +} + +fn svg_view_box_attribute_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, +) -> [f64; 4] { + svg_owner_attribute_value(scope, owner, "viewBox") + .as_deref() + .and_then(parse_svg_view_box_value) + .unwrap_or([0.0; 4]) +} + +fn parse_svg_view_box_value(raw: &str) -> Option<[f64; 4]> { + let values = svg_geometry::parse_number_list(raw)?; + let [x, y, width, height] = values.as_slice() else { + return None; + }; + if *width < 0.0 || *height < 0.0 { + return None; + } + let values = [*x, *y, *width, *height].map(|value| f64::from(value as f32)); + values + .iter() + .all(|value| value.is_finite()) + .then_some(values) +} + +pub(super) fn sync_svg_animated_preserve_aspect_ratio_from_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + animated: v8::Local<'s, v8::Object>, + owner: v8::Local<'s, v8::Object>, +) { + let value = svg_preserve_aspect_ratio_attribute_value(scope, owner); + for slot in [ + SVG_ANIMATED_PRESERVE_ASPECT_RATIO_BASE_VAL_SLOT, + SVG_ANIMATED_PRESERVE_ASPECT_RATIO_ANIM_VAL_SLOT, + ] { + if let Some(aspect_ratio) = get_private_value(scope, animated, slot) + .and_then(|value| v8::Local::::try_from(value).ok()) + { + set_svg_preserve_aspect_ratio_value(scope, aspect_ratio, value); + } + } +} + +pub(super) fn sync_svg_preserve_aspect_ratio_from_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + aspect_ratio: v8::Local<'s, v8::Object>, +) { + let Some(owner) = get_private_value( + scope, + aspect_ratio, + SVG_PRESERVE_ASPECT_RATIO_OWNER_ELEMENT_SLOT, + ) + .and_then(|value| v8::Local::::try_from(value).ok()) else { + return; + }; + let value = svg_preserve_aspect_ratio_attribute_value(scope, owner); + set_svg_preserve_aspect_ratio_value(scope, aspect_ratio, value); +} + +pub(super) fn set_svg_preserve_aspect_ratio_value( + scope: &mut v8::PinScope<'_, '_>, + aspect_ratio: v8::Local<'_, v8::Object>, + [align, meet_or_slice]: [u32; 2], +) { + set_private_value( + scope, + aspect_ratio, + SVG_PRESERVE_ASPECT_RATIO_ALIGN_SLOT, + v8::Integer::new_from_unsigned(scope, align).into(), + ); + set_private_value( + scope, + aspect_ratio, + SVG_PRESERVE_ASPECT_RATIO_MEET_OR_SLICE_SLOT, + v8::Integer::new_from_unsigned(scope, meet_or_slice).into(), + ); +} + +pub(super) fn reflect_svg_preserve_aspect_ratio_to_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + aspect_ratio: v8::Local<'s, v8::Object>, +) { + let Some(owner) = get_private_value( + scope, + aspect_ratio, + SVG_PRESERVE_ASPECT_RATIO_OWNER_ELEMENT_SLOT, + ) + .and_then(|value| v8::Local::::try_from(value).ok()) else { + return; + }; + let align = svg_number_slot(scope, aspect_ratio, SVG_PRESERVE_ASPECT_RATIO_ALIGN_SLOT) + .unwrap_or(SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MID as f64) as u32; + let meet_or_slice = svg_number_slot( + scope, + aspect_ratio, + SVG_PRESERVE_ASPECT_RATIO_MEET_OR_SLICE_SLOT, + ) + .unwrap_or(SVG_MEET_OR_SLICE_MEET as f64) as u32; + let (Some(align), Some(meet_or_slice)) = ( + serialize_svg_preserve_aspect_ratio_align(align), + serialize_svg_meet_or_slice(meet_or_slice), + ) else { + return; + }; + let serialized = format!("{align} {meet_or_slice}"); + set_svg_owner_attribute_value(scope, owner, "preserveAspectRatio", &serialized); + if let Some(animated) = get_private_value(scope, owner, SVG_FIT_PRESERVE_ASPECT_RATIO_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + { + sync_svg_animated_preserve_aspect_ratio_from_owner_attribute(scope, animated, owner); + } +} + +fn svg_preserve_aspect_ratio_attribute_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, +) -> [u32; 2] { + svg_owner_attribute_value(scope, owner, "preserveAspectRatio") + .as_deref() + .and_then(parse_svg_preserve_aspect_ratio_value) + .unwrap_or([ + SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MID, + SVG_MEET_OR_SLICE_MEET, + ]) +} + +fn parse_svg_preserve_aspect_ratio_value(raw: &str) -> Option<[u32; 2]> { + let mut tokens = raw.split_ascii_whitespace(); + let mut align = tokens.next()?; + if align == "defer" { + align = tokens.next()?; + } + let align = parse_svg_preserve_aspect_ratio_align(align)?; + let meet_or_slice = match tokens.next() { + None | Some("meet") => SVG_MEET_OR_SLICE_MEET, + Some("slice") => SVG_MEET_OR_SLICE_SLICE, + Some(_) => return None, + }; + tokens.next().is_none().then_some([align, meet_or_slice]) +} + +fn parse_svg_preserve_aspect_ratio_align(value: &str) -> Option { + match value { + "none" => Some(SVG_PRESERVE_ASPECT_RATIO_NONE), + "xMinYMin" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MIN), + "xMidYMin" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MIN), + "xMaxYMin" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MIN), + "xMinYMid" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MID), + "xMidYMid" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MID), + "xMaxYMid" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MID), + "xMinYMax" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MAX), + "xMidYMax" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MAX), + "xMaxYMax" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MAX), + _ => None, + } +} + +fn serialize_svg_preserve_aspect_ratio_align(value: u32) -> Option<&'static str> { + match value { + SVG_PRESERVE_ASPECT_RATIO_NONE => Some("none"), + SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MIN => Some("xMinYMin"), + SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MIN => Some("xMidYMin"), + SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MIN => Some("xMaxYMin"), + SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MID => Some("xMinYMid"), + SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MID => Some("xMidYMid"), + SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MID => Some("xMaxYMid"), + SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MAX => Some("xMinYMax"), + SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MAX => Some("xMidYMax"), + SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MAX => Some("xMaxYMax"), + _ => None, + } +} + +fn serialize_svg_meet_or_slice(value: u32) -> Option<&'static str> { + match value { + SVG_MEET_OR_SLICE_MEET => Some("meet"), + SVG_MEET_OR_SLICE_SLICE => Some("slice"), + _ => None, + } +} + +pub(super) fn set_svg_animated_string_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + animated: v8::Local<'s, v8::Object>, + owner: v8::Local<'s, v8::Object>, + attribute: &str, +) { + set_private_value( + scope, + animated, + SVG_ANIMATED_STRING_OWNER_ELEMENT_SLOT, + owner.into(), + ); + set_private_value( + scope, + animated, + SVG_ANIMATED_STRING_OWNER_ATTRIBUTE_SLOT, + v8_string(scope, attribute) + .unwrap_or_else(|| v8str(scope, "")) + .into(), + ); +} + +pub(super) fn sync_svg_animated_string_from_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + animated: v8::Local<'s, v8::Object>, +) { + let Some(owner) = get_private_value(scope, animated, SVG_ANIMATED_STRING_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let Some(attribute) = + get_private_value(scope, animated, SVG_ANIMATED_STRING_OWNER_ATTRIBUTE_SLOT) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + .filter(|value| !value.is_empty()) + else { + return; + }; + let value = svg_owner_attribute_value(scope, owner, &attribute).unwrap_or_default(); + set_svg_animated_string_values(scope, animated, &value); +} + +pub(super) fn set_svg_animated_string_values( + scope: &mut v8::PinScope<'_, '_>, + animated: v8::Local<'_, v8::Object>, + value: &str, +) { + let value = v8_string(scope, value).unwrap_or_else(|| v8str(scope, "")); + set_private_value( + scope, + animated, + SVG_ANIMATED_STRING_BASE_VAL_SLOT, + value.into(), + ); + set_private_value( + scope, + animated, + SVG_ANIMATED_STRING_ANIM_VAL_SLOT, + value.into(), + ); +} + pub(super) fn set_svg_animated_number_owner_attribute<'s>( scope: &mut v8::PinScope<'s, '_>, animated: v8::Local<'s, v8::Object>, @@ -1817,6 +3502,57 @@ pub(super) fn sync_svg_animated_number_from_owner_attribute<'s>( .as_deref() .and_then(parse_svg_number_value) .unwrap_or(0.0); + set_svg_animated_number_values(scope, animated, value); +} + +pub(super) fn sync_svg_animated_number_from_stored_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + animated: v8::Local<'s, v8::Object>, +) { + if svg_animated_number_property_for_object(scope, animated).is_some() { + sync_svg_animated_number_from_property(scope, animated); + return; + } + let Some(owner) = get_private_value(scope, animated, SVG_ANIMATED_NUMBER_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let Some(attribute) = + get_private_value(scope, animated, SVG_ANIMATED_NUMBER_OWNER_ATTRIBUTE_SLOT) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + .filter(|value| !value.is_empty()) + else { + return; + }; + sync_svg_animated_number_from_owner_attribute(scope, animated, owner, &attribute); +} + +pub(super) fn sync_svg_animated_number_from_property<'s>( + scope: &mut v8::PinScope<'s, '_>, + animated: v8::Local<'s, v8::Object>, +) { + let Some(property) = svg_animated_number_property_for_object(scope, animated) else { + return; + }; + let Some(owner) = get_private_value(scope, animated, SVG_ANIMATED_NUMBER_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let value = svg_owner_attribute_value(scope, owner, property.attribute) + .as_deref() + .and_then(|raw| parse_svg_animated_number(property, raw)) + .unwrap_or(property.initial_value); + set_svg_animated_number_values(scope, animated, value); +} + +pub(super) fn set_svg_animated_number_values( + scope: &mut v8::PinScope<'_, '_>, + animated: v8::Local<'_, v8::Object>, + value: f64, +) { let value = v8::Number::new(scope, value); set_private_value( scope, @@ -1836,6 +3572,10 @@ pub(super) fn reflect_svg_animated_number_to_owner_attribute<'s>( scope: &mut v8::PinScope<'s, '_>, animated: v8::Local<'s, v8::Object>, ) { + if svg_animated_number_property_for_object(scope, animated).is_some() { + reflect_svg_animated_number_property_to_owner_attribute(scope, animated); + return; + } let Some(owner) = get_private_value(scope, animated, SVG_ANIMATED_NUMBER_OWNER_ELEMENT_SLOT) .and_then(|value| v8::Local::::try_from(value).ok()) else { @@ -1852,7 +3592,7 @@ pub(super) fn reflect_svg_animated_number_to_owner_attribute<'s>( let value = svg_number_slot(scope, animated, SVG_ANIMATED_NUMBER_BASE_VAL_SLOT).unwrap_or(0.0); let value = svg_geometry::serialize_number(value); let Ok((runtime_ptr, handle)) = - crate::native_bridge::node_runtime_and_handle_from_object(scope, owner) + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner) else { return; }; @@ -1860,6 +3600,85 @@ pub(super) fn reflect_svg_animated_number_to_owner_attribute<'s>( let _ = runtime.set_attribute(scope, runtime_ptr, handle, &attribute, &value); } +pub(super) fn reflect_svg_animated_number_property_to_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + animated: v8::Local<'s, v8::Object>, +) { + let Some(property) = svg_animated_number_property_for_object(scope, animated) else { + return; + }; + let Some(owner) = get_private_value(scope, animated, SVG_ANIMATED_NUMBER_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let value = svg_number_slot(scope, animated, SVG_ANIMATED_NUMBER_BASE_VAL_SLOT) + .unwrap_or(property.initial_value); + let serialized = match property.component { + SvgAnimatedNumberComponent::Scalar | SvgAnimatedNumberComponent::NumberOrPercentage => { + svg_geometry::serialize_number(value) + } + SvgAnimatedNumberComponent::PairFirst | SvgAnimatedNumberComponent::PairSecondOrFirst => { + let (mut first, mut second) = + svg_owner_attribute_value(scope, owner, property.attribute) + .as_deref() + .and_then(parse_svg_number_pair) + .unwrap_or((property.initial_value, property.initial_value)); + match property.component { + SvgAnimatedNumberComponent::PairFirst => first = value, + SvgAnimatedNumberComponent::PairSecondOrFirst => second = value, + SvgAnimatedNumberComponent::Scalar + | SvgAnimatedNumberComponent::NumberOrPercentage => unreachable!(), + } + format!( + "{} {}", + svg_geometry::serialize_number(first), + svg_geometry::serialize_number(second) + ) + } + }; + let Ok((runtime_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner) + else { + return; + }; + let runtime = unsafe { &mut *runtime_ptr }; + let _ = runtime.set_attribute(scope, runtime_ptr, handle, property.attribute, &serialized); +} + +pub(super) fn parse_svg_animated_number( + property: SvgAnimatedNumberProperty, + value: &str, +) -> Option { + match property.component { + SvgAnimatedNumberComponent::Scalar => parse_svg_number_value(value), + SvgAnimatedNumberComponent::PairFirst => { + parse_svg_number_pair(value).map(|(first, _)| first) + } + SvgAnimatedNumberComponent::PairSecondOrFirst => { + parse_svg_number_pair(value).map(|(_, second)| second) + } + SvgAnimatedNumberComponent::NumberOrPercentage => parse_svg_number_or_percentage(value), + } +} + +fn parse_svg_number_pair(value: &str) -> Option<(f64, f64)> { + let values = svg_geometry::parse_number_list(value)?; + match values.as_slice() { + [first] => Some((*first, *first)), + [first, second] => Some((*first, *second)), + _ => None, + } +} + +fn parse_svg_number_or_percentage(value: &str) -> Option { + let value = value.trim(); + match value.strip_suffix('%') { + Some(percentage) => parse_svg_number_value(percentage).map(|value| value / 100.0), + None => parse_svg_number_value(value), + } +} + pub(super) fn set_svg_length_owner_attribute<'s>( scope: &mut v8::PinScope<'s, '_>, length: v8::Local<'s, v8::Object>, @@ -1915,8 +3734,34 @@ pub(super) fn svg_owner_attribute_value<'s>( ) -> Option { let (runtime_ptr, handle) = 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, + ) +} + +fn set_svg_owner_attribute_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + attribute: &str, + value: &str, +) { + let Ok((runtime_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner) + else { + return; + }; let runtime = unsafe { &mut *runtime_ptr }; - runtime.dom_host().get_attribute(handle, attribute) + let _ = runtime.set_attribute(scope, runtime_ptr, handle, attribute, value); } pub(super) fn parse_svg_length_value(raw: &str) -> Option { @@ -1927,6 +3772,7 @@ pub(super) fn svg_parsed_length_from_svg_length(length: SvgLength) -> SvgParsedL SvgParsedLength { value: length.value, unit_type: svg_length_unit_type(length.unit), + unit: length.unit, raw: None, } } @@ -1937,6 +3783,17 @@ pub(super) fn svg_length_unit_type(unit: SvgLengthUnit) -> u32 { SvgLengthUnit::Percentage => SVG_LENGTH_TYPE_PERCENTAGE, SvgLengthUnit::Ems => SVG_LENGTH_TYPE_EMS, SvgLengthUnit::Exs => SVG_LENGTH_TYPE_EXS, + SvgLengthUnit::Ch + | SvgLengthUnit::Rem + | SvgLengthUnit::Lh + | SvgLengthUnit::Rlh + | SvgLengthUnit::Cap + | SvgLengthUnit::Ic + | SvgLengthUnit::Q + | SvgLengthUnit::Vw + | SvgLengthUnit::Vh + | SvgLengthUnit::Vmin + | SvgLengthUnit::Vmax => SVG_LENGTH_TYPE_UNKNOWN, SvgLengthUnit::Px => SVG_LENGTH_TYPE_PX, SvgLengthUnit::Cm => SVG_LENGTH_TYPE_CM, SvgLengthUnit::Mm => SVG_LENGTH_TYPE_MM, @@ -1968,3 +3825,296 @@ pub(super) fn svg_length_unit_from_type(unit_type: u32) -> SvgLengthUnit { _ => SvgLengthUnit::Number, } } + +fn svg_length_unit_slot<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> SvgLengthUnit { + get_private_value(scope, object, SVG_LENGTH_UNIT_SUFFIX_SLOT) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + .and_then(|suffix| svg_geometry::parse_length(&format!("1{suffix}"))) + .map(|length| length.unit) + .unwrap_or(SvgLengthUnit::Number) +} + +pub(super) fn svg_length_value_in_user_units<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> f64 { + let specified_value = + svg_length_number_slot(scope, object, SVG_LENGTH_VALUE_IN_SPECIFIED_UNITS_SLOT) + .unwrap_or(0.0); + let unit = svg_length_unit_slot(scope, object); + if resolve_svg_absolute_length(1.0, unit).is_some() { + return svg_length_number_slot(scope, object, SVG_LENGTH_VALUE_SLOT) + .unwrap_or(specified_value); + } + if let Some(value) = + resolve_svg_length_user_value_for_unit(scope, object, specified_value, unit) + { + set_private_value( + scope, + object, + SVG_LENGTH_VALUE_SLOT, + v8::Number::new(scope, value).into(), + ); + return value; + } + resolve_svg_length_without_context(specified_value, unit) + .or_else(|| svg_length_number_slot(scope, object, SVG_LENGTH_VALUE_SLOT)) + .unwrap_or(specified_value) +} + +pub(super) fn convert_svg_length_to_unit<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + unit_type: u32, +) -> bool { + let unit = svg_length_unit_from_type(unit_type); + let value = svg_length_value_in_user_units(scope, object); + let Some(factor) = svg_length_unit_factor(scope, object, unit).filter(|factor| *factor != 0.0) + else { + return false; + }; + set_private_value( + scope, + object, + SVG_LENGTH_UNIT_TYPE_SLOT, + v8::Number::new(scope, unit_type as f64).into(), + ); + set_private_value( + scope, + object, + SVG_LENGTH_UNIT_SUFFIX_SLOT, + v8_string(scope, unit.suffix()) + .unwrap_or_else(|| v8str(scope, "")) + .into(), + ); + set_svg_length_specified_value(scope, object, value / factor, value, unit); + true +} + +fn svg_length_unit_factor<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + unit: SvgLengthUnit, +) -> Option { + resolve_svg_length_user_value_for_unit(scope, object, 1.0, unit) + .or_else(|| resolve_svg_length_without_context(1.0, unit)) +} + +fn resolve_svg_length_without_context(value: f64, unit: SvgLengthUnit) -> Option { + if let Some(value) = resolve_svg_absolute_length(value, unit) { + return Some(value); + } + matches!(unit, SvgLengthUnit::Percentage).then_some(value) +} + +fn resolve_svg_length_user_value_for_unit<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + value: f64, + unit: SvgLengthUnit, +) -> Option { + if let Some(value) = resolve_svg_absolute_length(value, unit) { + return Some(value); + } + let owner = svg_length_owner_element(scope, object)?; + let attribute = svg_length_owner_attribute(scope, object).unwrap_or_default(); + let (runtime_ptr, handle) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner).ok()?; + let runtime = unsafe { &*runtime_ptr }; + let connected = runtime + .dom_host() + .node(handle) + .is_some_and(|node| node.is_connected()); + let basis = if connected { + svg_length_percentage_basis(runtime, handle, &attribute) + } else { + 100.0 + }; + let context = svg_length_numeric_context(runtime, handle, unit, connected); + match unit { + SvgLengthUnit::Percentage => Some(value * basis / 100.0), + SvgLengthUnit::Ems => context.font_size_px.map(|basis| value * basis), + SvgLengthUnit::Exs | SvgLengthUnit::Ch => { + context.font_size_px.map(|basis| value * basis * 0.5) + } + SvgLengthUnit::Rem => context.root_font_size_px.map(|basis| value * basis), + SvgLengthUnit::Lh | SvgLengthUnit::Rlh => context.line_height_px.map(|basis| value * basis), + SvgLengthUnit::Cap => context.font_size_px.map(|basis| value * basis * 0.7), + SvgLengthUnit::Ic => context.font_size_px.map(|basis| value * basis), + SvgLengthUnit::Vw => context.viewport_width_px.map(|basis| value * basis / 100.0), + SvgLengthUnit::Vh => context + .viewport_height_px + .map(|basis| value * basis / 100.0), + SvgLengthUnit::Vmin => { + Some(value * context.viewport_width_px?.min(context.viewport_height_px?) / 100.0) + } + SvgLengthUnit::Vmax => { + Some(value * context.viewport_width_px?.max(context.viewport_height_px?) / 100.0) + } + SvgLengthUnit::Number + | SvgLengthUnit::Px + | SvgLengthUnit::Cm + | SvgLengthUnit::Mm + | SvgLengthUnit::Q + | SvgLengthUnit::In + | SvgLengthUnit::Pt + | SvgLengthUnit::Pc => unreachable!("absolute SVG length handled above"), + } +} + +fn resolve_svg_absolute_length(value: f64, unit: SvgLengthUnit) -> Option { + let pixels = match unit { + SvgLengthUnit::Number | SvgLengthUnit::Px => value, + SvgLengthUnit::Cm => value * 96.0 / 2.54, + SvgLengthUnit::Mm => value * 96.0 / 25.4, + SvgLengthUnit::Q => value * 96.0 / 101.6, + SvgLengthUnit::In => value * 96.0, + SvgLengthUnit::Pt => value * 96.0 / 72.0, + SvgLengthUnit::Pc => value * 16.0, + _ => return None, + }; + Some(pixels) +} + +fn svg_length_owner_element<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> Option> { + get_private_value(scope, object, SVG_LENGTH_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + .or_else(|| { + let list = get_private_value(scope, object, SVG_VALUE_LIST_ITEM_OWNER_LIST_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok())?; + get_private_value(scope, list, SVG_VALUE_LIST_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + }) +} + +fn svg_length_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> Option { + get_private_value(scope, object, SVG_LENGTH_OWNER_ATTRIBUTE_SLOT) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + .or_else(|| { + let list = get_private_value(scope, object, SVG_VALUE_LIST_ITEM_OWNER_LIST_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok())?; + get_private_value(scope, list, SVG_VALUE_LIST_OWNER_ATTRIBUTE_SLOT) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + }) +} + +fn svg_length_numeric_context( + runtime: &crate::native_bridge::JsContextHost, + handle: crate::document_runtime::DomHandle, + unit: SvgLengthUnit, + connected: bool, +) -> moli_css_parse::CssNumericContext { + if !connected { + return moli_css_parse::CssNumericContext::default(); + } + let font_size_px = svg_computed_pixel_value(runtime, handle, "font-size"); + let line_height_px = svg_computed_pixel_value(runtime, handle, "line-height") + .or_else(|| font_size_px.map(|font_size| font_size * 1.2)); + let document = runtime.dom_host().owner_document_handle(handle); + let root = document.and_then(|document| { + runtime + .dom_host() + .document_element_handle_for_document(document) + }); + let root_font_size_px = root + .and_then(|root| svg_computed_pixel_value(runtime, root, "font-size")) + .or(font_size_px); + let root_line_height_px = root + .and_then(|root| svg_computed_pixel_value(runtime, root, "line-height")) + .or_else(|| root_font_size_px.map(|font_size| font_size * 1.2)); + let viewport = runtime.style_viewport(); + moli_css_parse::CssNumericContext { + font_size_px, + root_font_size_px, + line_height_px: if matches!(unit, SvgLengthUnit::Rlh) { + root_line_height_px + } else { + line_height_px + }, + viewport_width_px: viewport.width, + viewport_height_px: viewport.height, + ..moli_css_parse::CssNumericContext::default() + } +} + +fn svg_computed_pixel_value( + runtime: &crate::native_bridge::JsContextHost, + handle: crate::document_runtime::DomHandle, + property: &str, +) -> Option { + let value = crate::native_bridge::element::computed_style_property_for_handle( + runtime, handle, property, + ); + moli_css_parse::parse_px_length(&value, moli_css_parse::UnitlessLength::Any) +} + +fn svg_length_percentage_basis( + runtime: &crate::native_bridge::JsContextHost, + handle: crate::document_runtime::DomHandle, + attribute: &str, +) -> f64 { + let (width, height) = svg_nearest_viewport_dimensions(runtime, handle).unwrap_or_else(|| { + let viewport = runtime.style_viewport(); + ( + viewport.width.unwrap_or(100.0), + viewport.height.unwrap_or(100.0), + ) + }); + match attribute { + "x" | "x1" | "x2" | "cx" | "rx" | "width" | "markerWidth" | "refX" => width, + "y" | "y1" | "y2" | "cy" | "ry" | "height" | "markerHeight" | "refY" => height, + _ => ((width * width + height * height) / 2.0).sqrt(), + } +} + +fn svg_nearest_viewport_dimensions( + runtime: &crate::native_bridge::JsContextHost, + handle: crate::document_runtime::DomHandle, +) -> Option<(f64, f64)> { + let dom = runtime.dom_host(); + let mut current = Some(handle); + while let Some(candidate) = current { + let node = dom.node(candidate)?; + if node + .as_element() + .is_some_and(|element| element.is_svg_element("svg")) + { + if let Some(view_box) = dom.get_attribute(candidate, "viewBox") + && let Some([_, _, width, height]) = parse_svg_view_box_value(&view_box) + && width > 0.0 + && height > 0.0 + { + return Some((width, height)); + } + let width = dom + .get_attribute(candidate, "width") + .as_deref() + .and_then(|value| { + moli_css_parse::parse_px_length(value, moli_css_parse::UnitlessLength::Any) + }); + let height = dom + .get_attribute(candidate, "height") + .as_deref() + .and_then(|value| { + moli_css_parse::parse_px_length(value, moli_css_parse::UnitlessLength::Any) + }); + if let (Some(width), Some(height)) = (width, height) { + return Some((width, height)); + } + } + current = node.parent_node(); + } + None +} 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 8828bec1f7..a51f6924f2 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs @@ -2,12 +2,59 @@ use super::builders::*; use super::*; use crate::util::serialize_v8_array; use crate::web_api_interfaces; +use moli_webapi_declare::DataPropertyDescriptorDeclaration; -const SVG_RECT_ANIMATED_LENGTH_ATTRIBUTES: &[&str] = &["x", "y", "width", "height", "rx", "ry"]; -const SVG_CIRCLE_ANIMATED_LENGTH_ATTRIBUTES: &[&str] = &["cx", "cy", "r"]; -const SVG_ELLIPSE_ANIMATED_LENGTH_ATTRIBUTES: &[&str] = &["cx", "cy", "rx", "ry"]; -const SVG_LINE_ANIMATED_LENGTH_ATTRIBUTES: &[&str] = &["x1", "y1", "x2", "y2"]; -const SVG_BOX_ANIMATED_LENGTH_ATTRIBUTES: &[&str] = &["x", "y", "width", "height"]; +const SVG_RECT_ANIMATED_LENGTH_ATTRIBUTES: &[(&str, &str)] = &[ + ("x", "0"), + ("y", "0"), + ("width", "0"), + ("height", "0"), + ("rx", "0"), + ("ry", "0"), +]; +const SVG_CIRCLE_ANIMATED_LENGTH_ATTRIBUTES: &[(&str, &str)] = + &[("cx", "0"), ("cy", "0"), ("r", "0")]; +const SVG_ELLIPSE_ANIMATED_LENGTH_ATTRIBUTES: &[(&str, &str)] = + &[("cx", "0"), ("cy", "0"), ("rx", "0"), ("ry", "0")]; +const SVG_LINE_ANIMATED_LENGTH_ATTRIBUTES: &[(&str, &str)] = + &[("x1", "0"), ("y1", "0"), ("x2", "0"), ("y2", "0")]; +const SVG_BOX_ANIMATED_LENGTH_ATTRIBUTES: &[(&str, &str)] = + &[("x", "0"), ("y", "0"), ("width", "0"), ("height", "0")]; +const SVG_FILTER_ANIMATED_LENGTH_ATTRIBUTES: &[(&str, &str)] = &[ + ("x", "-10%"), + ("y", "-10%"), + ("width", "120%"), + ("height", "120%"), +]; +const SVG_FILTER_PRIMITIVE_ANIMATED_LENGTH_ATTRIBUTES: &[(&str, &str)] = &[ + ("x", "0%"), + ("y", "0%"), + ("width", "100%"), + ("height", "100%"), +]; +const SVG_LINEAR_GRADIENT_ANIMATED_LENGTH_ATTRIBUTES: &[(&str, &str)] = + &[("x1", "0%"), ("y1", "0%"), ("x2", "100%"), ("y2", "0%")]; +const SVG_RADIAL_GRADIENT_ANIMATED_LENGTH_ATTRIBUTES: &[(&str, &str)] = &[ + ("cx", "50%"), + ("cy", "50%"), + ("r", "50%"), + ("fx", "50%"), + ("fy", "50%"), + ("fr", "0%"), +]; +const SVG_MARKER_ANIMATED_LENGTH_ATTRIBUTES: &[(&str, &str)] = &[ + ("refX", "0"), + ("refY", "0"), + ("markerWidth", "3"), + ("markerHeight", "3"), +]; +const SVG_SVG_ANIMATED_LENGTH_ATTRIBUTES: &[(&str, &str)] = &[ + ("x", "0"), + ("y", "0"), + ("width", "100%"), + ("height", "100%"), +]; +const SVG_TEXT_PATH_ANIMATED_LENGTH_ATTRIBUTES: &[(&str, &str)] = &[("startOffset", "0")]; const SVG_LENGTH_ACCESSOR_NAMES: &[&str] = &[ "unitType", "value", @@ -15,9 +62,51 @@ const SVG_LENGTH_ACCESSOR_NAMES: &[&str] = &[ "valueAsString", ]; const SVG_ANIMATED_ACCESSOR_NAMES: &[&str] = &["baseVal", "animVal"]; +const SVG_FIT_TO_VIEW_BOX_ACCESSOR_NAMES: &[&str] = &["viewBox", "preserveAspectRatio"]; +const SVG_PRESERVE_ASPECT_RATIO_ACCESSOR_NAMES: &[&str] = &["align", "meetOrSlice"]; const SVG_TRANSFORM_ACCESSOR_NAMES: &[&str] = &["type", "matrix", "angle"]; const SVG_MATRIX_ACCESSOR_NAMES: &[&str] = &["a", "b", "c", "d", "e", "f"]; +pub(super) fn configure_svg_string_list_indexed_property_handler( + template: v8::Local<'_, v8::ObjectTemplate>, +) { + template.set_indexed_property_handler( + v8::IndexedPropertyHandlerConfiguration::new() + .getter(svg_string_list_indexed_getter) + .setter(svg_string_list_indexed_setter) + .query(svg_string_list_indexed_query) + .deleter(svg_string_list_indexed_deleter) + .enumerator(svg_string_list_indexed_enumerator) + .definer(svg_string_list_indexed_definer) + .descriptor(svg_string_list_indexed_descriptor), + ); +} + +pub(super) fn configure_svg_value_list_indexed_property_handler( + template: v8::Local<'_, v8::ObjectTemplate>, +) { + template.set_indexed_property_handler( + v8::IndexedPropertyHandlerConfiguration::new() + .getter(svg_value_list_indexed_getter) + .setter(svg_value_list_indexed_setter) + .query(svg_value_list_indexed_query) + .deleter(svg_value_list_indexed_deleter) + .enumerator(svg_value_list_indexed_enumerator) + .definer(svg_value_list_indexed_definer) + .descriptor(svg_value_list_indexed_descriptor), + ); +} + +fn svg_dom_matrix_2d_init_arg<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: &v8::FunctionCallbackArguments<'s>, + prefix: &'static str, +) -> Option { + let [a, b, c, d, e, f] = + super::super::geometry_runtime::dom_matrix_2d_init_arg(scope, args, 0, prefix)?; + Some(SvgMatrixComponents { a, b, c, d, e, f }) +} + const SVG_TEXT_POSITIONING_LIST_ATTRIBUTES: &[(&str, &str, SvgListKind)] = &[ ("x", SVG_TEXT_POSITIONING_X_SLOT, SvgListKind::Length), ("y", SVG_TEXT_POSITIONING_Y_SLOT, SvgListKind::Length), @@ -30,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>, @@ -118,6 +212,209 @@ pub(super) fn svg_element_owner_svg_element_getter<'s>( rv.set(owner.into()); } +fn require_svg_fit_to_view_box_receiver<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, + member: &str, +) -> bool { + let Ok((runtime_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, receiver) + else { + webidl::throw_type_error( + scope, + &format!("SVGFitToViewBox.{member} called on incompatible receiver."), + ); + return false; + }; + let is_supported_element = unsafe { &*runtime_ptr } + .dom_host() + .node(handle) + .is_some_and(|node| { + node.namespace() == Some(crate::native_bridge::document::SVG_NS) + && node.local_name().is_some_and(|local_name| match member { + "viewBox" => { + matches!(local_name, "svg" | "symbol" | "marker" | "pattern" | "view") + } + "preserveAspectRatio" => matches!( + local_name, + "svg" | "symbol" | "marker" | "pattern" | "view" | "image" | "feImage" + ), + _ => false, + }) + }); + if !is_supported_element { + webidl::throw_type_error( + scope, + &format!("SVGFitToViewBox.{member} called on incompatible receiver."), + ); + } + is_supported_element +} + +pub(super) fn svg_fit_to_view_box_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(member) = callback_data_item( + scope, + &args, + SVG_FIT_TO_VIEW_BOX_ACCESSOR_NAMES, + "SVGFitToViewBox attributes", + ) else { + rv.set_undefined(); + return; + }; + let owner = args.this(); + if !require_svg_fit_to_view_box_receiver(scope, owner, member) { + return; + } + let (slot, value) = match member { + "viewBox" => { + let slot = SVG_FIT_VIEW_BOX_SLOT; + if let Some(value) = get_private_value(scope, owner, slot) { + if let Ok(animated) = v8::Local::::try_from(value) { + sync_svg_animated_rect_from_owner_attribute(scope, animated, owner); + } + (slot, value) + } else { + ( + slot, + build_svg_animated_rect_for_view_box(scope, owner).into(), + ) + } + } + "preserveAspectRatio" => { + let slot = SVG_FIT_PRESERVE_ASPECT_RATIO_SLOT; + if let Some(value) = get_private_value(scope, owner, slot) { + if let Ok(animated) = v8::Local::::try_from(value) { + sync_svg_animated_preserve_aspect_ratio_from_owner_attribute( + scope, animated, owner, + ); + } + (slot, value) + } else { + ( + slot, + build_svg_animated_preserve_aspect_ratio(scope, owner).into(), + ) + } + } + _ => { + rv.set_undefined(); + return; + } + }; + set_private_value(scope, owner, slot, value); + rv.set(value); +} + +pub(super) fn svg_element_class_name_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + svg_animated_string_attribute_getter(scope, args, rv, SVG_ELEMENT_CLASS_NAME_SLOT, "class"); +} + +pub(super) fn svg_uri_href_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + svg_animated_string_attribute_getter(scope, args, rv, SVG_URI_HREF_SLOT, "href"); +} + +pub(super) fn svg_mpath_href_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + // The binding checks the concrete receiver in the callee realm; its + // cached SVGAnimatedString belongs to the element's realm. + let Some(context) = args.this().get_creation_context(scope) else { + return; + }; + let scope = &mut v8::ContextScope::new(scope, context); + svg_uri_href_getter(scope, args, rv); +} + +pub(super) fn svg_fe_convolve_matrix_preserve_alpha_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + 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, + "SVGFEConvolveMatrixElement.preserveAlpha called on incompatible receiver.", + ); + return; + }; + let runtime = unsafe { &*runtime_ptr }; + if !runtime.dom_host().node(handle).is_some_and(|node| { + node.namespace() == Some(crate::native_bridge::document::SVG_NS) + && node.local_name() == Some("feConvolveMatrix") + }) { + webidl::throw_type_error( + scope, + "SVGFEConvolveMatrixElement.preserveAlpha called on incompatible receiver.", + ); + return; + } + svg_animated_boolean_attribute_getter( + scope, + owner, + rv, + SVG_FE_CONVOLVE_MATRIX_PRESERVE_ALPHA_SLOT, + "preserveAlpha", + false, + ); +} + +fn svg_animated_boolean_attribute_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + mut rv: v8::ReturnValue<'_, v8::Value>, + slot: &'static str, + attribute: &'static str, + initial_value: bool, +) { + if let Some(value) = get_private_value(scope, owner, slot) { + if let Ok(animated) = v8::Local::::try_from(value) { + sync_svg_animated_boolean_from_owner_attribute(scope, animated); + } + rv.set(value); + return; + } + let value = build_svg_animated_boolean_for_attribute(scope, owner, attribute, initial_value); + set_private_value(scope, owner, slot, value.into()); + rv.set(value.into()); +} + +pub(super) fn svg_animated_string_attribute_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, + slot: &str, + attribute: &str, +) { + let owner = args.this(); + if let Some(value) = get_private_value(scope, owner, slot) { + if let Ok(animated) = v8::Local::::try_from(value) { + sync_svg_animated_string_from_owner_attribute(scope, animated); + } + rv.set(value); + return; + } + let value = build_svg_animated_string_for_attribute(scope, owner, attribute); + set_private_value(scope, owner, slot, value.into()); + rv.set(value.into()); +} + pub(super) fn svg_rect_animated_length_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -188,31 +485,272 @@ pub(super) fn svg_box_animated_length_getter<'s>( ); } +pub(super) fn svg_filter_animated_length_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + svg_element_animated_length_getter( + scope, + &args, + rv, + SVG_FILTER_ANIMATED_LENGTH_ATTRIBUTES, + "SVGFilterElement animated length attributes", + ); +} + +pub(super) fn svg_filter_primitive_animated_length_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + svg_element_animated_length_getter( + scope, + &args, + rv, + SVG_FILTER_PRIMITIVE_ANIMATED_LENGTH_ATTRIBUTES, + "SVGFilterPrimitiveStandardAttributes animated length attributes", + ); +} + +pub(super) fn svg_pattern_animated_length_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + svg_element_animated_length_getter( + scope, + &args, + rv, + SVG_BOX_ANIMATED_LENGTH_ATTRIBUTES, + "SVGPatternElement animated length attributes", + ); +} + +pub(super) fn svg_linear_gradient_animated_length_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + svg_element_animated_length_getter( + scope, + &args, + rv, + SVG_LINEAR_GRADIENT_ANIMATED_LENGTH_ATTRIBUTES, + "SVGLinearGradientElement animated length attributes", + ); +} + +pub(super) fn svg_radial_gradient_animated_length_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + svg_element_animated_length_getter( + scope, + &args, + rv, + SVG_RADIAL_GRADIENT_ANIMATED_LENGTH_ATTRIBUTES, + "SVGRadialGradientElement animated length attributes", + ); +} + +pub(super) fn svg_marker_animated_length_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + svg_element_animated_length_getter( + scope, + &args, + rv, + SVG_MARKER_ANIMATED_LENGTH_ATTRIBUTES, + "SVGMarkerElement animated length attributes", + ); +} + +pub(super) fn svg_mask_animated_length_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + svg_element_animated_length_getter( + scope, + &args, + rv, + SVG_FILTER_ANIMATED_LENGTH_ATTRIBUTES, + "SVGMaskElement animated length attributes", + ); +} + +pub(super) fn svg_svg_animated_length_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + svg_element_animated_length_getter( + scope, + &args, + rv, + SVG_SVG_ANIMATED_LENGTH_ATTRIBUTES, + "SVGSVGElement animated length attributes", + ); +} + +pub(super) fn svg_text_path_animated_length_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + svg_element_animated_length_getter( + scope, + &args, + rv, + SVG_TEXT_PATH_ANIMATED_LENGTH_ATTRIBUTES, + "SVGTextPathElement animated length attributes", + ); +} + fn svg_element_animated_length_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: &v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, - attributes: &'static [&'static str], + attributes: &'static [(&'static str, &'static str)], label: &'static str, ) { - let Some(name) = callback_data_item(scope, args, attributes, label) else { + let Some((name, initial_value)) = callback_data_item(scope, args, attributes, label) else { rv.set_undefined(); return; }; + svg_animated_length_attribute_getter(scope, args.this(), rv, name, initial_value); +} + +fn svg_animated_length_attribute_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + mut rv: v8::ReturnValue<'_, v8::Value>, + name: &'static str, + initial_value: &'static str, +) { let slot = svg_animated_length_attribute_slot(name); - let owner = args.this(); if let Some(value) = get_private_value(scope, owner, slot) { if let Ok(object) = v8::Local::::try_from(value) { - sync_svg_animated_length_from_owner_attribute(scope, object, owner, name); + sync_svg_animated_length_from_owner_attribute( + scope, + object, + owner, + name, + initial_value, + ); } rv.set(value); return; } - let value = build_svg_animated_length_for_attribute(scope, owner, name); + let value = build_svg_animated_length_for_attribute(scope, owner, name, initial_value); set_private_value(scope, owner, slot, value.into()); rv.set(value.into()); } +fn svg_marker_runtime_and_handle<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, + member: &str, +) -> Option<( + *mut crate::native_bridge::JsContextHost, + crate::document_runtime::DomHandle, +)> { + let Ok((runtime_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, receiver) + else { + webidl::throw_type_error( + scope, + &format!("SVGMarkerElement.{member} called on incompatible receiver."), + ); + return None; + }; + let is_marker = unsafe { &*runtime_ptr } + .dom_host() + .node(handle) + .and_then(|node| node.as_element()) + .is_some_and(|element| element.is_svg_element("marker")); + if !is_marker { + webidl::throw_type_error( + scope, + &format!("SVGMarkerElement.{member} called on incompatible receiver."), + ); + return None; + } + Some((runtime_ptr, handle)) +} + +pub(super) fn svg_marker_orient_angle_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let owner = args.this(); + if svg_marker_runtime_and_handle(scope, owner, "orientAngle").is_none() { + return; + } + if let Some(value) = get_private_value(scope, owner, SVG_MARKER_ORIENT_ANGLE_SLOT) { + if let Ok(animated) = v8::Local::::try_from(value) { + sync_svg_animated_angle_from_owner_attribute(scope, animated, owner, "orient"); + } + rv.set(value); + return; + } + let value = build_svg_animated_angle_for_attribute(scope, owner, "orient"); + set_private_value(scope, owner, SVG_MARKER_ORIENT_ANGLE_SLOT, value.into()); + rv.set(value.into()); +} + +fn set_svg_marker_orient_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + marker: v8::Local<'s, v8::Object>, + member: &str, + value: &str, +) -> bool { + let Some((runtime_ptr, handle)) = svg_marker_runtime_and_handle(scope, marker, member) else { + return false; + }; + let runtime = unsafe { &mut *runtime_ptr }; + let _ = runtime.set_attribute(scope, runtime_ptr, handle, "orient", value); + true +} + +pub(super) fn svg_marker_set_orient_to_auto_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if set_svg_marker_orient_attribute(scope, args.this(), "setOrientToAuto", "auto") { + rv.set_undefined(); + } +} + +pub(super) fn svg_marker_set_orient_to_angle_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Ok(angle) = v8::Local::::try_from(args.get(0)) else { + webidl::throw_type_error( + scope, + "SVGMarkerElement.setOrientToAngle requires an SVGAngle.", + ); + return; + }; + if !require_svg_receiver(scope, angle, "SVGAngle", "setOrientToAngle argument") { + return; + } + sync_svg_angle_from_owner_attribute(scope, angle); + let value = svg_angle_string_slot(scope, angle, SVG_ANGLE_VALUE_AS_STRING_SLOT) + .unwrap_or_else(|| "0".to_owned()); + if set_svg_marker_orient_attribute(scope, args.this(), "setOrientToAngle", &value) { + rv.set_undefined(); + } +} + pub(super) fn svg_graphics_transform_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -221,6 +759,65 @@ pub(super) fn svg_graphics_transform_getter<'s>( svg_transform_attribute_getter(scope, args, rv, SVG_GRAPHICS_TRANSFORM_SLOT, "transform"); } +pub(super) fn svg_graphics_test_string_list_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + if !require_svg_graphics_element_receiver(scope, args.this(), "SVGTests") { + return; + } + svg_test_string_list_getter(scope, args, rv); +} + +pub(super) fn svg_test_string_list_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some((attribute, slot)) = callback_data_item( + scope, + &args, + SVG_TEST_STRING_LIST_ATTRIBUTES, + "SVGTests string list attributes", + ) else { + rv.set_undefined(); + return; + }; + let receiver = args.this(); + let Some(context) = receiver.get_creation_context(scope) else { + return; + }; + let scope = &mut v8::ContextScope::new(scope, context); + if let Some(value) = get_private_value(scope, receiver, slot) { + if let Ok(list) = v8::Local::::try_from(value) { + sync_svg_string_list_from_owner_attribute(scope, list); + } + rv.set(value); + return; + } + let list = build_svg_string_list_for_attribute(scope, receiver, attribute); + set_private_value(scope, receiver, slot, list.into()); + rv.set(list.into()); +} + +fn require_svg_graphics_element_receiver<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, + member: &str, +) -> bool { + require_svg_interface_receiver(scope, receiver, "SVGGraphicsElement", member) +} + +fn require_svg_interface_receiver<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, + interface: &str, + member: &str, +) -> bool { + require_svg_receiver(scope, receiver, interface, member) +} + pub(super) fn svg_pattern_transform_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -287,43 +884,173 @@ 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>, - mut rv: v8::ReturnValue<'_, v8::Value>, + rv: v8::ReturnValue<'_, v8::Value>, ) { - let holder = args.this(); - if let Some(value) = get_private_value(scope, holder, SVG_TEXT_CONTENT_TEXT_LENGTH_SLOT) { - rv.set(value); - return; - } - let value = build_svg_animated_length(scope, 0.0); - set_private_value( - scope, - holder, - SVG_TEXT_CONTENT_TEXT_LENGTH_SLOT, - value.into(), - ); - rv.set(value.into()); + svg_animated_length_attribute_getter(scope, args.this(), rv, "textLength", "0"); } -pub(super) fn svg_text_content_length_adjust_getter<'s>( +pub(super) fn svg_element_animated_enumeration_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + let Some(property) = callback_data_item( + scope, + &args, + SVG_ANIMATED_ENUMERATION_PROPERTIES, + "SVG animated enumeration properties", + ) else { + rv.set_undefined(); + return; + }; let holder = args.this(); - if let Some(value) = get_private_value(scope, holder, SVG_TEXT_CONTENT_LENGTH_ADJUST_SLOT) { + if let Some(value) = get_private_value(scope, holder, property.cache_slot) { + if let Ok(animated) = v8::Local::::try_from(value) { + sync_svg_animated_enumeration_from_owner_attribute(scope, animated); + } rv.set(value); return; } - let value = build_svg_animated_enumeration(scope, SVG_LENGTH_ADJUST_SPACING); - set_private_value( + let value = build_svg_animated_enumeration_for_property(scope, holder, property); + set_private_value(scope, holder, property.cache_slot, value.into()); + rv.set(value.into()); +} + +pub(super) fn svg_element_animated_number_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(property) = callback_data_item( scope, - holder, - SVG_TEXT_CONTENT_LENGTH_ADJUST_SLOT, - value.into(), - ); + &args, + SVG_ANIMATED_NUMBER_PROPERTIES, + "SVG animated number properties", + ) else { + rv.set_undefined(); + return; + }; + let holder = args.this(); + if !require_svg_interface_receiver(scope, holder, property.interface, property.name) { + return; + } + let cache_slot = svg_animated_number_cache_slot(property); + if let Some(value) = get_private_value(scope, holder, &cache_slot) { + if let Ok(animated) = v8::Local::::try_from(value) { + sync_svg_animated_number_from_property(scope, animated); + } + rv.set(value); + return; + } + let value = build_svg_animated_number_for_property(scope, holder, property); + set_private_value(scope, holder, &cache_slot, value.into()); + rv.set(value.into()); +} + +pub(super) fn svg_element_animated_integer_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(property) = callback_data_item( + scope, + &args, + SVG_ANIMATED_INTEGER_PROPERTIES, + "SVG animated integer properties", + ) else { + rv.set_undefined(); + return; + }; + let holder = args.this(); + let Ok((runtime_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, holder) + else { + webidl::throw_type_error( + scope, + &format!( + "{}.{} called on incompatible receiver.", + property.interface, property.name + ), + ); + return; + }; + let is_expected_element = unsafe { &*runtime_ptr } + .dom_host() + .node(handle) + .and_then(|node| node.as_element()) + .is_some_and(|element| element.is_svg_element(property.local_name)); + if !is_expected_element { + webidl::throw_type_error( + scope, + &format!( + "{}.{} called on incompatible receiver.", + property.interface, property.name + ), + ); + return; + } + if let Some(value) = get_private_value(scope, holder, property.cache_slot) { + if let Ok(animated) = v8::Local::::try_from(value) { + sync_svg_animated_integer_from_owner_attribute(scope, animated); + } + rv.set(value); + return; + } + let value = build_svg_animated_integer_for_property(scope, holder, property); + set_private_value(scope, holder, property.cache_slot, value.into()); rv.set(value.into()); } @@ -354,6 +1081,148 @@ pub(super) fn svg_text_positioning_list_getter<'s>( rv.set(value); } +pub(super) fn svg_animated_string_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(name) = callback_data_item( + scope, + &args, + SVG_ANIMATED_ACCESSOR_NAMES, + "SVGAnimatedString attributes", + ) else { + rv.set_undefined(); + return; + }; + if !require_svg_receiver( + scope, + args.this(), + "SVGAnimatedString", + &format!("{name} getter"), + ) { + return; + } + let slot = match name { + "baseVal" => SVG_ANIMATED_STRING_BASE_VAL_SLOT, + "animVal" => SVG_ANIMATED_STRING_ANIM_VAL_SLOT, + _ => { + rv.set_undefined(); + return; + } + }; + let animated = args.this(); + sync_svg_animated_string_from_owner_attribute(scope, animated); + rv.set( + get_private_value(scope, animated, slot).unwrap_or_else(|| v8::String::empty(scope).into()), + ); +} + +pub(super) fn svg_animated_boolean_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(name) = callback_data_item( + scope, + &args, + SVG_ANIMATED_ACCESSOR_NAMES, + "SVGAnimatedBoolean attributes", + ) else { + rv.set_undefined(); + return; + }; + if !require_svg_receiver( + scope, + args.this(), + "SVGAnimatedBoolean", + &format!("{name} getter"), + ) { + return; + } + let slot = match name { + "baseVal" => SVG_ANIMATED_BOOLEAN_BASE_VAL_SLOT, + "animVal" => SVG_ANIMATED_BOOLEAN_ANIM_VAL_SLOT, + _ => { + rv.set_undefined(); + return; + } + }; + sync_svg_animated_boolean_from_owner_attribute(scope, args.this()); + let value = get_private_value(scope, args.this(), slot).is_some_and(|value| value.is_true()); + rv.set(v8::Boolean::new(scope, value).into()); +} + +pub(super) fn svg_animated_boolean_setter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(name) = callback_data_item( + scope, + &args, + SVG_ANIMATED_ACCESSOR_NAMES, + "SVGAnimatedBoolean attributes", + ) else { + return; + }; + if !require_svg_receiver( + scope, + args.this(), + "SVGAnimatedBoolean", + &format!("{name} setter"), + ) || name != "baseVal" + { + return; + } + let value = match webidl::convert::( + scope, + args.get(0), + webidl::Context::member("SVGAnimatedBoolean", "baseVal"), + ) { + Ok(value) => value.0, + Err(error) => { + webidl::throw_error(scope, &error); + return; + } + }; + set_svg_animated_boolean_values(scope, args.this(), value); + reflect_svg_animated_boolean_to_owner_attribute(scope, args.this()); +} + +pub(super) fn svg_animated_string_setter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + if !require_svg_receiver(scope, args.this(), "SVGAnimatedString", "baseVal setter") { + return; + } + let animated = args.this(); + let Some(owner) = get_private_value(scope, animated, SVG_ANIMATED_STRING_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let Some(attribute) = + get_private_value(scope, animated, SVG_ANIMATED_STRING_OWNER_ATTRIBUTE_SLOT) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + .filter(|value| !value.is_empty()) + else { + return; + }; + let Some(value) = crate::native_bridge::element::set_svg_animated_string_base_value( + scope, + owner, + &attribute, + args.get(0), + ) else { + return; + }; + set_svg_animated_string_values(scope, animated, &value); +} + pub(super) fn svg_animated_length_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -389,6 +1258,223 @@ pub(super) fn svg_animated_length_getter<'s>( ); } +pub(super) fn svg_animated_angle_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(name) = callback_data_item( + scope, + &args, + SVG_ANIMATED_ACCESSOR_NAMES, + "SVGAnimatedAngle attributes", + ) else { + rv.set_undefined(); + return; + }; + if !require_svg_receiver( + scope, + args.this(), + "SVGAnimatedAngle", + &format!("{name} getter"), + ) { + return; + } + let slot = match name { + "baseVal" => SVG_ANIMATED_ANGLE_BASE_VAL_SLOT, + "animVal" => SVG_ANIMATED_ANGLE_ANIM_VAL_SLOT, + _ => { + rv.set_undefined(); + return; + } + }; + rv.set( + get_private_value(scope, args.this(), slot).unwrap_or_else(|| v8::undefined(scope).into()), + ); +} + +pub(super) fn svg_animated_rect_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(name) = callback_data_item( + scope, + &args, + SVG_ANIMATED_ACCESSOR_NAMES, + "SVGAnimatedRect attributes", + ) else { + rv.set_undefined(); + return; + }; + if !require_svg_receiver( + scope, + args.this(), + "SVGAnimatedRect", + &format!("{name} getter"), + ) { + return; + } + let slot = match name { + "baseVal" => SVG_ANIMATED_RECT_BASE_VAL_SLOT, + "animVal" => SVG_ANIMATED_RECT_ANIM_VAL_SLOT, + _ => { + rv.set_undefined(); + return; + } + }; + rv.set( + get_private_value(scope, args.this(), slot).unwrap_or_else(|| v8::undefined(scope).into()), + ); +} + +pub(super) fn svg_animated_preserve_aspect_ratio_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(name) = callback_data_item( + scope, + &args, + SVG_ANIMATED_ACCESSOR_NAMES, + "SVGAnimatedPreserveAspectRatio attributes", + ) else { + rv.set_undefined(); + return; + }; + if !require_svg_receiver( + scope, + args.this(), + "SVGAnimatedPreserveAspectRatio", + &format!("{name} getter"), + ) { + return; + } + let slot = match name { + "baseVal" => SVG_ANIMATED_PRESERVE_ASPECT_RATIO_BASE_VAL_SLOT, + "animVal" => SVG_ANIMATED_PRESERVE_ASPECT_RATIO_ANIM_VAL_SLOT, + _ => { + rv.set_undefined(); + return; + } + }; + rv.set( + get_private_value(scope, args.this(), slot).unwrap_or_else(|| v8::undefined(scope).into()), + ); +} + +pub(super) fn svg_preserve_aspect_ratio_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(name) = callback_data_item( + scope, + &args, + SVG_PRESERVE_ASPECT_RATIO_ACCESSOR_NAMES, + "SVGPreserveAspectRatio attributes", + ) else { + rv.set_undefined(); + return; + }; + let aspect_ratio = args.this(); + if !require_svg_receiver( + scope, + aspect_ratio, + "SVGPreserveAspectRatio", + &format!("{name} getter"), + ) { + return; + } + sync_svg_preserve_aspect_ratio_from_owner_attribute(scope, aspect_ratio); + let (slot, initial_value) = match name { + "align" => ( + SVG_PRESERVE_ASPECT_RATIO_ALIGN_SLOT, + SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MID, + ), + "meetOrSlice" => ( + SVG_PRESERVE_ASPECT_RATIO_MEET_OR_SLICE_SLOT, + SVG_MEET_OR_SLICE_MEET, + ), + _ => { + rv.set_undefined(); + return; + } + }; + let value = svg_number_slot(scope, aspect_ratio, slot).unwrap_or(initial_value as f64) as u32; + rv.set(v8::Integer::new_from_unsigned(scope, value).into()); +} + +pub(super) fn svg_preserve_aspect_ratio_setter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(name) = callback_data_item( + scope, + &args, + SVG_PRESERVE_ASPECT_RATIO_ACCESSOR_NAMES, + "SVGPreserveAspectRatio attributes", + ) else { + return; + }; + let aspect_ratio = args.this(); + if !require_svg_receiver( + scope, + aspect_ratio, + "SVGPreserveAspectRatio", + &format!("{name} setter"), + ) { + return; + } + if get_private_value( + scope, + aspect_ratio, + SVG_PRESERVE_ASPECT_RATIO_READ_ONLY_SLOT, + ) + .is_some_and(|value| value.boolean_value(scope)) + { + throw_dom_exception( + scope, + "NoModificationAllowedError", + 7, + "The SVG preserveAspectRatio value is read-only.", + ); + return; + } + let value = match webidl::convert::( + scope, + args.get(0), + webidl::Context::member("SVGPreserveAspectRatio", name), + ) { + Ok(value) => u32::from(value.0), + Err(error) => { + webidl::throw_error(scope, &error); + return; + } + }; + let slot = match name { + "align" + if (SVG_PRESERVE_ASPECT_RATIO_NONE..=SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MAX) + .contains(&value) => + { + SVG_PRESERVE_ASPECT_RATIO_ALIGN_SLOT + } + "meetOrSlice" if (SVG_MEET_OR_SLICE_MEET..=SVG_MEET_OR_SLICE_SLICE).contains(&value) => { + SVG_PRESERVE_ASPECT_RATIO_MEET_OR_SLICE_SLOT + } + _ => return, + }; + sync_svg_preserve_aspect_ratio_from_owner_attribute(scope, aspect_ratio); + set_private_value( + scope, + aspect_ratio, + slot, + v8::Integer::new_from_unsigned(scope, value).into(), + ); + reflect_svg_preserve_aspect_ratio_to_owner_attribute(scope, aspect_ratio); +} + pub(super) fn svg_animated_length_list_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -441,15 +1527,24 @@ pub(super) fn svg_length_getter<'s>( if !require_svg_receiver(scope, args.this(), "SVGLength", &format!("{name} getter")) { return; } + sync_svg_length_from_owner_attribute(scope, args.this()); match name { "unitType" => { let value = svg_length_number_slot(scope, args.this(), SVG_LENGTH_UNIT_TYPE_SLOT) .unwrap_or(SVG_LENGTH_TYPE_NUMBER as f64); rv.set(v8::Integer::new_from_unsigned(scope, value as u32).into()); } - "value" | "valueInSpecifiedUnits" => { - let value = - svg_length_number_slot(scope, args.this(), SVG_LENGTH_VALUE_SLOT).unwrap_or(0.0); + "value" => { + let value = svg_length_value_in_user_units(scope, args.this()); + rv.set(v8::Number::new(scope, value).into()); + } + "valueInSpecifiedUnits" => { + let value = svg_length_number_slot( + scope, + args.this(), + SVG_LENGTH_VALUE_IN_SPECIFIED_UNITS_SLOT, + ) + .unwrap_or(0.0); rv.set(v8::Number::new(scope, value).into()); } "valueAsString" => { @@ -462,6 +1557,117 @@ pub(super) fn svg_length_getter<'s>( } } +pub(super) fn svg_angle_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(name) = callback_data_item( + scope, + &args, + SVG_LENGTH_ACCESSOR_NAMES, + "SVGAngle attributes", + ) else { + rv.set_undefined(); + return; + }; + if !require_svg_receiver(scope, args.this(), "SVGAngle", &format!("{name} getter")) { + return; + } + sync_svg_angle_from_owner_attribute(scope, args.this()); + match name { + "unitType" => { + let value = svg_angle_number_slot(scope, args.this(), SVG_ANGLE_UNIT_TYPE_SLOT) + .unwrap_or(SVG_ANGLE_TYPE_UNSPECIFIED as f64); + rv.set(v8::Integer::new_from_unsigned(scope, value as u32).into()); + } + "value" => { + let value = + svg_angle_number_slot(scope, args.this(), SVG_ANGLE_VALUE_SLOT).unwrap_or(0.0); + rv.set(v8::Number::new(scope, value).into()); + } + "valueInSpecifiedUnits" => { + let value = + svg_angle_number_slot(scope, args.this(), SVG_ANGLE_VALUE_IN_SPECIFIED_UNITS_SLOT) + .unwrap_or(0.0); + rv.set(v8::Number::new(scope, value).into()); + } + "valueAsString" => rv.set( + get_private_value(scope, args.this(), SVG_ANGLE_VALUE_AS_STRING_SLOT) + .unwrap_or_else(|| v8str(scope, "0").into()), + ), + _ => rv.set_undefined(), + } +} + +pub(super) fn svg_angle_setter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(name) = callback_data_item( + scope, + &args, + SVG_LENGTH_ACCESSOR_NAMES, + "SVGAngle attributes", + ) else { + return; + }; + if !require_svg_receiver(scope, args.this(), "SVGAngle", &format!("{name} setter")) { + return; + } + if svg_angle_is_read_only(scope, args.this()) { + throw_dom_exception( + scope, + "NoModificationAllowedError", + 7, + "The SVG angle is read-only.", + ); + return; + } + sync_svg_angle_from_owner_attribute(scope, args.this()); + match name { + "value" | "valueInSpecifiedUnits" => { + let value = match webidl::convert::( + scope, + args.get(0), + webidl::Context::member("SVGAngle", name), + ) { + Ok(value) => value.0, + Err(error) => { + webidl::throw_error(scope, &error); + return; + } + }; + if name == "value" { + set_svg_angle_value_degrees(scope, args.this(), value); + } else { + set_svg_angle_value_in_specified_units(scope, args.this(), value); + } + } + "valueAsString" => { + let value = match webidl::convert::( + scope, + args.get(0), + webidl::Context::member("SVGAngle", "valueAsString"), + ) { + Ok(value) => value.0, + Err(error) => { + webidl::throw_error(scope, &error); + return; + } + }; + let Some(parsed) = parse_svg_angle_value(&value) else { + throw_dom_exception(scope, "SyntaxError", 12, "Invalid SVG angle value."); + return; + }; + set_svg_angle_parsed_value(scope, args.this(), &parsed); + } + _ => return, + } + reflect_svg_angle_to_owner_attribute(scope, args.this()); +} + pub(super) fn svg_length_setter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -480,10 +1686,10 @@ pub(super) fn svg_length_setter<'s>( } match name { "value" | "valueInSpecifiedUnits" => { - let value = match webidl::convert::( + let value = match webidl::convert::( scope, args.get(0), - webidl::Context::member("SVGLength", "value"), + webidl::Context::member("SVGLength", name), ) { Ok(value) => value.0, Err(error) => { @@ -491,7 +1697,15 @@ pub(super) fn svg_length_setter<'s>( return; } }; - set_svg_length_numeric_value(scope, args.this(), value, SVG_LENGTH_TYPE_NUMBER); + if !require_writable_svg_length(scope, args.this()) { + return; + } + sync_svg_length_from_owner_attribute(scope, args.this()); + if name == "value" { + set_svg_length_value_in_user_units(scope, args.this(), value); + } else { + set_svg_length_value_in_specified_units(scope, args.this(), value); + } reflect_svg_length_to_owner_attribute(scope, args.this()); reflect_svg_value_list_item_to_owner_list(scope, args.this(), SvgListKind::Length); } @@ -507,7 +1721,13 @@ pub(super) fn svg_length_setter<'s>( return; } }; - let parsed = parse_svg_length_value(&string_value).unwrap_or_default(); + if !require_writable_svg_length(scope, args.this()) { + return; + } + let Some(parsed) = parse_svg_length_value(&string_value) else { + throw_dom_exception(scope, "SyntaxError", 12, "Invalid SVG length value."); + return; + }; set_svg_length_parsed_value(scope, args.this(), parsed); reflect_svg_length_to_owner_attribute(scope, args.this()); reflect_svg_value_list_item_to_owner_list(scope, args.this(), SvgListKind::Length); @@ -516,6 +1736,24 @@ pub(super) fn svg_length_setter<'s>( } } +fn require_writable_svg_length<'s>( + scope: &mut v8::PinScope<'s, '_>, + length: v8::Local<'s, v8::Object>, +) -> bool { + if get_private_value(scope, length, SVG_LENGTH_READ_ONLY_SLOT) + .is_some_and(|value| value.is_true()) + { + throw_dom_exception( + scope, + "NoModificationAllowedError", + 7, + "The SVG length is read-only.", + ); + return false; + } + true +} + pub(super) fn svg_number_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -586,10 +1824,54 @@ pub(super) fn svg_animated_number_getter<'s>( return; } }; - let value = svg_number_slot(scope, args.this(), slot).unwrap_or(0.0); + sync_svg_animated_number_from_stored_owner_attribute(scope, args.this()); + let initial_value = svg_animated_number_property_for_object(scope, args.this()) + .map(|property| property.initial_value) + .unwrap_or_default(); + let value = svg_number_slot(scope, args.this(), slot).unwrap_or(initial_value); rv.set(v8::Number::new(scope, value).into()); } +pub(super) fn svg_animated_integer_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(name) = callback_data_item( + scope, + &args, + SVG_ANIMATED_ACCESSOR_NAMES, + "SVGAnimatedInteger attributes", + ) else { + rv.set_undefined(); + return; + }; + if !require_svg_receiver( + scope, + args.this(), + "SVGAnimatedInteger", + &format!("{name} getter"), + ) { + return; + } + let slot = match name { + "baseVal" => SVG_ANIMATED_INTEGER_BASE_VAL_SLOT, + "animVal" => SVG_ANIMATED_INTEGER_ANIM_VAL_SLOT, + _ => { + rv.set_undefined(); + return; + } + }; + sync_svg_animated_integer_from_owner_attribute(scope, args.this()); + let initial_value = svg_animated_integer_property(scope, args.this()) + .map(|property| property.initial_value) + .unwrap_or_default(); + let value = get_private_value(scope, args.this(), slot) + .and_then(|value| value.int32_value(scope)) + .unwrap_or(initial_value); + rv.set(v8::Integer::new(scope, value).into()); +} + pub(super) fn svg_animated_number_list_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -655,8 +1937,11 @@ pub(super) fn svg_animated_enumeration_getter<'s>( return; } }; - let value = svg_length_number_slot(scope, args.this(), slot) - .unwrap_or(SVG_LENGTH_ADJUST_SPACING as f64); + sync_svg_animated_enumeration_from_owner_attribute(scope, args.this()); + let initial_value = svg_animated_enumeration_property(scope, args.this()) + .map(|property| property.initial_value) + .unwrap_or(SVG_LENGTH_ADJUST_SPACING); + let value = svg_length_number_slot(scope, args.this(), slot).unwrap_or(initial_value as f64); rv.set(v8::Integer::new_from_unsigned(scope, value as u32).into()); } @@ -693,19 +1978,20 @@ pub(super) fn svg_animated_enumeration_setter<'s>( return; } }; - let value = v8::Integer::new_from_unsigned(scope, value.into()); - set_private_value( - scope, - args.this(), - SVG_ANIMATED_ENUMERATION_BASE_VAL_SLOT, - value.into(), - ); - set_private_value( - scope, - args.this(), - SVG_ANIMATED_ENUMERATION_ANIM_VAL_SLOT, - value.into(), - ); + let value = u32::from(value); + let Some(property) = svg_animated_enumeration_property(scope, args.this()) else { + webidl::throw_type_error( + scope, + "SVGAnimatedEnumeration has no enumeration definition.", + ); + return; + }; + if serialize_svg_animated_enumeration(property.kind, value).is_none() { + webidl::throw_type_error(scope, "Invalid SVGAnimatedEnumeration value."); + return; + } + set_svg_animated_enumeration_values(scope, args.this(), value); + reflect_svg_animated_enumeration_to_owner_attribute(scope, args.this()); } pub(super) fn svg_animated_number_setter<'s>( @@ -730,7 +2016,7 @@ pub(super) fn svg_animated_number_setter<'s>( { return; } - let value = match webidl::convert::( + let value = match webidl::convert::( scope, args.get(0), webidl::Context::member("SVGAnimatedNumber", "baseVal"), @@ -741,21 +2027,47 @@ pub(super) fn svg_animated_number_setter<'s>( return; } }; - set_private_value( - scope, - args.this(), - SVG_ANIMATED_NUMBER_BASE_VAL_SLOT, - v8::Number::new(scope, value).into(), - ); - set_private_value( - scope, - args.this(), - SVG_ANIMATED_NUMBER_ANIM_VAL_SLOT, - v8::Number::new(scope, value).into(), - ); + set_svg_animated_number_values(scope, args.this(), value); reflect_svg_animated_number_to_owner_attribute(scope, args.this()); } +pub(super) fn svg_animated_integer_setter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(name) = callback_data_item( + scope, + &args, + SVG_ANIMATED_ACCESSOR_NAMES, + "SVGAnimatedInteger attributes", + ) else { + return; + }; + if !require_svg_receiver( + scope, + args.this(), + "SVGAnimatedInteger", + &format!("{name} setter"), + ) || name != "baseVal" + { + return; + } + let value = match webidl::convert::( + scope, + args.get(0), + webidl::Context::member("SVGAnimatedInteger", "baseVal"), + ) { + Ok(value) => value.0, + Err(error) => { + webidl::throw_error(scope, &error); + return; + } + }; + set_svg_animated_integer_values(scope, args.this(), value); + reflect_svg_animated_integer_to_owner_attribute(scope, args.this()); +} + pub(super) fn svg_animated_transform_list_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -791,12 +2103,446 @@ pub(super) fn svg_animated_transform_list_getter<'s>( ); } +pub(super) fn svg_string_list_length_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(items) = require_svg_string_list_items(scope, args.this(), "length getter") else { + return; + }; + rv.set_uint32(items.length()); +} + +pub(super) fn svg_string_list_clear_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if require_svg_string_list_items(scope, args.this(), "clear").is_none() { + return; + } + set_svg_string_list_items(scope, args.this(), v8::Array::new(scope, 0)); + reflect_svg_string_list_to_owner_attribute(scope, args.this()); + rv.set_undefined(); +} + +pub(super) fn svg_string_list_initialize_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if require_svg_string_list_items(scope, args.this(), "initialize").is_none() { + return; + } + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + let Some(item) = svg_string_list_dom_string(scope, parsed.item, "SVGStringList.initialize", 1) + else { + return; + }; + let items = v8::Array::new(scope, 1); + let _ = items.set_index(scope, 0, item); + set_svg_string_list_items(scope, args.this(), items); + reflect_svg_string_list_to_owner_attribute(scope, args.this()); + rv.set(item); +} + +pub(super) fn svg_string_list_get_item_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(items) = require_svg_string_list_items(scope, args.this(), "getItem") else { + return; + }; + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + let Some(item) = svg_list_item_or_throw(scope, items, parsed.index) else { + return; + }; + rv.set(item); +} + +pub(super) fn svg_string_list_insert_item_before_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(items) = require_svg_string_list_items(scope, args.this(), "insertItemBefore") else { + return; + }; + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + let Some(item) = + svg_string_list_dom_string(scope, parsed.item, "SVGStringList.insertItemBefore", 1) + else { + return; + }; + let length = items.length(); + let index = parsed.index.min(length); + let array_length = i32::try_from(length.saturating_add(1)).unwrap_or(i32::MAX); + let next = v8::Array::new(scope, array_length); + for old_index in 0..length { + let new_index = if old_index < index { + old_index + } else { + old_index + 1 + }; + if let Some(value) = items.get_index(scope, old_index) { + let _ = next.set_index(scope, new_index, value); + } + } + let _ = next.set_index(scope, index, item); + set_svg_string_list_items(scope, args.this(), next); + reflect_svg_string_list_to_owner_attribute(scope, args.this()); + rv.set(item); +} + +pub(super) fn svg_string_list_replace_item_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(items) = require_svg_string_list_items(scope, args.this(), "replaceItem") else { + return; + }; + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + let Some(item) = svg_string_list_dom_string(scope, parsed.item, "SVGStringList.replaceItem", 1) + else { + return; + }; + if parsed.index >= items.length() { + webidl::throw_index_size_error(scope); + return; + } + let _ = items.set_index(scope, parsed.index, item); + reflect_svg_string_list_to_owner_attribute(scope, args.this()); + rv.set(item); +} + +pub(super) fn svg_string_list_remove_item_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(items) = require_svg_string_list_items(scope, args.this(), "removeItem") else { + return; + }; + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + let length = items.length(); + let Some(removed) = svg_list_item_or_throw(scope, items, parsed.index) else { + return; + }; + let next_length = i32::try_from(length.saturating_sub(1)).unwrap_or(i32::MAX); + let next = v8::Array::new(scope, next_length); + for old_index in 0..length { + if old_index == parsed.index { + continue; + } + let new_index = if old_index < parsed.index { + old_index + } else { + old_index - 1 + }; + if let Some(value) = items.get_index(scope, old_index) { + let _ = next.set_index(scope, new_index, value); + } + } + set_svg_string_list_items(scope, args.this(), next); + reflect_svg_string_list_to_owner_attribute(scope, args.this()); + rv.set(removed); +} + +pub(super) fn svg_string_list_append_item_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(items) = require_svg_string_list_items(scope, args.this(), "appendItem") else { + return; + }; + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + let Some(item) = svg_string_list_dom_string(scope, parsed.item, "SVGStringList.appendItem", 1) + else { + return; + }; + let _ = items.set_index(scope, items.length(), item); + reflect_svg_string_list_to_owner_attribute(scope, args.this()); + rv.set(item); +} + +fn require_svg_string_list_items<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, + member: &str, +) -> Option> { + if svg_string_list_items(scope, list).is_none() { + webidl::throw_type_error( + scope, + &format!("SVGStringList.{member} called on incompatible receiver."), + ); + return None; + } + sync_svg_string_list_from_owner_attribute(scope, list); + svg_string_list_items(scope, list) +} + +fn svg_string_list_dom_string<'s>( + scope: &mut v8::PinScope<'s, '_>, + value: v8::Local<'s, v8::Value>, + operation: &'static str, + argument_index: usize, +) -> Option> { + let value = match webidl::convert::( + scope, + value, + webidl::Context::argument(operation, argument_index), + ) { + Ok(value) => value.0, + Err(error) => { + webidl::throw_error(scope, &error); + return None; + } + }; + v8_string(scope, &value).map(Into::into) +} + +fn svg_string_list_intercepted_items<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, +) -> Option> { + svg_string_list_items(scope, list)?; + sync_svg_string_list_from_owner_attribute(scope, list); + svg_string_list_items(scope, list) +} + +fn svg_string_list_indexed_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) -> v8::Intercepted { + let Some(value) = svg_string_list_intercepted_items(scope, args.holder()) + .filter(|items| index < items.length()) + .and_then(|items| items.get_index(scope, index)) + else { + return v8::Intercepted::kNo; + }; + rv.set(value); + v8::Intercepted::kYes +} + +fn svg_string_list_indexed_setter<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + value: v8::Local<'s, v8::Value>, + args: v8::PropertyCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Boolean>, +) -> v8::Intercepted { + let list = args.holder(); + let Some(items) = svg_string_list_intercepted_items(scope, list) else { + return v8::Intercepted::kNo; + }; + let Some(value) = svg_string_list_dom_string(scope, value, "SVGStringList indexed setter", 2) + else { + return v8::Intercepted::kYes; + }; + if index >= items.length() { + webidl::throw_index_size_error(scope); + return v8::Intercepted::kYes; + } + let _ = items.set_index(scope, index, value); + reflect_svg_string_list_to_owner_attribute(scope, list); + v8::Intercepted::kYes +} + +fn svg_string_list_indexed_query<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Integer>, +) -> v8::Intercepted { + let Some(items) = svg_string_list_intercepted_items(scope, args.holder()) else { + return v8::Intercepted::kNo; + }; + if index >= items.length() { + return v8::Intercepted::kNo; + } + rv.set_int32(v8::PropertyAttribute::NONE.as_u32() as i32); + v8::Intercepted::kYes +} + +fn svg_string_list_indexed_deleter<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Boolean>, +) -> v8::Intercepted { + let Some(items) = svg_string_list_intercepted_items(scope, args.holder()) else { + return v8::Intercepted::kNo; + }; + if index >= items.length() { + return v8::Intercepted::kNo; + } + rv.set_bool(false); + v8::Intercepted::kYes +} + +fn svg_string_list_indexed_definer<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + descriptor: &v8::PropertyDescriptor, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Boolean>, +) -> v8::Intercepted { + if descriptor.has_get() || descriptor.has_set() { + rv.set_bool(false); + return v8::Intercepted::kYes; + } + let value = if descriptor.has_value() { + v8::Local::new(scope, descriptor.value()) + } else { + v8::undefined(scope).into() + }; + svg_string_list_indexed_setter(scope, index, value, args, rv) +} + +fn svg_string_list_indexed_enumerator<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Array>, +) { + let length = svg_string_list_intercepted_items(scope, args.holder()) + .map(|items| items.length()) + .unwrap_or(0); + let keys = (0..length) + .map(|index| v8::Integer::new_from_unsigned(scope, index).into()) + .collect::>(); + rv.set(v8::Array::new_with_elements(scope, &keys)); +} + +fn svg_string_list_indexed_descriptor<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) -> v8::Intercepted { + let Some(value) = svg_string_list_intercepted_items(scope, args.holder()) + .filter(|items| index < items.length()) + .and_then(|items| items.get_index(scope, index)) + else { + return v8::Intercepted::kNo; + }; + let Ok(descriptor) = DataPropertyDescriptorDeclaration::new(value, true, true).bind(scope) + else { + return v8::Intercepted::kNo; + }; + rv.set(descriptor.into()); + v8::Intercepted::kYes +} + +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"), + } +} + +fn svg_value_list_kind<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, +) -> Option { + if web_api_interfaces::SVGLengthList::is_instance(scope, list) { + 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 + } +} + +fn require_svg_value_list_items<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, + kind: SvgListKind, + member: &str, + writable: bool, +) -> Option> { + let (_, interface) = svg_value_list_metadata(kind); + if !require_svg_receiver(scope, list, interface, member) { + return None; + } + if writable && svg_value_list_is_read_only(scope, list) { + throw_dom_exception( + scope, + "NoModificationAllowedError", + 7, + "The SVG value list is read-only.", + ); + return None; + } + sync_svg_value_list_from_owner_attribute(scope, list, kind); + 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>, rv: v8::ReturnValue<'_, v8::Value>, ) { - svg_value_list_length_getter(scope, args, rv, SvgListKind::Length, "SVGLengthList"); + svg_value_list_length_getter(scope, args, rv, SvgListKind::Length); } pub(super) fn svg_number_list_length_getter<'s>( @@ -804,7 +2550,7 @@ pub(super) fn svg_number_list_length_getter<'s>( args: v8::FunctionCallbackArguments<'s>, rv: v8::ReturnValue<'_, v8::Value>, ) { - svg_value_list_length_getter(scope, args, rv, SvgListKind::Number, "SVGNumberList"); + svg_value_list_length_getter(scope, args, rv, SvgListKind::Number); } pub(super) fn svg_value_list_length_getter<'s>( @@ -812,13 +2558,13 @@ pub(super) fn svg_value_list_length_getter<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, kind: SvgListKind, - interface: &'static str, ) { - if !require_svg_receiver(scope, args.this(), interface, "length getter") { + let Some(items) = + require_svg_value_list_items(scope, args.this(), kind, "length getter", false) + else { return; - } - let length = svg_value_list_items(scope, args.this(), kind).length(); - rv.set(v8::Integer::new_from_unsigned(scope, length).into()); + }; + rv.set(v8::Integer::new_from_unsigned(scope, items.length()).into()); } pub(super) fn svg_length_list_clear_callback<'s>( @@ -843,6 +2589,9 @@ pub(super) fn svg_value_list_clear_callback<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, kind: SvgListKind, ) { + if require_svg_value_list_items(scope, args.this(), kind, "clear", true).is_none() { + return; + } set_svg_value_list_items(scope, args.this(), v8::Array::new(scope, 0), kind); reflect_svg_value_list_to_owner_attribute(scope, args.this(), kind); rv.set_undefined(); @@ -870,10 +2619,15 @@ pub(super) fn svg_value_list_initialize_callback<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, kind: SvgListKind, ) { + if require_svg_value_list_items(scope, args.this(), kind, "initialize", true).is_none() { + return; + } let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - let item = svg_value_list_item_or_default(scope, parsed.item, kind); + let Some(item) = svg_value_list_item_or_throw(scope, parsed.item, kind) else { + return; + }; let Some(items) = serialize_v8_array(scope, [item]) else { return; }; @@ -904,10 +2658,13 @@ pub(super) fn svg_value_list_get_item_callback<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, kind: SvgListKind, ) { + let Some(items) = require_svg_value_list_items(scope, args.this(), kind, "getItem", false) + else { + return; + }; let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - let items = svg_value_list_items(scope, args.this(), kind); let Some(item) = svg_list_item_or_throw(scope, items, parsed.index) else { return; }; @@ -936,11 +2693,17 @@ pub(super) fn svg_value_list_insert_item_before_callback<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, kind: SvgListKind, ) { + let Some(items) = + require_svg_value_list_items(scope, args.this(), kind, "insertItemBefore", true) + else { + return; + }; let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - let item = svg_value_list_item_or_default(scope, parsed.item, kind); - let items = svg_value_list_items(scope, args.this(), kind); + let Some(item) = svg_value_list_item_or_throw(scope, parsed.item, kind) else { + return; + }; let length = items.length(); let index = parsed.index.min(length); let next = v8::Array::new(scope, (length + 1) as i32); @@ -982,15 +2745,20 @@ pub(super) fn svg_value_list_replace_item_callback<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, kind: SvgListKind, ) { + let Some(items) = require_svg_value_list_items(scope, args.this(), kind, "replaceItem", true) + else { + return; + }; let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - let items = svg_value_list_items(scope, args.this(), kind); + let Some(item) = svg_value_list_item_or_throw(scope, parsed.item, kind) else { + return; + }; if parsed.index >= items.length() { webidl::throw_index_size_error(scope); return; } - let item = svg_value_list_item_or_default(scope, parsed.item, kind); if let Some(replaced) = items .get_index(scope, parsed.index) .and_then(|value| v8::Local::::try_from(value).ok()) @@ -1027,11 +2795,14 @@ pub(super) fn svg_value_list_remove_item_callback<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, kind: SvgListKind, ) { + let Some(items) = require_svg_value_list_items(scope, args.this(), kind, "removeItem", true) + else { + return; + }; let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; let index = parsed.index; - let items = svg_value_list_items(scope, args.this(), kind); let length = items.length(); if index >= length { webidl::throw_index_size_error(scope); @@ -1081,17 +2852,168 @@ pub(super) fn svg_value_list_append_item_callback<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, kind: SvgListKind, ) { + let Some(items) = require_svg_value_list_items(scope, args.this(), kind, "appendItem", true) + else { + return; + }; let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - let item = svg_value_list_item_or_default(scope, parsed.item, kind); - let items = svg_value_list_items(scope, args.this(), kind); + let Some(item) = svg_value_list_item_or_throw(scope, parsed.item, kind) else { + return; + }; set_svg_value_list_item_owner_list(scope, item, args.this()); let _ = items.set_index(scope, items.length(), item.into()); reflect_svg_value_list_to_owner_attribute(scope, args.this(), kind); rv.set(item.into()); } +fn svg_value_list_intercepted_items<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, +) -> Option<(SvgListKind, v8::Local<'s, v8::Array>)> { + let kind = svg_value_list_kind(scope, list)?; + sync_svg_value_list_from_owner_attribute(scope, list, kind); + Some((kind, svg_value_list_items(scope, list, kind))) +} + +fn svg_value_list_indexed_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) -> v8::Intercepted { + let Some(value) = svg_value_list_intercepted_items(scope, args.holder()) + .map(|(_, items)| items) + .filter(|items| index < items.length()) + .and_then(|items| items.get_index(scope, index)) + else { + return v8::Intercepted::kNo; + }; + rv.set(value); + v8::Intercepted::kYes +} + +fn svg_value_list_indexed_setter<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + value: v8::Local<'s, v8::Value>, + args: v8::PropertyCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Boolean>, +) -> v8::Intercepted { + let list = args.holder(); + let Some(kind) = svg_value_list_kind(scope, list) else { + return v8::Intercepted::kNo; + }; + let Some(items) = require_svg_value_list_items(scope, list, kind, "indexed setter", true) + else { + return v8::Intercepted::kYes; + }; + let Some(item) = svg_value_list_item_or_throw(scope, value, kind) else { + return v8::Intercepted::kYes; + }; + if index >= items.length() { + webidl::throw_index_size_error(scope); + return v8::Intercepted::kYes; + } + if let Some(replaced) = items + .get_index(scope, index) + .and_then(|value| v8::Local::::try_from(value).ok()) + { + clear_svg_value_list_item_owner_list(scope, replaced); + } + let _ = items.set_index(scope, index, item.into()); + set_svg_value_list_item_owner_list(scope, item, list); + reflect_svg_value_list_to_owner_attribute(scope, list, kind); + v8::Intercepted::kYes +} + +fn svg_value_list_indexed_query<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Integer>, +) -> v8::Intercepted { + let Some((_, items)) = svg_value_list_intercepted_items(scope, args.holder()) else { + return v8::Intercepted::kNo; + }; + if index >= items.length() { + return v8::Intercepted::kNo; + } + rv.set_int32(v8::PropertyAttribute::NONE.as_u32() as i32); + v8::Intercepted::kYes +} + +fn svg_value_list_indexed_deleter<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Boolean>, +) -> v8::Intercepted { + let Some((_, items)) = svg_value_list_intercepted_items(scope, args.holder()) else { + return v8::Intercepted::kNo; + }; + if index >= items.length() { + return v8::Intercepted::kNo; + } + rv.set_bool(false); + v8::Intercepted::kYes +} + +fn svg_value_list_indexed_definer<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + descriptor: &v8::PropertyDescriptor, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Boolean>, +) -> v8::Intercepted { + if descriptor.has_get() || descriptor.has_set() { + rv.set_bool(false); + return v8::Intercepted::kYes; + } + let value = if descriptor.has_value() { + v8::Local::new(scope, descriptor.value()) + } else { + v8::undefined(scope).into() + }; + svg_value_list_indexed_setter(scope, index, value, args, rv) +} + +fn svg_value_list_indexed_enumerator<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Array>, +) { + let length = svg_value_list_intercepted_items(scope, args.holder()) + .map(|(_, items)| items.length()) + .unwrap_or(0); + let keys = (0..length) + .map(|index| v8::Integer::new_from_unsigned(scope, index).into()) + .collect::>(); + rv.set(v8::Array::new_with_elements(scope, &keys)); +} + +fn svg_value_list_indexed_descriptor<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) -> v8::Intercepted { + let Some(value) = svg_value_list_intercepted_items(scope, args.holder()) + .map(|(_, items)| items) + .filter(|items| index < items.length()) + .and_then(|items| items.get_index(scope, index)) + else { + return v8::Intercepted::kNo; + }; + let Ok(descriptor) = DataPropertyDescriptorDeclaration::new(value, true, true).bind(scope) + else { + return v8::Intercepted::kNo; + }; + rv.set(descriptor.into()); + v8::Intercepted::kYes +} + pub(super) fn svg_transform_list_length_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -1359,12 +3281,84 @@ pub(super) fn svg_transform_set_matrix_callback<'s>( rv.set_undefined(); } -pub(super) fn svg_svg_element_create_matrix_callback<'s>( +fn require_svg_svg_element_receiver<'s>( scope: &mut v8::PinScope<'s, '_>, - _args: v8::FunctionCallbackArguments<'s>, + receiver: v8::Local<'s, v8::Object>, + member: &str, +) -> bool { + let Ok((runtime_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, receiver) + else { + webidl::throw_type_error( + scope, + &format!("SVGSVGElement.{member} called on incompatible receiver."), + ); + return false; + }; + let is_svg_element = unsafe { &*runtime_ptr } + .dom_host() + .node(handle) + .and_then(|node| node.as_element()) + .is_some_and(|element| element.is_svg_element("svg")); + if !is_svg_element { + webidl::throw_type_error( + scope, + &format!("SVGSVGElement.{member} called on incompatible receiver."), + ); + } + is_svg_element +} + +pub(super) fn svg_svg_element_create_number_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - rv.set(build_svg_matrix(scope, SvgMatrixComponents::identity()).into()); + if require_svg_svg_element_receiver(scope, args.this(), "createSVGNumber") { + rv.set(build_svg_number(scope, 0.0).into()); + } +} + +pub(super) fn svg_svg_element_create_length_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if require_svg_svg_element_receiver(scope, args.this(), "createSVGLength") { + let length = build_svg_length(scope, 0.0); + set_svg_length_owner_attribute(scope, length, args.this(), ""); + rv.set(length.into()); + } +} + +pub(super) fn svg_svg_element_create_matrix_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if require_svg_svg_element_receiver(scope, args.this(), "createSVGMatrix") { + rv.set(build_svg_matrix(scope, SvgMatrixComponents::identity()).into()); + } +} + +pub(super) fn svg_svg_element_create_angle_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if require_svg_svg_element_receiver(scope, args.this(), "createSVGAngle") { + rv.set(build_svg_angle(scope).into()); + } +} + +pub(super) fn svg_svg_element_create_point_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if require_svg_svg_element_receiver(scope, args.this(), "createSVGPoint") { + rv.set(super::super::geometry_runtime::build_svg_point_object(scope).into()); + } } pub(super) fn svg_svg_element_deselect_all_callback<'s>( @@ -1423,9 +3417,12 @@ pub(super) fn svg_svg_element_deselect_all_callback<'s>( pub(super) fn svg_svg_element_create_transform_callback<'s>( scope: &mut v8::PinScope<'s, '_>, - _args: v8::FunctionCallbackArguments<'s>, + args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if !require_svg_svg_element_receiver(scope, args.this(), "createSVGTransform") { + return; + } rv.set( build_svg_transform(scope, SvgTransform::matrix(SvgMatrixComponents::identity())).into(), ); @@ -1436,13 +3433,14 @@ pub(super) fn svg_svg_element_create_transform_from_matrix_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - let Some(parsed) = webidl::parse_args::(scope, &args) else { + if !require_svg_svg_element_receiver(scope, args.this(), "createSVGTransformFromMatrix") { + return; + } + let Some(components) = + svg_dom_matrix_2d_init_arg(scope, &args, "SVGSVGElement.createSVGTransformFromMatrix") + else { return; }; - let Some(matrix) = cloned_svg_matrix_value_or_throw(scope, parsed.matrix) else { - return; - }; - let components = svg_matrix_components(scope, matrix); rv.set(build_svg_transform(scope, SvgTransform::matrix(components)).into()); } @@ -1729,12 +3727,62 @@ pub(super) fn svg_length_new_value_specified_units_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if !require_svg_receiver(scope, args.this(), "SVGLength", "newValueSpecifiedUnits") { + return; + } let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; + if !require_writable_svg_length(scope, args.this()) { + return; + } + if !svg_length_unit_type_is_supported(parsed.unit_type as u32) { + throw_dom_exception( + scope, + "NotSupportedError", + 9, + "The SVG length unit type is not supported.", + ); + return; + } set_svg_length_numeric_value(scope, args.this(), parsed.value, parsed.unit_type as u32); reflect_svg_length_to_owner_attribute(scope, args.this()); + reflect_svg_value_list_item_to_owner_list(scope, args.this(), SvgListKind::Length); + rv.set_undefined(); +} + +pub(super) fn svg_angle_new_value_specified_units_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if !require_svg_receiver(scope, args.this(), "SVGAngle", "newValueSpecifiedUnits") { + return; + } + if svg_angle_is_read_only(scope, args.this()) { + throw_dom_exception( + scope, + "NoModificationAllowedError", + 7, + "The SVG angle is read-only.", + ); + return; + } + let Some(parsed) = webidl::parse_args::(scope, &args) + else { + return; + }; + if !set_svg_angle_new_value(scope, args.this(), parsed.unit_type as u32, parsed.value) { + throw_dom_exception( + scope, + "NotSupportedError", + 9, + "The SVG angle unit type is not supported.", + ); + return; + } + reflect_svg_angle_to_owner_attribute(scope, args.this()); rv.set_undefined(); } @@ -1743,13 +3791,76 @@ pub(super) fn svg_length_convert_to_specified_units_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if !require_svg_receiver(scope, args.this(), "SVGLength", "convertToSpecifiedUnits") { + return; + } let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - let value = svg_length_number_slot(scope, args.this(), SVG_LENGTH_VALUE_SLOT).unwrap_or(0.0); - set_svg_length_numeric_value(scope, args.this(), value, parsed.unit_type as u32); + if !require_writable_svg_length(scope, args.this()) { + return; + } + sync_svg_length_from_owner_attribute(scope, args.this()); + if !svg_length_unit_type_is_supported(parsed.unit_type as u32) { + throw_dom_exception( + scope, + "NotSupportedError", + 9, + "The SVG length unit type is not supported.", + ); + return; + } + if !convert_svg_length_to_unit(scope, args.this(), parsed.unit_type as u32) { + throw_dom_exception( + scope, + "NotSupportedError", + 9, + "The SVG length could not be resolved in the requested unit.", + ); + return; + } reflect_svg_length_to_owner_attribute(scope, args.this()); + reflect_svg_value_list_item_to_owner_list(scope, args.this(), SvgListKind::Length); + rv.set_undefined(); +} + +fn svg_length_unit_type_is_supported(unit_type: u32) -> bool { + (SVG_LENGTH_TYPE_NUMBER..=SVG_LENGTH_TYPE_PC).contains(&unit_type) +} + +pub(super) fn svg_angle_convert_to_specified_units_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if !require_svg_receiver(scope, args.this(), "SVGAngle", "convertToSpecifiedUnits") { + return; + } + if svg_angle_is_read_only(scope, args.this()) { + throw_dom_exception( + scope, + "NoModificationAllowedError", + 7, + "The SVG angle is read-only.", + ); + return; + } + sync_svg_angle_from_owner_attribute(scope, args.this()); + let Some(parsed) = webidl::parse_args::(scope, &args) + else { + return; + }; + if !convert_svg_angle_to_unit(scope, args.this(), parsed.unit_type as u32) { + throw_dom_exception( + scope, + "NotSupportedError", + 9, + "The SVG angle unit type is not supported.", + ); + return; + } + reflect_svg_angle_to_owner_attribute(scope, args.this()); rv.set_undefined(); } diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/filters.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/filters.rs new file mode 100644 index 0000000000..72fe336349 --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/filters.rs @@ -0,0 +1,129 @@ +use moli_webapi_declare::WebApiFunctionTemplate; + +use super::SVG_URI_HREF_SLOT; +use super::callbacks::{ + svg_animated_string_attribute_getter, svg_filter_primitive_animated_length_getter, + svg_fit_to_view_box_getter, +}; +use crate::{ + util::{callback_data_index_value, callback_data_item}, + web_api_interfaces, +}; + +// A mixin's attributes are installed on each concrete interface. Generate its +// receiver checks there so, for example, a flood getter cannot accept a tile. +macro_rules! filter_primitive { + ($declaration:ident, $interface:ident, { + $(($field:ident, $name:literal, $getter:ident, $index:literal)),* $(,)? + }) => { + #[derive(WebApiFunctionTemplate)] + #[webapi(interface = web_api_interfaces::$interface, enumerable, receiver)] + struct $declaration { + #[webapi(accessor_property = "x", getter = animated_length_getter, data = callback_data_index_value(scope, 0))] + x: (), + #[webapi(accessor_property = "y", getter = animated_length_getter, data = callback_data_index_value(scope, 1))] + y: (), + #[webapi(accessor_property = "width", getter = animated_length_getter, data = callback_data_index_value(scope, 2))] + width: (), + #[webapi(accessor_property = "height", getter = animated_length_getter, data = callback_data_index_value(scope, 3))] + height: (), + #[webapi(accessor_property = "result", getter = animated_string_getter, data = callback_data_index_value(scope, 0))] + result: (), + $( + #[webapi(accessor_property = $name, getter = $getter, data = callback_data_index_value(scope, $index))] + $field: (), + )* + } + }; +} + +filter_primitive!(ComponentTransferDeclaration, SVGFEComponentTransferElement, { + (in1, "in1", animated_string_getter, 1), +}); +filter_primitive!(FloodDeclaration, SVGFEFloodElement, {}); +filter_primitive!(ImageDeclaration, SVGFEImageElement, { + (href, "href", animated_string_getter, 2), + (preserve_aspect_ratio, "preserveAspectRatio", preserve_aspect_ratio_getter, 1), +}); +filter_primitive!(MergeDeclaration, SVGFEMergeElement, {}); +filter_primitive!(TileDeclaration, SVGFETileElement, { + (in1, "in1", animated_string_getter, 1), +}); + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGFEMergeNodeElement, enumerable, receiver)] +struct MergeNodeDeclaration { + #[webapi(accessor_property = "in1", getter = animated_string_getter, data = callback_data_index_value(scope, 1))] + in1: (), +} + +pub(super) fn install_bindings<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, + name: &str, +) { + let prototype = template.prototype_template(scope); + match name { + "SVGFEComponentTransferElement" => { + ComponentTransferDeclaration::initialize_prototype_template(scope, prototype) + } + "SVGFEFloodElement" => FloodDeclaration::initialize_prototype_template(scope, prototype), + "SVGFEImageElement" => ImageDeclaration::initialize_prototype_template(scope, prototype), + "SVGFEMergeElement" => MergeDeclaration::initialize_prototype_template(scope, prototype), + "SVGFEMergeNodeElement" => { + MergeNodeDeclaration::initialize_prototype_template(scope, prototype) + } + "SVGFETileElement" => TileDeclaration::initialize_prototype_template(scope, prototype), + _ => {} + } +} + +fn animated_length_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + // Tear-off objects belong to the element's realm, including when the + // accessor was obtained from a different Window. + let Some(context) = args.this().get_creation_context(scope) else { + return; + }; + let scope = &mut v8::ContextScope::new(scope, context); + svg_filter_primitive_animated_length_getter(scope, args, rv); +} + +fn animated_string_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some((slot, attribute)) = callback_data_item( + scope, + &args, + &[ + ("__moliSvgFilterResult", "result"), + ("__moliSvgFilterInput", "in"), + (SVG_URI_HREF_SLOT, "href"), + ], + "SVG filter string attributes", + ) else { + return; + }; + let Some(context) = args.this().get_creation_context(scope) else { + return; + }; + let scope = &mut v8::ContextScope::new(scope, context); + svg_animated_string_attribute_getter(scope, args, rv, slot, attribute); +} + +fn preserve_aspect_ratio_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(context) = args.this().get_creation_context(scope) else { + return; + }; + let scope = &mut v8::ContextScope::new(scope, context); + svg_fit_to_view_box_getter(scope, args, rv); +} 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 e26593b89b..6e483e4565 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, }; @@ -13,40 +18,132 @@ use moli_svg::{ SvgLength, SvgLengthUnit, SvgMatrixComponents, SvgTransform, SvgTransformKind, }; +mod animation; mod bindings; mod builders; mod callbacks; +mod filters; mod path_data; mod rect; const SVG_GRAPHICS_TRANSFORM_SLOT: &str = "__moliSvgGraphicsTransform"; +const SVG_GRAPHICS_REQUIRED_EXTENSIONS_SLOT: &str = "__moliSvgGraphicsRequiredExtensions"; +const SVG_GRAPHICS_SYSTEM_LANGUAGE_SLOT: &str = "__moliSvgGraphicsSystemLanguage"; const SVG_PATTERN_TRANSFORM_SLOT: &str = "__moliSvgPatternTransform"; const SVG_GRADIENT_TRANSFORM_SLOT: &str = "__moliSvgGradientTransform"; const SVG_GEOMETRY_PATH_LENGTH_SLOT: &str = "__moliSvgGeometryPathLength"; +const SVG_ELEMENT_CLASS_NAME_SLOT: &str = "__moliSvgElementClassName"; +const SVG_URI_HREF_SLOT: &str = "__moliSvgUriHref"; +const SVG_ANIMATED_STRING_BASE_VAL_SLOT: &str = "__moliSvgAnimatedStringBaseVal"; +const SVG_ANIMATED_STRING_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedStringAnimVal"; +const SVG_ANIMATED_STRING_OWNER_ELEMENT_SLOT: &str = "__moliSvgAnimatedStringOwnerElement"; +const SVG_ANIMATED_STRING_OWNER_ATTRIBUTE_SLOT: &str = "__moliSvgAnimatedStringOwnerAttribute"; +const SVG_ANIMATED_BOOLEAN_BASE_VAL_SLOT: &str = "__moliSvgAnimatedBooleanBaseVal"; +const SVG_ANIMATED_BOOLEAN_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedBooleanAnimVal"; +const SVG_ANIMATED_BOOLEAN_OWNER_ELEMENT_SLOT: &str = "__moliSvgAnimatedBooleanOwnerElement"; +const SVG_ANIMATED_BOOLEAN_OWNER_ATTRIBUTE_SLOT: &str = "__moliSvgAnimatedBooleanOwnerAttribute"; +const SVG_ANIMATED_BOOLEAN_INITIAL_VALUE_SLOT: &str = "__moliSvgAnimatedBooleanInitialValue"; +const SVG_FE_CONVOLVE_MATRIX_PRESERVE_ALPHA_SLOT: &str = "__moliSvgFeConvolveMatrixPreserveAlpha"; const SVG_ANIMATED_LENGTH_BASE_VAL_SLOT: &str = "__moliSvgAnimatedLengthBaseVal"; const SVG_ANIMATED_LENGTH_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedLengthAnimVal"; +const SVG_MARKER_ORIENT_ANGLE_SLOT: &str = "__moliSvgMarkerOrientAngle"; +const SVG_MARKER_UNITS_SLOT: &str = "__moliSvgMarkerUnits"; +const SVG_MARKER_ORIENT_TYPE_SLOT: &str = "__moliSvgMarkerOrientType"; +const SVG_ANIMATED_ANGLE_BASE_VAL_SLOT: &str = "__moliSvgAnimatedAngleBaseVal"; +const SVG_ANIMATED_ANGLE_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedAngleAnimVal"; +const SVG_FIT_VIEW_BOX_SLOT: &str = "__moliSvgFitViewBox"; +const SVG_FIT_PRESERVE_ASPECT_RATIO_SLOT: &str = "__moliSvgFitPreserveAspectRatio"; +const SVG_ANIMATED_RECT_BASE_VAL_SLOT: &str = "__moliSvgAnimatedRectBaseVal"; +const SVG_ANIMATED_RECT_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedRectAnimVal"; +const SVG_ANIMATED_PRESERVE_ASPECT_RATIO_BASE_VAL_SLOT: &str = + "__moliSvgAnimatedPreserveAspectRatioBaseVal"; +const SVG_ANIMATED_PRESERVE_ASPECT_RATIO_ANIM_VAL_SLOT: &str = + "__moliSvgAnimatedPreserveAspectRatioAnimVal"; +const SVG_PRESERVE_ASPECT_RATIO_ALIGN_SLOT: &str = "__moliSvgPreserveAspectRatioAlign"; +const SVG_PRESERVE_ASPECT_RATIO_MEET_OR_SLICE_SLOT: &str = + "__moliSvgPreserveAspectRatioMeetOrSlice"; +const SVG_PRESERVE_ASPECT_RATIO_OWNER_ELEMENT_SLOT: &str = + "__moliSvgPreserveAspectRatioOwnerElement"; +const SVG_PRESERVE_ASPECT_RATIO_READ_ONLY_SLOT: &str = "__moliSvgPreserveAspectRatioReadOnly"; +const SVG_ANGLE_OWNER_ELEMENT_SLOT: &str = "__moliSvgAngleOwnerElement"; +const SVG_ANGLE_OWNER_ATTRIBUTE_SLOT: &str = "__moliSvgAngleOwnerAttribute"; +const SVG_ANGLE_UNIT_TYPE_SLOT: &str = "__moliSvgAngleUnitType"; +const SVG_ANGLE_VALUE_SLOT: &str = "__moliSvgAngleValue"; +const SVG_ANGLE_VALUE_IN_SPECIFIED_UNITS_SLOT: &str = "__moliSvgAngleValueInSpecifiedUnits"; +const SVG_ANGLE_VALUE_AS_STRING_SLOT: &str = "__moliSvgAngleValueAsString"; +const SVG_ANGLE_READ_ONLY_SLOT: &str = "__moliSvgAngleReadOnly"; const SVG_ANIMATED_LENGTH_LIST_BASE_VAL_SLOT: &str = "__moliSvgAnimatedLengthListBaseVal"; const SVG_ANIMATED_LENGTH_LIST_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedLengthListAnimVal"; const SVG_LENGTH_LIST_ITEMS_SLOT: &str = "__moliSvgLengthListItems"; const SVG_LENGTH_OWNER_ELEMENT_SLOT: &str = "__moliSvgLengthOwnerElement"; const SVG_LENGTH_OWNER_ATTRIBUTE_SLOT: &str = "__moliSvgLengthOwnerAttribute"; +const SVG_LENGTH_INITIAL_VALUE_SLOT: &str = "__moliSvgLengthInitialValue"; +const SVG_LENGTH_READ_ONLY_SLOT: &str = "__moliSvgLengthReadOnly"; const SVG_LENGTH_UNIT_TYPE_SLOT: &str = "__moliSvgLengthUnitType"; +const SVG_LENGTH_UNIT_SUFFIX_SLOT: &str = "__moliSvgLengthUnitSuffix"; const SVG_LENGTH_VALUE_SLOT: &str = "__moliSvgLengthValue"; +const SVG_LENGTH_VALUE_IN_SPECIFIED_UNITS_SLOT: &str = "__moliSvgLengthValueInSpecifiedUnits"; const SVG_LENGTH_VALUE_AS_STRING_SLOT: &str = "__moliSvgLengthValueAsString"; const SVG_NUMBER_VALUE_SLOT: &str = "__moliSvgNumberValue"; const SVG_ANIMATED_NUMBER_OWNER_ELEMENT_SLOT: &str = "__moliSvgAnimatedNumberOwnerElement"; const SVG_ANIMATED_NUMBER_OWNER_ATTRIBUTE_SLOT: &str = "__moliSvgAnimatedNumberOwnerAttribute"; +const SVG_ANIMATED_NUMBER_PROPERTY_INDEX_SLOT: &str = "__moliSvgAnimatedNumberPropertyIndex"; const SVG_ANIMATED_NUMBER_BASE_VAL_SLOT: &str = "__moliSvgAnimatedNumberBaseVal"; const SVG_ANIMATED_NUMBER_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedNumberAnimVal"; +const SVG_ANIMATED_INTEGER_BASE_VAL_SLOT: &str = "__moliSvgAnimatedIntegerBaseVal"; +const SVG_ANIMATED_INTEGER_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedIntegerAnimVal"; +const SVG_ANIMATED_INTEGER_OWNER_ELEMENT_SLOT: &str = "__moliSvgAnimatedIntegerOwnerElement"; +const SVG_ANIMATED_INTEGER_PROPERTY_INDEX_SLOT: &str = "__moliSvgAnimatedIntegerPropertyIndex"; 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"; +const SVG_STRING_LIST_SYNCED_ATTRIBUTE_VALUE_SLOT: &str = "__moliSvgStringListSyncedAttributeValue"; +const SVG_STRING_LIST_SYNCED_ATTRIBUTE_PRESENT_SLOT: &str = + "__moliSvgStringListSyncedAttributePresent"; const SVG_VALUE_LIST_OWNER_ELEMENT_SLOT: &str = "__moliSvgValueListOwnerElement"; const SVG_VALUE_LIST_OWNER_ATTRIBUTE_SLOT: &str = "__moliSvgValueListOwnerAttribute"; const SVG_VALUE_LIST_ITEM_OWNER_LIST_SLOT: &str = "__moliSvgValueListItemOwnerList"; +const SVG_VALUE_LIST_READ_ONLY_SLOT: &str = "__moliSvgValueListReadOnly"; const SVG_VALUE_LIST_SYNCED_ATTRIBUTE_VALUE_SLOT: &str = "__moliSvgValueListSyncedAttributeValue"; const SVG_ANIMATED_ENUMERATION_BASE_VAL_SLOT: &str = "__moliSvgAnimatedEnumerationBaseVal"; const SVG_ANIMATED_ENUMERATION_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedEnumerationAnimVal"; +const SVG_ANIMATED_ENUMERATION_OWNER_ELEMENT_SLOT: &str = + "__moliSvgAnimatedEnumerationOwnerElement"; +const SVG_ANIMATED_ENUMERATION_PROPERTY_INDEX_SLOT: &str = + "__moliSvgAnimatedEnumerationPropertyIndex"; +const SVG_CLIP_PATH_UNITS_SLOT: &str = "__moliSvgClipPathUnits"; +const SVG_FILTER_UNITS_SLOT: &str = "__moliSvgFilterUnits"; +const SVG_PRIMITIVE_UNITS_SLOT: &str = "__moliSvgPrimitiveUnits"; +const SVG_GRADIENT_UNITS_SLOT: &str = "__moliSvgGradientUnits"; +const SVG_GRADIENT_SPREAD_METHOD_SLOT: &str = "__moliSvgGradientSpreadMethod"; +const SVG_MASK_UNITS_SLOT: &str = "__moliSvgMaskUnits"; +const SVG_MASK_CONTENT_UNITS_SLOT: &str = "__moliSvgMaskContentUnits"; +const SVG_PATTERN_UNITS_SLOT: &str = "__moliSvgPatternUnits"; +const SVG_PATTERN_CONTENT_UNITS_SLOT: &str = "__moliSvgPatternContentUnits"; +const SVG_COMPONENT_TRANSFER_TYPE_SLOT: &str = "__moliSvgComponentTransferType"; +const SVG_FE_BLEND_MODE_SLOT: &str = "__moliSvgFeBlendMode"; +const SVG_FE_COLOR_MATRIX_TYPE_SLOT: &str = "__moliSvgFeColorMatrixType"; +const SVG_FE_COMPOSITE_OPERATOR_SLOT: &str = "__moliSvgFeCompositeOperator"; +const SVG_FE_CONVOLVE_MATRIX_EDGE_MODE_SLOT: &str = "__moliSvgFeConvolveMatrixEdgeMode"; +const SVG_FE_CONVOLVE_MATRIX_ORDER_X_SLOT: &str = "__moliSvgFeConvolveMatrixOrderX"; +const SVG_FE_CONVOLVE_MATRIX_ORDER_Y_SLOT: &str = "__moliSvgFeConvolveMatrixOrderY"; +const SVG_FE_CONVOLVE_MATRIX_TARGET_X_SLOT: &str = "__moliSvgFeConvolveMatrixTargetX"; +const SVG_FE_CONVOLVE_MATRIX_TARGET_Y_SLOT: &str = "__moliSvgFeConvolveMatrixTargetY"; +const SVG_FE_DISPLACEMENT_MAP_X_CHANNEL_SLOT: &str = "__moliSvgFeDisplacementMapXChannel"; +const SVG_FE_DISPLACEMENT_MAP_Y_CHANNEL_SLOT: &str = "__moliSvgFeDisplacementMapYChannel"; +const SVG_FE_MORPHOLOGY_OPERATOR_SLOT: &str = "__moliSvgFeMorphologyOperator"; +const SVG_FE_TURBULENCE_STITCH_TILES_SLOT: &str = "__moliSvgFeTurbulenceStitchTiles"; +const SVG_FE_TURBULENCE_TYPE_SLOT: &str = "__moliSvgFeTurbulenceType"; +const SVG_FE_TURBULENCE_NUM_OCTAVES_SLOT: &str = "__moliSvgFeTurbulenceNumOctaves"; +const SVG_TEXT_PATH_METHOD_SLOT: &str = "__moliSvgTextPathMethod"; +const SVG_TEXT_PATH_SPACING_SLOT: &str = "__moliSvgTextPathSpacing"; +const SVG_TEXT_PATH_SIDE_SLOT: &str = "__moliSvgTextPathSide"; const SVG_ANIMATED_TRANSFORM_LIST_BASE_VAL_SLOT: &str = "__moliSvgAnimatedTransformListBaseVal"; const SVG_ANIMATED_TRANSFORM_LIST_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedTransformListAnimVal"; const SVG_TRANSFORM_LIST_ITEMS_SLOT: &str = "__moliSvgTransformListItems"; @@ -72,10 +169,68 @@ const SVG_TEXT_POSITIONING_DX_SLOT: &str = "__moliSvgTextPositioningDx"; const SVG_TEXT_POSITIONING_DY_SLOT: &str = "__moliSvgTextPositioningDy"; const SVG_TEXT_POSITIONING_ROTATE_SLOT: &str = "__moliSvgTextPositioningRotate"; +const SVG_TEST_STRING_LIST_ATTRIBUTES: &[(&str, &str)] = &[ + ("requiredExtensions", SVG_GRAPHICS_REQUIRED_EXTENSIONS_SLOT), + ("systemLanguage", SVG_GRAPHICS_SYSTEM_LANGUAGE_SLOT), +]; + #[derive(Clone, Copy)] enum SvgListKind { Length, Number, + Point, +} + +#[derive(Clone, Copy)] +enum SvgAnimatedEnumerationKind { + Keywords(&'static [(&'static str, u32)]), + MarkerOrient, +} + +#[derive(Clone, Copy)] +struct SvgAnimatedEnumerationProperty { + index: usize, + attribute: &'static str, + cache_slot: &'static str, + initial_value: u32, + kind: SvgAnimatedEnumerationKind, +} + +#[derive(Clone, Copy)] +enum SvgAnimatedIntegerComponent { + Scalar, + PairFirst, + PairSecondOrFirst, +} + +#[derive(Clone, Copy)] +struct SvgAnimatedIntegerProperty { + index: usize, + interface: &'static str, + local_name: &'static str, + name: &'static str, + attribute: &'static str, + cache_slot: &'static str, + initial_value: i32, + component: SvgAnimatedIntegerComponent, +} + +#[derive(Clone, Copy)] +enum SvgAnimatedNumberComponent { + Scalar, + PairFirst, + PairSecondOrFirst, + NumberOrPercentage, +} + +#[derive(Clone, Copy)] +struct SvgAnimatedNumberProperty { + index: usize, + interface: &'static str, + name: &'static str, + attribute: &'static str, + initial_value: f64, + component: SvgAnimatedNumberComponent, } #[derive(webidl::WebIdlArgs)] @@ -183,7 +338,7 @@ struct SvgMatrixRotateFromVectorArgs { struct SvgLengthNewValueSpecifiedUnitsArgs { #[webidl(required, converter = "unsigned_short")] unit_type: u16, - #[webidl(required)] + #[webidl(required, converter = "double")] value: f64, } @@ -194,6 +349,22 @@ struct SvgLengthConvertToSpecifiedUnitsArgs { unit_type: u16, } +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "SVG angle newValueSpecifiedUnits")] +struct SvgAngleNewValueSpecifiedUnitsArgs { + #[webidl(required, converter = "unsigned_short")] + unit_type: u16, + #[webidl(required, converter = "double")] + value: f64, +} + +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "SVG angle convertToSpecifiedUnits")] +struct SvgAngleConvertToSpecifiedUnitsArgs { + #[webidl(required, converter = "unsigned_short")] + unit_type: u16, +} + #[derive(webidl::WebIdlArgs)] #[webidl(prefix = "SVGGeometryElement.getPointAtLength")] struct SvgGeometryPointAtLengthArgs { @@ -236,6 +407,37 @@ const SVG_LENGTH_TYPE_IN: u32 = 8; const SVG_LENGTH_TYPE_PT: u32 = 9; const SVG_LENGTH_TYPE_PC: u32 = 10; +const SVG_ANGLE_TYPE_UNKNOWN: u32 = 0; +const SVG_ANGLE_TYPE_UNSPECIFIED: u32 = 1; +const SVG_ANGLE_TYPE_DEG: u32 = 2; +const SVG_ANGLE_TYPE_RAD: u32 = 3; +const SVG_ANGLE_TYPE_GRAD: u32 = 4; + +const SVG_PRESERVE_ASPECT_RATIO_UNKNOWN: u32 = 0; +const SVG_PRESERVE_ASPECT_RATIO_NONE: u32 = 1; +const SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MIN: u32 = 2; +const SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MIN: u32 = 3; +const SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MIN: u32 = 4; +const SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MID: u32 = 5; +const SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MID: u32 = 6; +const SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MID: u32 = 7; +const SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MAX: u32 = 8; +const SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MAX: u32 = 9; +const SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MAX: u32 = 10; + +const SVG_MEET_OR_SLICE_UNKNOWN: u32 = 0; +const SVG_MEET_OR_SLICE_MEET: u32 = 1; +const SVG_MEET_OR_SLICE_SLICE: u32 = 2; + +const SVG_MARKER_UNITS_UNKNOWN: u32 = 0; +const SVG_MARKER_UNITS_USER_SPACE_ON_USE: u32 = 1; +const SVG_MARKER_UNITS_STROKE_WIDTH: u32 = 2; + +const SVG_MARKER_ORIENT_UNKNOWN: u32 = 0; +const SVG_MARKER_ORIENT_AUTO: u32 = 1; +const SVG_MARKER_ORIENT_ANGLE: u32 = 2; +const SVG_MARKER_ORIENT_AUTO_START_REVERSE: u32 = 3; + const SVG_TRANSFORM_TYPE_UNKNOWN: u32 = 0; const SVG_TRANSFORM_TYPE_MATRIX: u32 = 1; const SVG_TRANSFORM_TYPE_TRANSLATE: u32 = 2; @@ -257,23 +459,863 @@ const SVG_SPREAD_METHOD_PAD: u32 = 1; const SVG_SPREAD_METHOD_REFLECT: u32 = 2; const SVG_SPREAD_METHOD_REPEAT: u32 = 3; +const SVG_COMPONENT_TRANSFER_TYPE_UNKNOWN: u32 = 0; +const SVG_COMPONENT_TRANSFER_TYPE_IDENTITY: u32 = 1; +const SVG_COMPONENT_TRANSFER_TYPE_TABLE: u32 = 2; +const SVG_COMPONENT_TRANSFER_TYPE_DISCRETE: u32 = 3; +const SVG_COMPONENT_TRANSFER_TYPE_LINEAR: u32 = 4; +const SVG_COMPONENT_TRANSFER_TYPE_GAMMA: u32 = 5; + +const SVG_FE_BLEND_MODE_UNKNOWN: u32 = 0; +const SVG_FE_BLEND_MODE_NORMAL: u32 = 1; +const SVG_FE_BLEND_MODE_MULTIPLY: u32 = 2; +const SVG_FE_BLEND_MODE_SCREEN: u32 = 3; +const SVG_FE_BLEND_MODE_DARKEN: u32 = 4; +const SVG_FE_BLEND_MODE_LIGHTEN: u32 = 5; +const SVG_FE_BLEND_MODE_OVERLAY: u32 = 6; +const SVG_FE_BLEND_MODE_COLOR_DODGE: u32 = 7; +const SVG_FE_BLEND_MODE_COLOR_BURN: u32 = 8; +const SVG_FE_BLEND_MODE_HARD_LIGHT: u32 = 9; +const SVG_FE_BLEND_MODE_SOFT_LIGHT: u32 = 10; +const SVG_FE_BLEND_MODE_DIFFERENCE: u32 = 11; +const SVG_FE_BLEND_MODE_EXCLUSION: u32 = 12; +const SVG_FE_BLEND_MODE_HUE: u32 = 13; +const SVG_FE_BLEND_MODE_SATURATION: u32 = 14; +const SVG_FE_BLEND_MODE_COLOR: u32 = 15; +const SVG_FE_BLEND_MODE_LUMINOSITY: u32 = 16; + +const SVG_FE_COLOR_MATRIX_TYPE_UNKNOWN: u32 = 0; +const SVG_FE_COLOR_MATRIX_TYPE_MATRIX: u32 = 1; +const SVG_FE_COLOR_MATRIX_TYPE_SATURATE: u32 = 2; +const SVG_FE_COLOR_MATRIX_TYPE_HUE_ROTATE: u32 = 3; +const SVG_FE_COLOR_MATRIX_TYPE_LUMINANCE_TO_ALPHA: u32 = 4; + +const SVG_FE_COMPOSITE_OPERATOR_UNKNOWN: u32 = 0; +const SVG_FE_COMPOSITE_OPERATOR_OVER: u32 = 1; +const SVG_FE_COMPOSITE_OPERATOR_IN: u32 = 2; +const SVG_FE_COMPOSITE_OPERATOR_OUT: u32 = 3; +const SVG_FE_COMPOSITE_OPERATOR_ATOP: u32 = 4; +const SVG_FE_COMPOSITE_OPERATOR_XOR: u32 = 5; +const SVG_FE_COMPOSITE_OPERATOR_LIGHTER: u32 = 6; +const SVG_FE_COMPOSITE_OPERATOR_ARITHMETIC: u32 = 7; + +const SVG_EDGE_MODE_UNKNOWN: u32 = 0; +const SVG_EDGE_MODE_DUPLICATE: u32 = 1; +const SVG_EDGE_MODE_WRAP: u32 = 2; +const SVG_EDGE_MODE_NONE: u32 = 3; + +const SVG_CHANNEL_UNKNOWN: u32 = 0; +const SVG_CHANNEL_R: u32 = 1; +const SVG_CHANNEL_G: u32 = 2; +const SVG_CHANNEL_B: u32 = 3; +const SVG_CHANNEL_A: u32 = 4; + +const SVG_MORPHOLOGY_OPERATOR_UNKNOWN: u32 = 0; +const SVG_MORPHOLOGY_OPERATOR_ERODE: u32 = 1; +const SVG_MORPHOLOGY_OPERATOR_DILATE: u32 = 2; + +const SVG_TURBULENCE_TYPE_UNKNOWN: u32 = 0; +const SVG_TURBULENCE_TYPE_FRACTAL_NOISE: u32 = 1; +const SVG_TURBULENCE_TYPE_TURBULENCE: u32 = 2; + +const SVG_STITCH_TYPE_UNKNOWN: u32 = 0; +const SVG_STITCH_TYPE_STITCH: u32 = 1; +const SVG_STITCH_TYPE_NO_STITCH: u32 = 2; + +const SVG_TEXT_PATH_METHOD_TYPE_UNKNOWN: u32 = 0; +const SVG_TEXT_PATH_METHOD_TYPE_ALIGN: u32 = 1; +const SVG_TEXT_PATH_METHOD_TYPE_STRETCH: u32 = 2; + +const SVG_TEXT_PATH_SPACING_TYPE_UNKNOWN: u32 = 0; +const SVG_TEXT_PATH_SPACING_TYPE_AUTO: u32 = 1; +const SVG_TEXT_PATH_SPACING_TYPE_EXACT: u32 = 2; + +const SVG_TEXT_PATH_SIDE_TYPE_UNKNOWN: u32 = 0; +const SVG_TEXT_PATH_SIDE_TYPE_LEFT: u32 = 1; +const SVG_TEXT_PATH_SIDE_TYPE_RIGHT: u32 = 2; + +const SVG_UNIT_TYPE_VALUES: &[(&str, u32)] = &[ + ("userSpaceOnUse", SVG_UNIT_TYPE_USER_SPACE_ON_USE), + ("objectBoundingBox", SVG_UNIT_TYPE_OBJECT_BOUNDING_BOX), +]; +const SVG_SPREAD_METHOD_VALUES: &[(&str, u32)] = &[ + ("pad", SVG_SPREAD_METHOD_PAD), + ("reflect", SVG_SPREAD_METHOD_REFLECT), + ("repeat", SVG_SPREAD_METHOD_REPEAT), +]; +const SVG_LENGTH_ADJUST_VALUES: &[(&str, u32)] = &[ + ("spacing", SVG_LENGTH_ADJUST_SPACING), + ("spacingAndGlyphs", SVG_LENGTH_ADJUST_SPACING_AND_GLYPHS), +]; +const SVG_MARKER_UNITS_VALUES: &[(&str, u32)] = &[ + ("userSpaceOnUse", SVG_MARKER_UNITS_USER_SPACE_ON_USE), + ("strokeWidth", SVG_MARKER_UNITS_STROKE_WIDTH), +]; +const SVG_COMPONENT_TRANSFER_TYPE_VALUES: &[(&str, u32)] = &[ + ("identity", SVG_COMPONENT_TRANSFER_TYPE_IDENTITY), + ("table", SVG_COMPONENT_TRANSFER_TYPE_TABLE), + ("discrete", SVG_COMPONENT_TRANSFER_TYPE_DISCRETE), + ("linear", SVG_COMPONENT_TRANSFER_TYPE_LINEAR), + ("gamma", SVG_COMPONENT_TRANSFER_TYPE_GAMMA), +]; +const SVG_FE_BLEND_MODE_VALUES: &[(&str, u32)] = &[ + ("normal", SVG_FE_BLEND_MODE_NORMAL), + ("multiply", SVG_FE_BLEND_MODE_MULTIPLY), + ("screen", SVG_FE_BLEND_MODE_SCREEN), + ("darken", SVG_FE_BLEND_MODE_DARKEN), + ("lighten", SVG_FE_BLEND_MODE_LIGHTEN), + ("overlay", SVG_FE_BLEND_MODE_OVERLAY), + ("color-dodge", SVG_FE_BLEND_MODE_COLOR_DODGE), + ("color-burn", SVG_FE_BLEND_MODE_COLOR_BURN), + ("hard-light", SVG_FE_BLEND_MODE_HARD_LIGHT), + ("soft-light", SVG_FE_BLEND_MODE_SOFT_LIGHT), + ("difference", SVG_FE_BLEND_MODE_DIFFERENCE), + ("exclusion", SVG_FE_BLEND_MODE_EXCLUSION), + ("hue", SVG_FE_BLEND_MODE_HUE), + ("saturation", SVG_FE_BLEND_MODE_SATURATION), + ("color", SVG_FE_BLEND_MODE_COLOR), + ("luminosity", SVG_FE_BLEND_MODE_LUMINOSITY), +]; +const SVG_FE_COLOR_MATRIX_TYPE_VALUES: &[(&str, u32)] = &[ + ("matrix", SVG_FE_COLOR_MATRIX_TYPE_MATRIX), + ("saturate", SVG_FE_COLOR_MATRIX_TYPE_SATURATE), + ("hueRotate", SVG_FE_COLOR_MATRIX_TYPE_HUE_ROTATE), + ( + "luminanceToAlpha", + SVG_FE_COLOR_MATRIX_TYPE_LUMINANCE_TO_ALPHA, + ), +]; +const SVG_FE_COMPOSITE_OPERATOR_VALUES: &[(&str, u32)] = &[ + ("over", SVG_FE_COMPOSITE_OPERATOR_OVER), + ("in", SVG_FE_COMPOSITE_OPERATOR_IN), + ("out", SVG_FE_COMPOSITE_OPERATOR_OUT), + ("atop", SVG_FE_COMPOSITE_OPERATOR_ATOP), + ("xor", SVG_FE_COMPOSITE_OPERATOR_XOR), + ("lighter", SVG_FE_COMPOSITE_OPERATOR_LIGHTER), + ("arithmetic", SVG_FE_COMPOSITE_OPERATOR_ARITHMETIC), +]; +const SVG_EDGE_MODE_VALUES: &[(&str, u32)] = &[ + ("duplicate", SVG_EDGE_MODE_DUPLICATE), + ("wrap", SVG_EDGE_MODE_WRAP), + ("none", SVG_EDGE_MODE_NONE), +]; +const SVG_CHANNEL_VALUES: &[(&str, u32)] = &[ + ("R", SVG_CHANNEL_R), + ("G", SVG_CHANNEL_G), + ("B", SVG_CHANNEL_B), + ("A", SVG_CHANNEL_A), +]; +const SVG_MORPHOLOGY_OPERATOR_VALUES: &[(&str, u32)] = &[ + ("erode", SVG_MORPHOLOGY_OPERATOR_ERODE), + ("dilate", SVG_MORPHOLOGY_OPERATOR_DILATE), +]; +const SVG_TURBULENCE_TYPE_VALUES: &[(&str, u32)] = &[ + ("fractalNoise", SVG_TURBULENCE_TYPE_FRACTAL_NOISE), + ("turbulence", SVG_TURBULENCE_TYPE_TURBULENCE), +]; +const SVG_STITCH_TYPE_VALUES: &[(&str, u32)] = &[ + ("stitch", SVG_STITCH_TYPE_STITCH), + ("noStitch", SVG_STITCH_TYPE_NO_STITCH), +]; +const SVG_TEXT_PATH_METHOD_TYPE_VALUES: &[(&str, u32)] = &[ + ("align", SVG_TEXT_PATH_METHOD_TYPE_ALIGN), + ("stretch", SVG_TEXT_PATH_METHOD_TYPE_STRETCH), +]; +const SVG_TEXT_PATH_SPACING_TYPE_VALUES: &[(&str, u32)] = &[ + ("auto", SVG_TEXT_PATH_SPACING_TYPE_AUTO), + ("exact", SVG_TEXT_PATH_SPACING_TYPE_EXACT), +]; +const SVG_TEXT_PATH_SIDE_TYPE_VALUES: &[(&str, u32)] = &[ + ("left", SVG_TEXT_PATH_SIDE_TYPE_LEFT), + ("right", SVG_TEXT_PATH_SIDE_TYPE_RIGHT), +]; + +const SVG_ANIMATED_ENUMERATION_PROPERTIES: &[SvgAnimatedEnumerationProperty] = &[ + SvgAnimatedEnumerationProperty { + index: 0, + attribute: "clipPathUnits", + cache_slot: SVG_CLIP_PATH_UNITS_SLOT, + initial_value: SVG_UNIT_TYPE_USER_SPACE_ON_USE, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_UNIT_TYPE_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 1, + attribute: "filterUnits", + cache_slot: SVG_FILTER_UNITS_SLOT, + initial_value: SVG_UNIT_TYPE_OBJECT_BOUNDING_BOX, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_UNIT_TYPE_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 2, + attribute: "primitiveUnits", + cache_slot: SVG_PRIMITIVE_UNITS_SLOT, + initial_value: SVG_UNIT_TYPE_USER_SPACE_ON_USE, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_UNIT_TYPE_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 3, + attribute: "gradientUnits", + cache_slot: SVG_GRADIENT_UNITS_SLOT, + initial_value: SVG_UNIT_TYPE_OBJECT_BOUNDING_BOX, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_UNIT_TYPE_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 4, + attribute: "spreadMethod", + cache_slot: SVG_GRADIENT_SPREAD_METHOD_SLOT, + initial_value: SVG_SPREAD_METHOD_PAD, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_SPREAD_METHOD_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 5, + attribute: "maskUnits", + cache_slot: SVG_MASK_UNITS_SLOT, + initial_value: SVG_UNIT_TYPE_OBJECT_BOUNDING_BOX, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_UNIT_TYPE_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 6, + attribute: "maskContentUnits", + cache_slot: SVG_MASK_CONTENT_UNITS_SLOT, + initial_value: SVG_UNIT_TYPE_USER_SPACE_ON_USE, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_UNIT_TYPE_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 7, + attribute: "patternUnits", + cache_slot: SVG_PATTERN_UNITS_SLOT, + initial_value: SVG_UNIT_TYPE_OBJECT_BOUNDING_BOX, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_UNIT_TYPE_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 8, + attribute: "patternContentUnits", + cache_slot: SVG_PATTERN_CONTENT_UNITS_SLOT, + initial_value: SVG_UNIT_TYPE_USER_SPACE_ON_USE, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_UNIT_TYPE_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 9, + attribute: "lengthAdjust", + cache_slot: SVG_TEXT_CONTENT_LENGTH_ADJUST_SLOT, + initial_value: SVG_LENGTH_ADJUST_SPACING, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_LENGTH_ADJUST_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 10, + attribute: "markerUnits", + cache_slot: SVG_MARKER_UNITS_SLOT, + initial_value: SVG_MARKER_UNITS_STROKE_WIDTH, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_MARKER_UNITS_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 11, + attribute: "orient", + cache_slot: SVG_MARKER_ORIENT_TYPE_SLOT, + initial_value: SVG_MARKER_ORIENT_ANGLE, + kind: SvgAnimatedEnumerationKind::MarkerOrient, + }, + SvgAnimatedEnumerationProperty { + index: 12, + attribute: "type", + cache_slot: SVG_COMPONENT_TRANSFER_TYPE_SLOT, + initial_value: SVG_COMPONENT_TRANSFER_TYPE_IDENTITY, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_COMPONENT_TRANSFER_TYPE_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 13, + attribute: "mode", + cache_slot: SVG_FE_BLEND_MODE_SLOT, + initial_value: SVG_FE_BLEND_MODE_NORMAL, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_FE_BLEND_MODE_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 14, + attribute: "type", + cache_slot: SVG_FE_COLOR_MATRIX_TYPE_SLOT, + initial_value: SVG_FE_COLOR_MATRIX_TYPE_MATRIX, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_FE_COLOR_MATRIX_TYPE_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 15, + attribute: "operator", + cache_slot: SVG_FE_COMPOSITE_OPERATOR_SLOT, + initial_value: SVG_FE_COMPOSITE_OPERATOR_OVER, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_FE_COMPOSITE_OPERATOR_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 16, + attribute: "edgeMode", + cache_slot: SVG_FE_CONVOLVE_MATRIX_EDGE_MODE_SLOT, + initial_value: SVG_EDGE_MODE_DUPLICATE, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_EDGE_MODE_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 17, + attribute: "xChannelSelector", + cache_slot: SVG_FE_DISPLACEMENT_MAP_X_CHANNEL_SLOT, + initial_value: SVG_CHANNEL_A, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_CHANNEL_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 18, + attribute: "yChannelSelector", + cache_slot: SVG_FE_DISPLACEMENT_MAP_Y_CHANNEL_SLOT, + initial_value: SVG_CHANNEL_A, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_CHANNEL_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 19, + attribute: "operator", + cache_slot: SVG_FE_MORPHOLOGY_OPERATOR_SLOT, + initial_value: SVG_MORPHOLOGY_OPERATOR_ERODE, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_MORPHOLOGY_OPERATOR_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 20, + attribute: "stitchTiles", + cache_slot: SVG_FE_TURBULENCE_STITCH_TILES_SLOT, + initial_value: SVG_STITCH_TYPE_NO_STITCH, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_STITCH_TYPE_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 21, + attribute: "type", + cache_slot: SVG_FE_TURBULENCE_TYPE_SLOT, + initial_value: SVG_TURBULENCE_TYPE_TURBULENCE, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_TURBULENCE_TYPE_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 22, + attribute: "method", + cache_slot: SVG_TEXT_PATH_METHOD_SLOT, + initial_value: SVG_TEXT_PATH_METHOD_TYPE_ALIGN, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_TEXT_PATH_METHOD_TYPE_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 23, + attribute: "spacing", + cache_slot: SVG_TEXT_PATH_SPACING_SLOT, + initial_value: SVG_TEXT_PATH_SPACING_TYPE_EXACT, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_TEXT_PATH_SPACING_TYPE_VALUES), + }, + SvgAnimatedEnumerationProperty { + index: 24, + attribute: "side", + cache_slot: SVG_TEXT_PATH_SIDE_SLOT, + initial_value: SVG_TEXT_PATH_SIDE_TYPE_LEFT, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_TEXT_PATH_SIDE_TYPE_VALUES), + }, +]; + +const fn svg_animated_number_property( + index: usize, + interface: &'static str, + name: &'static str, + attribute: &'static str, + initial_value: f64, + component: SvgAnimatedNumberComponent, +) -> SvgAnimatedNumberProperty { + SvgAnimatedNumberProperty { + index, + interface, + name, + attribute, + initial_value, + component, + } +} + +const SVG_ANIMATED_NUMBER_PROPERTIES: &[SvgAnimatedNumberProperty] = &[ + svg_animated_number_property( + 0, + "SVGComponentTransferFunctionElement", + "slope", + "slope", + 1.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 1, + "SVGComponentTransferFunctionElement", + "intercept", + "intercept", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 2, + "SVGComponentTransferFunctionElement", + "amplitude", + "amplitude", + 1.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 3, + "SVGComponentTransferFunctionElement", + "exponent", + "exponent", + 1.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 4, + "SVGComponentTransferFunctionElement", + "offset", + "offset", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 5, + "SVGFECompositeElement", + "k1", + "k1", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 6, + "SVGFECompositeElement", + "k2", + "k2", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 7, + "SVGFECompositeElement", + "k3", + "k3", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 8, + "SVGFECompositeElement", + "k4", + "k4", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 9, + "SVGFEConvolveMatrixElement", + "divisor", + "divisor", + 1.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 10, + "SVGFEConvolveMatrixElement", + "bias", + "bias", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 11, + "SVGFEConvolveMatrixElement", + "kernelUnitLengthX", + "kernelUnitLength", + 0.0, + SvgAnimatedNumberComponent::PairFirst, + ), + svg_animated_number_property( + 12, + "SVGFEConvolveMatrixElement", + "kernelUnitLengthY", + "kernelUnitLength", + 0.0, + SvgAnimatedNumberComponent::PairSecondOrFirst, + ), + svg_animated_number_property( + 13, + "SVGFEDiffuseLightingElement", + "surfaceScale", + "surfaceScale", + 1.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 14, + "SVGFEDiffuseLightingElement", + "diffuseConstant", + "diffuseConstant", + 1.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 15, + "SVGFEDiffuseLightingElement", + "kernelUnitLengthX", + "kernelUnitLength", + 0.0, + SvgAnimatedNumberComponent::PairFirst, + ), + svg_animated_number_property( + 16, + "SVGFEDiffuseLightingElement", + "kernelUnitLengthY", + "kernelUnitLength", + 0.0, + SvgAnimatedNumberComponent::PairSecondOrFirst, + ), + svg_animated_number_property( + 17, + "SVGFEDisplacementMapElement", + "scale", + "scale", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 18, + "SVGFEDistantLightElement", + "azimuth", + "azimuth", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 19, + "SVGFEDistantLightElement", + "elevation", + "elevation", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 20, + "SVGFEDropShadowElement", + "dx", + "dx", + 2.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 21, + "SVGFEDropShadowElement", + "dy", + "dy", + 2.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 22, + "SVGFEDropShadowElement", + "stdDeviationX", + "stdDeviation", + 2.0, + SvgAnimatedNumberComponent::PairFirst, + ), + svg_animated_number_property( + 23, + "SVGFEDropShadowElement", + "stdDeviationY", + "stdDeviation", + 2.0, + SvgAnimatedNumberComponent::PairSecondOrFirst, + ), + svg_animated_number_property( + 24, + "SVGFEGaussianBlurElement", + "stdDeviationX", + "stdDeviation", + 0.0, + SvgAnimatedNumberComponent::PairFirst, + ), + svg_animated_number_property( + 25, + "SVGFEGaussianBlurElement", + "stdDeviationY", + "stdDeviation", + 0.0, + SvgAnimatedNumberComponent::PairSecondOrFirst, + ), + svg_animated_number_property( + 26, + "SVGFEMorphologyElement", + "radiusX", + "radius", + 0.0, + SvgAnimatedNumberComponent::PairFirst, + ), + svg_animated_number_property( + 27, + "SVGFEMorphologyElement", + "radiusY", + "radius", + 0.0, + SvgAnimatedNumberComponent::PairSecondOrFirst, + ), + svg_animated_number_property( + 28, + "SVGFEOffsetElement", + "dx", + "dx", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 29, + "SVGFEOffsetElement", + "dy", + "dy", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 30, + "SVGFEPointLightElement", + "x", + "x", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 31, + "SVGFEPointLightElement", + "y", + "y", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 32, + "SVGFEPointLightElement", + "z", + "z", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 33, + "SVGFESpecularLightingElement", + "surfaceScale", + "surfaceScale", + 1.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 34, + "SVGFESpecularLightingElement", + "specularConstant", + "specularConstant", + 1.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 35, + "SVGFESpecularLightingElement", + "specularExponent", + "specularExponent", + 1.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 36, + "SVGFESpecularLightingElement", + "kernelUnitLengthX", + "kernelUnitLength", + 0.0, + SvgAnimatedNumberComponent::PairFirst, + ), + svg_animated_number_property( + 37, + "SVGFESpecularLightingElement", + "kernelUnitLengthY", + "kernelUnitLength", + 0.0, + SvgAnimatedNumberComponent::PairSecondOrFirst, + ), + svg_animated_number_property( + 38, + "SVGFESpotLightElement", + "x", + "x", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 39, + "SVGFESpotLightElement", + "y", + "y", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 40, + "SVGFESpotLightElement", + "z", + "z", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 41, + "SVGFESpotLightElement", + "pointsAtX", + "pointsAtX", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 42, + "SVGFESpotLightElement", + "pointsAtY", + "pointsAtY", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 43, + "SVGFESpotLightElement", + "pointsAtZ", + "pointsAtZ", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 44, + "SVGFESpotLightElement", + "specularExponent", + "specularExponent", + 1.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 45, + "SVGFESpotLightElement", + "limitingConeAngle", + "limitingConeAngle", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 46, + "SVGFETurbulenceElement", + "baseFrequencyX", + "baseFrequency", + 0.0, + SvgAnimatedNumberComponent::PairFirst, + ), + svg_animated_number_property( + 47, + "SVGFETurbulenceElement", + "baseFrequencyY", + "baseFrequency", + 0.0, + SvgAnimatedNumberComponent::PairSecondOrFirst, + ), + svg_animated_number_property( + 48, + "SVGFETurbulenceElement", + "seed", + "seed", + 0.0, + SvgAnimatedNumberComponent::Scalar, + ), + svg_animated_number_property( + 49, + "SVGStopElement", + "offset", + "offset", + 0.0, + SvgAnimatedNumberComponent::NumberOrPercentage, + ), +]; + +const SVG_ANIMATED_INTEGER_PROPERTIES: &[SvgAnimatedIntegerProperty] = &[ + SvgAnimatedIntegerProperty { + index: 0, + interface: "SVGFEConvolveMatrixElement", + local_name: "feConvolveMatrix", + name: "orderX", + attribute: "order", + cache_slot: SVG_FE_CONVOLVE_MATRIX_ORDER_X_SLOT, + initial_value: 3, + component: SvgAnimatedIntegerComponent::PairFirst, + }, + SvgAnimatedIntegerProperty { + index: 1, + interface: "SVGFEConvolveMatrixElement", + local_name: "feConvolveMatrix", + name: "orderY", + attribute: "order", + cache_slot: SVG_FE_CONVOLVE_MATRIX_ORDER_Y_SLOT, + initial_value: 3, + component: SvgAnimatedIntegerComponent::PairSecondOrFirst, + }, + SvgAnimatedIntegerProperty { + index: 2, + interface: "SVGFEConvolveMatrixElement", + local_name: "feConvolveMatrix", + name: "targetX", + attribute: "targetX", + cache_slot: SVG_FE_CONVOLVE_MATRIX_TARGET_X_SLOT, + initial_value: 0, + component: SvgAnimatedIntegerComponent::Scalar, + }, + SvgAnimatedIntegerProperty { + index: 3, + interface: "SVGFEConvolveMatrixElement", + local_name: "feConvolveMatrix", + name: "targetY", + attribute: "targetY", + cache_slot: SVG_FE_CONVOLVE_MATRIX_TARGET_Y_SLOT, + initial_value: 0, + component: SvgAnimatedIntegerComponent::Scalar, + }, + SvgAnimatedIntegerProperty { + index: 4, + interface: "SVGFETurbulenceElement", + local_name: "feTurbulence", + name: "numOctaves", + attribute: "numOctaves", + cache_slot: SVG_FE_TURBULENCE_NUM_OCTAVES_SLOT, + initial_value: 1, + component: SvgAnimatedIntegerComponent::Scalar, + }, +]; + pub(in crate::context_bootstrap) fn install_svg_template_bindings<'s>( scope: &mut v8::PinScope<'s, '_, ()>, template: v8::Local<'s, v8::FunctionTemplate>, name: &str, ) { bindings::install_svg_element_accessor_bindings(scope, template, name); + animation::install_bindings(scope, template, name); + filters::install_bindings(scope, template, name); + bindings::install_svg_enumeration_constant_bindings(scope, template, name); match name { "SVGLength" => bindings::install_svg_length_bindings(scope, template), + "SVGAngle" => bindings::install_svg_angle_bindings(scope, template), "SVGNumber" => bindings::install_svg_number_bindings(scope, template), "SVGRect" => rect::install_bindings(scope, template), + "SVGAnimatedString" => bindings::install_svg_animated_string_bindings(scope, template), + "SVGAnimatedBoolean" => bindings::install_svg_animated_boolean_bindings(scope, template), "SVGAnimatedLength" => bindings::install_svg_animated_length_bindings(scope, template), + "SVGAnimatedAngle" => bindings::install_svg_animated_angle_bindings(scope, template), + "SVGAnimatedRect" => bindings::install_svg_animated_rect_bindings(scope, template), + "SVGPreserveAspectRatio" => { + bindings::install_svg_preserve_aspect_ratio_bindings(scope, template) + } + "SVGAnimatedPreserveAspectRatio" => { + bindings::install_svg_animated_preserve_aspect_ratio_bindings(scope, template) + } "SVGLengthList" => bindings::install_svg_length_list_bindings(scope, template), "SVGAnimatedLengthList" => { bindings::install_svg_animated_length_list_bindings(scope, template) } "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) } @@ -293,7 +1335,15 @@ pub(in crate::context_bootstrap) fn install_svg_template_bindings<'s>( "SVGTextContentElement" => { bindings::install_svg_text_content_element_bindings(scope, template) } + "SVGMarkerElement" => bindings::install_svg_marker_element_bindings(scope, template), "SVGSVGElement" => bindings::install_svg_svg_element_bindings(scope, template), _ => {} } } + +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/context_bootstrap/svg_runtime/rect.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/rect.rs index 674b851734..171c59fe05 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/rect.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/rect.rs @@ -1,11 +1,11 @@ -//! Detached SVGRect values created by SVGSVGElement.createSVGRect(). +//! Native SVGRect values, detached or bound to SVGAnimatedRect viewBox values. //! -//! JSXGraph uses this method to detect SVG support. Returning a DOMRect would +//! JSXGraph uses createSVGRect() to detect SVG support. Returning a DOMRect would //! pass that check but expose the wrong interface and double (not float) fields. use crate::web_api_interfaces; use crate::{ - native_bridge::node_runtime_and_handle_from_object_or_detached, + native_bridge::{node_runtime_and_handle_from_object_or_detached, throw_dom_exception}, util::{callback_data_index_value, callback_data_item, get_private_value, set_private_value}, webidl, }; @@ -15,6 +15,8 @@ const X: &str = "__moliSvgRectX"; const Y: &str = "__moliSvgRectY"; const WIDTH: &str = "__moliSvgRectWidth"; const HEIGHT: &str = "__moliSvgRectHeight"; +const VIEW_BOX_OWNER: &str = "__moliSvgRectViewBoxOwner"; +const READ_ONLY: &str = "__moliSvgRectReadOnly"; const FIELDS: &[(&str, &str)] = &[("x", X), ("y", Y), ("width", WIDTH), ("height", HEIGHT)]; #[derive(WebApiObject)] @@ -82,6 +84,60 @@ pub(super) fn create_svg_rect<'s>( rv.set(rect.into()); } +pub(super) fn build_svg_view_box_rect<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + values: [f64; 4], + read_only: bool, +) -> v8::Local<'s, v8::Object> { + let [x, y, width, height] = values.map(|value| f64::from(value as f32)); + let rect = SvgRectObjectDeclaration::new(x, y, width, height) + .bind(scope) + .expect("SVGRect declaration should bind"); + set_private_value(scope, rect, VIEW_BOX_OWNER, owner.into()); + set_private_value( + scope, + rect, + READ_ONLY, + v8::Boolean::new(scope, read_only).into(), + ); + rect +} + +pub(super) fn svg_view_box_rect_owner<'s>( + scope: &mut v8::PinScope<'s, '_>, + rect: v8::Local<'s, v8::Object>, +) -> Option> { + get_private_value(scope, rect, VIEW_BOX_OWNER) + .and_then(|value| v8::Local::::try_from(value).ok()) +} + +pub(super) fn svg_view_box_rect_values<'s>( + scope: &mut v8::PinScope<'s, '_>, + rect: v8::Local<'s, v8::Object>, +) -> [f64; 4] { + [X, Y, WIDTH, HEIGHT].map(|slot| { + get_private_value(scope, rect, slot) + .and_then(|value| v8::Local::::try_from(value).ok()) + .map_or(0.0, |value| value.value()) + }) +} + +pub(super) fn set_svg_view_box_rect_values<'s>( + scope: &mut v8::PinScope<'s, '_>, + rect: v8::Local<'s, v8::Object>, + values: [f64; 4], +) { + for (slot, value) in [X, Y, WIDTH, HEIGHT].into_iter().zip(values) { + set_private_value( + scope, + rect, + slot, + v8::Number::new(scope, f64::from(value as f32)).into(), + ); + } +} + fn field_for_receiver<'s>( scope: &mut v8::PinScope<'s, '_>, args: &v8::FunctionCallbackArguments<'s>, @@ -101,6 +157,7 @@ fn get_field<'s>( let Some((_, slot)) = field_for_receiver(scope, &args) else { return; }; + super::builders::sync_svg_view_box_rect_from_owner(scope, args.this()); if let Some(value) = get_private_value(scope, args.this(), slot) { rv.set(value); } @@ -114,6 +171,17 @@ fn set_field<'s>( let Some((name, slot)) = field_for_receiver(scope, &args) else { return; }; + if get_private_value(scope, args.this(), READ_ONLY) + .is_some_and(|value| value.boolean_value(scope)) + { + throw_dom_exception( + scope, + "NoModificationAllowedError", + 7, + "The SVG rectangle is read-only.", + ); + return; + } let value = match webidl::convert::( scope, args.get(0), @@ -131,10 +199,14 @@ fn set_field<'s>( webidl::throw_type_error(scope, "SVGRect value is outside the finite float range."); return; } + // ToNumber may re-enter JS and change the owner's viewBox. Preserve those + // changes to the other fields before applying this one-field mutation. + super::builders::sync_svg_view_box_rect_from_owner(scope, args.this()); set_private_value( scope, args.this(), slot, v8::Number::new(scope, f64::from(value)).into(), ); + super::builders::reflect_svg_view_box_rect_mutation(scope, args.this()); } diff --git a/moli-renderer-v8/src/host/events.rs b/moli-renderer-v8/src/host/events.rs index f29ae25359..bbf87d1ce6 100644 --- a/moli-renderer-v8/src/host/events.rs +++ b/moli-renderer-v8/src/host/events.rs @@ -226,7 +226,11 @@ fn invoke_event_handler_property<'s>( event_type: &str, event: v8::Local<'s, v8::Object>, ) { - let handler_name = format!("on{event_type}"); + let Some(handler_name) = + crate::native_bridge::element::event_handler_content_attribute_name(event_type) + else { + return; + }; let (callback_id, temporary) = match registry.event_handler_property_value(target, event_type) { Some(EventHandlerPropertyValue::Callback(callback_id)) => (callback_id, false), Some(EventHandlerPropertyValue::Null) => { diff --git a/moli-renderer-v8/src/native_bridge/document.rs b/moli-renderer-v8/src/native_bridge/document.rs index 4a6f6becb8..af2dc7e255 100644 --- a/moli-renderer-v8/src/native_bridge/document.rs +++ b/moli-renderer-v8/src/native_bridge/document.rs @@ -297,6 +297,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"; pub(crate) const MATHML_NS: &str = "http://www.w3.org/1998/Math/MathML"; #[derive(WebApiFunctionTemplate)] diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs index d28cbc9463..7baab27cf7 100644 --- a/moli-renderer-v8/src/native_bridge/element.rs +++ b/moli-renderer-v8/src/native_bridge/element.rs @@ -100,6 +100,7 @@ mod trusted_types; pub(crate) use script_execution::{ inline_script_source_for_execution, prepare_inline_classic_frame_script_job_for_execution, }; +pub(crate) use trusted_types::set_svg_animated_string_base_value; use trusted_types::{ TrustedScriptElementSink, trusted_script_element_sink_string, trusted_script_url_sink_string, }; @@ -259,7 +260,8 @@ pub(crate) use event_handlers::compile_window_body_onmessageerror_attribute; use event_handlers::install_global_event_handler_template_bindings as install_global_event_handler_templates_for_owner; pub(crate) use event_handlers::{ EventAttributeHandlerScope, GlobalEventHandlerOwner, compile_event_attribute_handler_for_owner, - initialize_parser_inserted_body_window_event_handlers, + event_handler_content_attribute_name, initialize_parser_inserted_body_window_event_handlers, + node_event_handler_getter_function, node_event_handler_setter_function, }; use event_handlers::{ body_onerror_getter_function, body_onerror_setter_function, body_onload_getter_function, diff --git a/moli-renderer-v8/src/native_bridge/element/event_handlers/generic.rs b/moli-renderer-v8/src/native_bridge/element/event_handlers/generic.rs index 8515680b39..9b14226d08 100644 --- a/moli-renderer-v8/src/native_bridge/element/event_handlers/generic.rs +++ b/moli-renderer-v8/src/native_bridge/element/event_handlers/generic.rs @@ -508,6 +508,12 @@ fn event_handler_slot_name(name: &str) -> String { fn event_handler_event_type(name: &str) -> Option<&str> { name.strip_prefix("on") .filter(|event_type| !event_type.is_empty()) + .map(|event_type| match event_type { + "begin" => "beginEvent", + "end" => "endEvent", + "repeat" => "repeatEvent", + event_type => event_type, + }) } fn legacy_lenient_this_event_handler(name: &str) -> bool { @@ -537,3 +543,14 @@ pub(super) fn invalidate_node_event_attribute_handler( }; runtime.clear_event_handler_property(EventTargetHandle::Node(handle), event_type); } + +pub(crate) fn event_handler_content_attribute_name(event_type: &str) -> Option { + let event_type = match event_type { + "begin" | "end" | "repeat" => return None, + "beginEvent" => "begin", + "endEvent" => "end", + "repeatEvent" => "repeat", + event_type => event_type, + }; + Some(format!("on{event_type}")) +} diff --git a/moli-renderer-v8/src/native_bridge/element/event_handlers/mod.rs b/moli-renderer-v8/src/native_bridge/element/event_handlers/mod.rs index fd8dcb2303..9bea791b87 100644 --- a/moli-renderer-v8/src/native_bridge/element/event_handlers/mod.rs +++ b/moli-renderer-v8/src/native_bridge/element/event_handlers/mod.rs @@ -12,7 +12,11 @@ pub(crate) use body_window::compile_window_body_onmessageerror_attribute; pub(super) use body_window::{ body_onmessageerror_getter_function, body_onmessageerror_setter_function, }; -pub(crate) use generic::{GlobalEventHandlerOwner, install_global_event_handler_template_bindings}; +pub(crate) use generic::{ + GlobalEventHandlerOwner, event_handler_content_attribute_name, + install_global_event_handler_template_bindings, node_event_handler_getter_function, + node_event_handler_setter_function, +}; pub(crate) use shared::{EventAttributeHandlerScope, compile_event_attribute_handler_for_owner}; fn is_body_or_frameset_element( 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 2872ce2f48..cf08ac435e 100644 --- a/moli-renderer-v8/src/native_bridge/element/trusted_types.rs +++ b/moli-renderer-v8/src/native_bridge/element/trusted_types.rs @@ -156,3 +156,88 @@ pub(crate) fn trusted_script_source_for_execution( scope, source, sink, ) } + +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>, + attribute: &str, + value: v8::Local<'s, v8::Value>, +) -> Option { + let (runtime_ptr, handle) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner).ok()?; + let script_href = { + let element = unsafe { &*runtime_ptr } + .dom_host() + .node(handle)? + .as_element()?; + element.namespace() == "http://www.w3.org/2000/svg" + && element.local_name() == "script" + && attribute == "href" + }; + let value = if script_href { + let requirements = unsafe { &*runtime_ptr }.trusted_types_for_script_requirements(scope); + crate::context_bootstrap::trusted_script_url_string_or_throw( + scope, + value, + requirements, + "SVGScriptElement href", + "baseVal", + )? + } else { + match crate::webidl::convert::( + scope, + value, + crate::webidl::Context::member("SVGAnimatedString", "baseVal"), + ) { + Ok(value) => value.0, + Err(error) => { + crate::webidl::throw_error(scope, &error); + return None; + } + } + }; + let (runtime_ptr, handle) = + 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); + 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/browser_api/trusted_types.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs index 91be9d465f..b0b0cdd870 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs @@ -1606,3 +1606,114 @@ fn service_worker_register_converts_options_before_the_default_policy() { ]) ); } + +#[test] +fn svg_script_href_base_val_uses_the_owner_reflected_trusted_script_url_sink() { + let mut vm = new_storage_test_vm("https://svg-script-href-trusted-types.test/"); + vm.set_response_content_security_policies(&["require-trusted-types-for 'script'".to_owned()]); + + let result = vm + .eval( + r#" +(() => { + const violations = []; + document.addEventListener("securitypolicyviolation", event => { + violations.push({ + blockedURI: event.blockedURI, + effectiveDirective: event.effectiveDirective, + sample: event.sample + }); + }); + globalThis.__svgScriptHrefViolations = violations; + + const svgScript = document.createElementNS("http://www.w3.org/2000/svg", "script"); + const href = svgScript.href; + let illegalConstructor = false; + try { + new SVGAnimatedString(); + } catch (error) { + illegalConstructor = error instanceof TypeError; + } + let plainRejected = false; + try { + href.baseVal = "plain.js"; + } catch (error) { + plainRejected = error instanceof TypeError; + } + const rejectedAttribute = svgScript.getAttribute("href"); + + const explicit = trustedTypes.createPolicy("svg-script-href", { + createScriptURL: value => value + }); + href.baseVal = explicit.createScriptURL("trusted.js"); + const trustedValues = [ + svgScript.getAttribute("href"), + href.baseVal, + href.animVal + ]; + svgScript.setAttribute("href", explicit.createScriptURL("attribute.js")); + const externallySynced = [href.baseVal, href.animVal]; + const className = svgScript.className; + className.baseVal = explicit.createScriptURL("trusted-class"); + const use = document.createElementNS("http://www.w3.org/2000/svg", "use"); + const useHref = use.href; + useHref.baseVal = explicit.createScriptURL("trusted-use"); + + const defaultCalls = []; + trustedTypes.createPolicy("default", { + createScriptURL: (value, type, sink) => { + defaultCalls.push([value, type, sink]); + return `safe-${value}`; + } + }); + href.baseVal = "default-input"; + className.baseVal = "plain-class"; + useHref.baseVal = "plain-use"; + + return JSON.stringify({ + shape: [ + href instanceof SVGAnimatedString, + Object.prototype.toString.call(href), + svgScript.href === href, + illegalConstructor + ], + plainRejected, + rejectedAttribute, + trustedValues, + externallySynced, + ordinaryAnimatedStrings: [ + className instanceof SVGAnimatedString, + svgScript.className === className, + className.baseVal, + className.animVal, + svgScript.getAttribute("class"), + useHref instanceof SVGAnimatedString, + use.href === useHref, + useHref.baseVal, + useHref.animVal, + use.getAttribute("href") + ], + defaulted: [svgScript.getAttribute("href"), href.baseVal, href.animVal], + defaultCalls, + violations + }); +})() +"#, + ) + .expect("SVGScriptElement href Trusted Types sink probe should evaluate"); + + assert_eq!( + result, + r#"{"shape":[true,"[object SVGAnimatedString]",true,true],"plainRejected":true,"rejectedAttribute":null,"trustedValues":["trusted.js","trusted.js","trusted.js"],"externallySynced":["attribute.js","attribute.js"],"ordinaryAnimatedStrings":[true,true,"plain-class","plain-class","plain-class",true,true,"plain-use","plain-use","plain-use"],"defaulted":["safe-default-input","safe-default-input","safe-default-input"],"defaultCalls":[["default-input","TrustedScriptURL","SVGScriptElement href"]],"violations":[]}"# + ); + + assert_eq!( + drain_pre_domcontentloaded_non_script_page_tasks_for_test(&mut vm), + 1 + ); + assert_eq!( + vm.eval("JSON.stringify(globalThis.__svgScriptHrefViolations)") + .expect("queued SVGScriptElement href violation should be observable"), + r#"[{"blockedURI":"trusted-types-sink","effectiveDirective":"require-trusted-types-for","sample":"SVGScriptElement href|plain.js"}]"# + ); +} 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 8f8c98dce2..5027448e7e 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 @@ -2728,3 +2728,236 @@ fn detached_plain_document_all_matches_chromium_htmldda_surface() { r#"{"ownAll":false,"protoGetter":"function","allType":"undefined","loose":true,"strict":false,"bool":false,"string":"[object HTMLAllCollection]","tag":"[object HTMLAllCollection]","length":0,"noArgNull":true,"itemZeroNull":true,"itemMissNull":true,"itemMethodNull":true,"namedMethodNull":true}"# ); } + +#[test] +fn svg_value_lists_enforce_item_types_indices_and_read_only_anim_values() { + let mut vm = new_storage_test_vm("https://svg-value-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 SVG_NS = "http://www.w3.org/2000/svg"; + const text = document.createElementNS(SVG_NS, "text"); + const svg = document.createElementNS(SVG_NS, "svg"); + text.setAttribute("x", "10 20"); + text.setAttribute("rotate", "15 30"); + + const lengths = text.x; + const lengthBase = lengths.baseVal; + const lengthAnim = lengths.animVal; + const numbers = text.rotate; + const numberBase = numbers.baseVal; + const numberAnim = numbers.animVal; + const invalidItems = [30, "invalid", text, null]; + + for (const item of invalidItems) { + assert(errorName(() => lengthBase.initialize(item)) === "TypeError", + "SVGLengthList.initialize item type"); + assert(errorName(() => lengthBase.insertItemBefore(item, 0)) === "TypeError", + "SVGLengthList.insertItemBefore item type"); + assert(errorName(() => lengthBase.replaceItem(item, 0)) === "TypeError", + "SVGLengthList.replaceItem item type"); + assert(errorName(() => lengthBase.appendItem(item)) === "TypeError", + "SVGLengthList.appendItem item type"); + assert(errorName(() => { lengthBase[0] = item; }) === "TypeError", + "SVGLengthList indexed setter item type"); + } + + const length = svg.createSVGLength(); + length.value = 42; + lengthBase[0] = length; + assert(lengthBase[0] === length, "SVGLengthList indexed getter"); + assert(text.getAttribute("x") === "42 20", "SVGLengthList indexed reflection"); + + const number = svg.createSVGNumber(); + number.value = 7; + numberBase[1] = number; + assert(numberBase[1] === number, "SVGNumberList indexed getter"); + assert(text.getAttribute("rotate") === "15 7", "SVGNumberList indexed reflection"); + assert(errorName(() => lengthBase.appendItem(number)) === "TypeError", + "SVGLengthList rejects SVGNumber"); + assert(errorName(() => numberBase.appendItem(length)) === "TypeError", + "SVGNumberList rejects SVGLength"); + + text.setAttribute("x", "1 2 3"); + assert(lengthBase.length === 3 && lengthBase[2].value === 3, + "saved baseVal resynchronizes"); + assert(text.x.animVal.length === 3 && text.x.animVal[2].value === 3, + "animVal resynchronizes after direct baseVal access"); + + assert(errorName(() => lengthAnim.clear()) === "NoModificationAllowedError", + "SVGLengthList animVal clear"); + assert(errorName(() => { lengthAnim[0] = length; }) === "NoModificationAllowedError", + "SVGLengthList animVal indexed setter"); + assert(errorName(() => numberAnim.appendItem(number)) === "NoModificationAllowedError", + "SVGNumberList animVal appendItem"); + assert(errorName(() => SVGLengthList.prototype.clear.call({})) === "TypeError", + "SVGLengthList receiver brand"); + return "ok"; + })() + "#, + ) + .expect("SVG value list semantics probe should evaluate"); + + 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/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 1b25532462..42fa08daa4 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 @@ -1,5 +1,115 @@ use super::*; +#[test] +#[ignore = "requires native DOM attribute UTF-16 storage migration"] +fn svg_animated_string_attributes_preserve_lone_surrogates() { + let mut vm = new_parsed_test_vm("https://svg-utf16.test/", ""); + assert_eq!( + vm.eval(r#"(() => { + const value = 'before\uD800after'; + for (const [tag, property, attribute] of [['feTile', 'in1', 'in'], ['mpath', 'href', 'href']]) { + const element = document.createElementNS('http://www.w3.org/2000/svg', tag); + const animated = element[property]; + animated.baseVal = value; + if (animated.baseVal !== value || animated.animVal !== value || element.getAttribute(attribute) !== value) return false; + } + return true; + })()"#).unwrap(), + "true" + ); +} + +#[test] +fn svg_animated_lengths_reflect_initial_and_content_attribute_values() { + let mut vm = new_parsed_test_vm( + "https://svg-animated-length-reflection.test/", + "", + ); + + let result = vm + .eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const ns = "http://www.w3.org/2000/svg"; + const cases = [ + ["filter", SVGFilterElement, [ + ["x", "-10%"], ["y", "-10%"], + ["width", "120%"], ["height", "120%"], + ]], + ["feBlend", SVGFEBlendElement, [ + ["x", "0%"], ["y", "0%"], + ["width", "100%"], ["height", "100%"], + ]], + ["linearGradient", SVGLinearGradientElement, [ + ["x1", "0%"], ["y1", "0%"], ["x2", "100%"], ["y2", "0%"], + ]], + ["marker", SVGMarkerElement, [ + ["refX", "0"], ["refY", "0"], + ["markerWidth", "3"], ["markerHeight", "3"], + ]], + ["mask", SVGMaskElement, [ + ["x", "-10%"], ["y", "-10%"], + ["width", "120%"], ["height", "120%"], + ]], + ["pattern", SVGPatternElement, [ + ["x", "0"], ["y", "0"], ["width", "0"], ["height", "0"], + ]], + ["radialGradient", SVGRadialGradientElement, [ + ["cx", "50%"], ["cy", "50%"], ["r", "50%"], + ["fx", "50%"], ["fy", "50%"], ["fr", "0%"], + ]], + ["svg", SVGSVGElement, [ + ["x", "0"], ["y", "0"], ["width", "100%"], ["height", "100%"], + ]], + ["text", SVGTextContentElement, [["textLength", "0"]]], + ["textPath", SVGTextPathElement, [["startOffset", "0"]]], + ]; + + for (const [tag, owner, attributes] of cases) { + const element = document.createElementNS(ns, tag); + for (const [name, initial] of attributes) { + const descriptor = Object.getOwnPropertyDescriptor(owner.prototype, name); + assert(typeof descriptor?.get === "function", `${owner.name}.${name} getter`); + assert(descriptor.set === undefined, `${owner.name}.${name} setter`); + assert(descriptor.enumerable && descriptor.configurable, + `${owner.name}.${name} flags`); + + const animated = element[name]; + assert(animated instanceof SVGAnimatedLength, `${tag}.${name} interface`); + assert(element[name] === animated, `${tag}.${name} SameObject`); + assert(animated.baseVal.valueAsString === initial, + `${tag}.${name} initial baseVal`); + assert(animated.animVal.valueAsString === initial, + `${tag}.${name} initial animVal`); + + element.setAttribute(name, "42"); + assert(element[name] === animated, `${tag}.${name} SameObject after set`); + assert(animated.baseVal.valueAsString === "42" && + animated.animVal.valueAsString === "42", `${tag}.${name} content update`); + + element.setAttribute(name, "foobar"); + assert(element[name] === animated, `${tag}.${name} SameObject after invalid`); + assert(animated.baseVal.valueAsString === initial && + animated.animVal.valueAsString === initial, `${tag}.${name} invalid fallback`); + + element.removeAttribute(name); + assert(element[name] === animated, `${tag}.${name} SameObject after remove`); + assert(animated.baseVal.valueAsString === initial && + animated.animVal.valueAsString === initial, `${tag}.${name} removed fallback`); + } + } + return "ok"; + })() + "#, + ) + .expect("SVG animated length reflection probe should evaluate"); + + assert_eq!(result, "ok"); +} + #[test] fn svg_enumeration_constants_have_webidl_descriptors() { let mut vm = new_storage_test_vm("https://svg-enumeration-constants.test/"); @@ -522,3 +632,1718 @@ fn element_methods_and_dataset_live_on_owner_prototypes() { assert_eq!(result, "ok"); } + +#[test] +fn svg_historical_interfaces_keep_current_element_constructors_only() { + let mut vm = new_parsed_test_vm( + "https://svg-historical-interfaces.test/", + "", + ); + + let result = vm + .eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const ns = "http://www.w3.org/2000/svg"; + const interfaces = [ + ["clipPath", "SVGClipPathElement"], + ["filter", "SVGFilterElement"], + ["mask", "SVGMaskElement"], + ["view", "SVGViewElement"], + ]; + const removedUnitTypeConstants = [ + "SVG_UNIT_TYPE_UNKNOWN", + "SVG_UNIT_TYPE_USERSPACEONUSE", + "SVG_UNIT_TYPE_OBJECTBOUNDINGBOX", + ]; + + for (const [localName, interfaceName] of interfaces) { + const constructor = globalThis[interfaceName]; + assert(typeof constructor === "function", `${interfaceName} constructor`); + assert(Object.getPrototypeOf(constructor.prototype) === SVGElement.prototype, + `${interfaceName} prototype parent`); + assert(document.createElementNS(ns, localName) instanceof constructor, + `${localName} interface`); + for (const constant of removedUnitTypeConstants) { + assert(!(constant in constructor), `${interfaceName}.${constant} removed`); + } + } + + assert(!("viewTarget" in SVGViewElement.prototype), + "SVGViewElement.prototype.viewTarget removed"); + return "ok"; + })() + "#, + ) + .expect("SVG historical interface probe should evaluate"); + + assert_eq!(result, "ok"); +} + +#[test] +fn svg_animated_enumerations_reflect_typed_content_attributes() { + let mut vm = new_parsed_test_vm( + "https://svg-animated-enumerations.test/", + "", + ); + + let result = vm + .eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const ns = "http://www.w3.org/2000/svg"; + const unitUser = SVGUnitTypes.SVG_UNIT_TYPE_USERSPACEONUSE; + const unitBox = SVGUnitTypes.SVG_UNIT_TYPE_OBJECTBOUNDINGBOX; + const cases = [ + ["clipPath", "SVGClipPathElement", "clipPathUnits", unitUser, unitBox, "objectBoundingBox", 3], + ["filter", "SVGFilterElement", "filterUnits", unitBox, unitUser, "userSpaceOnUse", 3], + ["filter", "SVGFilterElement", "primitiveUnits", unitUser, unitBox, "objectBoundingBox", 3], + ["linearGradient", "SVGGradientElement", "gradientUnits", unitBox, unitUser, "userSpaceOnUse", 3], + ["linearGradient", "SVGGradientElement", "spreadMethod", SVGGradientElement.SVG_SPREADMETHOD_PAD, SVGGradientElement.SVG_SPREADMETHOD_REPEAT, "repeat", 4], + ["mask", "SVGMaskElement", "maskUnits", unitBox, unitUser, "userSpaceOnUse", 3], + ["mask", "SVGMaskElement", "maskContentUnits", unitUser, unitBox, "objectBoundingBox", 3], + ["pattern", "SVGPatternElement", "patternUnits", unitBox, unitUser, "userSpaceOnUse", 3], + ["pattern", "SVGPatternElement", "patternContentUnits", unitUser, unitBox, "objectBoundingBox", 3], + ["text", "SVGTextContentElement", "lengthAdjust", SVGTextContentElement.LENGTHADJUST_SPACING, SVGTextContentElement.LENGTHADJUST_SPACINGANDGLYPHS, "spacingAndGlyphs", 3], + ]; + + assert(typeof SVGUnitTypes === "function", "SVGUnitTypes interface"); + for (const [name, value] of [ + ["SVG_UNIT_TYPE_UNKNOWN", 0], + ["SVG_UNIT_TYPE_USERSPACEONUSE", 1], + ["SVG_UNIT_TYPE_OBJECTBOUNDINGBOX", 2], + ]) { + assert(SVGUnitTypes[name] === value, `SVGUnitTypes.${name}`); + assert(SVGUnitTypes.prototype[name] === value, `SVGUnitTypes.prototype.${name}`); + } + let illegalConstructor = false; + try { + new SVGUnitTypes(); + } catch (error) { + illegalConstructor = error instanceof TypeError; + } + assert(illegalConstructor, "SVGUnitTypes illegal constructor"); + + for (const [tag, interfaceName, property, initial, alternate, serialized, invalid] of cases) { + const element = document.createElementNS(ns, tag); + const animated = element[property]; + assert(animated instanceof SVGAnimatedEnumeration, `${property} interface`); + assert(element[property] === animated, `${property} SameObject`); + assert(animated.baseVal === initial && animated.animVal === initial, + `${property} initial value`); + + element.setAttribute(property, serialized); + assert(animated.baseVal === alternate && animated.animVal === alternate, + `${property} content attribute update`); + element.setAttribute(property, "invalid"); + assert(animated.baseVal === initial, `${property} invalid content attribute`); + element.removeAttribute(property); + assert(animated.baseVal === initial, `${property} removed content attribute`); + + animated.baseVal = alternate; + assert(element.getAttribute(property) === serialized, `${property} reflected value`); + assert(animated.baseVal === alternate && animated.animVal === alternate, + `${property} reflected enumeration`); + let rejected = false; + try { + animated.baseVal = invalid; + } catch (error) { + rejected = error instanceof TypeError; + } + assert(rejected, `${property} invalid IDL value`); + assert(animated.baseVal === alternate, `${property} preserved after rejection`); + + const owner = globalThis[interfaceName].prototype; + const descriptor = Object.getOwnPropertyDescriptor(owner, property); + assert(typeof descriptor.get === "function", `${interfaceName}.${property} getter`); + assert(descriptor.enumerable && descriptor.configurable, + `${interfaceName}.${property} flags`); + } + return "ok"; + })() + "#, + ) + .expect("SVG animated enumeration probe should evaluate"); + + assert_eq!(result, "ok"); +} + +#[test] +fn svg_animated_boolean_reflects_preserve_alpha() { + let mut vm = new_parsed_test_vm( + "https://svg-animated-boolean.test/", + "", + ); + + let result = vm + .eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const ns = "http://www.w3.org/2000/svg"; + const element = document.createElementNS(ns, "feConvolveMatrix"); + + assert(typeof SVGAnimatedBoolean === "function", "animated boolean constructor"); + assert(typeof SVGFEConvolveMatrixElement === "function", "element constructor"); + assert(element instanceof SVGFEConvolveMatrixElement, "element interface"); + assert(Object.getPrototypeOf(SVGFEConvolveMatrixElement.prototype) === SVGElement.prototype, + "element prototype parent"); + + for (const constructor of [SVGAnimatedBoolean, SVGFEConvolveMatrixElement]) { + let illegalConstructor = false; + try { + new constructor(); + } catch (error) { + illegalConstructor = error instanceof TypeError; + } + assert(illegalConstructor, `${constructor.name} illegal constructor`); + } + + const elementDescriptor = Object.getOwnPropertyDescriptor( + SVGFEConvolveMatrixElement.prototype, + "preserveAlpha", + ); + assert(typeof elementDescriptor.get === "function", "preserveAlpha getter"); + assert(elementDescriptor.set === undefined, "preserveAlpha readonly"); + assert(elementDescriptor.enumerable && elementDescriptor.configurable, + "preserveAlpha descriptor flags"); + + const animated = element.preserveAlpha; + assert(animated instanceof SVGAnimatedBoolean, "animated boolean interface"); + assert(Object.prototype.toString.call(animated) === "[object SVGAnimatedBoolean]", + "animated boolean tag"); + assert(element.preserveAlpha === animated, "preserveAlpha SameObject"); + assert(!element.hasAttribute("preserveAlpha"), "getter does not create attribute"); + assert(animated.baseVal === false && animated.animVal === false, "initial value"); + + const baseDescriptor = Object.getOwnPropertyDescriptor( + SVGAnimatedBoolean.prototype, + "baseVal", + ); + const animDescriptor = Object.getOwnPropertyDescriptor( + SVGAnimatedBoolean.prototype, + "animVal", + ); + assert(typeof baseDescriptor.get === "function" && + typeof baseDescriptor.set === "function", "baseVal descriptor"); + assert(typeof animDescriptor.get === "function" && animDescriptor.set === undefined, + "animVal descriptor"); + assert(baseDescriptor.enumerable && baseDescriptor.configurable && + animDescriptor.enumerable && animDescriptor.configurable, + "animated boolean descriptor flags"); + + element.setAttribute("preserveAlpha", "true"); + assert(animated.baseVal === true && animated.animVal === true, + "true content attribute"); + element.setAttribute("preserveAlpha", "false"); + assert(animated.baseVal === false && animated.animVal === false, + "false content attribute"); + element.setAttribute("preserveAlpha", "TRUE"); + assert(animated.baseVal === false && animated.animVal === false, + "invalid content attribute uses initial value"); + element.removeAttribute("preserveAlpha"); + assert(animated.baseVal === false && animated.animVal === false, + "removed content attribute uses initial value"); + + animated.baseVal = true; + assert(element.getAttribute("preserveAlpha") === "true", "true reflection"); + assert(animated.baseVal === true && animated.animVal === true, + "true reflected values"); + animated.baseVal = null; + assert(element.getAttribute("preserveAlpha") === "false", "false reflection"); + assert(animated.baseVal === false && animated.animVal === false, + "false reflected values"); + animated.baseVal = {}; + assert(element.getAttribute("preserveAlpha") === "true", "WebIDL ToBoolean"); + + let incompatibleAnimatedReceiver = false; + try { + baseDescriptor.get.call({}); + } catch (error) { + incompatibleAnimatedReceiver = error instanceof TypeError; + } + assert(incompatibleAnimatedReceiver, "animated boolean receiver brand"); + + let incompatibleElementReceiver = false; + try { + elementDescriptor.get.call(document.createElementNS(ns, "rect")); + } catch (error) { + incompatibleElementReceiver = error instanceof TypeError; + } + assert(incompatibleElementReceiver, "element receiver brand"); + return "ok"; + })() + "#, + ) + .expect("SVG animated boolean probe should evaluate"); + + assert_eq!(result, "ok"); +} + +#[test] +fn svg_string_lists_reflect_conditional_processing_attributes() { + let mut vm = new_parsed_test_vm( + "https://svg-string-list.test/", + "", + ); + + let result = vm + .eval( + r##" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const ns = "http://www.w3.org/2000/svg"; + const text = document.createElementNS(ns, "text"); + + assert(typeof SVGStringList === "function", "SVGStringList constructor"); + let illegalConstructor = false; + try { + new SVGStringList(); + } catch (error) { + illegalConstructor = error instanceof TypeError; + } + assert(illegalConstructor, "SVGStringList illegal constructor"); + + for (const name of ["requiredExtensions", "systemLanguage"]) { + const descriptor = Object.getOwnPropertyDescriptor( + SVGGraphicsElement.prototype, + name, + ); + assert(typeof descriptor.get === "function" && descriptor.set === undefined, + `${name} readonly descriptor`); + assert(descriptor.enumerable && descriptor.configurable, `${name} flags`); + } + for (const name of ["length", "numberOfItems"]) { + const descriptor = Object.getOwnPropertyDescriptor(SVGStringList.prototype, name); + assert(typeof descriptor.get === "function" && descriptor.set === undefined, + `${name} descriptor`); + assert(descriptor.enumerable && descriptor.configurable, `${name} flags`); + } + const methodLengths = { + clear: 0, + initialize: 1, + getItem: 1, + insertItemBefore: 2, + replaceItem: 2, + removeItem: 1, + appendItem: 1, + }; + for (const [name, length] of Object.entries(methodLengths)) { + const descriptor = Object.getOwnPropertyDescriptor(SVGStringList.prototype, name); + assert(typeof descriptor.value === "function", `${name} method`); + assert(descriptor.value.length === length, `${name} arity`); + assert(descriptor.enumerable && descriptor.configurable && descriptor.writable, + `${name} flags`); + } + + const languages = text.systemLanguage; + assert(languages instanceof SVGStringList, "systemLanguage interface"); + assert(Object.prototype.toString.call(languages) === "[object SVGStringList]", + "SVGStringList tag"); + assert(text.systemLanguage === languages, "systemLanguage SameObject"); + assert(languages.length === 0 && languages.numberOfItems === 0, + "absent attribute empty list"); + assert(!text.hasAttribute("systemLanguage"), "getter does not create attribute"); + + const parsingCases = [ + ["en,fr,de", ["en", "fr", "de"]], + ["en, fr, de", ["en", "fr", "de"]], + ["en ,fr ,de", ["en", "fr", "de"]], + ["en , fr , de", ["en", "fr", "de"]], + [" en, fr ", ["en", "fr"]], + [" \t\nen, fr\t\n ", ["en", "fr"]], + ["en", ["en"]], + ["en-US, zh-Hans, pt-BR", ["en-US", "zh-Hans", "pt-BR"]], + ["en,,fr", ["en", "", "fr"]], + ["", [""]], + [",", ["", ""]], + ["123, 456", ["123", "456"]], + ["not-a-lang, ???, @#$", ["not-a-lang", "???", "@#$"]], + ]; + for (const [raw, expected] of parsingCases) { + text.setAttribute("systemLanguage", raw); + assert(text.systemLanguage === languages, `SameObject after ${raw}`); + assert(languages.length === expected.length, `length for ${raw}`); + assert(languages.numberOfItems === expected.length, + `numberOfItems for ${raw}`); + assert(Object.keys(languages).join() === expected.map((_, index) => index).join(), + `supported indices for ${raw}`); + for (let index = 0; index < expected.length; index++) { + assert(languages.getItem(index) === expected[index], + `getItem ${index} for ${raw}`); + assert(languages[index] === expected[index], `index ${index} for ${raw}`); + } + } + + text.removeAttribute("systemLanguage"); + assert(languages.length === 0, "removed attribute empty list"); + const extensions = text.requiredExtensions; + assert(text.requiredExtensions === extensions, "requiredExtensions SameObject"); + text.setAttribute("requiredExtensions", " one\t two\nthree "); + assert(extensions.length === 3 && extensions[0] === "one" && + extensions[1] === "two" && extensions[2] === "three", + "requiredExtensions space-separated parsing"); + + assert(languages.initialize("en") === "en", "initialize return"); + assert(text.getAttribute("systemLanguage") === "en", "initialize reflection"); + assert(languages.appendItem("fr") === "fr", "append return"); + assert(text.getAttribute("systemLanguage") === "en,fr", "append reflection"); + assert(languages.insertItemBefore("de", 1) === "de", "insert return"); + assert(text.getAttribute("systemLanguage") === "en,de,fr", "insert reflection"); + assert(languages.insertItemBefore("it", 99) === "it", "clamped insert return"); + assert(text.getAttribute("systemLanguage") === "en,de,fr,it", + "clamped insert reflection"); + assert(languages.replaceItem("zh", 1) === "zh", "replace return"); + assert(text.getAttribute("systemLanguage") === "en,zh,fr,it", + "replace reflection"); + languages[2] = "pt-BR"; + assert(text.getAttribute("systemLanguage") === "en,zh,pt-BR,it", + "indexed setter reflection"); + Object.defineProperty(languages, "0", {value: "es"}); + assert(text.getAttribute("systemLanguage") === "es,zh,pt-BR,it", + "indexed definer reflection"); + const indexDescriptor = Object.getOwnPropertyDescriptor(languages, "0"); + assert(indexDescriptor.value === "es" && indexDescriptor.writable && + indexDescriptor.enumerable && indexDescriptor.configurable, + "indexed property descriptor"); + assert(delete languages[0] === false, "supported index cannot be deleted"); + assert(languages.removeItem(1) === "zh", "remove return"); + assert(text.getAttribute("systemLanguage") === "es,pt-BR,it", + "remove reflection"); + languages.clear(); + assert(text.getAttribute("systemLanguage") === "" && languages.length === 0, + "clear reflection"); + + extensions.initialize("alpha"); + extensions.appendItem("beta"); + assert(text.getAttribute("requiredExtensions") === "alpha beta", + "requiredExtensions space serialization"); + assert(extensions.appendItem(null) === "null", "DOMString conversion"); + assert(text.getAttribute("requiredExtensions") === "alpha beta null", + "converted string reflection"); + + text.setAttribute("systemLanguage", "en,fr"); + for (const operation of [ + () => languages.getItem(9), + () => languages.replaceItem("x", 9), + () => languages.removeItem(9), + () => { languages[9] = "x"; }, + ]) { + let indexError = false; + try { + operation(); + } catch (error) { + indexError = error instanceof DOMException && error.name === "IndexSizeError"; + } + assert(indexError, "out-of-range operation"); + } + + let incompatibleListReceiver = false; + try { + SVGStringList.prototype.getItem.call({}, 0); + } catch (error) { + incompatibleListReceiver = error instanceof TypeError; + } + assert(incompatibleListReceiver, "SVGStringList receiver brand"); + + const systemLanguageGetter = Object.getOwnPropertyDescriptor( + SVGGraphicsElement.prototype, + "systemLanguage", + ).get; + let incompatibleElementReceiver = false; + try { + systemLanguageGetter.call(document.createElementNS(ns, "filter")); + } catch (error) { + incompatibleElementReceiver = error instanceof TypeError; + } + assert(incompatibleElementReceiver, "SVGGraphicsElement receiver brand"); + return "ok"; + })() + "##, + ) + .expect("SVG string list probe should evaluate"); + + assert_eq!(result, "ok"); +} + +#[test] +fn svg_marker_orient_angle_is_a_live_animated_angle() { + let mut vm = new_parsed_test_vm( + "https://svg-animated-angle.test/", + "", + ); + + let result = vm + .eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const close = (actual, expected) => Math.abs(actual - expected) < 1e-9; + const ns = "http://www.w3.org/2000/svg"; + const marker = document.createElementNS(ns, "marker"); + + for (const constructor of [SVGMarkerElement, SVGAnimatedAngle, SVGAngle]) { + assert(typeof constructor === "function", `${constructor.name} constructor`); + let illegalConstructor = false; + try { + new constructor(); + } catch (error) { + illegalConstructor = error instanceof TypeError; + } + assert(illegalConstructor, `${constructor.name} illegal constructor`); + } + assert(marker instanceof SVGMarkerElement, "marker interface"); + assert(Object.getPrototypeOf(SVGMarkerElement.prototype) === SVGElement.prototype, + "marker prototype parent"); + + const markerDescriptor = Object.getOwnPropertyDescriptor( + SVGMarkerElement.prototype, + "orientAngle", + ); + assert(typeof markerDescriptor.get === "function" && + markerDescriptor.set === undefined, "orientAngle readonly descriptor"); + assert(markerDescriptor.enumerable && markerDescriptor.configurable, + "orientAngle descriptor flags"); + + const animated = marker.orientAngle; + const base = animated.baseVal; + const anim = animated.animVal; + assert(animated instanceof SVGAnimatedAngle, "animated angle interface"); + assert(base instanceof SVGAngle && anim instanceof SVGAngle, "angle interfaces"); + assert(base !== anim, "base and animated values are distinct"); + assert(marker.orientAngle === animated, "orientAngle SameObject"); + assert(base.value === 0 && anim.value === 0, "initial values"); + assert(base.unitType === SVGAngle.SVG_ANGLETYPE_UNSPECIFIED, "initial unit"); + assert(!marker.hasAttribute("orient"), "getter does not create attribute"); + assert(marker.orientType instanceof SVGAnimatedEnumeration, "orientType interface"); + assert(marker.orientType.baseVal === SVGMarkerElement.SVG_MARKER_ORIENT_ANGLE, + "initial orientType"); + assert(marker.markerUnits instanceof SVGAnimatedEnumeration, + "markerUnits interface"); + assert(marker.markerUnits.baseVal === + SVGMarkerElement.SVG_MARKERUNITS_STROKEWIDTH, "initial markerUnits"); + marker.markerUnits.baseVal = SVGMarkerElement.SVG_MARKERUNITS_USERSPACEONUSE; + assert(marker.getAttribute("markerUnits") === "userSpaceOnUse", + "markerUnits reflection"); + + base.value = 100; + assert(base.value === 100 && marker.orientAngle.baseVal.value === 100, + "cached base value is live"); + assert(marker.getAttribute("orient") === "100", "value reflects to orient"); + marker.orientAngle.baseVal = -1; + assert(marker.orientAngle.baseVal === base && base.value === 100, + "baseVal assignment is ignored"); + + marker.setAttribute("orient", "1.5707963267948966rad"); + assert(close(base.value, 90), "content attribute converts to degrees"); + assert(base.unitType === SVGAngle.SVG_ANGLETYPE_RAD, "content attribute unit"); + assert(close(base.valueInSpecifiedUnits, Math.PI / 2), "specified value"); + assert(anim.unitType === SVGAngle.SVG_ANGLETYPE_RAD && close(anim.value, 90), + "animVal tracks content attribute"); + assert(marker.orientType.baseVal === SVGMarkerElement.SVG_MARKER_ORIENT_ANGLE, + "angle orientType"); + + marker.setOrientToAuto(); + assert(marker.getAttribute("orient") === "auto", "setOrientToAuto reflection"); + assert(marker.orientType.baseVal === SVGMarkerElement.SVG_MARKER_ORIENT_AUTO, + "automatic orientType"); + assert(base.value === 0 && base.unitType === SVGAngle.SVG_ANGLETYPE_UNSPECIFIED, + "automatic orient angle"); + + marker.setAttribute("orient", "400grad"); + assert(base.value === 360 && base.unitType === SVGAngle.SVG_ANGLETYPE_GRAD, + "grad content attribute"); + + const svg = document.createElementNS(ns, "svg"); + const standalone = svg.createSVGAngle(); + assert(standalone instanceof SVGAngle, "createSVGAngle result"); + standalone.newValueSpecifiedUnits(SVGAngle.SVG_ANGLETYPE_RAD, Math.PI); + assert(close(standalone.value, 180), "newValueSpecifiedUnits conversion"); + standalone.convertToSpecifiedUnits(SVGAngle.SVG_ANGLETYPE_GRAD); + assert(close(standalone.valueInSpecifiedUnits, 200), "unit conversion"); + assert(standalone.valueAsString === "200grad", "unit serialization"); + marker.setOrientToAngle(standalone); + assert(marker.getAttribute("orient") === "200grad", "setOrientToAngle reflection"); + assert(marker.orientAngle.baseVal.value === 180, "setOrientToAngle value"); + + let readonlyAnim = false; + try { + anim.value = 1; + } catch (error) { + readonlyAnim = error.name === "NoModificationAllowedError"; + } + assert(readonlyAnim, "animVal is read-only"); + + let incompatibleReceiver = false; + try { + markerDescriptor.get.call(document.createElementNS(ns, "rect")); + } catch (error) { + incompatibleReceiver = error instanceof TypeError; + } + assert(incompatibleReceiver, "marker receiver brand"); + return "ok"; + })() + "#, + ) + .expect("SVG animated angle probe should evaluate"); + + assert_eq!(result, "ok"); +} + +#[test] +fn svg_filter_and_text_path_enumerations_reflect_typed_content_attributes() { + let mut vm = new_parsed_test_vm( + "https://svg-filter-enumerations.test/", + "", + ); + + let result = vm + .eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const ns = "http://www.w3.org/2000/svg"; + const interfaceCases = [ + ["feBlend", SVGFEBlendElement, SVGElement], + ["feColorMatrix", SVGFEColorMatrixElement, SVGElement], + ["feComposite", SVGFECompositeElement, SVGElement], + ["feConvolveMatrix", SVGFEConvolveMatrixElement, SVGElement], + ["feDisplacementMap", SVGFEDisplacementMapElement, SVGElement], + ["feMorphology", SVGFEMorphologyElement, SVGElement], + ["feTurbulence", SVGFETurbulenceElement, SVGElement], + ["feFuncA", SVGFEFuncAElement, SVGComponentTransferFunctionElement], + ["feFuncB", SVGFEFuncBElement, SVGComponentTransferFunctionElement], + ["feFuncG", SVGFEFuncGElement, SVGComponentTransferFunctionElement], + ["feFuncR", SVGFEFuncRElement, SVGComponentTransferFunctionElement], + ["textPath", SVGTextPathElement, SVGTextContentElement], + ]; + + for (const [tag, constructor, parent] of interfaceCases) { + const element = document.createElementNS(ns, tag); + assert(element instanceof constructor, `${tag} interface`); + assert(Object.getPrototypeOf(constructor.prototype) === parent.prototype, + `${constructor.name} parent`); + let illegalConstructor = false; + try { + new constructor(); + } catch (error) { + illegalConstructor = error instanceof TypeError; + } + assert(illegalConstructor, `${constructor.name} illegal constructor`); + } + + const constantGroups = [ + [SVGComponentTransferFunctionElement, [ + ["SVG_FECOMPONENTTRANSFER_TYPE_UNKNOWN", 0], + ["SVG_FECOMPONENTTRANSFER_TYPE_IDENTITY", 1], + ["SVG_FECOMPONENTTRANSFER_TYPE_TABLE", 2], + ["SVG_FECOMPONENTTRANSFER_TYPE_DISCRETE", 3], + ["SVG_FECOMPONENTTRANSFER_TYPE_LINEAR", 4], + ["SVG_FECOMPONENTTRANSFER_TYPE_GAMMA", 5], + ]], + [SVGFEBlendElement, [ + ["SVG_FEBLEND_MODE_UNKNOWN", 0], + ["SVG_FEBLEND_MODE_NORMAL", 1], + ["SVG_FEBLEND_MODE_MULTIPLY", 2], + ["SVG_FEBLEND_MODE_SCREEN", 3], + ["SVG_FEBLEND_MODE_DARKEN", 4], + ["SVG_FEBLEND_MODE_LIGHTEN", 5], + ["SVG_FEBLEND_MODE_OVERLAY", 6], + ["SVG_FEBLEND_MODE_COLOR_DODGE", 7], + ["SVG_FEBLEND_MODE_COLOR_BURN", 8], + ["SVG_FEBLEND_MODE_HARD_LIGHT", 9], + ["SVG_FEBLEND_MODE_SOFT_LIGHT", 10], + ["SVG_FEBLEND_MODE_DIFFERENCE", 11], + ["SVG_FEBLEND_MODE_EXCLUSION", 12], + ["SVG_FEBLEND_MODE_HUE", 13], + ["SVG_FEBLEND_MODE_SATURATION", 14], + ["SVG_FEBLEND_MODE_COLOR", 15], + ["SVG_FEBLEND_MODE_LUMINOSITY", 16], + ]], + [SVGFEColorMatrixElement, [ + ["SVG_FECOLORMATRIX_TYPE_UNKNOWN", 0], + ["SVG_FECOLORMATRIX_TYPE_MATRIX", 1], + ["SVG_FECOLORMATRIX_TYPE_SATURATE", 2], + ["SVG_FECOLORMATRIX_TYPE_HUEROTATE", 3], + ["SVG_FECOLORMATRIX_TYPE_LUMINANCETOALPHA", 4], + ]], + [SVGFECompositeElement, [ + ["SVG_FECOMPOSITE_OPERATOR_UNKNOWN", 0], + ["SVG_FECOMPOSITE_OPERATOR_OVER", 1], + ["SVG_FECOMPOSITE_OPERATOR_IN", 2], + ["SVG_FECOMPOSITE_OPERATOR_OUT", 3], + ["SVG_FECOMPOSITE_OPERATOR_ATOP", 4], + ["SVG_FECOMPOSITE_OPERATOR_XOR", 5], + ["SVG_FECOMPOSITE_OPERATOR_LIGHTER", 6], + ["SVG_FECOMPOSITE_OPERATOR_ARITHMETIC", 7], + ]], + [SVGFEConvolveMatrixElement, [ + ["SVG_EDGEMODE_UNKNOWN", 0], + ["SVG_EDGEMODE_DUPLICATE", 1], + ["SVG_EDGEMODE_WRAP", 2], + ["SVG_EDGEMODE_NONE", 3], + ]], + [SVGFEDisplacementMapElement, [ + ["SVG_CHANNEL_UNKNOWN", 0], + ["SVG_CHANNEL_R", 1], + ["SVG_CHANNEL_G", 2], + ["SVG_CHANNEL_B", 3], + ["SVG_CHANNEL_A", 4], + ]], + [SVGFEMorphologyElement, [ + ["SVG_MORPHOLOGY_OPERATOR_UNKNOWN", 0], + ["SVG_MORPHOLOGY_OPERATOR_ERODE", 1], + ["SVG_MORPHOLOGY_OPERATOR_DILATE", 2], + ]], + [SVGFETurbulenceElement, [ + ["SVG_TURBULENCE_TYPE_UNKNOWN", 0], + ["SVG_TURBULENCE_TYPE_FRACTALNOISE", 1], + ["SVG_TURBULENCE_TYPE_TURBULENCE", 2], + ["SVG_STITCHTYPE_UNKNOWN", 0], + ["SVG_STITCHTYPE_STITCH", 1], + ["SVG_STITCHTYPE_NOSTITCH", 2], + ]], + [SVGTextPathElement, [ + ["TEXTPATH_METHODTYPE_UNKNOWN", 0], + ["TEXTPATH_METHODTYPE_ALIGN", 1], + ["TEXTPATH_METHODTYPE_STRETCH", 2], + ["TEXTPATH_SPACINGTYPE_UNKNOWN", 0], + ["TEXTPATH_SPACINGTYPE_AUTO", 1], + ["TEXTPATH_SPACINGTYPE_EXACT", 2], + ["TEXTPATH_SIDETYPE_UNKNOWN", 0], + ["TEXTPATH_SIDETYPE_LEFT", 1], + ["TEXTPATH_SIDETYPE_RIGHT", 2], + ]], + ]; + for (const [constructor, constants] of constantGroups) { + for (const [name, expected] of constants) { + assert(constructor[name] === expected, `${constructor.name}.${name}`); + assert(constructor.prototype[name] === expected, + `${constructor.name}.prototype.${name}`); + } + } + + const blend = document.createElementNS(ns, "feBlend"); + const blendModes = [ + ["normal", 1], ["multiply", 2], ["screen", 3], ["darken", 4], + ["lighten", 5], ["overlay", 6], ["color-dodge", 7], ["color-burn", 8], + ["hard-light", 9], ["soft-light", 10], ["difference", 11], + ["exclusion", 12], ["hue", 13], ["saturation", 14], ["color", 15], + ["luminosity", 16], + ]; + for (const [keyword, value] of blendModes) { + blend.setAttribute("mode", keyword); + assert(blend.mode.baseVal === value, `feBlend parses ${keyword}`); + blend.mode.baseVal = value; + assert(blend.getAttribute("mode") === keyword, `feBlend serializes ${keyword}`); + } + + const cases = [ + ["feFuncR", SVGComponentTransferFunctionElement, "type", 1, 5, "gamma", 6], + ["feBlend", SVGFEBlendElement, "mode", 1, 16, "luminosity", 17], + ["feColorMatrix", SVGFEColorMatrixElement, "type", 1, 4, "luminanceToAlpha", 5], + ["feComposite", SVGFECompositeElement, "operator", 1, 7, "arithmetic", 8], + ["feConvolveMatrix", SVGFEConvolveMatrixElement, "edgeMode", 1, 3, "none", 4], + ["feDisplacementMap", SVGFEDisplacementMapElement, "xChannelSelector", 4, 1, "R", 5], + ["feDisplacementMap", SVGFEDisplacementMapElement, "yChannelSelector", 4, 3, "B", 5], + ["feMorphology", SVGFEMorphologyElement, "operator", 1, 2, "dilate", 3], + ["feTurbulence", SVGFETurbulenceElement, "stitchTiles", 2, 1, "stitch", 3], + ["feTurbulence", SVGFETurbulenceElement, "type", 2, 1, "fractalNoise", 3], + ["textPath", SVGTextPathElement, "method", 1, 2, "stretch", 3], + ["textPath", SVGTextPathElement, "spacing", 2, 1, "auto", 3], + ["textPath", SVGTextPathElement, "side", 1, 2, "right", 3], + ]; + + for (const [tag, owner, property, initial, alternate, serialized, invalid] of cases) { + const element = document.createElementNS(ns, tag); + const animated = element[property]; + assert(animated instanceof SVGAnimatedEnumeration, `${tag}.${property} interface`); + assert(element[property] === animated, `${tag}.${property} SameObject`); + assert(animated.baseVal === initial && animated.animVal === initial, + `${tag}.${property} initial value`); + + element.setAttribute(property, serialized); + assert(animated.baseVal === alternate && animated.animVal === alternate, + `${tag}.${property} content attribute update`); + element.setAttribute(property, "invalid"); + assert(animated.baseVal === initial, `${tag}.${property} invalid content attribute`); + element.removeAttribute(property); + assert(animated.baseVal === initial, `${tag}.${property} removed content attribute`); + + animated.baseVal = alternate; + assert(element.getAttribute(property) === serialized, + `${tag}.${property} reflected value`); + let rejected = false; + try { + animated.baseVal = invalid; + } catch (error) { + rejected = error instanceof TypeError; + } + assert(rejected, `${tag}.${property} invalid IDL value`); + assert(animated.baseVal === alternate, + `${tag}.${property} preserved after rejection`); + + const descriptor = Object.getOwnPropertyDescriptor(owner.prototype, property); + assert(typeof descriptor.get === "function", `${owner.name}.${property} getter`); + assert(descriptor.enumerable && descriptor.configurable, + `${owner.name}.${property} flags`); + } + return "ok"; + })() + "#, + ) + .expect("SVG filter enumeration probe should evaluate"); + + assert_eq!(result, "ok"); +} + +#[test] +fn svg_animated_integers_reflect_filter_content_attributes() { + let mut vm = new_parsed_test_vm( + "https://svg-animated-integer.test/", + "", + ); + + let result = vm + .eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const ns = "http://www.w3.org/2000/svg"; + const convolve = document.createElementNS(ns, "feConvolveMatrix"); + const turbulence = document.createElementNS(ns, "feTurbulence"); + + assert(typeof SVGAnimatedInteger === "function", "constructor exposed"); + let illegalConstructor = false; + try { + new SVGAnimatedInteger(); + } catch (error) { + illegalConstructor = error instanceof TypeError; + } + assert(illegalConstructor, "illegal constructor"); + + const baseDescriptor = Object.getOwnPropertyDescriptor( + SVGAnimatedInteger.prototype, + "baseVal", + ); + const animDescriptor = Object.getOwnPropertyDescriptor( + SVGAnimatedInteger.prototype, + "animVal", + ); + assert(typeof baseDescriptor.get === "function" && + typeof baseDescriptor.set === "function", "baseVal descriptor"); + assert(typeof animDescriptor.get === "function" && animDescriptor.set === undefined, + "animVal descriptor"); + assert(baseDescriptor.enumerable && baseDescriptor.configurable && + animDescriptor.enumerable && animDescriptor.configurable, + "animated integer descriptor flags"); + + const orderX = convolve.orderX; + const orderY = convolve.orderY; + const targetX = convolve.targetX; + const targetY = convolve.targetY; + const numOctaves = turbulence.numOctaves; + for (const [owner, property, value, initial] of [ + [convolve, "orderX", orderX, 3], + [convolve, "orderY", orderY, 3], + [convolve, "targetX", targetX, 0], + [convolve, "targetY", targetY, 0], + [turbulence, "numOctaves", numOctaves, 1], + ]) { + assert(value instanceof SVGAnimatedInteger, `${property} interface`); + assert(Object.prototype.toString.call(value) === "[object SVGAnimatedInteger]", + `${property} tag`); + assert(owner[property] === value, `${property} SameObject`); + assert(value.baseVal === initial && value.animVal === initial, + `${property} initial value`); + const descriptor = Object.getOwnPropertyDescriptor( + Object.getPrototypeOf(owner), + property, + ); + assert(typeof descriptor.get === "function" && descriptor.set === undefined, + `${property} element descriptor`); + assert(descriptor.enumerable && descriptor.configurable, + `${property} element descriptor flags`); + } + + convolve.setAttribute("order", "5"); + assert(orderX.baseVal === 5 && orderY.baseVal === 5, "single order value"); + convolve.setAttribute("order", "5 7"); + assert(orderX.baseVal === 5 && orderY.baseVal === 7, "paired order values"); + convolve.setAttribute("order", "5, 7"); + assert(orderX.baseVal === 5 && orderY.baseVal === 7, "comma order values"); + convolve.setAttribute("order", "-1.5"); + assert(orderX.baseVal === 3 && orderY.baseVal === 3, "invalid order value"); + convolve.removeAttribute("order"); + assert(orderX.baseVal === 3 && orderY.baseVal === 3, "removed order value"); + + convolve.setAttribute("targetX", "42"); + convolve.setAttribute("targetY", "-4"); + turbulence.setAttribute("numOctaves", "6"); + assert(targetX.baseVal === 42 && targetY.baseVal === -4, + "target content attributes"); + assert(numOctaves.baseVal === 6, "numOctaves content attribute"); + turbulence.setAttribute("numOctaves", "invalid"); + assert(numOctaves.baseVal === 1, "invalid numOctaves value"); + + targetX.baseVal = -1; + assert(convolve.getAttribute("targetX") === "-1", "negative reflection"); + assert(targetX.baseVal === -1 && targetX.animVal === -1, + "negative reflected values"); + targetX.baseVal = 300; + assert(targetX.baseVal === 300, "positive assignment"); + targetX.baseVal = "aString"; + assert(targetX.baseVal === 0 && convolve.getAttribute("targetX") === "0", + "WebIDL string conversion"); + targetX.baseVal = convolve; + assert(targetX.baseVal === 0, "WebIDL object conversion"); + + orderX.baseVal = -2; + assert(orderX.baseVal === -2 && orderY.baseVal === 3, + "orderX reflected independently"); + assert(convolve.getAttribute("order") === "-2 3", "orderX serialization"); + orderY.baseVal = 8; + assert(orderX.baseVal === -2 && orderY.baseVal === 8, + "orderY reflected independently"); + assert(convolve.getAttribute("order") === "-2 8", "orderY serialization"); + + let incompatibleAnimatedReceiver = false; + try { + baseDescriptor.get.call({}); + } catch (error) { + incompatibleAnimatedReceiver = error instanceof TypeError; + } + assert(incompatibleAnimatedReceiver, "animated integer receiver brand"); + + const orderDescriptor = Object.getOwnPropertyDescriptor( + SVGFEConvolveMatrixElement.prototype, + "orderX", + ); + let incompatibleElementReceiver = false; + try { + orderDescriptor.get.call(turbulence); + } catch (error) { + incompatibleElementReceiver = error instanceof TypeError; + } + assert(incompatibleElementReceiver, "element receiver brand"); + return "ok"; + })() + "#, + ) + .expect("SVG animated integer probe should evaluate"); + + assert_eq!(result, "ok"); +} + +#[test] +fn svg_svg_element_value_factories_create_typed_objects() { + let mut vm = new_parsed_test_vm( + "https://svg-value-factories.test/", + "", + ); + + let result = vm + .eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const rejectsTypeError = callback => { + try { + callback(); + return false; + } catch (error) { + return error instanceof TypeError; + } + }; + const rejectsDom = (name, callback) => { + try { + callback(); + return false; + } catch (error) { + return error.name === name; + } + }; + const ns = "http://www.w3.org/2000/svg"; + const svg = document.createElementNS(ns, "svg"); + const wrongReceiver = document.createElementNS(ns, "rect"); + + const factoryNames = [ + "createSVGNumber", + "createSVGLength", + "createSVGAngle", + "createSVGPoint", + "createSVGMatrix", + "createSVGRect", + "createSVGTransform", + "createSVGTransformFromMatrix", + ]; + for (const name of factoryNames) { + const descriptor = Object.getOwnPropertyDescriptor(SVGSVGElement.prototype, name); + assert(typeof descriptor.value === "function", `${name} method`); + assert(descriptor.value.length === 0, `${name} arity`); + assert(descriptor.enumerable && descriptor.configurable && descriptor.writable, + `${name} descriptor flags`); + assert(rejectsTypeError(() => descriptor.value.call(wrongReceiver)), + `${name} receiver brand`); + } + + const number = svg.createSVGNumber(); + const length = svg.createSVGLength(); + const point = svg.createSVGPoint(); + const rect = svg.createSVGRect(); + assert(number instanceof SVGNumber && number.value === 0, "SVGNumber result"); + assert(length instanceof SVGLength && length.value === 0 && + length.unitType === SVGLength.SVG_LENGTHTYPE_NUMBER, "SVGLength result"); + assert(point instanceof DOMPoint && point instanceof SVGPoint, + "SVGPoint result interface"); + assert(point.x === 0 && point.y === 0 && point.z === 0 && point.w === 1, + "SVGPoint defaults"); + assert(rect instanceof SVGRect && !(rect instanceof DOMRect), + "SVGRect result interface"); + assert(rect.x === 0 && rect.y === 0 && rect.width === 0 && rect.height === 0, + "SVGRect defaults"); + assert(svg.createSVGNumber() !== number && svg.createSVGLength() !== length && + svg.createSVGPoint() !== point && svg.createSVGRect() !== rect, + "factories return new objects"); + + number.value = 2; + assert(number.value === 2, "SVGNumber assignment"); + assert(rejectsTypeError(() => { number.value = NaN; }), + "SVGNumber rejects NaN"); + assert(number.value === 2, "SVGNumber preserves rejected assignment"); + + length.valueAsString = "2px"; + assert(length.value === 2 && length.valueAsString === "2px", + "SVGLength assignment"); + assert(rejectsDom("NotSupportedError", () => { + length.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_UNKNOWN); + }), "SVGLength rejects unsupported unit"); + assert(rejectsDom("SyntaxError", () => { length.valueAsString = "10deg"; }), + "SVGLength rejects invalid syntax"); + assert(rejectsTypeError(() => { length.value = NaN; }), + "SVGLength rejects NaN"); + assert(length.value === 2 && length.valueAsString === "2px", + "SVGLength preserves rejected assignments"); + + point.x = 100; + point.y = 200; + assert(point.x === 100 && point.y === 200, "SVGPoint assignment"); + for (const invalid of [point, NaN, Infinity]) { + assert(rejectsTypeError(() => { point.x = invalid; }), + "SVGPoint rejects non-finite values"); + assert(point.x === 100, "SVGPoint preserves rejected assignment"); + } + point.y = null; + assert(point.y === 0, "SVGPoint converts null"); + + Object.assign(rect, { x: 100, y: 200, width: 300, height: 400 }); + assert(rect.x === 100 && rect.y === 200 && rect.width === 300 && + rect.height === 400, "SVGRect assignment"); + for (const [property, invalid] of [ + ["x", rect], + ["y", "aString"], + ["width", svg], + ["height", NaN], + ]) { + assert(rejectsTypeError(() => { rect[property] = invalid; }), + `SVGRect rejects invalid ${property}`); + } + rect.y = null; + assert(rect.y === 0, "SVGRect converts null"); + + const regularPoint = new DOMPoint(); + const regularRect = new DOMRect(); + regularPoint.x = NaN; + regularRect.x = Infinity; + assert(Number.isNaN(regularPoint.x) && regularRect.x === Infinity, + "regular geometry objects remain unrestricted"); + return "ok"; + })() + "#, + ) + .expect("SVG value factory probe should evaluate"); + + assert_eq!(result, "ok"); +} + +#[test] +fn svg_fit_to_view_box_attributes_are_live_and_reflected() { + let mut vm = new_parsed_test_vm( + "https://svg-fit-to-view-box.test/", + "", + ); + + let result = vm + .eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const throws = (callback, expected, message) => { + try { + callback(); + } catch (error) { + assert(error.name === expected, `${message}: ${error.name}`); + return; + } + throw new Error(`${message}: did not throw`); + }; + const accessor = (prototype, name) => { + const descriptor = Object.getOwnPropertyDescriptor(prototype, name); + assert(typeof descriptor?.get === "function", `${prototype.constructor.name}.${name} getter`); + assert(descriptor.set === undefined, `${prototype.constructor.name}.${name} setter`); + assert(descriptor.enumerable && descriptor.configurable, + `${prototype.constructor.name}.${name} flags`); + return descriptor; + }; + const ns = "http://www.w3.org/2000/svg"; + + for (const constructor of [ + SVGAnimatedRect, + SVGPreserveAspectRatio, + SVGAnimatedPreserveAspectRatio, + ]) { + assert(typeof constructor === "function", `${constructor.name} constructor`); + throws(() => new constructor(), "TypeError", `${constructor.name} illegal constructor`); + } + + const fitCases = [ + ["svg", SVGSVGElement], + ["symbol", SVGSymbolElement], + ["marker", SVGMarkerElement], + ["pattern", SVGPatternElement], + ["view", SVGViewElement], + ]; + for (const [localName, constructor] of fitCases) { + accessor(constructor.prototype, "viewBox"); + accessor(constructor.prototype, "preserveAspectRatio"); + const element = document.createElementNS(ns, localName); + const viewBox = element.viewBox; + const aspectRatio = element.preserveAspectRatio; + assert(viewBox instanceof SVGAnimatedRect, `${localName}.viewBox interface`); + assert(aspectRatio instanceof SVGAnimatedPreserveAspectRatio, + `${localName}.preserveAspectRatio interface`); + assert(element.viewBox === viewBox, `${localName}.viewBox SameObject`); + assert(element.preserveAspectRatio === aspectRatio, + `${localName}.preserveAspectRatio SameObject`); + } + + const imageDescriptor = accessor( + SVGImageElement.prototype, + "preserveAspectRatio", + ); + assert(!("viewBox" in SVGImageElement.prototype), "image has no viewBox"); + const image = document.createElementNS(ns, "image"); + assert(image.preserveAspectRatio instanceof SVGAnimatedPreserveAspectRatio, + "image preserveAspectRatio interface"); + assert(image.preserveAspectRatio === image.preserveAspectRatio, + "image preserveAspectRatio SameObject"); + + const svg = document.createElementNS(ns, "svg"); + const animatedRect = svg.viewBox; + const baseRect = animatedRect.baseVal; + const animRect = animatedRect.animVal; + assert(baseRect instanceof SVGRect && animRect instanceof SVGRect, + "viewBox rectangle interfaces"); + for (const rect of [baseRect, animRect]) { + assert(!(rect instanceof DOMRect), "viewBox uses native SVGRect, not DOMRect"); + assert(Object.getPrototypeOf(rect) === SVGRect.prototype, + "viewBox native rectangle prototype"); + assert(Object.prototype.toString.call(rect) === "[object SVGRect]", + "viewBox native rectangle tag"); + } + assert(baseRect !== animRect, "viewBox base and animated rectangles are distinct"); + assert(Object.prototype.toString.call(animatedRect) === "[object SVGAnimatedRect]", + "animated rectangle tag"); + assert(baseRect.x === 0 && baseRect.y === 0 && + baseRect.width === 0 && baseRect.height === 0, "initial viewBox"); + assert(!svg.hasAttribute("viewBox"), "viewBox getter does not create attribute"); + + svg.setAttribute("viewBox", "10 20 30 40"); + assert(baseRect.x === 10 && baseRect.y === 20 && + baseRect.width === 30 && baseRect.height === 40, + "cached base rectangle tracks content attribute"); + assert(animRect.x === 10 && animRect.y === 20 && + animRect.width === 30 && animRect.height === 40, + "cached animated rectangle tracks content attribute"); + baseRect.x = 11; + assert(svg.getAttribute("viewBox") === "11 20 30 40", + "base rectangle mutation reflects to viewBox"); + assert(baseRect.x === 11 && animRect.x === 11, + "viewBox values remain synchronized after mutation"); + throws(() => { animRect.x = 12; }, "NoModificationAllowedError", + "animated rectangle is read-only"); + svg.setAttribute("viewBox", "0 0 -1 2"); + assert(baseRect.x === 0 && baseRect.width === 0, + "invalid viewBox uses the initial value"); + svg.removeAttribute("viewBox"); + assert(baseRect.x === 0 && baseRect.height === 0, + "removed viewBox uses the initial value"); + + const animatedAspectRatio = svg.preserveAspectRatio; + const baseAspectRatio = animatedAspectRatio.baseVal; + const animAspectRatio = animatedAspectRatio.animVal; + assert(baseAspectRatio instanceof SVGPreserveAspectRatio && + animAspectRatio instanceof SVGPreserveAspectRatio, + "preserveAspectRatio value interfaces"); + assert(baseAspectRatio !== animAspectRatio, + "preserveAspectRatio base and animated values are distinct"); + assert(baseAspectRatio.align === + SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMID, + "initial preserveAspectRatio align"); + assert(baseAspectRatio.meetOrSlice === + SVGPreserveAspectRatio.SVG_MEETORSLICE_MEET, + "initial preserveAspectRatio meetOrSlice"); + assert(!svg.hasAttribute("preserveAspectRatio"), + "preserveAspectRatio getter does not create attribute"); + + svg.setAttribute("preserveAspectRatio", "xMinYMax slice"); + assert(baseAspectRatio.align === + SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMINYMAX, + "cached base aspect ratio tracks content attribute"); + assert(animAspectRatio.meetOrSlice === + SVGPreserveAspectRatio.SVG_MEETORSLICE_SLICE, + "cached animated aspect ratio tracks content attribute"); + baseAspectRatio.align = + SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMAXYMIN; + baseAspectRatio.meetOrSlice = SVGPreserveAspectRatio.SVG_MEETORSLICE_SLICE; + assert(svg.getAttribute("preserveAspectRatio") === "xMaxYMin slice", + "base aspect ratio mutation reflects canonical syntax"); + assert(animAspectRatio.align === + SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMAXYMIN, + "animated aspect ratio tracks base mutation"); + baseAspectRatio.align = SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_UNKNOWN; + baseAspectRatio.meetOrSlice = SVGPreserveAspectRatio.SVG_MEETORSLICE_UNKNOWN; + assert(svg.getAttribute("preserveAspectRatio") === "xMaxYMin slice", + "unknown enumeration assignments are ignored"); + throws(() => { animAspectRatio.align = 1; }, "NoModificationAllowedError", + "animated aspect ratio align is read-only"); + throws(() => { animAspectRatio.meetOrSlice = 1; }, + "NoModificationAllowedError", "animated aspect ratio meetOrSlice is read-only"); + svg.setAttribute("preserveAspectRatio", "invalid"); + assert(baseAspectRatio.align === + SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMID && + animAspectRatio.meetOrSlice === SVGPreserveAspectRatio.SVG_MEETORSLICE_MEET, + "invalid preserveAspectRatio uses the initial value"); + svg.removeAttribute("preserveAspectRatio"); + assert(baseAspectRatio.align === + SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMID, + "removed preserveAspectRatio uses the initial value"); + + const constants = [ + ["SVG_PRESERVEASPECTRATIO_UNKNOWN", 0], + ["SVG_PRESERVEASPECTRATIO_NONE", 1], + ["SVG_PRESERVEASPECTRATIO_XMINYMIN", 2], + ["SVG_PRESERVEASPECTRATIO_XMIDYMIN", 3], + ["SVG_PRESERVEASPECTRATIO_XMAXYMIN", 4], + ["SVG_PRESERVEASPECTRATIO_XMINYMID", 5], + ["SVG_PRESERVEASPECTRATIO_XMIDYMID", 6], + ["SVG_PRESERVEASPECTRATIO_XMAXYMID", 7], + ["SVG_PRESERVEASPECTRATIO_XMINYMAX", 8], + ["SVG_PRESERVEASPECTRATIO_XMIDYMAX", 9], + ["SVG_PRESERVEASPECTRATIO_XMAXYMAX", 10], + ["SVG_MEETORSLICE_UNKNOWN", 0], + ["SVG_MEETORSLICE_MEET", 1], + ["SVG_MEETORSLICE_SLICE", 2], + ]; + for (const [name, value] of constants) { + assert(SVGPreserveAspectRatio[name] === value, `constructor ${name}`); + assert(SVGPreserveAspectRatio.prototype[name] === value, `prototype ${name}`); + } + + const viewBoxDescriptor = Object.getOwnPropertyDescriptor( + SVGSVGElement.prototype, + "viewBox", + ); + throws(() => viewBoxDescriptor.get.call(document.createElementNS(ns, "rect")), + "TypeError", "viewBox receiver brand"); + throws(() => imageDescriptor.get.call(document.createElementNS(ns, "rect")), + "TypeError", "image preserveAspectRatio receiver brand"); + const animatedRectDescriptor = Object.getOwnPropertyDescriptor( + SVGAnimatedRect.prototype, + "baseVal", + ); + throws(() => animatedRectDescriptor.get.call({}), "TypeError", + "animated rectangle receiver brand"); + const alignDescriptor = Object.getOwnPropertyDescriptor( + SVGPreserveAspectRatio.prototype, + "align", + ); + throws(() => alignDescriptor.get.call({}), "TypeError", + "preserveAspectRatio receiver brand"); + return "ok"; + })() + "#, + ) + .expect("SVG fit-to-view-box reflection probe should evaluate"); + + assert_eq!(result, "ok"); +} + +#[test] +fn svg_view_box_rects_preserve_native_float_and_reentrant_setter_semantics() { + let mut vm = new_parsed_test_vm( + "https://svg-view-box-native-rect.test/", + "", + ); + + let result = vm + .eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const throws = (callback, name) => { + try { callback(); } catch (error) { + assert(error.name === name, `expected ${name}, got ${error.name}`); + return; + } + throw new Error(`expected ${name}`); + }; + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + const fields = ["x", "y", "width", "height"]; + const values = rect => fields.map(field => rect[field]); + svg.setAttribute("viewBox", "1.2 2.4 3.6 4.8"); + const base = svg.viewBox.baseVal; + const anim = svg.viewBox.animVal; + const assertValues = expected => { + for (const rect of [base, anim]) { + assert(JSON.stringify(values(rect)) === JSON.stringify(expected.map(Math.fround)), + `float values: ${values(rect)}`); + } + }; + assertValues([1.2, 2.4, 3.6, 4.8]); + svg.setAttribute("viewBox", "5.5 6.6 7.7 8.8"); + assertValues([5.5, 6.6, 7.7, 8.8]); + base.x = "1.2"; + base.width = 1e20; + assertValues([1.2, 6.6, 1e20, 8.8]); + const reflected = svg.getAttribute("viewBox"); + assert(JSON.stringify(reflected.split(" ").map(Number).map(Math.fround)) === + JSON.stringify(values(base)), "float serialization round trip"); + for (const invalid of [NaN, Infinity, -Infinity, 1e40, undefined, Symbol()]) { + throws(() => { base.x = invalid; }, "TypeError"); + assert(svg.getAttribute("viewBox") === reflected, + "rejected float assignment preserves the attribute"); + } + throws(() => { anim.width = 1; }, "NoModificationAllowedError"); + const domRectX = Object.getOwnPropertyDescriptor(DOMRect.prototype, "x"); + throws(() => domRectX.get.call(base), "TypeError"); + throws(() => domRectX.set.call(anim, 7), "TypeError"); + assert(svg.getAttribute("viewBox") === reflected, + "read-only and foreign-interface setters preserve the attribute"); + + base.x = { + valueOf() { + svg.setAttribute("viewBox", "10 20 30 40"); + return 9; + } + }; + assert(svg.getAttribute("viewBox") === "9 20 30 40", + "numeric coercion preserves reentrant changes to the other fields"); + assertValues([9, 20, 30, 40]); + svg.setAttribute("viewBox", "0 0 1e40 1"); + assertValues([0, 0, 0, 0]); + return "ok"; + })() + "#, + ) + .expect("native SVG viewBox rectangle probe should evaluate"); + + assert_eq!(result, "ok"); +} + +#[test] +fn svg_animated_numbers_reflect_filter_and_stop_content_attributes() { + let mut vm = new_parsed_test_vm( + "https://svg-animated-number.test/", + "", + ); + + let result = vm + .eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const ns = "http://www.w3.org/2000/svg"; + const cases = [ + ["feFuncA", SVGFEFuncAElement, SVGComponentTransferFunctionElement, [ + ["slope", "slope", 1], ["intercept", "intercept", 0], + ["amplitude", "amplitude", 1], ["exponent", "exponent", 1], + ["offset", "offset", 0], + ]], + ["feComposite", SVGFECompositeElement, SVGFECompositeElement, [ + ["k1", "k1", 0], ["k2", "k2", 0], + ["k3", "k3", 0], ["k4", "k4", 0], + ]], + ["feConvolveMatrix", SVGFEConvolveMatrixElement, + SVGFEConvolveMatrixElement, [ + ["divisor", "divisor", 1], ["bias", "bias", 0], + ["kernelUnitLengthX", "kernelUnitLength", 0], + ["kernelUnitLengthY", "kernelUnitLength", 0], + ]], + ["feDiffuseLighting", SVGFEDiffuseLightingElement, + SVGFEDiffuseLightingElement, [ + ["surfaceScale", "surfaceScale", 1], + ["diffuseConstant", "diffuseConstant", 1], + ["kernelUnitLengthX", "kernelUnitLength", 0], + ["kernelUnitLengthY", "kernelUnitLength", 0], + ]], + ["feDisplacementMap", SVGFEDisplacementMapElement, + SVGFEDisplacementMapElement, [["scale", "scale", 0]]], + ["feDistantLight", SVGFEDistantLightElement, + SVGFEDistantLightElement, [ + ["azimuth", "azimuth", 0], ["elevation", "elevation", 0], + ]], + ["feDropShadow", SVGFEDropShadowElement, SVGFEDropShadowElement, [ + ["dx", "dx", 2], ["dy", "dy", 2], + ["stdDeviationX", "stdDeviation", 2], + ["stdDeviationY", "stdDeviation", 2], + ]], + ["feGaussianBlur", SVGFEGaussianBlurElement, + SVGFEGaussianBlurElement, [ + ["stdDeviationX", "stdDeviation", 0], + ["stdDeviationY", "stdDeviation", 0], + ]], + ["feMorphology", SVGFEMorphologyElement, SVGFEMorphologyElement, [ + ["radiusX", "radius", 0], ["radiusY", "radius", 0], + ]], + ["feOffset", SVGFEOffsetElement, SVGFEOffsetElement, [ + ["dx", "dx", 0], ["dy", "dy", 0], + ]], + ["fePointLight", SVGFEPointLightElement, SVGFEPointLightElement, [ + ["x", "x", 0], ["y", "y", 0], ["z", "z", 0], + ]], + ["feSpecularLighting", SVGFESpecularLightingElement, + SVGFESpecularLightingElement, [ + ["surfaceScale", "surfaceScale", 1], + ["specularConstant", "specularConstant", 1], + ["specularExponent", "specularExponent", 1], + ["kernelUnitLengthX", "kernelUnitLength", 0], + ["kernelUnitLengthY", "kernelUnitLength", 0], + ]], + ["feSpotLight", SVGFESpotLightElement, SVGFESpotLightElement, [ + ["x", "x", 0], ["y", "y", 0], ["z", "z", 0], + ["pointsAtX", "pointsAtX", 0], ["pointsAtY", "pointsAtY", 0], + ["pointsAtZ", "pointsAtZ", 0], + ["specularExponent", "specularExponent", 1], + ["limitingConeAngle", "limitingConeAngle", 0], + ]], + ["feTurbulence", SVGFETurbulenceElement, SVGFETurbulenceElement, [ + ["baseFrequencyX", "baseFrequency", 0], + ["baseFrequencyY", "baseFrequency", 0], ["seed", "seed", 0], + ]], + ["stop", SVGStopElement, SVGStopElement, [["offset", "offset", 0]]], + ]; + + assert(typeof SVGAnimatedNumber === "function", "constructor exposed"); + let illegalConstructor = false; + try { + new SVGAnimatedNumber(); + } catch (error) { + illegalConstructor = error instanceof TypeError; + } + assert(illegalConstructor, "illegal constructor"); + + const baseDescriptor = Object.getOwnPropertyDescriptor( + SVGAnimatedNumber.prototype, + "baseVal", + ); + const animDescriptor = Object.getOwnPropertyDescriptor( + SVGAnimatedNumber.prototype, + "animVal", + ); + assert(typeof baseDescriptor.get === "function" && + typeof baseDescriptor.set === "function", "baseVal descriptor"); + assert(typeof animDescriptor.get === "function" && animDescriptor.set === undefined, + "animVal descriptor"); + + for (const [tag, constructor, owner, properties] of cases) { + const element = document.createElementNS(ns, tag); + assert(element instanceof constructor, `${tag} interface`); + for (const [property, attribute, initial] of properties) { + const descriptor = Object.getOwnPropertyDescriptor(owner.prototype, property); + assert(typeof descriptor.get === "function" && descriptor.set === undefined, + `${owner.name}.${property} descriptor`); + assert(descriptor.enumerable && descriptor.configurable, + `${owner.name}.${property} descriptor flags`); + + const animated = element[property]; + assert(animated instanceof SVGAnimatedNumber, `${tag}.${property} interface`); + assert(element[property] === animated, `${tag}.${property} SameObject`); + assert(animated.baseVal === initial && animated.animVal === initial, + `${tag}.${property} initial value`); + + element.setAttribute(attribute, "42"); + assert(animated.baseVal === 42 && animated.animVal === 42, + `${tag}.${property} live content update`); + element.setAttribute(attribute, "foobar"); + assert(animated.baseVal === initial && animated.animVal === initial, + `${tag}.${property} invalid fallback`); + element.removeAttribute(attribute); + assert(animated.baseVal === initial && animated.animVal === initial, + `${tag}.${property} removed fallback`); + } + } + + const gaussian = document.createElementNS(ns, "feGaussianBlur"); + const deviationX = gaussian.stdDeviationX; + const deviationY = gaussian.stdDeviationY; + gaussian.setAttribute("stdDeviation", "5"); + assert(deviationX.baseVal === 5 && deviationY.baseVal === 5, + "single optional-number value"); + gaussian.setAttribute("stdDeviation", "5, 7"); + assert(deviationX.baseVal === 5 && deviationY.baseVal === 7, + "paired optional-number values"); + deviationX.baseVal = 3; + assert(gaussian.getAttribute("stdDeviation") === "3 7", + "first pair component reflection"); + deviationY.baseVal = 9; + assert(gaussian.getAttribute("stdDeviation") === "3 9" && + deviationX.baseVal === 3 && deviationY.animVal === 9, + "second pair component reflection"); + + const specular = document.createElementNS(ns, "feSpecularLighting"); + const surfaceScale = specular.surfaceScale; + surfaceScale.baseVal = -1; + assert(surfaceScale.baseVal === -1 && surfaceScale.animVal === -1 && + specular.getAttribute("surfaceScale") === "-1", "scalar reflection"); + surfaceScale.baseVal = 300; + for (const invalid of ["aString", NaN, Infinity, specular]) { + let rejected = false; + try { + surfaceScale.baseVal = invalid; + } catch (error) { + rejected = error instanceof TypeError; + } + assert(rejected && surfaceScale.baseVal === 300, + "restricted float rejects invalid values"); + } + + const stop = document.createElementNS(ns, "stop"); + const stopOffset = stop.offset; + stop.setAttribute("offset", "50%"); + assert(stopOffset.baseVal === 0.5 && stopOffset.animVal === 0.5, + "stop percentage content value"); + stopOffset.baseVal = 0.25; + assert(stop.getAttribute("offset") === "0.25", "stop number reflection"); + + let incompatibleAnimatedReceiver = false; + try { + baseDescriptor.get.call({}); + } catch (error) { + incompatibleAnimatedReceiver = error instanceof TypeError; + } + assert(incompatibleAnimatedReceiver, "animated number receiver brand"); + + const k1Descriptor = Object.getOwnPropertyDescriptor( + SVGFECompositeElement.prototype, + "k1", + ); + let incompatibleElementReceiver = false; + try { + k1Descriptor.get.call(specular); + } catch (error) { + incompatibleElementReceiver = error instanceof TypeError; + } + assert(incompatibleElementReceiver, "element receiver brand"); + return "ok"; + })() + "#, + ) + .expect("SVG animated number probe should evaluate"); + + assert_eq!(result, "ok"); +} + +#[test] +fn svg_lengths_preserve_specified_units_and_resolve_live_context() { + let mut vm = new_parsed_test_vm( + "https://svg-length-context.test/", + r#" + + + + + + + + + + + + + + "#, + ); + + let result = vm + .eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const close = (actual, expected, tolerance = 1e-6) => + Math.abs(actual - expected) <= tolerance; + const svg = document.getElementById("svg"); + + const standalone = svg.createSVGLength(); + standalone.valueAsString = "48px"; + standalone.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_CM); + assert(standalone.valueAsString === "1.27cm", + `absolute serialization: ${standalone.valueAsString}`); + assert(close(standalone.valueInSpecifiedUnits, 1.27), + "absolute specified value"); + assert(close(standalone.value, 48), "absolute user value"); + standalone.value = 96; + assert(close(standalone.valueInSpecifiedUnits, 2.54), + "value setter preserves unit"); + standalone.valueInSpecifiedUnits = 1; + assert(close(standalone.value, 96 / 2.54), + "specified setter resolves user value"); + + standalone.valueAsString = "40q"; + assert(standalone.unitType === SVGLength.SVG_LENGTHTYPE_UNKNOWN, + "modern unit type"); + assert(close(standalone.value, 96 / 2.54), "quarter millimeter value"); + standalone.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_MM); + assert(standalone.valueAsString === "10mm", "quarter millimeter conversion"); + + const rect = document.getElementById("rect"); + const x = rect.x.baseVal; + x.valueAsString = "3px"; + x.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PERCENTAGE); + assert(x.valueAsString === "2%" && close(x.value, 3), + `percentage conversion uses SVG viewport: ${x.valueAsString}|${x.value}|${x.valueInSpecifiedUnits}`); + x.valueAsString = "6px"; + x.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_EMS); + assert(x.valueAsString === "0.5em" && close(x.value, 6), + "em conversion uses owner font size"); + + svg.removeAttribute("viewBox"); + x.valueAsString = "50%"; + assert(close(x.value, 75), "initial percentage basis"); + svg.width.baseVal.value = 300; + assert(close(x.value, 150), "live percentage basis"); + + const ch = document.getElementById("text").x.baseVal[0]; + assert(ch.unitType === SVGLength.SVG_LENGTHTYPE_UNKNOWN, + "list modern unit type"); + assert(close(ch.value, 100), "ch resolves from owner font size"); + ch.value = 200; + assert(close(ch.valueInSpecifiedUnits, 20), "ch reverse conversion"); + + for (const [id, expected] of [ + ["cap", 140], + ["ic", 200], + ["lh", 240], + ["rlh", 240], + ["rem", 100], + ]) { + const modern = document.getElementById(id).x.baseVal[0] ?? + document.getElementById(id).x.baseVal; + assert(modern.unitType === SVGLength.SVG_LENGTHTYPE_UNKNOWN, + `${id} unit type`); + assert(close(modern.value, expected), `${id} context value`); + const specified = modern.valueInSpecifiedUnits; + modern.value = expected * 2; + assert(close(modern.valueInSpecifiedUnits, specified * 2), + `${id} reverse conversion`); + } + + const viewport = document.getElementById("viewport").x.baseVal; + const viewportValue = viewport.value; + viewport.value = viewportValue * 2; + assert(close(viewport.valueInSpecifiedUnits, 20), + "viewport reverse conversion"); + return "ok"; + })() + "#, + ) + .expect("SVG length context probe should evaluate"); + + assert_eq!(result, "ok"); +} + +#[test] +fn svg_switch_and_mpath_use_native_interfaces_and_live_href() { + let mut vm = new_parsed_test_vm("https://svg-switch-mpath.test/", ""); + assert_eq!( + vm.eval(include_str!("svg_switch_mpath.js")) + .expect("SVG switch and mpath fixture should evaluate"), + "true" + ); +} + +#[test] +fn svg_animation_elements_use_native_interfaces_and_event_handlers() { + let mut vm = new_parsed_test_vm( + "https://svg-animation.test/", + "", + ); + vm.eval(include_str!("svg_animation_interfaces.js")) + .unwrap(); + assert_eq!( + vm.eval("JSON.stringify(__nodeReplacementResults.failures)") + .unwrap(), + "[]" + ); +} + +#[test] +fn svg_animation_timing_shim_validates_arguments_before_reporting_unsupported() { + let mut vm = new_parsed_test_vm( + "https://svg-animation-timing.test/", + "", + ); + assert_eq!(vm.eval(r#" + (() => { + const other = document.getElementById('child').contentWindow; + for (const realm of [window, other]) { + const element = document.createElementNS('http://www.w3.org/2000/svg', 'animate'); + for (const name of ['beginElement', 'endElement', 'beginElementAt', 'endElementAt']) { + let error, conversions = 0; + try {realm.SVGAnimationElement.prototype[name].call(element, {valueOf() {conversions++; return 0.25;}});} + catch (caught) {error = caught;} + if (!(error instanceof realm.DOMException) || error.name !== 'NotSupportedError') throw Error(name + ' shim error'); + if (conversions !== (name.endsWith('At') ? 1 : 0)) throw Error(name + ' argument conversion'); + if (element.getCurrentTime() !== 0) throw Error('shim cannot advance the timeline'); + } + } + return true; + })() + "#).unwrap(), "true"); +} + +#[test] +fn svg_filter_primitives_expose_native_dom_attributes() { + let mut vm = new_parsed_test_vm("https://svg-filter-dom.test/", ""); + assert_eq!( + vm.eval(include_str!("svg_filter_primitives.js")) + .expect("SVG filter DOM fixture should evaluate"), + "true" + ); +} diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/svg_animation_interfaces.js b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/svg_animation_interfaces.js new file mode 100644 index 0000000000..8c2f80326c --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/svg_animation_interfaces.js @@ -0,0 +1,174 @@ +(() => { + const ns = 'http://www.w3.org/2000/svg', xlink = 'http://www.w3.org/1999/xlink'; + const cases = [['animate', 'SVGAnimateElement'], ['set', 'SVGSetElement'], + ['animateMotion', 'SVGAnimateMotionElement'], ['animateTransform', 'SVGAnimateTransformElement']]; + const rows = []; + const assert = (condition, message) => { if (!condition) throw Error(message); }; + const check = (name, run) => { + try { run(); rows.push({name, pass: true}); } + catch (error) { rows.push({name, pass: false, error: error.name, message: error.message}); } + }; + const throws = (run, Constructor, name = Constructor.name) => { + let error; + try { run(); } catch (caught) { error = caught; } + assert(error instanceof Constructor && error.name === name, name + ' required'); + }; + const child = document.getElementById('child').contentWindow; + const realms = [['main', window], ['child', child]]; + for (const [owner, realm] of realms) { + const Base = realm.SVGAnimationElement; + check(owner + '/base', () => { + assert(Object.getPrototypeOf(Base) === realm.SVGElement, 'base constructor inheritance'); + assert(Object.getPrototypeOf(Base.prototype) === realm.SVGElement.prototype, 'base prototype inheritance'); + throws(() => new Base(), realm.TypeError); + throws(() => Base(), realm.TypeError); + assert(!('onbegin' in realm.document.createElementNS(ns, 'rect')), 'only animation interfaces expose onbegin'); + }); + const documents = [['live', realm.document], + ['html', realm.document.implementation.createHTMLDocument('')], + ['xml', realm.document.implementation.createDocument(ns, 'svg')], + ['parser', new realm.DOMParser().parseFromString('', 'image/svg+xml')]]; + for (const [tag, name] of cases) { + const Constructor = realm[name]; + check(owner+'/'+tag+'/constructor', () => { + assert(Constructor.name === name && Constructor.length === 0, 'constructor metadata'); + assert(Object.getPrototypeOf(Constructor) === Base, 'constructor inheritance'); + assert(Object.getPrototypeOf(Constructor.prototype) === Base.prototype, 'prototype inheritance'); + throws(() => new Constructor(), realm.TypeError); + throws(() => Constructor(), realm.TypeError); + }); + for (const [kind, doc] of documents) { + const label=owner+'/'+kind+'/'+tag, element=doc.createElementNS(ns, tag); + check(label+'/factory', () => { + assert(Object.getPrototypeOf(element) === Constructor.prototype, 'native prototype'); + assert(element instanceof Base && element instanceof realm.SVGElement && !(element instanceof realm.SVGGraphicsElement), 'native inheritance'); + assert(Object.prototype.toString.call(element)==='[object '+name+']', 'native tag'); + assert(Object.getPrototypeOf(element.cloneNode())===Constructor.prototype, 'clone identity'); + assert(Object.getPrototypeOf(realm.document.importNode(element))===Constructor.prototype, 'import identity'); + assert(!(doc.createElementNS('urn:other', tag) instanceof Constructor), 'namespace-sensitive factory'); + assert(!(doc.createElementNS(ns, tag.toUpperCase()) instanceof Constructor), 'case-sensitive factory'); + }); + check(label+'/SVGTests', () => { + for (const attr of ['requiredExtensions', 'systemLanguage']) { + const list = element[attr]; + assert(list instanceof realm.SVGStringList && element[attr] === list, 'stable native list'); + list.appendItem('test'); + assert(element.getAttribute(attr)==='test', 'list writes native attribute'); + element.setAttribute(attr, 'replacement'); + assert(list.numberOfItems===1 && list.getItem(0)==='replacement', 'attribute refreshes cached list'); + element.removeAttribute(attr); + assert(list.numberOfItems===0, 'removal clears list'); + } + }); + check(label+'/target', () => { + const svg=doc.createElementNS(ns,'svg'), parent=doc.createElementNS(ns,'g'), target=doc.createElementNS(ns,'rect'); + target.id='svg-animation-target id'; parent.append(element); svg.append(parent,target); + assert(element.targetElement===null, 'detached subtree has no target'); + (doc.body || doc.documentElement).append(svg); + try { + assert(element.targetElement===parent, 'parent target'); + element.setAttributeNS(xlink,'other:href','#svg-animation-target%20id'); + assert(element.targetElement===target, 'namespaced fallback and decoded fragment'); + element.setAttribute('href','#missing'); + assert(element.targetElement===null, 'href takes precedence'); + element.setAttribute('href',''); + assert(element.targetElement===parent, 'empty href targets parent'); + element.removeAttribute('href'); + assert(element.targetElement===target, 'remove href restores xlink'); + element.setAttribute('href','https://external-svg.test/#svg-animation-target%20id'); + assert(element.targetElement===null, 'external URL does not resolve locally'); + element.setAttribute('href','#svg-animation-target%20id'); + target.id='renamed'; assert(element.targetElement===null, 'ID mutations refresh target'); + target.id='svg-animation-target id'; assert(element.targetElement===target, 'target restored'); + target.remove(); assert(element.targetElement===null, 'removed reference target'); + element.removeAttribute('href'); element.removeAttributeNS(xlink,'href'); + assert(element.targetElement===parent, 'fallback parent restored'); + } finally {svg.remove();} + assert(element.targetElement===null, 'removed animation loses target'); + }); + check(label+'/handlers', () => { + for (const [name,type] of [['onbegin','beginEvent'],['onend','endEvent'],['onrepeat','repeatEvent']]) { + const log=[], before=()=>log.push('before'),after=()=>log.push('after'); + const first=function(e){assert(this===element && e.type===type,'handler receiver and event');log.push('first');return false;}; + const replacement=function(){log.push('replacement');}; + assert(element[name]===null,'initial null'); + element.addEventListener(type,before); element[name]=first; element.addEventListener(type,after); element[name]=replacement; + element.dispatchEvent(new realm.Event(type)); + assert(log.slice().sort().join(',')==='after,before,replacement','replacement invokes current handler exactly once');log.length=0; + element.dispatchEvent(new realm.Event(name.slice(2))); + assert(log.length===0,'short event name does not invoke SVG handler'); + element[name]=null;element.dispatchEvent(new realm.Event(type)); + assert(log.join(',')==='before,after','null clears handler');log.length=0; + element[name]=first;const event=new realm.Event(type,{cancelable:true}); + assert(!element.dispatchEvent(event) && event.defaultPrevented,'return false cancels event'); + assert(log.slice().sort().join(',')==='after,before,first','reassignment restores the handler exactly once'); + element[name]=null;element.removeEventListener(type,before);element.removeEventListener(type,after); + } + }); + } + } + check(owner+'/parser', () => { + const svg=realm.document.createElementNS(ns,'svg'); + svg.innerHTML=''; + for (let i=0;i { + const svg=realm.document.createElementNS(ns,'svg'), element=realm.document.createElementNS(ns,'animate'); + svg.append(element);realm.document.body.append(svg); + try { + for(const [name,type] of [['onbegin','beginEvent'],['onend','endEvent'],['onrepeat','repeatEvent']]) { + element.setAttribute(name,'this.setAttribute("data-event", event.type)'); + element.dispatchEvent(new realm.Event(name.slice(2))); + assert(!element.hasAttribute('data-event'),'short name does not compile content handler'); + element.dispatchEvent(new realm.Event(type)); + assert(element.getAttribute('data-event')===type,'content handler observes canonical event type'); + element.removeAttribute(name);element.removeAttribute('data-event'); + element.dispatchEvent(new realm.Event(type)); + assert(!element.hasAttribute('data-event'),'removal unregisters content handler'); + } + } finally {svg.remove();} + }); + } + for(const [sourceName,source] of realms) for(const [ownerName,owner] of realms) { + check(sourceName+'->'+ownerName+'/receiver-and-realm', () => { + const element=owner.document.createElementNS(ns,'animate'); + const Base=source.SVGAnimationElement; + const revoked=Proxy.revocable(element,{});revoked.revoke(); + let traps=0,conversions=0; + const trap=()=>{traps++;throw Error('trap');}; + const bad=[{},Base.prototype,Object.create(element),new Proxy(element,{get:trap,getPrototypeOf:trap}),revoked.proxy,owner.document.createElementNS(ns,'rect')]; + for(const name of ['targetElement','requiredExtensions','systemLanguage','onbegin','onend','onrepeat']) { + const descriptor=Object.getOwnPropertyDescriptor(Base.prototype,name); + assert(descriptor.enumerable && descriptor.configurable && descriptor.get.length===0,'accessor descriptor'); + for(const value of bad) { + throws(()=>descriptor.get.call(value),source.TypeError); + if(descriptor.set) throws(()=>descriptor.set.call(value,()=>{}),source.TypeError); + } + } + for(const [name,length] of [['getStartTime',0],['getCurrentTime',0],['getSimpleDuration',0],['beginElement',0],['beginElementAt',1],['endElement',0],['endElementAt',1]]) { + const descriptor=Object.getOwnPropertyDescriptor(Base.prototype,name), method=descriptor.value; + assert(descriptor.enumerable && descriptor.configurable && descriptor.writable && method.length===length && method.name===name,'method descriptor'); + for(const value of bad) throws(()=>method.call(value,{valueOf(){conversions++;return 1;}}),source.TypeError); + if(length) { + throws(()=>method.call(element),source.TypeError); + for(const value of [NaN,Infinity,-Infinity,1e100,Symbol(),1n]) throws(()=>method.call(element,value),source.TypeError); + const sentinel={};let caught; + try {method.call(element,{valueOf(){throw sentinel;}});} catch(error){caught=error;} + assert(caught===sentinel,'conversion exception propagated'); + } + } + assert(traps===0 && conversions===0,'brand checked before any author code'); + assert(Base.prototype.getCurrentTime.call(element)===0,'detached current time'); + throws(()=>Base.prototype.getStartTime.call(element),source.DOMException,'InvalidStateError'); + throws(()=>Base.prototype.getSimpleDuration.call(element),source.DOMException,'NotSupportedError'); + const listGet=Object.getOwnPropertyDescriptor(Base.prototype,'systemLanguage').get; + assert(listGet.call(element) instanceof owner.SVGStringList,'list belongs to element realm'); + const graphicsGet=Object.getOwnPropertyDescriptor(source.SVGGraphicsElement.prototype,'systemLanguage').get; + throws(()=>graphicsGet.call(element),source.TypeError); + const handler=Object.getOwnPropertyDescriptor(Base.prototype,'onbegin'),callback=()=>{}; + handler.set.call(element,callback);assert(handler.get.call(element)===callback,'genuine cross-realm receiver');handler.set.call(element,null); + }); + } + globalThis.__nodeReplacementResults={rows,failures:rows.filter(row=>!row.pass),passed:rows.filter(row=>row.pass).length,total:rows.length}; + return rows.every(row=>row.pass); +})() diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/svg_filter_primitives.js b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/svg_filter_primitives.js new file mode 100644 index 0000000000..717b939803 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/svg_filter_primitives.js @@ -0,0 +1,130 @@ +(() => { + const check = (value, message) => { if (!value) throw new Error(message); }; + const throws = (callback, name, Constructor = Error) => { + let error; + try { callback(); } catch (caught) { error = caught; } + check(error && error.name === name && error instanceof Constructor, name + ' required'); + }; + const ns = 'http://www.w3.org/2000/svg'; + const xlink = 'http://www.w3.org/1999/xlink'; + const cases = [ + ['feComponentTransfer', SVGFEComponentTransferElement, ['in1']], + ['feFlood', SVGFEFloodElement, []], + ['feImage', SVGFEImageElement, ['href', 'preserveAspectRatio']], + ['feMerge', SVGFEMergeElement, []], + ['feMergeNode', SVGFEMergeNodeElement, ['in1']], + ['feTile', SVGFETileElement, ['in1']], + ]; + const documents = [document, document.implementation.createHTMLDocument(), + document.implementation.createDocument(ns, 'svg'), + new DOMParser().parseFromString('', 'image/svg+xml')]; + const standard = ['x', 'y', 'width', 'height', 'result']; + for (const [tag, Constructor, extra] of cases) { + check(Constructor.length === 0, tag + ' constructor length'); + check(Object.getPrototypeOf(Constructor) === SVGElement, tag + ' constructor inheritance'); + check(Object.getPrototypeOf(Constructor.prototype) === SVGElement.prototype, tag + ' prototype inheritance'); + throws(() => Constructor(), 'TypeError', TypeError); + throws(() => new Constructor(), 'TypeError', TypeError); + const attributes = [...(tag === 'feMergeNode' ? [] : standard), ...extra]; + for (const doc of documents) { + const element = doc.createElementNS(ns, tag); + check(Object.getPrototypeOf(element) === Constructor.prototype, tag + ' factory prototype'); + check(Object.prototype.toString.call(element) === '[object ' + Constructor.name + ']', tag + ' native tag'); + check(element instanceof SVGElement && !(element instanceof SVGGraphicsElement), tag + ' native inheritance'); + check(Object.getPrototypeOf(element.cloneNode()) === Constructor.prototype, tag + ' clone interface'); + check(Object.getPrototypeOf(document.importNode(element)) === Constructor.prototype, tag + ' import interface'); + check(!(doc.createElementNS(ns, tag.toUpperCase()) instanceof Constructor), tag + ' case-sensitive namespace factory'); + check(!(doc.createElementNS('urn:other', tag) instanceof Constructor), tag + ' namespace-sensitive factory'); + let traps = 0; + const revoked = Proxy.revocable(element, {});revoked.revoke(); + for (const name of attributes) { + const descriptor = Object.getOwnPropertyDescriptor(Constructor.prototype, name); + check(descriptor && descriptor.enumerable && descriptor.configurable && !descriptor.set, tag + '.' + name + ' readonly descriptor'); + check(descriptor.get.name === 'get ' + name && descriptor.get.length === 0, tag + '.' + name + ' getter metadata'); + const value = element[name]; + check(value === element[name] && !Object.hasOwn(element, name), tag + '.' + name + ' stable prototype accessor'); + for (const receiver of [{}, Constructor.prototype, Object.create(element), + new Proxy(element, {get() {traps++;throw Error('author trap');}}), revoked.proxy, + doc.createElementNS(ns, tag === 'feTile' ? 'feFlood' : 'feTile')]) { + throws(() => descriptor.get.call(receiver), 'TypeError', TypeError); + } + } + check(traps === 0, tag + ' brand check does not run Proxy traps'); + if (tag === 'feMergeNode') { + for (const name of standard) check(!(name in element), 'merge node has no primitive region'); + } else { + for (const [name, initial] of [['x','0%'],['y','0%'],['width','100%'],['height','100%']]) { + const animated = element[name]; + check(animated instanceof SVGAnimatedLength, tag + '.' + name + ' animated length'); + check(animated.baseVal.valueAsString === initial && animated.animVal.valueAsString === initial, tag + '.' + name + ' initial'); + element.setAttribute(name, '12'); + check(animated.baseVal.value === 12 && animated.animVal.value === 12, tag + '.' + name + ' live attribute reflection'); + animated.baseVal.valueAsString = '25%'; + check(element.getAttribute(name) === '25%' && animated.animVal.valueAsString === '25%', tag + '.' + name + ' reflected setter'); + throws(() => {animated.animVal.value = 1;}, 'NoModificationAllowedError', DOMException); + element.removeAttribute(name); + check(animated.baseVal.valueAsString === initial, tag + '.' + name + ' removal restores default'); + } + } + for (const name of ['result', 'in1'].filter(name => attributes.includes(name))) { + const attribute = name === 'in1' ? 'in' : name; + const value = element[name]; + check(value instanceof SVGAnimatedString && value.baseVal === '' && value.animVal === '', tag + '.' + name + ' initial string'); + element.setAttribute(attribute, 'SourceGraphic'); + check(value.baseVal === 'SourceGraphic' && value.animVal === 'SourceGraphic', tag + '.' + name + ' cached reflection'); + let conversions = 0; + value.baseVal = {toString() {conversions++;return 'SourceAlpha';}}; + check(conversions === 1 && element.getAttribute(attribute) === 'SourceAlpha' && value.animVal === 'SourceAlpha', tag + '.' + name + ' native mutation'); + element.removeAttribute(attribute); + check(value.baseVal === '' && value.animVal === '', tag + '.' + name + ' string removal'); + } + if (tag === 'feImage') { + const href = element.href; + element.setAttributeNS(xlink, 'xlink:href', '#fallback'); + check(href.baseVal === '#fallback', 'xlink href fallback'); + href.baseVal = '#changed'; + check(element.getAttributeNS(xlink, 'href') === '#changed' && !element.hasAttribute('href'), 'setter reflects existing xlink href'); + element.setAttribute('href', '#preferred'); + check(href.animVal === '#preferred', 'unprefixed href has priority'); + href.baseVal = '#updated'; + check(element.getAttribute('href') === '#updated' && element.getAttributeNS(xlink, 'href') === '#changed', 'setter follows href precedence'); + element.removeAttribute('href'); + check(href.baseVal === '#changed', 'removal restores xlink fallback'); + const ratio = element.preserveAspectRatio; + check(ratio instanceof SVGAnimatedPreserveAspectRatio && ratio.baseVal.align === 6 && ratio.baseVal.meetOrSlice === 1, 'initial preserveAspectRatio'); + element.setAttribute('preserveAspectRatio', 'xMaxYMax slice'); + check(ratio.baseVal.align === 10 && ratio.animVal.meetOrSlice === 2, 'preserveAspectRatio live reflection'); + ratio.baseVal.align = 2; + check(element.getAttribute('preserveAspectRatio') === 'xMinYMin slice', 'preserveAspectRatio reflected setter'); + throws(() => {ratio.animVal.align = 3;}, 'NoModificationAllowedError', DOMException); + } + } + } + const parsed = new DOMParser().parseFromString('' + cases.map(([tag]) => '<' + tag + '/>').join('') + '', 'image/svg+xml'); + const html = document.createElement('div'); + html.innerHTML = '' + cases.map(([tag]) => '<' + tag.toLowerCase() + '/>').join('') + ''; + for (const [i, [, Constructor]] of cases.entries()) { + check(parsed.documentElement.children[i] instanceof Constructor, 'XML parser interface ' + Constructor.name + ': ' + Object.prototype.toString.call(parsed.documentElement.children[i]) + ' ' + parsed.documentElement.innerHTML); + check(html.firstChild.children[i] instanceof Constructor, 'HTML foreign-content case adjustment'); + } + const frame = document.createElement('iframe');document.body.append(frame); + const other = frame.contentWindow; + for (const [tag, Constructor, extra] of cases) { + const foreign = other.document.createElementNS(ns, tag); + const attributes = [...(tag === 'feMergeNode' ? [] : standard), ...extra]; + for (const name of attributes) { + const getter = Object.getOwnPropertyDescriptor(Constructor.prototype, name).get; + const otherGetter = Object.getOwnPropertyDescriptor(other[Constructor.name].prototype, name).get; + const value = getter.call(foreign); + const ValueConstructor = ['x','y','width','height'].includes(name) ? 'SVGAnimatedLength' : name === 'preserveAspectRatio' ? 'SVGAnimatedPreserveAspectRatio' : 'SVGAnimatedString'; + check(value instanceof other[ValueConstructor] && !(value instanceof window[ValueConstructor]), tag + '.' + name + ' owner realm'); + throws(() => otherGetter.call({}), 'TypeError', other.TypeError); + } + } + const kept = other.document.createElementNS(ns, 'feTile'); + const input = kept.in1; + frame.remove(); + input.baseVal = 'after-removal'; + check(kept.getAttribute('in') === 'after-removal', 'retained filter value survives iframe removal'); + return true; +})() diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/svg_switch_mpath.js b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/svg_switch_mpath.js new file mode 100644 index 0000000000..f63e463c58 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/svg_switch_mpath.js @@ -0,0 +1,119 @@ +(() => { + const check = (ok, message) => { if (!ok) throw new Error(message); }; + const throws = (run, Constructor = TypeError) => { + let error; + try { run(); } catch (caught) { error = caught; } + check(error instanceof Constructor, 'expected ' + Constructor.name + ', got ' + error); + }; + const ns = 'http://www.w3.org/2000/svg'; + const xlink = 'http://www.w3.org/1999/xlink'; + const cases = [ + ['switch', SVGSwitchElement, SVGGraphicsElement], + ['mpath', SVGMPathElement, SVGElement], + ]; + const documents = [document, document.implementation.createHTMLDocument(), + document.implementation.createDocument(ns, 'svg'), + new DOMParser().parseFromString('', 'image/svg+xml')]; + for (const [tag, Constructor, Parent] of cases) { + check(Constructor.length === 0 && Constructor.name === 'SVG' + (tag === 'switch' ? 'Switch' : 'MPath') + 'Element', tag + ' constructor metadata'); + check(Object.getPrototypeOf(Constructor) === Parent, tag + ' constructor inheritance'); + check(Object.getPrototypeOf(Constructor.prototype) === Parent.prototype, tag + ' prototype inheritance'); + throws(() => Constructor()); + throws(() => new Constructor()); + for (const doc of documents) { + const element = doc.createElementNS(ns, 's:' + tag); + check(Object.getPrototypeOf(element) === Constructor.prototype, tag + ' factory prototype'); + check(element instanceof Parent && element instanceof SVGElement, tag + ' native inheritance'); + check((element instanceof SVGGraphicsElement) === (tag === 'switch'), tag + ' graphics identity'); + check(Object.prototype.toString.call(element) === '[object ' + Constructor.name + ']', tag + ' native tag'); + check(element.prefix === 's' && element.localName === tag, tag + ' qualified name'); + check(Object.getPrototypeOf(element.cloneNode(true)) === Constructor.prototype, tag + ' clone'); + check(Object.getPrototypeOf(document.importNode(element, true)) === Constructor.prototype, tag + ' import'); + check(!(doc.createElementNS(ns, tag.toUpperCase()) instanceof Constructor), tag + ' case-sensitive factory'); + check(!(doc.createElementNS('urn:other', tag) instanceof Constructor), tag + ' namespace-sensitive factory'); + check(!(doc.createElement(tag) instanceof Constructor), tag + ' HTML factory'); + let events = 0; + element.addEventListener('check', () => events++); + element.dispatchEvent(new Event('check')); + check(events === 1, tag + ' inherited EventTarget brand'); + if (tag === 'switch') { + element.setAttribute('transform', 'translate(4 7)'); + check(element.transform.baseVal.getItem(0).matrix.e === 4, 'inherited live transform'); + element.systemLanguage.appendItem('en'); + check(element.getAttribute('systemLanguage') === 'en', 'inherited SVGTests list'); + check(!('href' in element), 'switch has no URI-reference mixin'); + continue; + } + const descriptor = Object.getOwnPropertyDescriptor(Constructor.prototype, 'href'); + check(descriptor.enumerable && descriptor.configurable && !descriptor.set && + descriptor.get.name === 'get href' && descriptor.get.length === 0, 'href descriptor'); + const href = element.href; + check(href instanceof SVGAnimatedString && href === element.href && !Object.hasOwn(element, 'href'), 'cached href object'); + check(href.baseVal === '' && href.animVal === '', 'missing href'); + const observer = new MutationObserver(() => {}); + observer.observe(element, {attributes: true, attributeOldValue: true}); + element.setAttributeNS(xlink, 'xlink:href', '#fallback'); + check(href.baseVal === '#fallback', 'xlink fallback'); + href.baseVal = '#changed'; + check(element.getAttributeNS(xlink, 'href') === '#changed' && !element.hasAttribute('href'), 'write existing xlink'); + element.setAttribute('href', ''); + check(href.animVal === '', 'empty href overrides xlink'); + let conversions = 0; + href.baseVal = {toString() { conversions++; return '#preferred'; }}; + check(conversions === 1 && href.baseVal === '#preferred' && element.getAttribute('href') === '#preferred', 'convert and reflect href'); + check(element.getAttributeNS(xlink, 'href') === '#changed', 'preserve fallback attribute'); + const records = observer.takeRecords(); + check(records.length === 4 && records[1].attributeNamespace === xlink && + records[1].oldValue === '#fallback' && records[3].attributeNamespace === null && + records[3].oldValue === '', 'native mutation records'); + observer.disconnect(); + element.setAttribute('href', '#before-after'); + check(href.baseVal === '#before-after' && href.animVal === '#before-after', 'native attribute read'); + element.removeAttribute('href'); + check(href.baseVal === '#changed', 'removal reveals xlink'); + element.removeAttributeNS(xlink, 'href'); + check(href.baseVal === '' && href.animVal === '', 'remove both'); + check(!Reflect.set(href, 'animVal', '#ignored') && href.animVal === '', 'readonly animated value'); + const sentinel = {}; + try { href.baseVal = {toString() {throw sentinel;}}; throw Error('missing conversion exception'); } + catch (error) {check(error === sentinel && !element.hasAttribute('href'), 'conversion exception leaves native state unchanged');} + let traps = 0; + const revoked = Proxy.revocable(element, {}); revoked.revoke(); + for (const receiver of [{}, Constructor.prototype, Object.create(element), + new Proxy(element, {get() {traps++; throw Error('author trap');}}), revoked.proxy, + doc.createElementNS(ns, 'use'), doc.createElementNS(ns, 'switch')]) { + throws(() => descriptor.get.call(receiver)); + } + check(traps === 0, 'brand check does not run author Proxy traps'); + Object.setPrototypeOf(element, null); + check(descriptor.get.call(element) === href, 'native identity survives prototype mutation'); + href.baseVal = '#native'; + check(Element.prototype.getAttribute.call(element, 'href') === '#native', 'native mutation survives prototype mutation'); + } + } + const markup = ''; + const xml = new DOMParser().parseFromString(markup, 'image/svg+xml'); + const html = document.createElement('div'); html.innerHTML = markup; + for (const parent of [xml.documentElement, html.firstChild]) { + check(parent.children[0] instanceof SVGSwitchElement && parent.children[1] instanceof SVGMPathElement, 'parser interfaces'); + check(parent.children[1].href.baseVal === '#path', 'parser href'); + } + const frame = document.createElement('iframe'); document.body.append(frame); + const other = frame.contentWindow; + const foreign = other.document.createElementNS(ns, 'mpath'); + const getHref = Object.getOwnPropertyDescriptor(SVGMPathElement.prototype, 'href').get; + const otherGetHref = Object.getOwnPropertyDescriptor(other.SVGMPathElement.prototype, 'href').get; + const foreignHref = getHref.call(foreign); + check(foreignHref instanceof other.SVGAnimatedString && !(foreignHref instanceof SVGAnimatedString), 'cached value belongs to element realm'); + check(foreign.href === foreignHref, 'cross-realm SameObject'); + throws(() => otherGetHref.call({}), other.TypeError); + throws(() => otherGetHref.call(document.createElementNS(ns, 'use')), other.TypeError); + const local = document.createElementNS(ns, 'mpath'); + check(otherGetHref.call(local) instanceof SVGAnimatedString, 'foreign accessor with local element'); + const imported = document.importNode(foreign, true); + check(imported instanceof SVGMPathElement && !(imported instanceof other.SVGMPathElement), 'import selects destination realm'); + frame.remove(); + foreignHref.baseVal = '#retained'; + check(foreign.getAttribute('href') === '#retained', 'retained value after iframe removal'); + return true; +})() diff --git a/moli-renderer-v8/src/web_api_interfaces.rs b/moli-renderer-v8/src/web_api_interfaces.rs index 6a47c6f59d..8633b2b184 100644 --- a/moli-renderer-v8/src/web_api_interfaces.rs +++ b/moli-renderer-v8/src/web_api_interfaces.rs @@ -394,21 +394,56 @@ interfaces! { ResizeObserverSize; Response; SVGAElement: SVGGraphicsElement; - SVGAnimatedEnumeration; - SVGUnitTypes; - SVGAnimatedLength; - SVGAnimatedLengthList; - SVGAnimatedNumber; - SVGAnimatedNumberList; - SVGAnimatedTransformList; SVGAnimationElement: SVGElement; SVGAnimateElement: SVGAnimationElement; SVGAnimateMotionElement: SVGAnimationElement; SVGAnimateTransformElement: SVGAnimationElement; SVGSetElement: SVGAnimationElement; + SVGAnimatedEnumeration; + SVGUnitTypes; + SVGAnimatedString; + SVGAnimatedLength; + SVGAnimatedLengthList; + SVGAnimatedBoolean; + SVGFEConvolveMatrixElement: SVGElement; + SVGAngle; + SVGAnimatedAngle; + SVGMarkerElement: SVGElement; + SVGAnimatedInteger; + SVGAnimatedRect; + SVGPreserveAspectRatio; + SVGAnimatedPreserveAspectRatio; + SVGAnimatedNumber; + SVGAnimatedNumberList; + SVGAnimatedTransformList; SVGCircleElement: SVGGeometryElement; + SVGClipPathElement: SVGElement; + SVGFilterElement: SVGElement; + SVGMaskElement: SVGElement; + SVGViewElement: SVGElement; SVGDefsElement: SVGGraphicsElement; SVGDescElement: SVGElement; + SVGComponentTransferFunctionElement: SVGElement; + SVGFEFuncAElement: SVGComponentTransferFunctionElement; + SVGFEFuncBElement: SVGComponentTransferFunctionElement; + SVGFEFuncGElement: SVGComponentTransferFunctionElement; + SVGFEFuncRElement: SVGComponentTransferFunctionElement; + SVGFEBlendElement: SVGElement; + SVGFEColorMatrixElement: SVGElement; + SVGFECompositeElement: SVGElement; + SVGFEDisplacementMapElement: SVGElement; + SVGFEMorphologyElement: SVGElement; + SVGFETurbulenceElement: SVGElement; + SVGTextPathElement: SVGTextContentElement; + SVGFEDiffuseLightingElement: SVGElement; + SVGFEDistantLightElement: SVGElement; + SVGFEDropShadowElement: SVGElement; + SVGFEGaussianBlurElement: SVGElement; + SVGFEOffsetElement: SVGElement; + SVGFEPointLightElement: SVGElement; + SVGFESpecularLightingElement: SVGElement; + SVGFESpotLightElement: SVGElement; + SVGStopElement: SVGElement; SVGElement: Element; SVGEllipseElement: SVGGeometryElement; SVGFEComponentTransferElement: SVGElement; @@ -429,6 +464,7 @@ interfaces! { SVGLinearGradientElement: SVGGradientElement; SVGMatrix; SVGMetadataElement: SVGElement; + SVGMPathElement: SVGElement; SVGNumber; SVGNumberList; SVGPathElement: SVGGeometryElement; @@ -442,13 +478,14 @@ interfaces! { SVGScriptElement: SVGElement; SVGStyleElement: SVGElement; SVGSwitchElement: SVGGraphicsElement; - SVGMPathElement: SVGElement; SVGSymbolElement: SVGGraphicsElement; SVGTSpanElement: SVGTextPositioningElement; SVGTextContentElement: SVGGraphicsElement; SVGTextElement: SVGTextPositioningElement; SVGTextPositioningElement: SVGTextContentElement; SVGTitleElement: SVGElement; + SVGPointList; + SVGStringList; SVGTransform; SVGTransformList; SVGUseElement: SVGGraphicsElement; diff --git a/moli-svg/src/length.rs b/moli-svg/src/length.rs index d5b60eea0b..13b928ec5b 100644 --- a/moli-svg/src/length.rs +++ b/moli-svg/src/length.rs @@ -11,12 +11,23 @@ pub enum SvgLengthUnit { Percentage, Ems, Exs, + Ch, + Rem, + Lh, + Rlh, + Cap, + Ic, Px, Cm, Mm, + Q, In, Pt, Pc, + Vw, + Vh, + Vmin, + Vmax, } impl SvgLengthUnit { @@ -26,12 +37,23 @@ impl SvgLengthUnit { Self::Percentage => "%", Self::Ems => "em", Self::Exs => "ex", + Self::Ch => "ch", + Self::Rem => "rem", + Self::Lh => "lh", + Self::Rlh => "rlh", + Self::Cap => "cap", + Self::Ic => "ic", Self::Px => "px", Self::Cm => "cm", Self::Mm => "mm", + Self::Q => "q", Self::In => "in", Self::Pt => "pt", Self::Pc => "pc", + Self::Vw => "vw", + Self::Vh => "vh", + Self::Vmin => "vmin", + Self::Vmax => "vmax", } } } @@ -55,18 +77,20 @@ impl SvgLength { } pub fn parse_length(raw: &str) -> Option { - raw.trim() - .to_ascii_lowercase() + let normalized = raw.trim().to_ascii_lowercase(); + normalized .parse::() .ok() .map(svg_length) + .or_else(|| parse_extended_length(&normalized)) } pub fn parse_length_list(raw: &str) -> Option> { let normalized = raw.to_ascii_lowercase(); - LengthListParser::from(normalized.as_str()) + let parsed = LengthListParser::from(normalized.as_str()) .map(|length| length.ok().map(svg_length)) - .collect() + .collect::>>(); + parsed.or_else(|| parse_extended_length_list(&normalized)) } pub fn parse_number(raw: &str) -> Option { @@ -82,6 +106,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, @@ -99,3 +136,121 @@ fn svg_length(length: SvgTypesLength) -> SvgLength { }, } } + +fn parse_extended_length(raw: &str) -> Option { + let number_len = number_prefix_len(raw)?; + let value = raw[..number_len].parse::().ok()?; + if !value.is_finite() { + return None; + } + let unit = match &raw[number_len..] { + "ch" => SvgLengthUnit::Ch, + "rem" => SvgLengthUnit::Rem, + "lh" => SvgLengthUnit::Lh, + "rlh" => SvgLengthUnit::Rlh, + "cap" => SvgLengthUnit::Cap, + "ic" => SvgLengthUnit::Ic, + "q" => SvgLengthUnit::Q, + "vw" => SvgLengthUnit::Vw, + "vh" => SvgLengthUnit::Vh, + "vmin" => SvgLengthUnit::Vmin, + "vmax" => SvgLengthUnit::Vmax, + _ => return None, + }; + Some(SvgLength::new(value, unit)) +} + +fn parse_extended_length_list(raw: &str) -> Option> { + let bytes = raw.as_bytes(); + let mut index = 0; + let mut lengths = Vec::new(); + + skip_ascii_whitespace(bytes, &mut index); + if index == bytes.len() { + return Some(lengths); + } + + loop { + let number_len = number_prefix_len(&raw[index..])?; + let number_end = index + number_len; + let mut token_end = number_end; + if matches!(bytes.get(token_end), Some(b'%')) { + token_end += 1; + } else { + while matches!(bytes.get(token_end), Some(b'a'..=b'z')) { + token_end += 1; + } + } + lengths.push(parse_length(&raw[index..token_end])?); + index = token_end; + if index == bytes.len() { + return Some(lengths); + } + + let separator_start = index; + skip_ascii_whitespace(bytes, &mut index); + if index == bytes.len() { + return Some(lengths); + } + if matches!(bytes.get(index), Some(b',')) { + index += 1; + skip_ascii_whitespace(bytes, &mut index); + if index == bytes.len() || matches!(bytes.get(index), Some(b',')) { + return None; + } + } else if index == separator_start && !matches!(bytes.get(index), Some(b'+' | b'-')) { + return None; + } + } +} + +fn skip_ascii_whitespace(bytes: &[u8], index: &mut usize) { + while matches!( + bytes.get(*index), + Some(b'\t' | b'\n' | b'\x0c' | b'\r' | b' ') + ) { + *index += 1; + } +} + +fn number_prefix_len(raw: &str) -> Option { + let bytes = raw.as_bytes(); + let mut index = 0; + if matches!(bytes.get(index), Some(b'+' | b'-')) { + index += 1; + } + + let integer_start = index; + while matches!(bytes.get(index), Some(b'0'..=b'9')) { + index += 1; + } + let integer_digits = index - integer_start; + + let mut fraction_digits = 0; + if matches!(bytes.get(index), Some(b'.')) { + index += 1; + let fraction_start = index; + while matches!(bytes.get(index), Some(b'0'..=b'9')) { + index += 1; + } + fraction_digits = index - fraction_start; + } + if integer_digits == 0 && fraction_digits == 0 { + return None; + } + + if matches!(bytes.get(index), Some(b'e' | b'E')) { + index += 1; + if matches!(bytes.get(index), Some(b'+' | b'-')) { + index += 1; + } + let exponent_start = index; + while matches!(bytes.get(index), Some(b'0'..=b'9')) { + index += 1; + } + if exponent_start == index { + return None; + } + } + Some(index) +} diff --git a/moli-svg/src/lib.rs b/moli-svg/src/lib.rs index 998af75e0d..bf6853b8f2 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,8 +27,8 @@ 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, }; #[test] @@ -50,6 +51,23 @@ mod tests { 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(), @@ -420,6 +438,18 @@ mod tests { assert_close(lengths[4].value, -5.0); assert!(parse_length_list("1px nope 2px").is_none()); + let modern = parse_length_list("10cap, 2rlh 3vw-4q").unwrap(); + assert_eq!(modern.len(), 4); + assert_eq!(modern[0].unit, SvgLengthUnit::Cap); + assert_eq!(modern[1].unit, SvgLengthUnit::Rlh); + assert_eq!(modern[2].unit, SvgLengthUnit::Vw); + assert_close(modern[3].value, -4.0); + assert_eq!(modern[3].unit, SvgLengthUnit::Q); + assert_eq!(modern[0].serialize(), "10cap"); + assert_eq!(parse_length_list(" 10cap ").unwrap().len(), 1); + assert!(parse_length_list("10cap,").is_none()); + assert!(parse_length_list("10cap nope").is_none()); + assert_close(parse_number(" .5 ").unwrap(), 0.5); assert_eq!( parse_number_list("10 20,30-40").unwrap(),