fix(svg): resolve length units in context

This commit is contained in:
ldm0
2026-09-09 06:52:20 +08:00
parent 8d41fc0400
commit feda69de4a
6 changed files with 691 additions and 24 deletions
@@ -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<f64> {
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<f64> {
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<f64> {
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<f64> {
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<v8::Local<'s, v8::Object>> {
get_private_value(scope, object, SVG_LENGTH_OWNER_ELEMENT_SLOT)
.and_then(|value| v8::Local::<v8::Object>::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::<v8::Object>::try_from(value).ok())?;
get_private_value(scope, list, SVG_VALUE_LIST_OWNER_ELEMENT_SLOT)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
})
}
fn svg_length_owner_attribute<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<String> {
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::<v8::Object>::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<f64> {
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
}
@@ -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::<webidl::Double>(
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();
@@ -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";
@@ -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#"<!doctype html>
<html style="font-size:20px;line-height:24px">
<body>
<svg id="svg" width="150" height="50" viewBox="0 0 150 50">
<rect id="rect" style="font-size:12px"/>
<text id="text" x="10ch" style="font-size:20px"></text>
<text id="cap" x="10cap" style="font-size:20px"></text>
<text id="ic" x="10ic" style="font-size:20px"></text>
<text id="lh" x="10lh" style="font-size:20px;line-height:24px"></text>
<text id="rlh" x="10rlh" style="font-size:10px"></text>
<rect id="rem" x="5rem"/>
<rect id="viewport" x="10vw"/>
</svg>
</body>
</html>"#,
);
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(
+146 -4
View File
@@ -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<SvgLength> {
raw.trim()
.to_ascii_lowercase()
let normalized = raw.trim().to_ascii_lowercase();
normalized
.parse::<SvgTypesLength>()
.ok()
.map(svg_length)
.or_else(|| parse_extended_length(&normalized))
}
pub fn parse_length_list(raw: &str) -> Option<Vec<SvgLength>> {
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::<Option<Vec<_>>>();
parsed.or_else(|| parse_extended_length_list(&normalized))
}
pub fn parse_number(raw: &str) -> Option<f64> {
@@ -112,3 +136,121 @@ fn svg_length(length: SvgTypesLength) -> SvgLength {
},
}
}
fn parse_extended_length(raw: &str) -> Option<SvgLength> {
let number_len = number_prefix_len(raw)?;
let value = raw[..number_len].parse::<f64>().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<Vec<SvgLength>> {
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<usize> {
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)
}
+12
View File
@@ -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(),