fix(svg): reflect fit-to-view-box attributes

This commit is contained in:
ldm0
2026-09-09 06:52:20 +08:00
parent ae6ac0bf07
commit 0969746ced
8 changed files with 1183 additions and 3 deletions
@@ -1791,6 +1791,12 @@ pub(crate) fn finish_context_bootstrap(
("SVGAnimatedString", "SVGAnimatedString"),
("SVGAnimatedLength", "SVGAnimatedLength"),
("SVGAnimatedAngle", "SVGAnimatedAngle"),
("SVGAnimatedRect", "SVGAnimatedRect"),
("SVGPreserveAspectRatio", "SVGPreserveAspectRatio"),
(
"SVGAnimatedPreserveAspectRatio",
"SVGAnimatedPreserveAspectRatio",
),
("SVGLengthList", "SVGLengthList"),
("SVGAnimatedLengthList", "SVGAnimatedLengthList"),
("SVGAnimatedNumber", "SVGAnimatedNumber"),
@@ -138,6 +138,21 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[
parent: None,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
name: "SVGAnimatedRect",
parent: None,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
name: "SVGPreserveAspectRatio",
parent: None,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
name: "SVGAnimatedPreserveAspectRatio",
parent: None,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
name: "SVGLengthList",
parent: None,
@@ -575,6 +575,27 @@ macro_rules! define_svg_enumeration_constants {
};
}
define_svg_enumeration_constants!(
SvgPreserveAspectRatioTemplateConstantsDeclaration,
"SVGPreserveAspectRatio",
{
preserve_aspect_ratio_unknown => "SVG_PRESERVEASPECTRATIO_UNKNOWN" = SVG_PRESERVE_ASPECT_RATIO_UNKNOWN,
preserve_aspect_ratio_none => "SVG_PRESERVEASPECTRATIO_NONE" = SVG_PRESERVE_ASPECT_RATIO_NONE,
preserve_aspect_ratio_x_min_y_min => "SVG_PRESERVEASPECTRATIO_XMINYMIN" = SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MIN,
preserve_aspect_ratio_x_mid_y_min => "SVG_PRESERVEASPECTRATIO_XMIDYMIN" = SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MIN,
preserve_aspect_ratio_x_max_y_min => "SVG_PRESERVEASPECTRATIO_XMAXYMIN" = SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MIN,
preserve_aspect_ratio_x_min_y_mid => "SVG_PRESERVEASPECTRATIO_XMINYMID" = SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MID,
preserve_aspect_ratio_x_mid_y_mid => "SVG_PRESERVEASPECTRATIO_XMIDYMID" = SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MID,
preserve_aspect_ratio_x_max_y_mid => "SVG_PRESERVEASPECTRATIO_XMAXYMID" = SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MID,
preserve_aspect_ratio_x_min_y_max => "SVG_PRESERVEASPECTRATIO_XMINYMAX" = SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MAX,
preserve_aspect_ratio_x_mid_y_max => "SVG_PRESERVEASPECTRATIO_XMIDYMAX" = SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MAX,
preserve_aspect_ratio_x_max_y_max => "SVG_PRESERVEASPECTRATIO_XMAXYMAX" = SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MAX,
meet_or_slice_unknown => "SVG_MEETORSLICE_UNKNOWN" = SVG_MEET_OR_SLICE_UNKNOWN,
meet_or_slice_meet => "SVG_MEETORSLICE_MEET" = SVG_MEET_OR_SLICE_MEET,
meet_or_slice_slice => "SVG_MEETORSLICE_SLICE" = SVG_MEET_OR_SLICE_SLICE,
}
);
define_svg_enumeration_constants!(
SvgComponentTransferFunctionElementTemplateConstantsDeclaration,
"SVGComponentTransferFunctionElement",
@@ -973,6 +994,62 @@ struct SvgAnimatedAngleTemplateAccessorsDeclaration {
anim_val: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "SVGAnimatedRect", enumerable)]
struct SvgAnimatedRectTemplateAccessorsDeclaration {
#[webapi(
accessor_property = "baseVal",
getter = svg_animated_rect_getter,
data = callback_data_index_value(scope, 0)
)]
base_val: (),
#[webapi(
accessor_property = "animVal",
getter = svg_animated_rect_getter,
data = callback_data_index_value(scope, 1)
)]
anim_val: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "SVGPreserveAspectRatio", enumerable)]
struct SvgPreserveAspectRatioTemplateAccessorsDeclaration {
#[webapi(
accessor_property = "align",
getter = svg_preserve_aspect_ratio_getter,
setter = svg_preserve_aspect_ratio_setter,
data = callback_data_index_value(scope, 0)
)]
align: (),
#[webapi(
accessor_property = "meetOrSlice",
getter = svg_preserve_aspect_ratio_getter,
setter = svg_preserve_aspect_ratio_setter,
data = callback_data_index_value(scope, 1)
)]
meet_or_slice: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "SVGAnimatedPreserveAspectRatio", enumerable)]
struct SvgAnimatedPreserveAspectRatioTemplateAccessorsDeclaration {
#[webapi(
accessor_property = "baseVal",
getter = svg_animated_preserve_aspect_ratio_getter,
data = callback_data_index_value(scope, 0)
)]
base_val: (),
#[webapi(
accessor_property = "animVal",
getter = svg_animated_preserve_aspect_ratio_getter,
data = callback_data_index_value(scope, 1)
)]
anim_val: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "SVGNumber", enumerable)]
struct SvgNumberTemplateAccessorsDeclaration {
@@ -1252,6 +1329,35 @@ struct SvgGeometryElementPrototypeAccessorsDeclaration {
path_length: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "SVGFitToViewBox", enumerable)]
struct SvgFitToViewBoxPrototypeAccessorsDeclaration {
#[webapi(
accessor_property = "viewBox",
getter = svg_fit_to_view_box_getter,
data = callback_data_index_value(scope, 0)
)]
view_box: (),
#[webapi(
accessor_property = "preserveAspectRatio",
getter = svg_fit_to_view_box_getter,
data = callback_data_index_value(scope, 1)
)]
preserve_aspect_ratio: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "SVGImageElement", enumerable)]
struct SvgImagePreserveAspectRatioPrototypeAccessorsDeclaration {
#[webapi(
accessor_property = "preserveAspectRatio",
getter = svg_fit_to_view_box_getter,
data = callback_data_index_value(scope, 1)
)]
preserve_aspect_ratio: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "SVGTextContentElement", enumerable)]
struct SvgTextContentElementPrototypeAccessorsDeclaration {
@@ -1829,6 +1935,34 @@ pub(super) fn install_svg_animated_angle_bindings<'s>(
SvgAnimatedAngleTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto);
}
pub(super) fn install_svg_animated_rect_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
) {
let proto = template.prototype_template(scope);
SvgAnimatedRectTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto);
}
pub(super) fn install_svg_preserve_aspect_ratio_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
) {
let proto = template.prototype_template(scope);
SvgPreserveAspectRatioTemplateConstantsDeclaration::initialize_template(scope, template);
SvgPreserveAspectRatioTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto);
SvgPreserveAspectRatioTemplateConstantsDeclaration::initialize_prototype_template(scope, proto);
}
pub(super) fn install_svg_animated_preserve_aspect_ratio_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
) {
let proto = template.prototype_template(scope);
SvgAnimatedPreserveAspectRatioTemplateAccessorsDeclaration::initialize_prototype_template(
scope, proto,
);
}
pub(super) fn install_svg_number_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
@@ -2082,6 +2216,18 @@ pub(super) fn install_svg_element_accessor_bindings<'s>(
interface_name: &str,
) {
let prototype = template.prototype_template(scope);
if matches!(
interface_name,
"SVGSVGElement"
| "SVGSymbolElement"
| "SVGMarkerElement"
| "SVGPatternElement"
| "SVGViewElement"
) {
SvgFitToViewBoxPrototypeAccessorsDeclaration::initialize_prototype_template(
scope, prototype,
);
}
if matches!(
interface_name,
"SVGFEBlendElement"
@@ -2256,6 +2402,11 @@ pub(super) fn install_svg_element_accessor_bindings<'s>(
SvgUriReferencePrototypeAccessorsDeclaration::initialize_prototype_template(
scope, prototype,
);
if interface_name == "SVGImageElement" {
SvgImagePreserveAspectRatioPrototypeAccessorsDeclaration::initialize_prototype_template(
scope, prototype,
);
}
}
"SVGForeignObjectElement" => {
SvgGraphicsBoxElementPrototypeAccessorsDeclaration::initialize_prototype_template(
@@ -141,6 +141,46 @@ struct SvgAnimatedAngleObjectDeclaration<'scope> {
anim_val: v8::Local<'scope, v8::Object>,
}
#[derive(WebApiObject)]
#[webapi(
interface = "SVGAnimatedRect",
fallback_to_string_tag = "SVGAnimatedRect"
)]
struct SvgAnimatedRectObjectDeclaration<'scope> {
#[webapi(slot = SVG_ANIMATED_RECT_BASE_VAL_SLOT)]
base_val: v8::Local<'scope, v8::Object>,
#[webapi(slot = SVG_ANIMATED_RECT_ANIM_VAL_SLOT)]
anim_val: v8::Local<'scope, v8::Object>,
}
#[derive(WebApiObject)]
#[webapi(
interface = "SVGPreserveAspectRatio",
fallback_to_string_tag = "SVGPreserveAspectRatio"
)]
struct SvgPreserveAspectRatioObjectDeclaration<'scope> {
#[webapi(slot = SVG_PRESERVE_ASPECT_RATIO_ALIGN_SLOT)]
align: u32,
#[webapi(slot = SVG_PRESERVE_ASPECT_RATIO_MEET_OR_SLICE_SLOT)]
meet_or_slice: u32,
#[webapi(slot = SVG_PRESERVE_ASPECT_RATIO_OWNER_ELEMENT_SLOT)]
owner: v8::Local<'scope, v8::Object>,
#[webapi(slot = SVG_PRESERVE_ASPECT_RATIO_READ_ONLY_SLOT)]
read_only: bool,
}
#[derive(WebApiObject)]
#[webapi(
interface = "SVGAnimatedPreserveAspectRatio",
fallback_to_string_tag = "SVGAnimatedPreserveAspectRatio"
)]
struct SvgAnimatedPreserveAspectRatioObjectDeclaration<'scope> {
#[webapi(slot = SVG_ANIMATED_PRESERVE_ASPECT_RATIO_BASE_VAL_SLOT)]
base_val: v8::Local<'scope, v8::Object>,
#[webapi(slot = SVG_ANIMATED_PRESERVE_ASPECT_RATIO_ANIM_VAL_SLOT)]
anim_val: v8::Local<'scope, v8::Object>,
}
#[derive(WebApiObject)]
#[webapi(interface = "SVGLength", fallback_to_string_tag = "SVGLength")]
struct SvgLengthObjectDeclaration {
@@ -1127,6 +1167,34 @@ pub(super) fn build_svg_animated_angle_for_attribute<'s>(
.expect("SVGAnimatedAngle declaration should bind")
}
pub(super) fn build_svg_animated_rect_for_view_box<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner: v8::Local<'s, v8::Object>,
) -> v8::Local<'s, v8::Object> {
let values = svg_view_box_attribute_value(scope, owner);
let base_val = super::rect::build_svg_view_box_rect(scope, owner, values, false);
let anim_val = super::rect::build_svg_view_box_rect(scope, owner, values, true);
SvgAnimatedRectObjectDeclaration::new(base_val, anim_val)
.bind(scope)
.expect("SVGAnimatedRect declaration should bind")
}
pub(super) fn build_svg_animated_preserve_aspect_ratio<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner: v8::Local<'s, v8::Object>,
) -> v8::Local<'s, v8::Object> {
let [align, meet_or_slice] = svg_preserve_aspect_ratio_attribute_value(scope, owner);
let base_val = SvgPreserveAspectRatioObjectDeclaration::new(align, meet_or_slice, owner, false)
.bind(scope)
.expect("SVGPreserveAspectRatio base value declaration should bind");
let anim_val = SvgPreserveAspectRatioObjectDeclaration::new(align, meet_or_slice, owner, true)
.bind(scope)
.expect("SVGPreserveAspectRatio animated value declaration should bind");
SvgAnimatedPreserveAspectRatioObjectDeclaration::new(base_val, anim_val)
.bind(scope)
.expect("SVGAnimatedPreserveAspectRatio declaration should bind")
}
pub(super) fn build_svg_angle<'s>(scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Object> {
build_svg_angle_from_parsed(scope, &SvgParsedAngle::default(), false)
}
@@ -2857,6 +2925,248 @@ fn svg_animated_length_attribute_value<'s>(
.unwrap_or_default()
}
pub(super) fn sync_svg_animated_rect_from_owner_attribute<'s>(
scope: &mut v8::PinScope<'s, '_>,
animated: v8::Local<'s, v8::Object>,
owner: v8::Local<'s, v8::Object>,
) {
let values = svg_view_box_attribute_value(scope, owner);
for slot in [
SVG_ANIMATED_RECT_BASE_VAL_SLOT,
SVG_ANIMATED_RECT_ANIM_VAL_SLOT,
] {
if let Some(rect) = get_private_value(scope, animated, slot)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
{
super::rect::set_svg_view_box_rect_values(scope, rect, values);
}
}
}
pub(super) fn sync_svg_view_box_rect_from_owner<'s>(
scope: &mut v8::PinScope<'s, '_>,
rect: v8::Local<'s, v8::Object>,
) {
let Some(owner) = super::rect::svg_view_box_rect_owner(scope, rect) else {
return;
};
if let Some(animated) = get_private_value(scope, owner, SVG_FIT_VIEW_BOX_SLOT)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
{
sync_svg_animated_rect_from_owner_attribute(scope, animated, owner);
} else {
let values = svg_view_box_attribute_value(scope, owner);
super::rect::set_svg_view_box_rect_values(scope, rect, values);
}
}
pub(super) fn reflect_svg_view_box_rect_mutation<'s>(
scope: &mut v8::PinScope<'s, '_>,
rect: v8::Local<'s, v8::Object>,
) {
let Some(owner) = super::rect::svg_view_box_rect_owner(scope, rect) else {
return;
};
let values = super::rect::svg_view_box_rect_values(scope, rect);
// Keep float round trips, including integral values outside the i64 range.
let serialized = values
.map(|value| {
if value == 0.0 {
"0".to_owned()
} else {
(value as f32).to_string()
}
})
.join(" ");
set_svg_owner_attribute_value(scope, owner, "viewBox", &serialized);
if let Some(animated) = get_private_value(scope, owner, SVG_FIT_VIEW_BOX_SLOT)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
{
sync_svg_animated_rect_from_owner_attribute(scope, animated, owner);
}
}
fn svg_view_box_attribute_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner: v8::Local<'s, v8::Object>,
) -> [f64; 4] {
svg_owner_attribute_value(scope, owner, "viewBox")
.as_deref()
.and_then(parse_svg_view_box_value)
.unwrap_or([0.0; 4])
}
fn parse_svg_view_box_value(raw: &str) -> Option<[f64; 4]> {
let values = svg_geometry::parse_number_list(raw)?;
let [x, y, width, height] = values.as_slice() else {
return None;
};
if *width < 0.0 || *height < 0.0 {
return None;
}
let values = [*x, *y, *width, *height].map(|value| f64::from(value as f32));
values
.iter()
.all(|value| value.is_finite())
.then_some(values)
}
pub(super) fn sync_svg_animated_preserve_aspect_ratio_from_owner_attribute<'s>(
scope: &mut v8::PinScope<'s, '_>,
animated: v8::Local<'s, v8::Object>,
owner: v8::Local<'s, v8::Object>,
) {
let value = svg_preserve_aspect_ratio_attribute_value(scope, owner);
for slot in [
SVG_ANIMATED_PRESERVE_ASPECT_RATIO_BASE_VAL_SLOT,
SVG_ANIMATED_PRESERVE_ASPECT_RATIO_ANIM_VAL_SLOT,
] {
if let Some(aspect_ratio) = get_private_value(scope, animated, slot)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
{
set_svg_preserve_aspect_ratio_value(scope, aspect_ratio, value);
}
}
}
pub(super) fn sync_svg_preserve_aspect_ratio_from_owner_attribute<'s>(
scope: &mut v8::PinScope<'s, '_>,
aspect_ratio: v8::Local<'s, v8::Object>,
) {
let Some(owner) = get_private_value(
scope,
aspect_ratio,
SVG_PRESERVE_ASPECT_RATIO_OWNER_ELEMENT_SLOT,
)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok()) else {
return;
};
let value = svg_preserve_aspect_ratio_attribute_value(scope, owner);
set_svg_preserve_aspect_ratio_value(scope, aspect_ratio, value);
}
pub(super) fn set_svg_preserve_aspect_ratio_value(
scope: &mut v8::PinScope<'_, '_>,
aspect_ratio: v8::Local<'_, v8::Object>,
[align, meet_or_slice]: [u32; 2],
) {
set_private_value(
scope,
aspect_ratio,
SVG_PRESERVE_ASPECT_RATIO_ALIGN_SLOT,
v8::Integer::new_from_unsigned(scope, align).into(),
);
set_private_value(
scope,
aspect_ratio,
SVG_PRESERVE_ASPECT_RATIO_MEET_OR_SLICE_SLOT,
v8::Integer::new_from_unsigned(scope, meet_or_slice).into(),
);
}
pub(super) fn reflect_svg_preserve_aspect_ratio_to_owner_attribute<'s>(
scope: &mut v8::PinScope<'s, '_>,
aspect_ratio: v8::Local<'s, v8::Object>,
) {
let Some(owner) = get_private_value(
scope,
aspect_ratio,
SVG_PRESERVE_ASPECT_RATIO_OWNER_ELEMENT_SLOT,
)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok()) else {
return;
};
let align = svg_number_slot(scope, aspect_ratio, SVG_PRESERVE_ASPECT_RATIO_ALIGN_SLOT)
.unwrap_or(SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MID as f64) as u32;
let meet_or_slice = svg_number_slot(
scope,
aspect_ratio,
SVG_PRESERVE_ASPECT_RATIO_MEET_OR_SLICE_SLOT,
)
.unwrap_or(SVG_MEET_OR_SLICE_MEET as f64) as u32;
let (Some(align), Some(meet_or_slice)) = (
serialize_svg_preserve_aspect_ratio_align(align),
serialize_svg_meet_or_slice(meet_or_slice),
) else {
return;
};
let serialized = format!("{align} {meet_or_slice}");
set_svg_owner_attribute_value(scope, owner, "preserveAspectRatio", &serialized);
if let Some(animated) = get_private_value(scope, owner, SVG_FIT_PRESERVE_ASPECT_RATIO_SLOT)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
{
sync_svg_animated_preserve_aspect_ratio_from_owner_attribute(scope, animated, owner);
}
}
fn svg_preserve_aspect_ratio_attribute_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner: v8::Local<'s, v8::Object>,
) -> [u32; 2] {
svg_owner_attribute_value(scope, owner, "preserveAspectRatio")
.as_deref()
.and_then(parse_svg_preserve_aspect_ratio_value)
.unwrap_or([
SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MID,
SVG_MEET_OR_SLICE_MEET,
])
}
fn parse_svg_preserve_aspect_ratio_value(raw: &str) -> Option<[u32; 2]> {
let mut tokens = raw.split_ascii_whitespace();
let mut align = tokens.next()?;
if align == "defer" {
align = tokens.next()?;
}
let align = parse_svg_preserve_aspect_ratio_align(align)?;
let meet_or_slice = match tokens.next() {
None | Some("meet") => SVG_MEET_OR_SLICE_MEET,
Some("slice") => SVG_MEET_OR_SLICE_SLICE,
Some(_) => return None,
};
tokens.next().is_none().then_some([align, meet_or_slice])
}
fn parse_svg_preserve_aspect_ratio_align(value: &str) -> Option<u32> {
match value {
"none" => Some(SVG_PRESERVE_ASPECT_RATIO_NONE),
"xMinYMin" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MIN),
"xMidYMin" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MIN),
"xMaxYMin" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MIN),
"xMinYMid" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MID),
"xMidYMid" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MID),
"xMaxYMid" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MID),
"xMinYMax" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MAX),
"xMidYMax" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MAX),
"xMaxYMax" => Some(SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MAX),
_ => None,
}
}
fn serialize_svg_preserve_aspect_ratio_align(value: u32) -> Option<&'static str> {
match value {
SVG_PRESERVE_ASPECT_RATIO_NONE => Some("none"),
SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MIN => Some("xMinYMin"),
SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MIN => Some("xMidYMin"),
SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MIN => Some("xMaxYMin"),
SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MID => Some("xMinYMid"),
SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MID => Some("xMidYMid"),
SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MID => Some("xMaxYMid"),
SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MAX => Some("xMinYMax"),
SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MAX => Some("xMidYMax"),
SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MAX => Some("xMaxYMax"),
_ => None,
}
}
fn serialize_svg_meet_or_slice(value: u32) -> Option<&'static str> {
match value {
SVG_MEET_OR_SLICE_MEET => Some("meet"),
SVG_MEET_OR_SLICE_SLICE => Some("slice"),
_ => None,
}
}
pub(super) fn set_svg_animated_string_owner_attribute<'s>(
scope: &mut v8::PinScope<'s, '_>,
animated: v8::Local<'s, v8::Object>,
@@ -3066,6 +3376,21 @@ pub(super) fn svg_owner_attribute_value<'s>(
)
}
fn set_svg_owner_attribute_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner: v8::Local<'s, v8::Object>,
attribute: &str,
value: &str,
) {
let Ok((runtime_ptr, handle)) =
crate::native_bridge::node_runtime_and_handle_from_object(scope, owner)
else {
return;
};
let runtime = unsafe { &mut *runtime_ptr };
let _ = runtime.set_attribute(scope, runtime_ptr, handle, attribute, value);
}
pub(super) fn parse_svg_length_value(raw: &str) -> Option<SvgParsedLength> {
svg_geometry::parse_length(raw).map(svg_parsed_length_from_svg_length)
}
@@ -61,6 +61,8 @@ const SVG_LENGTH_ACCESSOR_NAMES: &[&str] = &[
"valueAsString",
];
const SVG_ANIMATED_ACCESSOR_NAMES: &[&str] = &["baseVal", "animVal"];
const SVG_FIT_TO_VIEW_BOX_ACCESSOR_NAMES: &[&str] = &["viewBox", "preserveAspectRatio"];
const SVG_PRESERVE_ASPECT_RATIO_ACCESSOR_NAMES: &[&str] = &["align", "meetOrSlice"];
const SVG_TRANSFORM_ACCESSOR_NAMES: &[&str] = &["type", "matrix", "angle"];
const SVG_MATRIX_ACCESSOR_NAMES: &[&str] = &["a", "b", "c", "d", "e", "f"];
@@ -133,6 +135,103 @@ fn require_svg_receiver<'s>(
false
}
fn require_svg_fit_to_view_box_receiver<'s>(
scope: &mut v8::PinScope<'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!("SVGFitToViewBox.{member} called on incompatible receiver."),
);
return false;
};
let is_supported_element = unsafe { &*runtime_ptr }
.dom_host()
.node(handle)
.is_some_and(|node| {
node.namespace() == Some(crate::native_bridge::document::SVG_NS)
&& node.local_name().is_some_and(|local_name| match member {
"viewBox" => {
matches!(local_name, "svg" | "symbol" | "marker" | "pattern" | "view")
}
"preserveAspectRatio" => matches!(
local_name,
"svg" | "symbol" | "marker" | "pattern" | "view" | "image"
),
_ => false,
})
});
if !is_supported_element {
webidl::throw_type_error(
scope,
&format!("SVGFitToViewBox.{member} called on incompatible receiver."),
);
}
is_supported_element
}
pub(super) fn svg_fit_to_view_box_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(member) = callback_data_item(
scope,
&args,
SVG_FIT_TO_VIEW_BOX_ACCESSOR_NAMES,
"SVGFitToViewBox attributes",
) else {
rv.set_undefined();
return;
};
let owner = args.this();
if !require_svg_fit_to_view_box_receiver(scope, owner, member) {
return;
}
let (slot, value) = match member {
"viewBox" => {
let slot = SVG_FIT_VIEW_BOX_SLOT;
if let Some(value) = get_private_value(scope, owner, slot) {
if let Ok(animated) = v8::Local::<v8::Object>::try_from(value) {
sync_svg_animated_rect_from_owner_attribute(scope, animated, owner);
}
(slot, value)
} else {
(
slot,
build_svg_animated_rect_for_view_box(scope, owner).into(),
)
}
}
"preserveAspectRatio" => {
let slot = SVG_FIT_PRESERVE_ASPECT_RATIO_SLOT;
if let Some(value) = get_private_value(scope, owner, slot) {
if let Ok(animated) = v8::Local::<v8::Object>::try_from(value) {
sync_svg_animated_preserve_aspect_ratio_from_owner_attribute(
scope, animated, owner,
);
}
(slot, value)
} else {
(
slot,
build_svg_animated_preserve_aspect_ratio(scope, owner).into(),
)
}
}
_ => {
rv.set_undefined();
return;
}
};
set_private_value(scope, owner, slot, value);
rv.set(value);
}
pub(super) fn svg_element_class_name_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -1113,6 +1212,192 @@ pub(super) fn svg_animated_angle_getter<'s>(
);
}
pub(super) fn svg_animated_rect_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(name) = callback_data_item(
scope,
&args,
SVG_ANIMATED_ACCESSOR_NAMES,
"SVGAnimatedRect attributes",
) else {
rv.set_undefined();
return;
};
if !require_svg_receiver(
scope,
args.this(),
SVG_ANIMATED_RECT_BASE_VAL_SLOT,
"SVGAnimatedRect",
&format!("{name} getter"),
) {
return;
}
let slot = match name {
"baseVal" => SVG_ANIMATED_RECT_BASE_VAL_SLOT,
"animVal" => SVG_ANIMATED_RECT_ANIM_VAL_SLOT,
_ => {
rv.set_undefined();
return;
}
};
rv.set(
get_private_value(scope, args.this(), slot).unwrap_or_else(|| v8::undefined(scope).into()),
);
}
pub(super) fn svg_animated_preserve_aspect_ratio_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(name) = callback_data_item(
scope,
&args,
SVG_ANIMATED_ACCESSOR_NAMES,
"SVGAnimatedPreserveAspectRatio attributes",
) else {
rv.set_undefined();
return;
};
if !require_svg_receiver(
scope,
args.this(),
SVG_ANIMATED_PRESERVE_ASPECT_RATIO_BASE_VAL_SLOT,
"SVGAnimatedPreserveAspectRatio",
&format!("{name} getter"),
) {
return;
}
let slot = match name {
"baseVal" => SVG_ANIMATED_PRESERVE_ASPECT_RATIO_BASE_VAL_SLOT,
"animVal" => SVG_ANIMATED_PRESERVE_ASPECT_RATIO_ANIM_VAL_SLOT,
_ => {
rv.set_undefined();
return;
}
};
rv.set(
get_private_value(scope, args.this(), slot).unwrap_or_else(|| v8::undefined(scope).into()),
);
}
pub(super) fn svg_preserve_aspect_ratio_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(name) = callback_data_item(
scope,
&args,
SVG_PRESERVE_ASPECT_RATIO_ACCESSOR_NAMES,
"SVGPreserveAspectRatio attributes",
) else {
rv.set_undefined();
return;
};
let aspect_ratio = args.this();
if !require_svg_receiver(
scope,
aspect_ratio,
SVG_PRESERVE_ASPECT_RATIO_ALIGN_SLOT,
"SVGPreserveAspectRatio",
&format!("{name} getter"),
) {
return;
}
sync_svg_preserve_aspect_ratio_from_owner_attribute(scope, aspect_ratio);
let (slot, initial_value) = match name {
"align" => (
SVG_PRESERVE_ASPECT_RATIO_ALIGN_SLOT,
SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MID,
),
"meetOrSlice" => (
SVG_PRESERVE_ASPECT_RATIO_MEET_OR_SLICE_SLOT,
SVG_MEET_OR_SLICE_MEET,
),
_ => {
rv.set_undefined();
return;
}
};
let value = svg_number_slot(scope, aspect_ratio, slot).unwrap_or(initial_value as f64) as u32;
rv.set(v8::Integer::new_from_unsigned(scope, value).into());
}
pub(super) fn svg_preserve_aspect_ratio_setter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(name) = callback_data_item(
scope,
&args,
SVG_PRESERVE_ASPECT_RATIO_ACCESSOR_NAMES,
"SVGPreserveAspectRatio attributes",
) else {
return;
};
let aspect_ratio = args.this();
if !require_svg_receiver(
scope,
aspect_ratio,
SVG_PRESERVE_ASPECT_RATIO_ALIGN_SLOT,
"SVGPreserveAspectRatio",
&format!("{name} setter"),
) {
return;
}
if get_private_value(
scope,
aspect_ratio,
SVG_PRESERVE_ASPECT_RATIO_READ_ONLY_SLOT,
)
.is_some_and(|value| value.boolean_value(scope))
{
throw_dom_exception(
scope,
"NoModificationAllowedError",
7,
"The SVG preserveAspectRatio value is read-only.",
);
return;
}
let value = match webidl::convert::<webidl::UnsignedShort>(
scope,
args.get(0),
webidl::Context::member("SVGPreserveAspectRatio", name),
) {
Ok(value) => u32::from(value.0),
Err(error) => {
webidl::throw_error(scope, &error);
return;
}
};
let slot = match name {
"align"
if (SVG_PRESERVE_ASPECT_RATIO_NONE..=SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MAX)
.contains(&value) =>
{
SVG_PRESERVE_ASPECT_RATIO_ALIGN_SLOT
}
"meetOrSlice" if (SVG_MEET_OR_SLICE_MEET..=SVG_MEET_OR_SLICE_SLICE).contains(&value) => {
SVG_PRESERVE_ASPECT_RATIO_MEET_OR_SLICE_SLOT
}
_ => return,
};
sync_svg_preserve_aspect_ratio_from_owner_attribute(scope, aspect_ratio);
set_private_value(
scope,
aspect_ratio,
slot,
v8::Integer::new_from_unsigned(scope, value).into(),
);
reflect_svg_preserve_aspect_ratio_to_owner_attribute(scope, aspect_ratio);
}
pub(super) fn svg_animated_length_list_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -43,6 +43,20 @@ const SVG_MARKER_UNITS_SLOT: &str = "__moliSvgMarkerUnits";
const SVG_MARKER_ORIENT_TYPE_SLOT: &str = "__moliSvgMarkerOrientType";
const SVG_ANIMATED_ANGLE_BASE_VAL_SLOT: &str = "__moliSvgAnimatedAngleBaseVal";
const SVG_ANIMATED_ANGLE_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedAngleAnimVal";
const SVG_FIT_VIEW_BOX_SLOT: &str = "__moliSvgFitViewBox";
const SVG_FIT_PRESERVE_ASPECT_RATIO_SLOT: &str = "__moliSvgFitPreserveAspectRatio";
const SVG_ANIMATED_RECT_BASE_VAL_SLOT: &str = "__moliSvgAnimatedRectBaseVal";
const SVG_ANIMATED_RECT_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedRectAnimVal";
const SVG_ANIMATED_PRESERVE_ASPECT_RATIO_BASE_VAL_SLOT: &str =
"__moliSvgAnimatedPreserveAspectRatioBaseVal";
const SVG_ANIMATED_PRESERVE_ASPECT_RATIO_ANIM_VAL_SLOT: &str =
"__moliSvgAnimatedPreserveAspectRatioAnimVal";
const SVG_PRESERVE_ASPECT_RATIO_ALIGN_SLOT: &str = "__moliSvgPreserveAspectRatioAlign";
const SVG_PRESERVE_ASPECT_RATIO_MEET_OR_SLICE_SLOT: &str =
"__moliSvgPreserveAspectRatioMeetOrSlice";
const SVG_PRESERVE_ASPECT_RATIO_OWNER_ELEMENT_SLOT: &str =
"__moliSvgPreserveAspectRatioOwnerElement";
const SVG_PRESERVE_ASPECT_RATIO_READ_ONLY_SLOT: &str = "__moliSvgPreserveAspectRatioReadOnly";
const SVG_ANGLE_OWNER_ELEMENT_SLOT: &str = "__moliSvgAngleOwnerElement";
const SVG_ANGLE_OWNER_ATTRIBUTE_SLOT: &str = "__moliSvgAngleOwnerAttribute";
const SVG_ANGLE_UNIT_TYPE_SLOT: &str = "__moliSvgAngleUnitType";
@@ -357,6 +371,22 @@ const SVG_ANGLE_TYPE_DEG: u32 = 2;
const SVG_ANGLE_TYPE_RAD: u32 = 3;
const SVG_ANGLE_TYPE_GRAD: u32 = 4;
const SVG_PRESERVE_ASPECT_RATIO_UNKNOWN: u32 = 0;
const SVG_PRESERVE_ASPECT_RATIO_NONE: u32 = 1;
const SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MIN: u32 = 2;
const SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MIN: u32 = 3;
const SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MIN: u32 = 4;
const SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MID: u32 = 5;
const SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MID: u32 = 6;
const SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MID: u32 = 7;
const SVG_PRESERVE_ASPECT_RATIO_X_MIN_Y_MAX: u32 = 8;
const SVG_PRESERVE_ASPECT_RATIO_X_MID_Y_MAX: u32 = 9;
const SVG_PRESERVE_ASPECT_RATIO_X_MAX_Y_MAX: u32 = 10;
const SVG_MEET_OR_SLICE_UNKNOWN: u32 = 0;
const SVG_MEET_OR_SLICE_MEET: u32 = 1;
const SVG_MEET_OR_SLICE_SLICE: u32 = 2;
const SVG_MARKER_UNITS_UNKNOWN: u32 = 0;
const SVG_MARKER_UNITS_USER_SPACE_ON_USE: u32 = 1;
const SVG_MARKER_UNITS_STROKE_WIDTH: u32 = 2;
@@ -805,6 +835,13 @@ pub(in crate::context_bootstrap) fn install_svg_template_bindings<'s>(
"SVGAnimatedBoolean" => bindings::install_svg_animated_boolean_bindings(scope, template),
"SVGAnimatedLength" => bindings::install_svg_animated_length_bindings(scope, template),
"SVGAnimatedAngle" => bindings::install_svg_animated_angle_bindings(scope, template),
"SVGAnimatedRect" => bindings::install_svg_animated_rect_bindings(scope, template),
"SVGPreserveAspectRatio" => {
bindings::install_svg_preserve_aspect_ratio_bindings(scope, template)
}
"SVGAnimatedPreserveAspectRatio" => {
bindings::install_svg_animated_preserve_aspect_ratio_bindings(scope, template)
}
"SVGLengthList" => bindings::install_svg_length_list_bindings(scope, template),
"SVGAnimatedLengthList" => {
bindings::install_svg_animated_length_list_bindings(scope, template)
@@ -1,10 +1,10 @@
//! Detached SVGRect values created by SVGSVGElement.createSVGRect().
//! Native SVGRect values, detached or bound to SVGAnimatedRect viewBox values.
//!
//! JSXGraph uses this method to detect SVG support. Returning a DOMRect would
//! JSXGraph uses createSVGRect() to detect SVG support. Returning a DOMRect would
//! pass that check but expose the wrong interface and double (not float) fields.
use crate::{
native_bridge::node_runtime_and_handle_from_object_or_detached,
native_bridge::{node_runtime_and_handle_from_object_or_detached, throw_dom_exception},
util::{callback_data_index_value, callback_data_item, get_private_value, set_private_value},
webidl,
};
@@ -14,6 +14,8 @@ const X: &str = "__moliSvgRectX";
const Y: &str = "__moliSvgRectY";
const WIDTH: &str = "__moliSvgRectWidth";
const HEIGHT: &str = "__moliSvgRectHeight";
const VIEW_BOX_OWNER: &str = "__moliSvgRectViewBoxOwner";
const READ_ONLY: &str = "__moliSvgRectReadOnly";
const FIELDS: &[(&str, &str)] = &[("x", X), ("y", Y), ("width", WIDTH), ("height", HEIGHT)];
#[derive(WebApiObject)]
@@ -81,6 +83,60 @@ pub(super) fn create_svg_rect<'s>(
rv.set(rect.into());
}
pub(super) fn build_svg_view_box_rect<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner: v8::Local<'s, v8::Object>,
values: [f64; 4],
read_only: bool,
) -> v8::Local<'s, v8::Object> {
let [x, y, width, height] = values.map(|value| f64::from(value as f32));
let rect = SvgRectObjectDeclaration::new(x, y, width, height)
.bind(scope)
.expect("SVGRect declaration should bind");
set_private_value(scope, rect, VIEW_BOX_OWNER, owner.into());
set_private_value(
scope,
rect,
READ_ONLY,
v8::Boolean::new(scope, read_only).into(),
);
rect
}
pub(super) fn svg_view_box_rect_owner<'s>(
scope: &mut v8::PinScope<'s, '_>,
rect: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
get_private_value(scope, rect, VIEW_BOX_OWNER)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
}
pub(super) fn svg_view_box_rect_values<'s>(
scope: &mut v8::PinScope<'s, '_>,
rect: v8::Local<'s, v8::Object>,
) -> [f64; 4] {
[X, Y, WIDTH, HEIGHT].map(|slot| {
get_private_value(scope, rect, slot)
.and_then(|value| v8::Local::<v8::Number>::try_from(value).ok())
.map_or(0.0, |value| value.value())
})
}
pub(super) fn set_svg_view_box_rect_values<'s>(
scope: &mut v8::PinScope<'s, '_>,
rect: v8::Local<'s, v8::Object>,
values: [f64; 4],
) {
for (slot, value) in [X, Y, WIDTH, HEIGHT].into_iter().zip(values) {
set_private_value(
scope,
rect,
slot,
v8::Number::new(scope, f64::from(value as f32)).into(),
);
}
}
fn field_for_receiver<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
@@ -100,6 +156,7 @@ fn get_field<'s>(
let Some((_, slot)) = field_for_receiver(scope, &args) else {
return;
};
super::builders::sync_svg_view_box_rect_from_owner(scope, args.this());
if let Some(value) = get_private_value(scope, args.this(), slot) {
rv.set(value);
}
@@ -113,6 +170,17 @@ fn set_field<'s>(
let Some((name, slot)) = field_for_receiver(scope, &args) else {
return;
};
if get_private_value(scope, args.this(), READ_ONLY)
.is_some_and(|value| value.boolean_value(scope))
{
throw_dom_exception(
scope,
"NoModificationAllowedError",
7,
"The SVG rectangle is read-only.",
);
return;
}
let value = match webidl::convert::<webidl::Double>(
scope,
args.get(0),
@@ -130,10 +198,14 @@ fn set_field<'s>(
webidl::throw_type_error(scope, "SVGRect value is outside the finite float range.");
return;
}
// ToNumber may re-enter JS and change the owner's viewBox. Preserve those
// changes to the other fields before applying this one-field mutation.
super::builders::sync_svg_view_box_rect_from_owner(scope, args.this());
set_private_value(
scope,
args.this(),
slot,
v8::Number::new(scope, f64::from(value)).into(),
);
super::builders::reflect_svg_view_box_rect_mutation(scope, args.this());
}
@@ -1605,6 +1605,295 @@ fn svg_animated_lengths_reflect_initial_and_content_attribute_values() {
assert_eq!(result, "ok");
}
#[test]
fn svg_fit_to_view_box_attributes_are_live_and_reflected() {
let mut vm = new_parsed_test_vm(
"https://svg-fit-to-view-box.test/",
"<!doctype html><html><head></head><body></body></html>",
);
let result = vm
.eval(
r#"
(() => {
const assert = (condition, message) => {
if (!condition) throw new Error(message);
};
const throws = (callback, expected, message) => {
try {
callback();
} catch (error) {
assert(error.name === expected, `${message}: ${error.name}`);
return;
}
throw new Error(`${message}: did not throw`);
};
const accessor = (prototype, name) => {
const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
assert(typeof descriptor?.get === "function", `${prototype.constructor.name}.${name} getter`);
assert(descriptor.set === undefined, `${prototype.constructor.name}.${name} setter`);
assert(descriptor.enumerable && descriptor.configurable,
`${prototype.constructor.name}.${name} flags`);
return descriptor;
};
const ns = "http://www.w3.org/2000/svg";
for (const constructor of [
SVGAnimatedRect,
SVGPreserveAspectRatio,
SVGAnimatedPreserveAspectRatio,
]) {
assert(typeof constructor === "function", `${constructor.name} constructor`);
throws(() => new constructor(), "TypeError", `${constructor.name} illegal constructor`);
}
const fitCases = [
["svg", SVGSVGElement],
["symbol", SVGSymbolElement],
["marker", SVGMarkerElement],
["pattern", SVGPatternElement],
["view", SVGViewElement],
];
for (const [localName, constructor] of fitCases) {
accessor(constructor.prototype, "viewBox");
accessor(constructor.prototype, "preserveAspectRatio");
const element = document.createElementNS(ns, localName);
const viewBox = element.viewBox;
const aspectRatio = element.preserveAspectRatio;
assert(viewBox instanceof SVGAnimatedRect, `${localName}.viewBox interface`);
assert(aspectRatio instanceof SVGAnimatedPreserveAspectRatio,
`${localName}.preserveAspectRatio interface`);
assert(element.viewBox === viewBox, `${localName}.viewBox SameObject`);
assert(element.preserveAspectRatio === aspectRatio,
`${localName}.preserveAspectRatio SameObject`);
}
const imageDescriptor = accessor(
SVGImageElement.prototype,
"preserveAspectRatio",
);
assert(!("viewBox" in SVGImageElement.prototype), "image has no viewBox");
const image = document.createElementNS(ns, "image");
assert(image.preserveAspectRatio instanceof SVGAnimatedPreserveAspectRatio,
"image preserveAspectRatio interface");
assert(image.preserveAspectRatio === image.preserveAspectRatio,
"image preserveAspectRatio SameObject");
const svg = document.createElementNS(ns, "svg");
const animatedRect = svg.viewBox;
const baseRect = animatedRect.baseVal;
const animRect = animatedRect.animVal;
assert(baseRect instanceof SVGRect && animRect instanceof SVGRect,
"viewBox rectangle interfaces");
for (const rect of [baseRect, animRect]) {
assert(!(rect instanceof DOMRect), "viewBox uses native SVGRect, not DOMRect");
assert(Object.getPrototypeOf(rect) === SVGRect.prototype,
"viewBox native rectangle prototype");
assert(Object.prototype.toString.call(rect) === "[object SVGRect]",
"viewBox native rectangle tag");
}
assert(baseRect !== animRect, "viewBox base and animated rectangles are distinct");
assert(Object.prototype.toString.call(animatedRect) === "[object SVGAnimatedRect]",
"animated rectangle tag");
assert(baseRect.x === 0 && baseRect.y === 0 &&
baseRect.width === 0 && baseRect.height === 0, "initial viewBox");
assert(!svg.hasAttribute("viewBox"), "viewBox getter does not create attribute");
svg.setAttribute("viewBox", "10 20 30 40");
assert(baseRect.x === 10 && baseRect.y === 20 &&
baseRect.width === 30 && baseRect.height === 40,
"cached base rectangle tracks content attribute");
assert(animRect.x === 10 && animRect.y === 20 &&
animRect.width === 30 && animRect.height === 40,
"cached animated rectangle tracks content attribute");
baseRect.x = 11;
assert(svg.getAttribute("viewBox") === "11 20 30 40",
"base rectangle mutation reflects to viewBox");
assert(baseRect.x === 11 && animRect.x === 11,
"viewBox values remain synchronized after mutation");
throws(() => { animRect.x = 12; }, "NoModificationAllowedError",
"animated rectangle is read-only");
svg.setAttribute("viewBox", "0 0 -1 2");
assert(baseRect.x === 0 && baseRect.width === 0,
"invalid viewBox uses the initial value");
svg.removeAttribute("viewBox");
assert(baseRect.x === 0 && baseRect.height === 0,
"removed viewBox uses the initial value");
const animatedAspectRatio = svg.preserveAspectRatio;
const baseAspectRatio = animatedAspectRatio.baseVal;
const animAspectRatio = animatedAspectRatio.animVal;
assert(baseAspectRatio instanceof SVGPreserveAspectRatio &&
animAspectRatio instanceof SVGPreserveAspectRatio,
"preserveAspectRatio value interfaces");
assert(baseAspectRatio !== animAspectRatio,
"preserveAspectRatio base and animated values are distinct");
assert(baseAspectRatio.align ===
SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMID,
"initial preserveAspectRatio align");
assert(baseAspectRatio.meetOrSlice ===
SVGPreserveAspectRatio.SVG_MEETORSLICE_MEET,
"initial preserveAspectRatio meetOrSlice");
assert(!svg.hasAttribute("preserveAspectRatio"),
"preserveAspectRatio getter does not create attribute");
svg.setAttribute("preserveAspectRatio", "xMinYMax slice");
assert(baseAspectRatio.align ===
SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMINYMAX,
"cached base aspect ratio tracks content attribute");
assert(animAspectRatio.meetOrSlice ===
SVGPreserveAspectRatio.SVG_MEETORSLICE_SLICE,
"cached animated aspect ratio tracks content attribute");
baseAspectRatio.align =
SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMAXYMIN;
baseAspectRatio.meetOrSlice = SVGPreserveAspectRatio.SVG_MEETORSLICE_SLICE;
assert(svg.getAttribute("preserveAspectRatio") === "xMaxYMin slice",
"base aspect ratio mutation reflects canonical syntax");
assert(animAspectRatio.align ===
SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMAXYMIN,
"animated aspect ratio tracks base mutation");
baseAspectRatio.align = SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_UNKNOWN;
baseAspectRatio.meetOrSlice = SVGPreserveAspectRatio.SVG_MEETORSLICE_UNKNOWN;
assert(svg.getAttribute("preserveAspectRatio") === "xMaxYMin slice",
"unknown enumeration assignments are ignored");
throws(() => { animAspectRatio.align = 1; }, "NoModificationAllowedError",
"animated aspect ratio align is read-only");
throws(() => { animAspectRatio.meetOrSlice = 1; },
"NoModificationAllowedError", "animated aspect ratio meetOrSlice is read-only");
svg.setAttribute("preserveAspectRatio", "invalid");
assert(baseAspectRatio.align ===
SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMID &&
animAspectRatio.meetOrSlice === SVGPreserveAspectRatio.SVG_MEETORSLICE_MEET,
"invalid preserveAspectRatio uses the initial value");
svg.removeAttribute("preserveAspectRatio");
assert(baseAspectRatio.align ===
SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMID,
"removed preserveAspectRatio uses the initial value");
const constants = [
["SVG_PRESERVEASPECTRATIO_UNKNOWN", 0],
["SVG_PRESERVEASPECTRATIO_NONE", 1],
["SVG_PRESERVEASPECTRATIO_XMINYMIN", 2],
["SVG_PRESERVEASPECTRATIO_XMIDYMIN", 3],
["SVG_PRESERVEASPECTRATIO_XMAXYMIN", 4],
["SVG_PRESERVEASPECTRATIO_XMINYMID", 5],
["SVG_PRESERVEASPECTRATIO_XMIDYMID", 6],
["SVG_PRESERVEASPECTRATIO_XMAXYMID", 7],
["SVG_PRESERVEASPECTRATIO_XMINYMAX", 8],
["SVG_PRESERVEASPECTRATIO_XMIDYMAX", 9],
["SVG_PRESERVEASPECTRATIO_XMAXYMAX", 10],
["SVG_MEETORSLICE_UNKNOWN", 0],
["SVG_MEETORSLICE_MEET", 1],
["SVG_MEETORSLICE_SLICE", 2],
];
for (const [name, value] of constants) {
assert(SVGPreserveAspectRatio[name] === value, `constructor ${name}`);
assert(SVGPreserveAspectRatio.prototype[name] === value, `prototype ${name}`);
}
const viewBoxDescriptor = Object.getOwnPropertyDescriptor(
SVGSVGElement.prototype,
"viewBox",
);
throws(() => viewBoxDescriptor.get.call(document.createElementNS(ns, "rect")),
"TypeError", "viewBox receiver brand");
throws(() => imageDescriptor.get.call(document.createElementNS(ns, "rect")),
"TypeError", "image preserveAspectRatio receiver brand");
const animatedRectDescriptor = Object.getOwnPropertyDescriptor(
SVGAnimatedRect.prototype,
"baseVal",
);
throws(() => animatedRectDescriptor.get.call({}), "TypeError",
"animated rectangle receiver brand");
const alignDescriptor = Object.getOwnPropertyDescriptor(
SVGPreserveAspectRatio.prototype,
"align",
);
throws(() => alignDescriptor.get.call({}), "TypeError",
"preserveAspectRatio receiver brand");
return "ok";
})()
"#,
)
.expect("SVG fit-to-view-box reflection probe should evaluate");
assert_eq!(result, "ok");
}
#[test]
fn svg_view_box_rects_preserve_native_float_and_reentrant_setter_semantics() {
let mut vm = new_parsed_test_vm(
"https://svg-view-box-native-rect.test/",
"<!doctype html><html><head></head><body></body></html>",
);
let result = vm
.eval(
r#"
(() => {
const assert = (condition, message) => {
if (!condition) throw new Error(message);
};
const throws = (callback, name) => {
try { callback(); } catch (error) {
assert(error.name === name, `expected ${name}, got ${error.name}`);
return;
}
throw new Error(`expected ${name}`);
};
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const fields = ["x", "y", "width", "height"];
const values = rect => fields.map(field => rect[field]);
svg.setAttribute("viewBox", "1.2 2.4 3.6 4.8");
const base = svg.viewBox.baseVal;
const anim = svg.viewBox.animVal;
const assertValues = expected => {
for (const rect of [base, anim]) {
assert(JSON.stringify(values(rect)) === JSON.stringify(expected.map(Math.fround)),
`float values: ${values(rect)}`);
}
};
assertValues([1.2, 2.4, 3.6, 4.8]);
svg.setAttribute("viewBox", "5.5 6.6 7.7 8.8");
assertValues([5.5, 6.6, 7.7, 8.8]);
base.x = "1.2";
base.width = 1e20;
assertValues([1.2, 6.6, 1e20, 8.8]);
const reflected = svg.getAttribute("viewBox");
assert(JSON.stringify(reflected.split(" ").map(Number).map(Math.fround)) ===
JSON.stringify(values(base)), "float serialization round trip");
for (const invalid of [NaN, Infinity, -Infinity, 1e40, undefined, Symbol()]) {
throws(() => { base.x = invalid; }, "TypeError");
assert(svg.getAttribute("viewBox") === reflected,
"rejected float assignment preserves the attribute");
}
throws(() => { anim.width = 1; }, "NoModificationAllowedError");
const domRectX = Object.getOwnPropertyDescriptor(DOMRect.prototype, "x");
throws(() => domRectX.get.call(base), "TypeError");
throws(() => domRectX.set.call(anim, 7), "TypeError");
assert(svg.getAttribute("viewBox") === reflected,
"read-only and foreign-interface setters preserve the attribute");
base.x = {
valueOf() {
svg.setAttribute("viewBox", "10 20 30 40");
return 9;
}
};
assert(svg.getAttribute("viewBox") === "9 20 30 40",
"numeric coercion preserves reentrant changes to the other fields");
assertValues([9, 20, 30, 40]);
svg.setAttribute("viewBox", "0 0 1e40 1");
assertValues([0, 0, 0, 0]);
return "ok";
})()
"#,
)
.expect("native SVG viewBox rectangle probe should evaluate");
assert_eq!(result, "ok");
}
#[test]
fn svg_historical_interfaces_keep_current_element_constructors_only() {
let mut vm = new_parsed_test_vm(