From 0969746cedd3fdbe726494ebf20db6e7ab0cc92b Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 1 Sep 2026 10:34:34 +0800 Subject: [PATCH] fix(svg): reflect fit-to-view-box attributes --- .../src/context_bootstrap/runtime_state.rs | 6 + .../src/context_bootstrap/specs/registry.rs | 15 + .../context_bootstrap/svg_runtime/bindings.rs | 151 ++++++++ .../context_bootstrap/svg_runtime/builders.rs | 325 ++++++++++++++++++ .../svg_runtime/callbacks.rs | 285 +++++++++++++++ .../src/context_bootstrap/svg_runtime/mod.rs | 37 ++ .../src/context_bootstrap/svg_runtime/rect.rs | 78 ++++- .../tests/dom_elements/live_document.rs | 289 ++++++++++++++++ 8 files changed, 1183 insertions(+), 3 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs index 302c76ff8e..573835e404 100644 --- a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs +++ b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs @@ -1791,6 +1791,12 @@ pub(crate) fn finish_context_bootstrap( ("SVGAnimatedString", "SVGAnimatedString"), ("SVGAnimatedLength", "SVGAnimatedLength"), ("SVGAnimatedAngle", "SVGAnimatedAngle"), + ("SVGAnimatedRect", "SVGAnimatedRect"), + ("SVGPreserveAspectRatio", "SVGPreserveAspectRatio"), + ( + "SVGAnimatedPreserveAspectRatio", + "SVGAnimatedPreserveAspectRatio", + ), ("SVGLengthList", "SVGLengthList"), ("SVGAnimatedLengthList", "SVGAnimatedLengthList"), ("SVGAnimatedNumber", "SVGAnimatedNumber"), diff --git a/moli-renderer-v8/src/context_bootstrap/specs/registry.rs b/moli-renderer-v8/src/context_bootstrap/specs/registry.rs index 60a94e5b68..ab921e8112 100644 --- a/moli-renderer-v8/src/context_bootstrap/specs/registry.rs +++ b/moli-renderer-v8/src/context_bootstrap/specs/registry.rs @@ -138,6 +138,21 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[ parent: None, kind: ConstructorKind::Illegal, }, + ConstructorSpec { + name: "SVGAnimatedRect", + parent: None, + kind: ConstructorKind::Illegal, + }, + ConstructorSpec { + name: "SVGPreserveAspectRatio", + parent: None, + kind: ConstructorKind::Illegal, + }, + ConstructorSpec { + name: "SVGAnimatedPreserveAspectRatio", + parent: None, + kind: ConstructorKind::Illegal, + }, ConstructorSpec { name: "SVGLengthList", parent: None, 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 8b3bfb7e63..2a31a7eca0 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs @@ -575,6 +575,27 @@ macro_rules! define_svg_enumeration_constants { }; } +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", @@ -973,6 +994,62 @@ struct SvgAnimatedAngleTemplateAccessorsDeclaration { anim_val: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(name = "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(name = "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(name = "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(name = "SVGNumber", enumerable)] struct SvgNumberTemplateAccessorsDeclaration { @@ -1252,6 +1329,35 @@ struct SvgGeometryElementPrototypeAccessorsDeclaration { path_length: (), } +#[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(name = "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(name = "SVGTextContentElement", enumerable)] struct SvgTextContentElementPrototypeAccessorsDeclaration { @@ -1829,6 +1935,34 @@ pub(super) fn install_svg_animated_angle_bindings<'s>( 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>, @@ -2082,6 +2216,18 @@ pub(super) fn install_svg_element_accessor_bindings<'s>( interface_name: &str, ) { let prototype = template.prototype_template(scope); + if matches!( + interface_name, + "SVGSVGElement" + | "SVGSymbolElement" + | "SVGMarkerElement" + | "SVGPatternElement" + | "SVGViewElement" + ) { + SvgFitToViewBoxPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } if matches!( interface_name, "SVGFEBlendElement" @@ -2256,6 +2402,11 @@ pub(super) fn install_svg_element_accessor_bindings<'s>( SvgUriReferencePrototypeAccessorsDeclaration::initialize_prototype_template( scope, prototype, ); + if interface_name == "SVGImageElement" { + SvgImagePreserveAspectRatioPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } } "SVGForeignObjectElement" => { SvgGraphicsBoxElementPrototypeAccessorsDeclaration::initialize_prototype_template( 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 03e68699b1..f1a6a3ff0e 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs @@ -141,6 +141,46 @@ struct SvgAnimatedAngleObjectDeclaration<'scope> { anim_val: v8::Local<'scope, v8::Object>, } +#[derive(WebApiObject)] +#[webapi( + interface = "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 = "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 = "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 = "SVGLength", fallback_to_string_tag = "SVGLength")] struct SvgLengthObjectDeclaration { @@ -1127,6 +1167,34 @@ pub(super) fn build_svg_animated_angle_for_attribute<'s>( .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) } @@ -2857,6 +2925,248 @@ fn svg_animated_length_attribute_value<'s>( .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>, @@ -3066,6 +3376,21 @@ pub(super) fn svg_owner_attribute_value<'s>( ) } +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(scope, owner) + else { + return; + }; + let runtime = unsafe { &mut *runtime_ptr }; + let _ = runtime.set_attribute(scope, runtime_ptr, handle, attribute, value); +} + pub(super) fn parse_svg_length_value(raw: &str) -> Option { svg_geometry::parse_length(raw).map(svg_parsed_length_from_svg_length) } 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 b618779080..5d66e44558 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs @@ -61,6 +61,8 @@ 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"]; @@ -133,6 +135,103 @@ fn require_svg_receiver<'s>( false } +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" + ), + _ => 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>, @@ -1113,6 +1212,192 @@ pub(super) fn svg_animated_angle_getter<'s>( ); } +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(), + SVG_ANIMATED_RECT_BASE_VAL_SLOT, + "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(), + SVG_ANIMATED_PRESERVE_ASPECT_RATIO_BASE_VAL_SLOT, + "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, + SVG_PRESERVE_ASPECT_RATIO_ALIGN_SLOT, + "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, + SVG_PRESERVE_ASPECT_RATIO_ALIGN_SLOT, + "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>, 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 40b7bd6476..a13db6352f 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs @@ -43,6 +43,20 @@ 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"; @@ -357,6 +371,22 @@ 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; @@ -805,6 +835,13 @@ pub(in crate::context_bootstrap) fn install_svg_template_bindings<'s>( "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) 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 c0c8ae7837..08eb781934 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/rect.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/rect.rs @@ -1,10 +1,10 @@ -//! 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::{ - 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, }; @@ -14,6 +14,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)] @@ -81,6 +83,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>, @@ -100,6 +156,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); } @@ -113,6 +170,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), @@ -130,10 +198,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/script_vm/tests/dom_elements/live_document.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document.rs index 375fe2693a..3fb7bd176c 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document.rs @@ -1605,6 +1605,295 @@ fn svg_animated_lengths_reflect_initial_and_content_attribute_values() { 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_historical_interfaces_keep_current_element_constructors_only() { let mut vm = new_parsed_test_vm(