From 28982eaead568e9cc9f69fa3c08e0910fd1a42c6 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 1 Sep 2026 08:44:06 +0800 Subject: [PATCH] fix(svg): expose standalone value factories --- .../src/context_bootstrap/dom_rect.rs | 42 ++++-- .../src/context_bootstrap/geometry_runtime.rs | 37 ++++- .../context_bootstrap/svg_runtime/bindings.rs | 28 ++++ .../svg_runtime/callbacks.rs | 141 ++++++++++++++++-- .../src/context_bootstrap/svg_runtime/mod.rs | 2 +- .../tests/dom_elements/live_document.rs | 133 +++++++++++++++++ moli-svg/src/length.rs | 9 +- moli-svg/src/lib.rs | 1 + 8 files changed, 366 insertions(+), 27 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap/dom_rect.rs b/moli-renderer-v8/src/context_bootstrap/dom_rect.rs index c4e85c3c6f..1be8b3324a 100644 --- a/moli-renderer-v8/src/context_bootstrap/dom_rect.rs +++ b/moli-renderer-v8/src/context_bootstrap/dom_rect.rs @@ -9,6 +9,7 @@ const DOM_RECT_WIDTH_SLOT: &str = "__moliDomRectWidth"; const DOM_RECT_HEIGHT_SLOT: &str = "__moliDomRectHeight"; const DOM_RECT_BRAND_SLOT: &str = "__moliDomRectBrand"; const DOM_RECT_MUTABLE_BRAND_SLOT: &str = "__moliDomRectMutableBrand"; +const DOM_RECT_RESTRICTED_NUMBER_SLOT: &str = "__moliDomRectRestrictedNumber"; #[derive(WebApiObject)] #[webapi(interface = "DOMRect")] @@ -298,6 +299,20 @@ pub(crate) fn build_dom_rect_object<'s>( .expect("DOMRect declaration should bind") } +pub(in crate::context_bootstrap) fn build_svg_rect_object<'s>( + scope: &mut v8::PinScope<'s, '_>, +) -> v8::Local<'s, v8::Object> { + let object = build_dom_rect_object(scope, 0.0, 0.0, 0.0, 0.0); + let restricted = v8::Boolean::new(scope, true); + set_private_value( + scope, + object, + DOM_RECT_RESTRICTED_NUMBER_SLOT, + restricted.into(), + ); + object +} + fn build_dom_rect_readonly_object<'s>( scope: &mut v8::PinScope<'s, '_>, x: f64, @@ -462,15 +477,24 @@ fn dom_rect_setter_callback<'s>( throw_type_error(scope, "Illegal invocation"); return; } - let value = match webidl::convert::( - scope, - args.get(0), - webidl::Context::member("DOMRect", slot), - ) { - Ok(value) => value.0, - Err(error) => { - webidl::throw_error(scope, &error); - return; + let context = webidl::Context::member("DOMRect", slot); + let restricted = get_private_value(scope, args.this(), DOM_RECT_RESTRICTED_NUMBER_SLOT) + .is_some_and(|value| value.boolean_value(scope)); + let value = if restricted { + match webidl::convert::(scope, args.get(0), context) { + Ok(value) => value.0, + Err(error) => { + webidl::throw_error(scope, &error); + return; + } + } + } else { + match webidl::convert::(scope, args.get(0), context) { + Ok(value) => value.0, + Err(error) => { + webidl::throw_error(scope, &error); + return; + } } }; set_private_value( diff --git a/moli-renderer-v8/src/context_bootstrap/geometry_runtime.rs b/moli-renderer-v8/src/context_bootstrap/geometry_runtime.rs index 683bb4b028..544cb01ae9 100644 --- a/moli-renderer-v8/src/context_bootstrap/geometry_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/geometry_runtime.rs @@ -14,6 +14,7 @@ const DOM_POINT_Z_SLOT: &str = "__moliDomPointZ"; const DOM_POINT_W_SLOT: &str = "__moliDomPointW"; const DOM_POINT_BRAND_SLOT: &str = "__moliDomPointBrand"; const DOM_POINT_MUTABLE_BRAND_SLOT: &str = "__moliDomPointMutableBrand"; +const DOM_POINT_RESTRICTED_NUMBER_SLOT: &str = "__moliDomPointRestrictedNumber"; const DOM_MATRIX_M11_SLOT: &str = "__moliDomMatrixM11"; const DOM_MATRIX_M12_SLOT: &str = "__moliDomMatrixM12"; @@ -820,6 +821,20 @@ pub(in crate::context_bootstrap) fn build_dom_point_object<'s>( .expect("DOMPoint declaration should bind") } +pub(in crate::context_bootstrap) fn build_svg_point_object<'s>( + scope: &mut v8::PinScope<'s, '_>, +) -> v8::Local<'s, v8::Object> { + let object = build_dom_point_object(scope, 0.0, 0.0, 0.0, 1.0); + let restricted = v8::Boolean::new(scope, true); + set_private_value( + scope, + object, + DOM_POINT_RESTRICTED_NUMBER_SLOT, + restricted.into(), + ); + object +} + fn build_dom_point_readonly_object<'s>( scope: &mut v8::PinScope<'s, '_>, x: f64, @@ -931,12 +946,22 @@ fn dom_point_setter_callback<'s>( throw_type_error(scope, "Illegal invocation"); return; } - let Some(value) = geometry_number_value( - scope, - args.get(0), - webidl::Context::member("DOMPoint", slot), - ) else { - return; + let context = webidl::Context::member("DOMPoint", slot); + let value = if get_private_value(scope, args.this(), DOM_POINT_RESTRICTED_NUMBER_SLOT) + .is_some_and(|value| value.boolean_value(scope)) + { + match webidl::convert::(scope, args.get(0), context) { + Ok(value) => value.0, + Err(error) => { + webidl::throw_error(scope, &error); + return; + } + } + } else { + let Some(value) = geometry_number_value(scope, args.get(0), context) else { + return; + }; + value }; set_private_value( scope, 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 528a899e7a..67168f1a6e 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs @@ -771,6 +771,20 @@ struct SvgSvgElementTemplateMethodsDeclaration { )] deselect_all: (), + #[webapi( + method = "createSVGNumber", + length = 0, + callback = svg_svg_element_create_number_callback + )] + create_svg_number: (), + + #[webapi( + method = "createSVGLength", + length = 0, + callback = svg_svg_element_create_length_callback + )] + create_svg_length: (), + #[webapi( method = "createSVGAngle", length = 0, @@ -778,6 +792,13 @@ struct SvgSvgElementTemplateMethodsDeclaration { )] create_svg_angle: (), + #[webapi( + method = "createSVGPoint", + length = 0, + callback = svg_svg_element_create_point_callback + )] + create_svg_point: (), + #[webapi( method = "createSVGMatrix", length = 0, @@ -785,6 +806,13 @@ struct SvgSvgElementTemplateMethodsDeclaration { )] create_svg_matrix: (), + #[webapi( + method = "createSVGRect", + length = 0, + callback = svg_svg_element_create_rect_callback + )] + create_svg_rect: (), + #[webapi( method = "createSVGTransform", length = 0, 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 e269ee5a3c..6589a59602 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs @@ -1150,7 +1150,7 @@ pub(super) fn svg_length_setter<'s>( } match name { "value" | "valueInSpecifiedUnits" => { - let value = match webidl::convert::( + let value = match webidl::convert::( scope, args.get(0), webidl::Context::member("SVGLength", "value"), @@ -1177,7 +1177,10 @@ pub(super) fn svg_length_setter<'s>( return; } }; - let parsed = parse_svg_length_value(&string_value).unwrap_or_default(); + let Some(parsed) = parse_svg_length_value(&string_value) else { + throw_dom_exception(scope, "SyntaxError", 12, "Invalid SVG length value."); + return; + }; set_svg_length_parsed_value(scope, args.this(), parsed); reflect_svg_length_to_owner_attribute(scope, args.this()); reflect_svg_value_list_item_to_owner_list(scope, args.this(), SvgListKind::Length); @@ -1218,7 +1221,7 @@ pub(super) fn svg_number_setter<'s>( ) { return; } - let value = match webidl::convert::( + let value = match webidl::convert::( scope, args.get(0), webidl::Context::member("SVGNumber", "value"), @@ -2497,20 +2500,92 @@ pub(super) fn svg_transform_set_matrix_callback<'s>( rv.set_undefined(); } -pub(super) fn svg_svg_element_create_matrix_callback<'s>( +fn require_svg_svg_element_receiver<'s>( scope: &mut v8::PinScope<'s, '_>, - _args: v8::FunctionCallbackArguments<'s>, + receiver: v8::Local<'s, v8::Object>, + member: &str, +) -> bool { + let Ok((runtime_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, receiver) + else { + webidl::throw_type_error( + scope, + &format!("SVGSVGElement.{member} called on incompatible receiver."), + ); + return false; + }; + let is_svg_element = unsafe { &*runtime_ptr } + .dom_host() + .node(handle) + .and_then(|node| node.as_element()) + .is_some_and(|element| element.is_svg_element("svg")); + if !is_svg_element { + webidl::throw_type_error( + scope, + &format!("SVGSVGElement.{member} called on incompatible receiver."), + ); + } + is_svg_element +} + +pub(super) fn svg_svg_element_create_number_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - rv.set(super::super::geometry_runtime::build_dom_matrix_identity_object(scope).into()); + if require_svg_svg_element_receiver(scope, args.this(), "createSVGNumber") { + rv.set(build_svg_number(scope, 0.0).into()); + } +} + +pub(super) fn svg_svg_element_create_length_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if require_svg_svg_element_receiver(scope, args.this(), "createSVGLength") { + rv.set(build_svg_length(scope, 0.0).into()); + } +} + +pub(super) fn svg_svg_element_create_matrix_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if require_svg_svg_element_receiver(scope, args.this(), "createSVGMatrix") { + rv.set(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>, + args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - rv.set(build_svg_angle(scope).into()); + if require_svg_svg_element_receiver(scope, args.this(), "createSVGAngle") { + rv.set(build_svg_angle(scope).into()); + } +} + +pub(super) fn svg_svg_element_create_point_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if require_svg_svg_element_receiver(scope, args.this(), "createSVGPoint") { + rv.set(super::super::geometry_runtime::build_svg_point_object(scope).into()); + } +} + +pub(super) fn svg_svg_element_create_rect_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if require_svg_svg_element_receiver(scope, args.this(), "createSVGRect") { + rv.set(super::super::dom_rect::build_svg_rect_object(scope).into()); + } } pub(super) fn svg_svg_element_deselect_all_callback<'s>( @@ -2565,9 +2640,12 @@ pub(super) fn svg_svg_element_deselect_all_callback<'s>( pub(super) fn svg_svg_element_create_transform_callback<'s>( scope: &mut v8::PinScope<'s, '_>, - _args: v8::FunctionCallbackArguments<'s>, + args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if !require_svg_svg_element_receiver(scope, args.this(), "createSVGTransform") { + return; + } rv.set( build_svg_transform(scope, SvgTransform::matrix(SvgMatrixComponents::identity())).into(), ); @@ -2578,6 +2656,9 @@ pub(super) fn svg_svg_element_create_transform_from_matrix_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if !require_svg_svg_element_receiver(scope, args.this(), "createSVGTransformFromMatrix") { + return; + } let Some(components) = svg_dom_matrix_2d_init_arg(scope, &args, "SVGSVGElement.createSVGTransformFromMatrix") else { @@ -2881,12 +2962,31 @@ pub(super) fn svg_length_new_value_specified_units_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if !require_svg_receiver( + scope, + args.this(), + SVG_LENGTH_UNIT_TYPE_SLOT, + "SVGLength", + "newValueSpecifiedUnits", + ) { + return; + } let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; + if !svg_length_unit_type_is_supported(parsed.unit_type as u32) { + throw_dom_exception( + scope, + "NotSupportedError", + 9, + "The SVG length unit type is not supported.", + ); + return; + } set_svg_length_numeric_value(scope, args.this(), parsed.value, parsed.unit_type as u32); reflect_svg_length_to_owner_attribute(scope, args.this()); + reflect_svg_value_list_item_to_owner_list(scope, args.this(), SvgListKind::Length); rv.set_undefined(); } @@ -2935,16 +3035,39 @@ pub(super) fn svg_length_convert_to_specified_units_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if !require_svg_receiver( + scope, + args.this(), + SVG_LENGTH_UNIT_TYPE_SLOT, + "SVGLength", + "convertToSpecifiedUnits", + ) { + return; + } let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; + if !svg_length_unit_type_is_supported(parsed.unit_type as u32) { + throw_dom_exception( + scope, + "NotSupportedError", + 9, + "The SVG length unit type is not supported.", + ); + return; + } let value = svg_length_number_slot(scope, args.this(), SVG_LENGTH_VALUE_SLOT).unwrap_or(0.0); set_svg_length_numeric_value(scope, args.this(), value, parsed.unit_type as u32); reflect_svg_length_to_owner_attribute(scope, args.this()); + reflect_svg_value_list_item_to_owner_list(scope, args.this(), SvgListKind::Length); rv.set_undefined(); } +fn svg_length_unit_type_is_supported(unit_type: u32) -> bool { + (SVG_LENGTH_TYPE_NUMBER..=SVG_LENGTH_TYPE_PC).contains(&unit_type) +} + pub(super) fn svg_angle_convert_to_specified_units_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, 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 8014a3f542..7f7097640d 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs @@ -287,7 +287,7 @@ struct SvgMatrixRotateFromVectorArgs { struct SvgLengthNewValueSpecifiedUnitsArgs { #[webidl(required, converter = "unsigned_short")] unit_type: u16, - #[webidl(required)] + #[webidl(required, converter = "double")] value: f64, } 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 f457bef643..bb1222acf3 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 @@ -2315,6 +2315,139 @@ fn svg_marker_orient_angle_is_a_live_animated_angle() { assert_eq!(result, "ok"); } +#[test] +fn svg_svg_element_value_factories_create_typed_objects() { + let mut vm = new_parsed_test_vm( + "https://svg-value-factories.test/", + "", + ); + + let result = vm + .eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const rejectsTypeError = callback => { + try { + callback(); + return false; + } catch (error) { + return error instanceof TypeError; + } + }; + const rejectsDom = (name, callback) => { + try { + callback(); + return false; + } catch (error) { + return error.name === name; + } + }; + const ns = "http://www.w3.org/2000/svg"; + const svg = document.createElementNS(ns, "svg"); + const wrongReceiver = document.createElementNS(ns, "rect"); + + const factoryNames = [ + "createSVGNumber", + "createSVGLength", + "createSVGAngle", + "createSVGPoint", + "createSVGMatrix", + "createSVGRect", + "createSVGTransform", + "createSVGTransformFromMatrix", + ]; + for (const name of factoryNames) { + const descriptor = Object.getOwnPropertyDescriptor(SVGSVGElement.prototype, name); + assert(typeof descriptor.value === "function", `${name} method`); + assert(descriptor.value.length === 0, `${name} arity`); + assert(descriptor.enumerable && descriptor.configurable && descriptor.writable, + `${name} descriptor flags`); + assert(rejectsTypeError(() => descriptor.value.call(wrongReceiver)), + `${name} receiver brand`); + } + + const number = svg.createSVGNumber(); + const length = svg.createSVGLength(); + const point = svg.createSVGPoint(); + const rect = svg.createSVGRect(); + assert(number instanceof SVGNumber && number.value === 0, "SVGNumber result"); + assert(length instanceof SVGLength && length.value === 0 && + length.unitType === SVGLength.SVG_LENGTHTYPE_NUMBER, "SVGLength result"); + assert(point instanceof DOMPoint && point instanceof SVGPoint, + "SVGPoint result interface"); + assert(point.x === 0 && point.y === 0 && point.z === 0 && point.w === 1, + "SVGPoint defaults"); + assert(rect instanceof DOMRect && rect instanceof SVGRect, + "SVGRect result interface"); + assert(rect.x === 0 && rect.y === 0 && rect.width === 0 && rect.height === 0, + "SVGRect defaults"); + assert(svg.createSVGNumber() !== number && svg.createSVGLength() !== length && + svg.createSVGPoint() !== point && svg.createSVGRect() !== rect, + "factories return new objects"); + + number.value = 2; + assert(number.value === 2, "SVGNumber assignment"); + assert(rejectsTypeError(() => { number.value = NaN; }), + "SVGNumber rejects NaN"); + assert(number.value === 2, "SVGNumber preserves rejected assignment"); + + length.valueAsString = "2px"; + assert(length.value === 2 && length.valueAsString === "2px", + "SVGLength assignment"); + assert(rejectsDom("NotSupportedError", () => { + length.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_UNKNOWN); + }), "SVGLength rejects unsupported unit"); + assert(rejectsDom("SyntaxError", () => { length.valueAsString = "10deg"; }), + "SVGLength rejects invalid syntax"); + assert(rejectsTypeError(() => { length.value = NaN; }), + "SVGLength rejects NaN"); + assert(length.value === 2 && length.valueAsString === "2px", + "SVGLength preserves rejected assignments"); + + point.x = 100; + point.y = 200; + assert(point.x === 100 && point.y === 200, "SVGPoint assignment"); + for (const invalid of [point, NaN, Infinity]) { + assert(rejectsTypeError(() => { point.x = invalid; }), + "SVGPoint rejects non-finite values"); + assert(point.x === 100, "SVGPoint preserves rejected assignment"); + } + point.y = null; + assert(point.y === 0, "SVGPoint converts null"); + + Object.assign(rect, { x: 100, y: 200, width: 300, height: 400 }); + assert(rect.x === 100 && rect.y === 200 && rect.width === 300 && + rect.height === 400, "SVGRect assignment"); + for (const [property, invalid] of [ + ["x", rect], + ["y", "aString"], + ["width", svg], + ["height", NaN], + ]) { + assert(rejectsTypeError(() => { rect[property] = invalid; }), + `SVGRect rejects invalid ${property}`); + } + rect.y = null; + assert(rect.y === 0, "SVGRect converts null"); + + const regularPoint = new DOMPoint(); + const regularRect = new DOMRect(); + regularPoint.x = NaN; + regularRect.x = Infinity; + assert(Number.isNaN(regularPoint.x) && regularRect.x === Infinity, + "regular geometry objects remain unrestricted"); + return "ok"; + })() + "#, + ) + .expect("SVG value factory probe should evaluate"); + + assert_eq!(result, "ok"); +} + #[test] fn svg_string_lists_reflect_conditional_processing_attributes() { let mut vm = new_parsed_test_vm( diff --git a/moli-svg/src/length.rs b/moli-svg/src/length.rs index a1dfbe9f96..d5b60eea0b 100644 --- a/moli-svg/src/length.rs +++ b/moli-svg/src/length.rs @@ -55,11 +55,16 @@ impl SvgLength { } pub fn parse_length(raw: &str) -> Option { - raw.trim().parse::().ok().map(svg_length) + raw.trim() + .to_ascii_lowercase() + .parse::() + .ok() + .map(svg_length) } pub fn parse_length_list(raw: &str) -> Option> { - LengthListParser::from(raw) + let normalized = raw.to_ascii_lowercase(); + LengthListParser::from(normalized.as_str()) .map(|length| length.ok().map(svg_length)) .collect() } diff --git a/moli-svg/src/lib.rs b/moli-svg/src/lib.rs index 017fa1150d..36898f43de 100644 --- a/moli-svg/src/lib.rs +++ b/moli-svg/src/lib.rs @@ -360,6 +360,7 @@ mod tests { assert_close(length.value, 1.5); assert_eq!(length.unit, SvgLengthUnit::Ems); assert_eq!(length.serialize(), "1.5em"); + assert_eq!(parse_length("1pX").unwrap().unit, SvgLengthUnit::Px); assert_eq!(serialize_number(3.0), "3"); assert_eq!(serialize_number(3.25), "3.25"); assert!(parse_length("1 px").is_none());