mirror of
https://github.com/lexmount/moli.git
synced 2026-10-05 16:00:54 +00:00
fix(svg): resolve length units in context
This commit is contained in:
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user