From feda69de4abce0f03be7371f49eca2884cba83f2 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 1 Sep 2026 12:15:45 +0800 Subject: [PATCH] fix(svg): resolve length units in context --- .../context_bootstrap/svg_runtime/builders.rs | 404 +++++++++++++++++- .../svg_runtime/callbacks.rs | 37 +- .../src/context_bootstrap/svg_runtime/mod.rs | 2 + .../tests/dom_elements/live_document.rs | 110 +++++ moli-svg/src/length.rs | 150 ++++++- moli-svg/src/lib.rs | 12 + 6 files changed, 691 insertions(+), 24 deletions(-) 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 2efbe6ee20..c6608f85aa 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs @@ -186,8 +186,12 @@ struct SvgAnimatedPreserveAspectRatioObjectDeclaration<'scope> { struct SvgLengthObjectDeclaration { #[webapi(slot = SVG_LENGTH_UNIT_TYPE_SLOT)] unit_type: u32, + #[webapi(slot = SVG_LENGTH_UNIT_SUFFIX_SLOT)] + unit_suffix: String, #[webapi(slot = SVG_LENGTH_VALUE_SLOT)] value: f64, + #[webapi(slot = SVG_LENGTH_VALUE_IN_SPECIFIED_UNITS_SLOT)] + value_in_specified_units: f64, #[webapi(slot = SVG_LENGTH_VALUE_AS_STRING_SLOT)] value_as_string: String, } @@ -1310,6 +1314,7 @@ pub(super) fn build_svg_length<'s>( SvgParsedLength { value, unit_type: SVG_LENGTH_TYPE_NUMBER, + unit: SvgLengthUnit::Number, raw: None, }, ) @@ -1322,11 +1327,19 @@ pub(super) fn build_svg_length_from_parsed<'s>( let value_as_string = parsed .raw .map(str::to_owned) - .or_else(|| Some(serialize_svg_length_value(parsed.value, parsed.unit_type))) + .or_else(|| Some(SvgLength::new(parsed.value, parsed.unit).serialize())) .unwrap_or_else(|| "0".to_owned()); - SvgLengthObjectDeclaration::new(parsed.unit_type, parsed.value, value_as_string) - .bind(scope) - .expect("SVGLength declaration should bind") + let value = + resolve_svg_length_without_context(parsed.value, parsed.unit).unwrap_or(parsed.value); + SvgLengthObjectDeclaration::new( + parsed.unit_type, + parsed.unit.suffix().to_owned(), + value, + parsed.value, + value_as_string, + ) + .bind(scope) + .expect("SVGLength declaration should bind") } pub(super) fn build_dom_point_like<'s>( @@ -2918,6 +2931,7 @@ pub(super) fn reflect_svg_angle_to_owner_attribute<'s>( pub(super) struct SvgParsedLength { value: f64, unit_type: u32, + unit: SvgLengthUnit, raw: Option<&'static str>, } @@ -2926,6 +2940,7 @@ impl Default for SvgParsedLength { Self { value: 0.0, unit_type: SVG_LENGTH_TYPE_NUMBER, + unit: SvgLengthUnit::Number, raw: Some("0"), } } @@ -2940,9 +2955,9 @@ pub(super) fn svg_length_number_slot<'s>( value.number_value(scope) } -pub(super) fn set_svg_length_numeric_value( - scope: &mut v8::PinScope<'_, '_>, - object: v8::Local<'_, v8::Object>, +pub(super) fn set_svg_length_numeric_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, value: f64, unit_type: u32, ) { @@ -2953,15 +2968,16 @@ pub(super) fn set_svg_length_numeric_value( SvgParsedLength { value, unit_type, + unit: svg_length_unit_from_type(unit_type), raw: None, }, ); set_svg_length_value_string(scope, object, &value_as_string); } -pub(super) fn set_svg_length_parsed_value( - scope: &mut v8::PinScope<'_, '_>, - object: v8::Local<'_, v8::Object>, +pub(super) fn set_svg_length_parsed_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, parsed: SvgParsedLength, ) { set_private_value( @@ -2970,12 +2986,71 @@ pub(super) fn set_svg_length_parsed_value( SVG_LENGTH_UNIT_TYPE_SLOT, v8::Number::new(scope, parsed.unit_type as f64).into(), ); + set_private_value( + scope, + object, + SVG_LENGTH_UNIT_SUFFIX_SLOT, + v8_string(scope, parsed.unit.suffix()) + .unwrap_or_else(|| v8str(scope, "")) + .into(), + ); + set_private_value( + scope, + object, + SVG_LENGTH_VALUE_IN_SPECIFIED_UNITS_SLOT, + v8::Number::new(scope, parsed.value).into(), + ); let raw = parsed .raw .map(str::to_owned) - .or_else(|| Some(serialize_svg_length_value(parsed.value, parsed.unit_type))) + .or_else(|| Some(SvgLength::new(parsed.value, parsed.unit).serialize())) .unwrap_or_else(|| "0".to_owned()); - set_svg_length_value(scope, object, parsed.value, &raw); + let value = resolve_svg_length_user_value_for_unit(scope, object, parsed.value, parsed.unit) + .or_else(|| resolve_svg_length_without_context(parsed.value, parsed.unit)) + .unwrap_or(parsed.value); + set_svg_length_value(scope, object, value, &raw); +} + +pub(super) fn set_svg_length_value_in_user_units<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + value: f64, +) { + let unit = svg_length_unit_slot(scope, object); + let specified_value = svg_length_unit_factor(scope, object, unit) + .filter(|factor| *factor != 0.0) + .map(|factor| value / factor) + .unwrap_or(value); + set_svg_length_specified_value(scope, object, specified_value, value, unit); +} + +pub(super) fn set_svg_length_value_in_specified_units<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + specified_value: f64, +) { + let unit = svg_length_unit_slot(scope, object); + let value = resolve_svg_length_user_value_for_unit(scope, object, specified_value, unit) + .or_else(|| resolve_svg_length_without_context(specified_value, unit)) + .unwrap_or(specified_value); + set_svg_length_specified_value(scope, object, specified_value, value, unit); +} + +fn set_svg_length_specified_value( + scope: &mut v8::PinScope<'_, '_>, + object: v8::Local<'_, v8::Object>, + specified_value: f64, + value: f64, + unit: SvgLengthUnit, +) { + set_private_value( + scope, + object, + SVG_LENGTH_VALUE_IN_SPECIFIED_UNITS_SLOT, + v8::Number::new(scope, specified_value).into(), + ); + let value_as_string = SvgLength::new(specified_value, unit).serialize(); + set_svg_length_value(scope, object, value, &value_as_string); } pub(super) fn set_svg_length_value( @@ -3650,6 +3725,7 @@ pub(super) fn svg_parsed_length_from_svg_length(length: SvgLength) -> SvgParsedL SvgParsedLength { value: length.value, unit_type: svg_length_unit_type(length.unit), + unit: length.unit, raw: None, } } @@ -3660,6 +3736,17 @@ pub(super) fn svg_length_unit_type(unit: SvgLengthUnit) -> u32 { SvgLengthUnit::Percentage => SVG_LENGTH_TYPE_PERCENTAGE, SvgLengthUnit::Ems => SVG_LENGTH_TYPE_EMS, SvgLengthUnit::Exs => SVG_LENGTH_TYPE_EXS, + SvgLengthUnit::Ch + | SvgLengthUnit::Rem + | SvgLengthUnit::Lh + | SvgLengthUnit::Rlh + | SvgLengthUnit::Cap + | SvgLengthUnit::Ic + | SvgLengthUnit::Q + | SvgLengthUnit::Vw + | SvgLengthUnit::Vh + | SvgLengthUnit::Vmin + | SvgLengthUnit::Vmax => SVG_LENGTH_TYPE_UNKNOWN, SvgLengthUnit::Px => SVG_LENGTH_TYPE_PX, SvgLengthUnit::Cm => SVG_LENGTH_TYPE_CM, SvgLengthUnit::Mm => SVG_LENGTH_TYPE_MM, @@ -3691,3 +3778,296 @@ pub(super) fn svg_length_unit_from_type(unit_type: u32) -> SvgLengthUnit { _ => SvgLengthUnit::Number, } } + +fn svg_length_unit_slot<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> SvgLengthUnit { + get_private_value(scope, object, SVG_LENGTH_UNIT_SUFFIX_SLOT) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + .and_then(|suffix| svg_geometry::parse_length(&format!("1{suffix}"))) + .map(|length| length.unit) + .unwrap_or(SvgLengthUnit::Number) +} + +pub(super) fn svg_length_value_in_user_units<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> f64 { + let specified_value = + svg_length_number_slot(scope, object, SVG_LENGTH_VALUE_IN_SPECIFIED_UNITS_SLOT) + .unwrap_or(0.0); + let unit = svg_length_unit_slot(scope, object); + if resolve_svg_absolute_length(1.0, unit).is_some() { + return svg_length_number_slot(scope, object, SVG_LENGTH_VALUE_SLOT) + .unwrap_or(specified_value); + } + if let Some(value) = + resolve_svg_length_user_value_for_unit(scope, object, specified_value, unit) + { + set_private_value( + scope, + object, + SVG_LENGTH_VALUE_SLOT, + v8::Number::new(scope, value).into(), + ); + return value; + } + resolve_svg_length_without_context(specified_value, unit) + .or_else(|| svg_length_number_slot(scope, object, SVG_LENGTH_VALUE_SLOT)) + .unwrap_or(specified_value) +} + +pub(super) fn convert_svg_length_to_unit<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + unit_type: u32, +) -> bool { + let unit = svg_length_unit_from_type(unit_type); + let value = svg_length_value_in_user_units(scope, object); + let Some(factor) = svg_length_unit_factor(scope, object, unit).filter(|factor| *factor != 0.0) + else { + return false; + }; + set_private_value( + scope, + object, + SVG_LENGTH_UNIT_TYPE_SLOT, + v8::Number::new(scope, unit_type as f64).into(), + ); + set_private_value( + scope, + object, + SVG_LENGTH_UNIT_SUFFIX_SLOT, + v8_string(scope, unit.suffix()) + .unwrap_or_else(|| v8str(scope, "")) + .into(), + ); + set_svg_length_specified_value(scope, object, value / factor, value, unit); + true +} + +fn svg_length_unit_factor<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + unit: SvgLengthUnit, +) -> Option { + resolve_svg_length_user_value_for_unit(scope, object, 1.0, unit) + .or_else(|| resolve_svg_length_without_context(1.0, unit)) +} + +fn resolve_svg_length_without_context(value: f64, unit: SvgLengthUnit) -> Option { + if let Some(value) = resolve_svg_absolute_length(value, unit) { + return Some(value); + } + matches!(unit, SvgLengthUnit::Percentage).then_some(value) +} + +fn resolve_svg_length_user_value_for_unit<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + value: f64, + unit: SvgLengthUnit, +) -> Option { + if let Some(value) = resolve_svg_absolute_length(value, unit) { + return Some(value); + } + let owner = svg_length_owner_element(scope, object)?; + let attribute = svg_length_owner_attribute(scope, object).unwrap_or_default(); + let (runtime_ptr, handle) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner).ok()?; + let runtime = unsafe { &*runtime_ptr }; + let connected = runtime + .dom_host() + .node(handle) + .is_some_and(|node| node.is_connected()); + let basis = if connected { + svg_length_percentage_basis(runtime, handle, &attribute) + } else { + 100.0 + }; + let context = svg_length_numeric_context(runtime, handle, unit, connected); + match unit { + SvgLengthUnit::Percentage => Some(value * basis / 100.0), + SvgLengthUnit::Ems => context.font_size_px.map(|basis| value * basis), + SvgLengthUnit::Exs | SvgLengthUnit::Ch => { + context.font_size_px.map(|basis| value * basis * 0.5) + } + SvgLengthUnit::Rem => context.root_font_size_px.map(|basis| value * basis), + SvgLengthUnit::Lh | SvgLengthUnit::Rlh => context.line_height_px.map(|basis| value * basis), + SvgLengthUnit::Cap => context.font_size_px.map(|basis| value * basis * 0.7), + SvgLengthUnit::Ic => context.font_size_px.map(|basis| value * basis), + SvgLengthUnit::Vw => context.viewport_width_px.map(|basis| value * basis / 100.0), + SvgLengthUnit::Vh => context + .viewport_height_px + .map(|basis| value * basis / 100.0), + SvgLengthUnit::Vmin => { + Some(value * context.viewport_width_px?.min(context.viewport_height_px?) / 100.0) + } + SvgLengthUnit::Vmax => { + Some(value * context.viewport_width_px?.max(context.viewport_height_px?) / 100.0) + } + SvgLengthUnit::Number + | SvgLengthUnit::Px + | SvgLengthUnit::Cm + | SvgLengthUnit::Mm + | SvgLengthUnit::Q + | SvgLengthUnit::In + | SvgLengthUnit::Pt + | SvgLengthUnit::Pc => unreachable!("absolute SVG length handled above"), + } +} + +fn resolve_svg_absolute_length(value: f64, unit: SvgLengthUnit) -> Option { + let pixels = match unit { + SvgLengthUnit::Number | SvgLengthUnit::Px => value, + SvgLengthUnit::Cm => value * 96.0 / 2.54, + SvgLengthUnit::Mm => value * 96.0 / 25.4, + SvgLengthUnit::Q => value * 96.0 / 101.6, + SvgLengthUnit::In => value * 96.0, + SvgLengthUnit::Pt => value * 96.0 / 72.0, + SvgLengthUnit::Pc => value * 16.0, + _ => return None, + }; + Some(pixels) +} + +fn svg_length_owner_element<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> Option> { + get_private_value(scope, object, SVG_LENGTH_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + .or_else(|| { + let list = get_private_value(scope, object, SVG_VALUE_LIST_ITEM_OWNER_LIST_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok())?; + get_private_value(scope, list, SVG_VALUE_LIST_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + }) +} + +fn svg_length_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> Option { + get_private_value(scope, object, SVG_LENGTH_OWNER_ATTRIBUTE_SLOT) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + .or_else(|| { + let list = get_private_value(scope, object, SVG_VALUE_LIST_ITEM_OWNER_LIST_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok())?; + get_private_value(scope, list, SVG_VALUE_LIST_OWNER_ATTRIBUTE_SLOT) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + }) +} + +fn svg_length_numeric_context( + runtime: &crate::native_bridge::JsContextHost, + handle: crate::document_runtime::DomHandle, + unit: SvgLengthUnit, + connected: bool, +) -> moli_css_parse::CssNumericContext { + if !connected { + return moli_css_parse::CssNumericContext::default(); + } + let font_size_px = svg_computed_pixel_value(runtime, handle, "font-size"); + let line_height_px = svg_computed_pixel_value(runtime, handle, "line-height") + .or_else(|| font_size_px.map(|font_size| font_size * 1.2)); + let document = runtime.dom_host().owner_document_handle(handle); + let root = document.and_then(|document| { + runtime + .dom_host() + .document_element_handle_for_document(document) + }); + let root_font_size_px = root + .and_then(|root| svg_computed_pixel_value(runtime, root, "font-size")) + .or(font_size_px); + let root_line_height_px = root + .and_then(|root| svg_computed_pixel_value(runtime, root, "line-height")) + .or_else(|| root_font_size_px.map(|font_size| font_size * 1.2)); + let viewport = runtime.style_viewport(); + moli_css_parse::CssNumericContext { + font_size_px, + root_font_size_px, + line_height_px: if matches!(unit, SvgLengthUnit::Rlh) { + root_line_height_px + } else { + line_height_px + }, + viewport_width_px: viewport.width, + viewport_height_px: viewport.height, + ..moli_css_parse::CssNumericContext::default() + } +} + +fn svg_computed_pixel_value( + runtime: &crate::native_bridge::JsContextHost, + handle: crate::document_runtime::DomHandle, + property: &str, +) -> Option { + let value = crate::native_bridge::element::computed_style_property_for_handle( + runtime, handle, property, + ); + moli_css_parse::parse_px_length(&value, moli_css_parse::UnitlessLength::Any) +} + +fn svg_length_percentage_basis( + runtime: &crate::native_bridge::JsContextHost, + handle: crate::document_runtime::DomHandle, + attribute: &str, +) -> f64 { + let (width, height) = svg_nearest_viewport_dimensions(runtime, handle).unwrap_or_else(|| { + let viewport = runtime.style_viewport(); + ( + viewport.width.unwrap_or(100.0), + viewport.height.unwrap_or(100.0), + ) + }); + match attribute { + "x" | "x1" | "x2" | "cx" | "rx" | "width" | "markerWidth" | "refX" => width, + "y" | "y1" | "y2" | "cy" | "ry" | "height" | "markerHeight" | "refY" => height, + _ => ((width * width + height * height) / 2.0).sqrt(), + } +} + +fn svg_nearest_viewport_dimensions( + runtime: &crate::native_bridge::JsContextHost, + handle: crate::document_runtime::DomHandle, +) -> Option<(f64, f64)> { + let dom = runtime.dom_host(); + let mut current = Some(handle); + while let Some(candidate) = current { + let node = dom.node(candidate)?; + if node + .as_element() + .is_some_and(|element| element.is_svg_element("svg")) + { + if let Some(view_box) = dom.get_attribute(candidate, "viewBox") + && let Some([_, _, width, height]) = parse_svg_view_box_value(&view_box) + && width > 0.0 + && height > 0.0 + { + return Some((width, height)); + } + let width = dom + .get_attribute(candidate, "width") + .as_deref() + .and_then(|value| { + moli_css_parse::parse_px_length(value, moli_css_parse::UnitlessLength::Any) + }); + let height = dom + .get_attribute(candidate, "height") + .as_deref() + .and_then(|value| { + moli_css_parse::parse_px_length(value, moli_css_parse::UnitlessLength::Any) + }); + if let (Some(width), Some(height)) = (width, height) { + return Some((width, height)); + } + } + current = node.parent_node(); + } + None +} diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs index ca78c74492..8fecd29843 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs @@ -1558,9 +1558,17 @@ pub(super) fn svg_length_getter<'s>( .unwrap_or(SVG_LENGTH_TYPE_NUMBER as f64); rv.set(v8::Integer::new_from_unsigned(scope, value as u32).into()); } - "value" | "valueInSpecifiedUnits" => { - let value = - svg_length_number_slot(scope, args.this(), SVG_LENGTH_VALUE_SLOT).unwrap_or(0.0); + "value" => { + let value = svg_length_value_in_user_units(scope, args.this()); + rv.set(v8::Number::new(scope, value).into()); + } + "valueInSpecifiedUnits" => { + let value = svg_length_number_slot( + scope, + args.this(), + SVG_LENGTH_VALUE_IN_SPECIFIED_UNITS_SLOT, + ) + .unwrap_or(0.0); rv.set(v8::Number::new(scope, value).into()); } "valueAsString" => { @@ -1723,7 +1731,7 @@ pub(super) fn svg_length_setter<'s>( let value = match webidl::convert::( scope, args.get(0), - webidl::Context::member("SVGLength", "value"), + webidl::Context::member("SVGLength", name), ) { Ok(value) => value.0, Err(error) => { @@ -1731,7 +1739,11 @@ pub(super) fn svg_length_setter<'s>( return; } }; - set_svg_length_numeric_value(scope, args.this(), value, SVG_LENGTH_TYPE_NUMBER); + if name == "value" { + set_svg_length_value_in_user_units(scope, args.this(), value); + } else { + set_svg_length_value_in_specified_units(scope, args.this(), value); + } reflect_svg_length_to_owner_attribute(scope, args.this()); reflect_svg_value_list_item_to_owner_list(scope, args.this(), SvgListKind::Length); } @@ -3354,7 +3366,9 @@ pub(super) fn svg_svg_element_create_length_callback<'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()); + let length = build_svg_length(scope, 0.0); + set_svg_length_owner_attribute(scope, length, args.this(), ""); + rv.set(length.into()); } } @@ -3857,8 +3871,15 @@ pub(super) fn svg_length_convert_to_specified_units_callback<'s>( ); return; } - let value = svg_length_number_slot(scope, args.this(), SVG_LENGTH_VALUE_SLOT).unwrap_or(0.0); - set_svg_length_numeric_value(scope, args.this(), value, parsed.unit_type as u32); + if !convert_svg_length_to_unit(scope, args.this(), parsed.unit_type as u32) { + throw_dom_exception( + scope, + "NotSupportedError", + 9, + "The SVG length could not be resolved in the requested unit.", + ); + return; + } reflect_svg_length_to_owner_attribute(scope, args.this()); reflect_svg_value_list_item_to_owner_list(scope, args.this(), SvgListKind::Length); rv.set_undefined(); 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 d7ffefbba7..d4e688fdf9 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs @@ -75,7 +75,9 @@ const SVG_LENGTH_LIST_ITEMS_SLOT: &str = "__moliSvgLengthListItems"; const SVG_LENGTH_OWNER_ELEMENT_SLOT: &str = "__moliSvgLengthOwnerElement"; const SVG_LENGTH_OWNER_ATTRIBUTE_SLOT: &str = "__moliSvgLengthOwnerAttribute"; const SVG_LENGTH_UNIT_TYPE_SLOT: &str = "__moliSvgLengthUnitType"; +const SVG_LENGTH_UNIT_SUFFIX_SLOT: &str = "__moliSvgLengthUnitSuffix"; const SVG_LENGTH_VALUE_SLOT: &str = "__moliSvgLengthValue"; +const SVG_LENGTH_VALUE_IN_SPECIFIED_UNITS_SLOT: &str = "__moliSvgLengthValueInSpecifiedUnits"; const SVG_LENGTH_VALUE_AS_STRING_SLOT: &str = "__moliSvgLengthValueAsString"; const SVG_NUMBER_VALUE_SLOT: &str = "__moliSvgNumberValue"; const SVG_ANIMATED_NUMBER_OWNER_ELEMENT_SLOT: &str = "__moliSvgAnimatedNumberOwnerElement"; 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 9bff837aa1..8dc3ff17b0 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 @@ -3033,6 +3033,116 @@ fn svg_svg_element_value_factories_create_typed_objects() { assert_eq!(result, "ok"); } +#[test] +fn svg_lengths_preserve_specified_units_and_resolve_live_context() { + let mut vm = new_parsed_test_vm( + "https://svg-length-context.test/", + r#" + + + + + + + + + + + + + + "#, + ); + + let result = vm + .eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const close = (actual, expected, tolerance = 1e-6) => + Math.abs(actual - expected) <= tolerance; + const svg = document.getElementById("svg"); + + const standalone = svg.createSVGLength(); + standalone.valueAsString = "48px"; + standalone.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_CM); + assert(standalone.valueAsString === "1.27cm", + `absolute serialization: ${standalone.valueAsString}`); + assert(close(standalone.valueInSpecifiedUnits, 1.27), + "absolute specified value"); + assert(close(standalone.value, 48), "absolute user value"); + standalone.value = 96; + assert(close(standalone.valueInSpecifiedUnits, 2.54), + "value setter preserves unit"); + standalone.valueInSpecifiedUnits = 1; + assert(close(standalone.value, 96 / 2.54), + "specified setter resolves user value"); + + standalone.valueAsString = "40q"; + assert(standalone.unitType === SVGLength.SVG_LENGTHTYPE_UNKNOWN, + "modern unit type"); + assert(close(standalone.value, 96 / 2.54), "quarter millimeter value"); + standalone.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_MM); + assert(standalone.valueAsString === "10mm", "quarter millimeter conversion"); + + const rect = document.getElementById("rect"); + const x = rect.x.baseVal; + x.valueAsString = "3px"; + x.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PERCENTAGE); + assert(x.valueAsString === "2%" && close(x.value, 3), + `percentage conversion uses SVG viewport: ${x.valueAsString}|${x.value}|${x.valueInSpecifiedUnits}`); + x.valueAsString = "6px"; + x.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_EMS); + assert(x.valueAsString === "0.5em" && close(x.value, 6), + "em conversion uses owner font size"); + + svg.removeAttribute("viewBox"); + x.valueAsString = "50%"; + assert(close(x.value, 75), "initial percentage basis"); + svg.width.baseVal.value = 300; + assert(close(x.value, 150), "live percentage basis"); + + const ch = document.getElementById("text").x.baseVal[0]; + assert(ch.unitType === SVGLength.SVG_LENGTHTYPE_UNKNOWN, + "list modern unit type"); + assert(close(ch.value, 100), "ch resolves from owner font size"); + ch.value = 200; + assert(close(ch.valueInSpecifiedUnits, 20), "ch reverse conversion"); + + for (const [id, expected] of [ + ["cap", 140], + ["ic", 200], + ["lh", 240], + ["rlh", 240], + ["rem", 100], + ]) { + const modern = document.getElementById(id).x.baseVal[0] ?? + document.getElementById(id).x.baseVal; + assert(modern.unitType === SVGLength.SVG_LENGTHTYPE_UNKNOWN, + `${id} unit type`); + assert(close(modern.value, expected), `${id} context value`); + const specified = modern.valueInSpecifiedUnits; + modern.value = expected * 2; + assert(close(modern.valueInSpecifiedUnits, specified * 2), + `${id} reverse conversion`); + } + + const viewport = document.getElementById("viewport").x.baseVal; + const viewportValue = viewport.value; + viewport.value = viewportValue * 2; + assert(close(viewport.valueInSpecifiedUnits, 20), + "viewport reverse conversion"); + return "ok"; + })() + "#, + ) + .expect("SVG length context probe should evaluate"); + + assert_eq!(result, "ok"); +} + #[test] fn svg_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 4f2dad9f75..13b928ec5b 100644 --- a/moli-svg/src/length.rs +++ b/moli-svg/src/length.rs @@ -11,12 +11,23 @@ pub enum SvgLengthUnit { Percentage, Ems, Exs, + Ch, + Rem, + Lh, + Rlh, + Cap, + Ic, Px, Cm, Mm, + Q, In, Pt, Pc, + Vw, + Vh, + Vmin, + Vmax, } impl SvgLengthUnit { @@ -26,12 +37,23 @@ impl SvgLengthUnit { Self::Percentage => "%", Self::Ems => "em", Self::Exs => "ex", + Self::Ch => "ch", + Self::Rem => "rem", + Self::Lh => "lh", + Self::Rlh => "rlh", + Self::Cap => "cap", + Self::Ic => "ic", Self::Px => "px", Self::Cm => "cm", Self::Mm => "mm", + Self::Q => "q", Self::In => "in", Self::Pt => "pt", Self::Pc => "pc", + Self::Vw => "vw", + Self::Vh => "vh", + Self::Vmin => "vmin", + Self::Vmax => "vmax", } } } @@ -55,18 +77,20 @@ impl SvgLength { } pub fn parse_length(raw: &str) -> Option { - raw.trim() - .to_ascii_lowercase() + let normalized = raw.trim().to_ascii_lowercase(); + normalized .parse::() .ok() .map(svg_length) + .or_else(|| parse_extended_length(&normalized)) } pub fn parse_length_list(raw: &str) -> Option> { let normalized = raw.to_ascii_lowercase(); - LengthListParser::from(normalized.as_str()) + let parsed = LengthListParser::from(normalized.as_str()) .map(|length| length.ok().map(svg_length)) - .collect() + .collect::>>(); + parsed.or_else(|| parse_extended_length_list(&normalized)) } pub fn parse_number(raw: &str) -> Option { @@ -112,3 +136,121 @@ fn svg_length(length: SvgTypesLength) -> SvgLength { }, } } + +fn parse_extended_length(raw: &str) -> Option { + let number_len = number_prefix_len(raw)?; + let value = raw[..number_len].parse::().ok()?; + if !value.is_finite() { + return None; + } + let unit = match &raw[number_len..] { + "ch" => SvgLengthUnit::Ch, + "rem" => SvgLengthUnit::Rem, + "lh" => SvgLengthUnit::Lh, + "rlh" => SvgLengthUnit::Rlh, + "cap" => SvgLengthUnit::Cap, + "ic" => SvgLengthUnit::Ic, + "q" => SvgLengthUnit::Q, + "vw" => SvgLengthUnit::Vw, + "vh" => SvgLengthUnit::Vh, + "vmin" => SvgLengthUnit::Vmin, + "vmax" => SvgLengthUnit::Vmax, + _ => return None, + }; + Some(SvgLength::new(value, unit)) +} + +fn parse_extended_length_list(raw: &str) -> Option> { + let bytes = raw.as_bytes(); + let mut index = 0; + let mut lengths = Vec::new(); + + skip_ascii_whitespace(bytes, &mut index); + if index == bytes.len() { + return Some(lengths); + } + + loop { + let number_len = number_prefix_len(&raw[index..])?; + let number_end = index + number_len; + let mut token_end = number_end; + if matches!(bytes.get(token_end), Some(b'%')) { + token_end += 1; + } else { + while matches!(bytes.get(token_end), Some(b'a'..=b'z')) { + token_end += 1; + } + } + lengths.push(parse_length(&raw[index..token_end])?); + index = token_end; + if index == bytes.len() { + return Some(lengths); + } + + let separator_start = index; + skip_ascii_whitespace(bytes, &mut index); + if index == bytes.len() { + return Some(lengths); + } + if matches!(bytes.get(index), Some(b',')) { + index += 1; + skip_ascii_whitespace(bytes, &mut index); + if index == bytes.len() || matches!(bytes.get(index), Some(b',')) { + return None; + } + } else if index == separator_start && !matches!(bytes.get(index), Some(b'+' | b'-')) { + return None; + } + } +} + +fn skip_ascii_whitespace(bytes: &[u8], index: &mut usize) { + while matches!( + bytes.get(*index), + Some(b'\t' | b'\n' | b'\x0c' | b'\r' | b' ') + ) { + *index += 1; + } +} + +fn number_prefix_len(raw: &str) -> Option { + let bytes = raw.as_bytes(); + let mut index = 0; + if matches!(bytes.get(index), Some(b'+' | b'-')) { + index += 1; + } + + let integer_start = index; + while matches!(bytes.get(index), Some(b'0'..=b'9')) { + index += 1; + } + let integer_digits = index - integer_start; + + let mut fraction_digits = 0; + if matches!(bytes.get(index), Some(b'.')) { + index += 1; + let fraction_start = index; + while matches!(bytes.get(index), Some(b'0'..=b'9')) { + index += 1; + } + fraction_digits = index - fraction_start; + } + if integer_digits == 0 && fraction_digits == 0 { + return None; + } + + if matches!(bytes.get(index), Some(b'e' | b'E')) { + index += 1; + if matches!(bytes.get(index), Some(b'+' | b'-')) { + index += 1; + } + let exponent_start = index; + while matches!(bytes.get(index), Some(b'0'..=b'9')) { + index += 1; + } + if exponent_start == index { + return None; + } + } + Some(index) +} diff --git a/moli-svg/src/lib.rs b/moli-svg/src/lib.rs index 4b15cc0d45..a7cc81986b 100644 --- a/moli-svg/src/lib.rs +++ b/moli-svg/src/lib.rs @@ -422,6 +422,18 @@ mod tests { assert_close(lengths[4].value, -5.0); assert!(parse_length_list("1px nope 2px").is_none()); + let modern = parse_length_list("10cap, 2rlh 3vw-4q").unwrap(); + assert_eq!(modern.len(), 4); + assert_eq!(modern[0].unit, SvgLengthUnit::Cap); + assert_eq!(modern[1].unit, SvgLengthUnit::Rlh); + assert_eq!(modern[2].unit, SvgLengthUnit::Vw); + assert_close(modern[3].value, -4.0); + assert_eq!(modern[3].unit, SvgLengthUnit::Q); + assert_eq!(modern[0].serialize(), "10cap"); + assert_eq!(parse_length_list(" 10cap ").unwrap().len(), 1); + assert!(parse_length_list("10cap,").is_none()); + assert!(parse_length_list("10cap nope").is_none()); + assert_close(parse_number(" .5 ").unwrap(), 0.5); assert_eq!( parse_number_list("10 20,30-40").unwrap(),