From 162f21a1bdaf2e8d14a3602d744cd46bc91adfb6 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 31 Aug 2026 04:17:09 +0800 Subject: [PATCH] fix(svg): implement marker orientation angle APIs --- .../src/native/element/interface_names.rs | 1 + moli-dom/src/native/element/tests.rs | 1 + .../bridge_descriptor/descriptors.rs | 1 + .../src/context_bootstrap/runtime_state.rs | 2 + .../src/context_bootstrap/specs/registry.rs | 8 + .../context_bootstrap/svg_runtime/bindings.rs | 212 ++++++++++ .../context_bootstrap/svg_runtime/builders.rs | 394 ++++++++++++++++++ .../svg_runtime/callbacks.rs | 354 ++++++++++++++++ .../src/context_bootstrap/svg_runtime/mod.rs | 62 +++ .../tests/dom_elements/live_document.rs | 126 ++++++ moli-renderer-v8/src/web_api_interfaces.rs | 3 + 11 files changed, 1164 insertions(+) diff --git a/moli-dom/src/native/element/interface_names.rs b/moli-dom/src/native/element/interface_names.rs index be62094d30..52983d896d 100644 --- a/moli-dom/src/native/element/interface_names.rs +++ b/moli-dom/src/native/element/interface_names.rs @@ -103,6 +103,7 @@ pub fn svg_element_interface_name(local_name: &str) -> &'static str { "image" => "SVGImageElement", "line" => "SVGLineElement", "linearGradient" => "SVGLinearGradientElement", + "marker" => "SVGMarkerElement", "mask" => "SVGMaskElement", "metadata" => "SVGMetadataElement", "path" => "SVGPathElement", diff --git a/moli-dom/src/native/element/tests.rs b/moli-dom/src/native/element/tests.rs index c9720a1b2a..9f81dc7350 100644 --- a/moli-dom/src/native/element/tests.rs +++ b/moli-dom/src/native/element/tests.rs @@ -277,6 +277,7 @@ 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"); 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 c29602c26f..c8cda9c5c0 100644 --- a/moli-renderer-v8/src/context_bootstrap/bridge_descriptor/descriptors.rs +++ b/moli-renderer-v8/src/context_bootstrap/bridge_descriptor/descriptors.rs @@ -179,6 +179,7 @@ const NODE_BRIDGE_DESCRIPTORS: &[BridgeDescriptor] = &[ ELEMENT_GROUPS, ), descriptor(web_api_interfaces::SVGMaskElement::DESCRIPTOR, ELEMENT_GROUPS), + descriptor(web_api_interfaces::SVGMarkerElement::DESCRIPTOR, ELEMENT_GROUPS), specialized_descriptor( web_api_interfaces::SVGPathElement::DESCRIPTOR, ELEMENT_GROUPS, diff --git a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs index 89ae3640de..98353c84b4 100644 --- a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs +++ b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs @@ -1725,10 +1725,12 @@ pub(crate) fn finish_context_bootstrap( ("XPathEvaluator", "XPathEvaluator"), ("XPathResult", "XPathResult"), ("SVGLength", "SVGLength"), + ("SVGAngle", "SVGAngle"), ("SVGNumber", "SVGNumber"), ("SVGRect", "SVGRect"), ("SVGAnimatedString", "SVGAnimatedString"), ("SVGAnimatedLength", "SVGAnimatedLength"), + ("SVGAnimatedAngle", "SVGAnimatedAngle"), ("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 1308232779..a05127aaef 100644 --- a/moli-renderer-v8/src/context_bootstrap/specs/registry.rs +++ b/moli-renderer-v8/src/context_bootstrap/specs/registry.rs @@ -88,6 +88,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, @@ -104,6 +108,10 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[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::SVGLengthList::DESCRIPTOR, kind: ConstructorKind::Illegal, diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs index 3e2253ef45..27db9bd7aa 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 { @@ -525,6 +561,66 @@ struct SvgGradientElementTemplateConstantsDeclaration { spread_method_repeat: (), } +#[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)] struct SvgSvgElementTemplateMethodsDeclaration { @@ -542,6 +638,13 @@ struct SvgSvgElementTemplateMethodsDeclaration { )] deselect_all: (), + #[webapi( + method = "createSVGAngle", + length = 0, + callback = svg_svg_element_create_angle_callback + )] + create_svg_angle: (), + #[webapi( method = "createSVGMatrix", length = 0, @@ -599,6 +702,41 @@ 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 { @@ -655,6 +793,24 @@ 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::SVGNumber, enumerable)] struct SvgNumberTemplateAccessorsDeclaration { @@ -1140,6 +1296,30 @@ struct SvgGraphicsBoxElementPrototypeAccessorsDeclaration { height: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGMarkerElement, enumerable)] +struct SvgMarkerElementPrototypeAccessorsDeclaration { + #[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 = "SVGElement", enumerable)] struct SvgElementPrototypeAccessorsDeclaration { @@ -1173,6 +1353,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>, @@ -1181,6 +1371,14 @@ 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_number_bindings<'s>( scope: &mut v8::PinScope<'s, '_, ()>, template: v8::Local<'s, v8::FunctionTemplate>, @@ -1359,6 +1557,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>, @@ -1452,6 +1659,11 @@ pub(super) fn install_svg_element_accessor_bindings<'s>( scope, prototype, ); } + "SVGMarkerElement" => { + SvgMarkerElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } "SVGElement" => { SvgElementPrototypeAccessorsDeclaration::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 611ce04829..68d8259f28 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs @@ -118,6 +118,18 @@ 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::SVGLength, fallback_to_string_tag = "SVGLength")] struct SvgLengthObjectDeclaration { @@ -129,6 +141,21 @@ struct SvgLengthObjectDeclaration { 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, @@ -633,6 +660,21 @@ pub(super) fn parse_svg_animated_enumeration( (SvgAnimatedEnumerationKind::LengthAdjust, "spacingAndGlyphs") => { Some(SVG_LENGTH_ADJUST_SPACING_AND_GLYPHS) } + (SvgAnimatedEnumerationKind::MarkerUnits, "userSpaceOnUse") => { + Some(SVG_MARKER_UNITS_USER_SPACE_ON_USE) + } + (SvgAnimatedEnumerationKind::MarkerUnits, "strokeWidth") => { + Some(SVG_MARKER_UNITS_STROKE_WIDTH) + } + (SvgAnimatedEnumerationKind::MarkerOrient, "auto") => Some(SVG_MARKER_ORIENT_AUTO), + (SvgAnimatedEnumerationKind::MarkerOrient, "auto-start-reverse") => { + Some(SVG_MARKER_ORIENT_AUTO_START_REVERSE) + } + (SvgAnimatedEnumerationKind::MarkerOrient, value) + if parse_svg_angle_value(value).is_some() => + { + Some(SVG_MARKER_ORIENT_ANGLE) + } _ => None, } } @@ -655,6 +697,17 @@ pub(super) fn serialize_svg_animated_enumeration( (SvgAnimatedEnumerationKind::LengthAdjust, SVG_LENGTH_ADJUST_SPACING_AND_GLYPHS) => { Some("spacingAndGlyphs") } + (SvgAnimatedEnumerationKind::MarkerUnits, SVG_MARKER_UNITS_USER_SPACE_ON_USE) => { + Some("userSpaceOnUse") + } + (SvgAnimatedEnumerationKind::MarkerUnits, SVG_MARKER_UNITS_STROKE_WIDTH) => { + Some("strokeWidth") + } + (SvgAnimatedEnumerationKind::MarkerOrient, SVG_MARKER_ORIENT_AUTO) => Some("auto"), + (SvgAnimatedEnumerationKind::MarkerOrient, SVG_MARKER_ORIENT_ANGLE) => Some("0"), + (SvgAnimatedEnumerationKind::MarkerOrient, SVG_MARKER_ORIENT_AUTO_START_REVERSE) => { + Some("auto-start-reverse") + } _ => None, } } @@ -919,6 +972,44 @@ pub(super) fn build_svg_animated_length_for_attribute<'s>( .expect("SVGAnimatedLength declaration should bind") } +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_orient_angle_value) + .unwrap_or_default(); + 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("SVGAnimatedAngle 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>( scope: &mut v8::PinScope<'s, '_>, value: f64, @@ -2150,6 +2241,309 @@ pub(super) fn svg_animated_length_attribute_slot(name: &str) -> &'static str { } } +#[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(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, 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 219ca9e0c6..9a8280d231 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs @@ -388,6 +388,112 @@ fn svg_element_animated_length_getter<'s>( 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, + SVG_ANGLE_UNIT_TYPE_SLOT, + "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>, @@ -791,6 +897,42 @@ 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(), + SVG_ANIMATED_ANGLE_BASE_VAL_SLOT, + "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_length_list_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -864,6 +1006,129 @@ 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(), + SVG_ANGLE_UNIT_TYPE_SLOT, + "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(), + SVG_ANGLE_UNIT_TYPE_SLOT, + "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>, @@ -2118,6 +2383,14 @@ pub(super) fn svg_svg_element_create_matrix_callback<'s>( rv.set(super::super::geometry_runtime::build_dom_matrix_identity_object(scope).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>, +) { + rv.set(build_svg_angle(scope).into()); +} + pub(super) fn svg_svg_element_deselect_all_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -2483,6 +2756,46 @@ pub(super) fn svg_length_new_value_specified_units_callback<'s>( 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(), + SVG_ANGLE_UNIT_TYPE_SLOT, + "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(); +} + pub(super) fn svg_length_convert_to_specified_units_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -2498,6 +2811,47 @@ pub(super) fn svg_length_convert_to_specified_units_callback<'s>( rv.set_undefined(); } +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(), + SVG_ANGLE_UNIT_TYPE_SLOT, + "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(); +} + pub(super) fn svg_graphics_get_bbox_callback<'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 529b133f88..d7aeb46f24 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs @@ -38,6 +38,18 @@ const SVG_ANIMATED_BOOLEAN_INITIAL_VALUE_SLOT: &str = "__moliSvgAnimatedBooleanI 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_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"; @@ -120,6 +132,8 @@ enum SvgAnimatedEnumerationKind { UnitType, SpreadMethod, LengthAdjust, + MarkerUnits, + MarkerOrient, } #[derive(Clone, Copy)] @@ -247,6 +261,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 { @@ -282,6 +312,21 @@ 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_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; @@ -374,6 +419,20 @@ const SVG_ANIMATED_ENUMERATION_PROPERTIES: &[SvgAnimatedEnumerationProperty] = & initial_value: SVG_LENGTH_ADJUST_SPACING, kind: SvgAnimatedEnumerationKind::LengthAdjust, }, + SvgAnimatedEnumerationProperty { + index: 10, + attribute: "markerUnits", + cache_slot: SVG_MARKER_UNITS_SLOT, + initial_value: SVG_MARKER_UNITS_STROKE_WIDTH, + kind: SvgAnimatedEnumerationKind::MarkerUnits, + }, + SvgAnimatedEnumerationProperty { + index: 11, + attribute: "orient", + cache_slot: SVG_MARKER_ORIENT_TYPE_SLOT, + initial_value: SVG_MARKER_ORIENT_ANGLE, + kind: SvgAnimatedEnumerationKind::MarkerOrient, + }, ]; pub(in crate::context_bootstrap) fn install_svg_template_bindings<'s>( @@ -384,11 +443,13 @@ pub(in crate::context_bootstrap) fn install_svg_template_bindings<'s>( bindings::install_svg_element_accessor_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), "SVGLengthList" => bindings::install_svg_length_list_bindings(scope, template), "SVGAnimatedLengthList" => { bindings::install_svg_animated_length_list_bindings(scope, template) @@ -415,6 +476,7 @@ pub(in crate::context_bootstrap) fn install_svg_template_bindings<'s>( bindings::install_svg_text_content_element_bindings(scope, template) } "SVGGradientElement" => bindings::install_svg_gradient_element_bindings(scope, template), + "SVGMarkerElement" => bindings::install_svg_marker_element_bindings(scope, template), "SVGSVGElement" => bindings::install_svg_svg_element_bindings(scope, template), _ => {} } 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 a72dcbcf3e..6f68df972b 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 @@ -1968,6 +1968,132 @@ fn svg_animated_boolean_reflects_preserve_alpha() { 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_string_lists_reflect_conditional_processing_attributes() { let mut vm = new_parsed_test_vm( diff --git a/moli-renderer-v8/src/web_api_interfaces.rs b/moli-renderer-v8/src/web_api_interfaces.rs index 81768c3596..b2c23508af 100644 --- a/moli-renderer-v8/src/web_api_interfaces.rs +++ b/moli-renderer-v8/src/web_api_interfaces.rs @@ -372,6 +372,9 @@ interfaces! { SVGAnimatedLengthList; SVGAnimatedBoolean; SVGFEConvolveMatrixElement: SVGElement; + SVGAngle; + SVGAnimatedAngle; + SVGMarkerElement: SVGElement; SVGAnimatedNumber; SVGAnimatedNumberList; SVGAnimatedTransformList;