fix(svg): expose standalone value factories

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