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(),