fix(svg): implement live point lists

This commit is contained in:
ldm0
2026-10-02 03:28:23 +08:00
parent 140a7bcf08
commit c8bcb1cc99
11 changed files with 522 additions and 12 deletions
@@ -14,6 +14,7 @@ const DOM_POINT_Y_SLOT: &str = "__moliDomPointY";
const DOM_POINT_Z_SLOT: &str = "__moliDomPointZ";
const DOM_POINT_W_SLOT: &str = "__moliDomPointW";
const DOM_POINT_RESTRICTED_NUMBER_SLOT: &str = "__moliDomPointRestrictedNumber";
const DOM_POINT_READ_ONLY_SLOT: &str = "__moliDomPointReadOnly";
const DOM_MATRIX_M11_SLOT: &str = "__moliDomMatrixM11";
const DOM_MATRIX_M12_SLOT: &str = "__moliDomMatrixM12";
@@ -788,7 +789,15 @@ pub(in crate::context_bootstrap) fn build_dom_point_object<'s>(
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);
build_svg_point_object_with_values(scope, 0.0, 0.0)
}
pub(in crate::context_bootstrap) fn build_svg_point_object_with_values<'s>(
scope: &mut v8::PinScope<'s, '_>,
x: f64,
y: f64,
) -> v8::Local<'s, v8::Object> {
let object = build_dom_point_object(scope, x, y, 0.0, 1.0);
let restricted = v8::Boolean::new(scope, true);
set_private_value(
scope,
@@ -799,6 +808,19 @@ pub(in crate::context_bootstrap) fn build_svg_point_object<'s>(
object
}
pub(in crate::context_bootstrap) fn set_svg_point_read_only<'s>(
scope: &mut v8::PinScope<'s, '_>,
point: v8::Local<'s, v8::Object>,
read_only: bool,
) {
set_private_value(
scope,
point,
DOM_POINT_READ_ONLY_SLOT,
v8::Boolean::new(scope, read_only).into(),
);
}
fn build_dom_point_readonly_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
x: f64,
@@ -910,6 +932,17 @@ fn dom_point_setter_callback<'s>(
throw_type_error(scope, "Illegal invocation");
return;
}
if get_private_value(scope, args.this(), DOM_POINT_READ_ONLY_SLOT)
.is_some_and(|value| value.boolean_value(scope))
{
throw_dom_exception(
scope,
"NoModificationAllowedError",
7,
"The SVG point is read-only.",
);
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))
@@ -933,6 +966,7 @@ fn dom_point_setter_callback<'s>(
slot,
v8::Number::new(scope, value).into(),
);
super::svg_runtime::reflect_svg_point_mutation(scope, args.this());
rv.set_undefined();
}
@@ -1907,6 +1907,7 @@ pub(crate) fn finish_context_bootstrap(
("SVGAnimatedNumber", "SVGAnimatedNumber"),
("SVGAnimatedInteger", "SVGAnimatedInteger"),
("SVGNumberList", "SVGNumberList"),
("SVGPointList", "SVGPointList"),
("SVGStringList", "SVGStringList"),
("SVGAnimatedNumberList", "SVGAnimatedNumberList"),
("SVGAnimatedBoolean", "SVGAnimatedBoolean"),
@@ -148,6 +148,10 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[
interface: web_api_interfaces::SVGNumberList::DESCRIPTOR,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
interface: web_api_interfaces::SVGPointList::DESCRIPTOR,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
interface: web_api_interfaces::SVGStringList::DESCRIPTOR,
kind: ConstructorKind::Illegal,
@@ -184,6 +184,51 @@ struct SvgNumberListTemplateMethodsDeclaration {
append_item: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::SVGPointList, enumerable)]
struct SvgPointListTemplateMethodsDeclaration {
#[webapi(method = "clear", length = 0, callback = svg_point_list_clear_callback)]
clear: (),
#[webapi(
method = "initialize",
length = 1,
callback = svg_point_list_initialize_callback
)]
initialize: (),
#[webapi(method = "getItem", length = 1, callback = svg_point_list_get_item_callback)]
get_item: (),
#[webapi(
method = "insertItemBefore",
length = 2,
callback = svg_point_list_insert_item_before_callback
)]
insert_item_before: (),
#[webapi(
method = "replaceItem",
length = 2,
callback = svg_point_list_replace_item_callback
)]
replace_item: (),
#[webapi(
method = "removeItem",
length = 1,
callback = svg_point_list_remove_item_callback
)]
remove_item: (),
#[webapi(
method = "appendItem",
length = 1,
callback = svg_point_list_append_item_callback
)]
append_item: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::SVGStringList, enumerable)]
struct SvgStringListTemplateMethodsDeclaration {
@@ -1180,6 +1225,19 @@ struct SvgNumberListTemplateAccessorsDeclaration {
number_of_items: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::SVGPointList, enumerable)]
struct SvgPointListTemplateAccessorsDeclaration {
#[webapi(accessor_property = "length", getter = svg_point_list_length_getter)]
length: (),
#[webapi(
accessor_property = "numberOfItems",
getter = svg_point_list_length_getter
)]
number_of_items: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::SVGStringList, enumerable)]
struct SvgStringListTemplateAccessorsDeclaration {
@@ -1485,6 +1543,24 @@ struct SvgGeometryElementPrototypeAccessorsDeclaration {
path_length: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "SVGAnimatedPoints", enumerable)]
struct SvgAnimatedPointsPrototypeAccessorsDeclaration {
#[webapi(
accessor_property = "points",
getter = svg_animated_points_getter,
data = callback_data_index_value(scope, 0)
)]
points: (),
#[webapi(
accessor_property = "animatedPoints",
getter = svg_animated_points_getter,
data = callback_data_index_value(scope, 1)
)]
animated_points: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "SVGFitToViewBox", enumerable)]
struct SvgFitToViewBoxPrototypeAccessorsDeclaration {
@@ -2189,6 +2265,13 @@ pub(super) fn install_svg_number_list_bindings<'s>(
install_svg_value_list_bindings(scope, template, SvgListKind::Number);
}
pub(super) fn install_svg_point_list_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
) {
install_svg_value_list_bindings(scope, template, SvgListKind::Point);
}
pub(super) fn install_svg_string_list_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
@@ -2283,6 +2366,10 @@ pub(super) fn install_svg_value_list_bindings<'s>(
SvgNumberListTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto);
SvgNumberListTemplateMethodsDeclaration::initialize_prototype_template(scope, proto);
}
SvgListKind::Point => {
SvgPointListTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto);
SvgPointListTemplateMethodsDeclaration::initialize_prototype_template(scope, proto);
}
}
}
@@ -2607,6 +2694,11 @@ pub(super) fn install_svg_element_accessor_bindings<'s>(
scope, prototype,
);
}
"SVGPolygonElement" | "SVGPolylineElement" => {
SvgAnimatedPointsPrototypeAccessorsDeclaration::initialize_prototype_template(
scope, prototype,
);
}
"SVGMarkerElement" => {
SvgMarkerElementPrototypeAccessorsDeclaration::initialize_prototype_template(
scope, prototype,
@@ -298,6 +298,33 @@ struct SvgNumberListObjectDeclaration<'scope> {
append_item: (),
}
#[derive(WebApiObject)]
#[webapi(interface = web_api_interfaces::SVGPointList, own_to_string_tag = "SVGPointList")]
struct SvgPointListObjectDeclaration<'scope> {
#[webapi(slot = SVG_POINT_LIST_ITEMS_SLOT)]
items: Vec<v8::Local<'scope, v8::Value>>,
#[webapi(slot = SVG_VALUE_LIST_READ_ONLY_SLOT)]
read_only: bool,
#[webapi(method, callback = svg_point_list_clear_callback, length = 0)]
clear: (),
#[webapi(method, callback = svg_point_list_initialize_callback, length = 1)]
initialize: (),
#[webapi(method, callback = svg_point_list_get_item_callback, length = 1)]
get_item: (),
#[webapi(
method,
callback = svg_point_list_insert_item_before_callback,
length = 2
)]
insert_item_before: (),
#[webapi(method, callback = svg_point_list_replace_item_callback, length = 2)]
replace_item: (),
#[webapi(method, callback = svg_point_list_remove_item_callback, length = 1)]
remove_item: (),
#[webapi(method, callback = svg_point_list_append_item_callback, length = 1)]
append_item: (),
}
#[derive(WebApiObject)]
#[webapi(interface = web_api_interfaces::SVGStringList, own_to_string_tag = "SVGStringList")]
struct SvgStringListObjectDeclaration<'scope> {
@@ -521,6 +548,7 @@ pub(super) fn build_svg_animated_value_list_for_attribute<'s>(
let object = match kind {
SvgListKind::Length => build_svg_animated_length_list(scope),
SvgListKind::Number => build_svg_animated_number_list(scope),
SvgListKind::Point => unreachable!("SVGPointList is not an animated wrapper"),
};
sync_svg_animated_value_list_from_owner_attribute(scope, object, owner, attribute, kind);
if let Some(base_val) = svg_animated_value_list_member(scope, object, "baseVal", kind) {
@@ -951,6 +979,32 @@ pub(super) fn build_svg_number_list<'s>(
object
}
pub(super) fn build_svg_point_list<'s>(
scope: &mut v8::PinScope<'s, '_>,
read_only: bool,
) -> v8::Local<'s, v8::Object> {
let template = v8::ObjectTemplate::new(scope);
configure_svg_value_list_indexed_property_handler(template);
let object = template
.new_instance(scope)
.expect("SVGPointList object template should instantiate");
SvgPointListObjectDeclaration::new(Vec::new(), read_only)
.bind_into(scope, object)
.expect("SVGPointList declaration should bind");
object
}
pub(super) fn build_svg_point_list_for_attribute<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner: v8::Local<'s, v8::Object>,
read_only: bool,
) -> v8::Local<'s, v8::Object> {
let list = build_svg_point_list(scope, read_only);
set_svg_value_list_owner_attribute(scope, list, owner, "points");
sync_svg_value_list_from_owner_attribute(scope, list, SvgListKind::Point);
list
}
pub(super) fn build_svg_string_list_for_attribute<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner: v8::Local<'s, v8::Object>,
@@ -1834,16 +1888,22 @@ pub(super) fn svg_value_list_item_or_throw<'s>(
value: v8::Local<'s, v8::Value>,
kind: SvgListKind,
) -> Option<v8::Local<'s, v8::Object>> {
let (brand_slot, interface) = match kind {
SvgListKind::Length => (SVG_LENGTH_VALUE_SLOT, "SVGLength"),
SvgListKind::Number => (SVG_NUMBER_VALUE_SLOT, "SVGNumber"),
};
let object = v8::Local::<v8::Object>::try_from(value).ok();
if let Some(object) = object
&& get_private_value(scope, object, brand_slot).is_some()
{
return Some(object);
let valid = object.is_some_and(|object| match kind {
SvgListKind::Length => get_private_value(scope, object, SVG_LENGTH_VALUE_SLOT).is_some(),
SvgListKind::Number => get_private_value(scope, object, SVG_NUMBER_VALUE_SLOT).is_some(),
SvgListKind::Point => {
dom_point_clone_data(scope, object).is_some_and(|(mutable, _)| mutable)
}
});
if valid {
return object;
}
let interface = match kind {
SvgListKind::Length => "SVGLength",
SvgListKind::Number => "SVGNumber",
SvgListKind::Point => "DOMPoint",
};
webidl::throw_type_error(
scope,
&format!("Argument 1 can not be converted to {interface}"),
@@ -1867,6 +1927,7 @@ pub(super) fn svg_value_list_items<'s>(
let slot = match kind {
SvgListKind::Length => SVG_LENGTH_LIST_ITEMS_SLOT,
SvgListKind::Number => SVG_NUMBER_LIST_ITEMS_SLOT,
SvgListKind::Point => SVG_POINT_LIST_ITEMS_SLOT,
};
get_private_value(scope, object, slot)
.and_then(|value| v8::Local::<v8::Array>::try_from(value).ok())
@@ -1882,12 +1943,23 @@ pub(super) fn set_svg_value_list_items<'s>(
let slot = match kind {
SvgListKind::Length => SVG_LENGTH_LIST_ITEMS_SLOT,
SvgListKind::Number => SVG_NUMBER_LIST_ITEMS_SLOT,
SvgListKind::Point => SVG_POINT_LIST_ITEMS_SLOT,
};
if let Some(current) = get_private_value(scope, object, slot)
.and_then(|value| v8::Local::<v8::Array>::try_from(value).ok())
{
detach_svg_value_list_items(scope, current);
}
if matches!(kind, SvgListKind::Point) && svg_value_list_is_read_only(scope, object) {
for index in 0..items.length() {
if let Some(point) = items
.get_index(scope, index)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
{
set_svg_point_read_only(scope, point, true);
}
}
}
attach_svg_value_list_items(scope, object, items);
set_private_value(scope, object, slot, items.into());
}
@@ -2060,6 +2132,11 @@ pub(super) fn build_svg_value_list_item_values_from_attribute<'s>(
.into_iter()
.map(|value| build_svg_number(scope, value))
.collect(),
SvgListKind::Point => svg_geometry::parse_point_list(raw)
.unwrap_or_default()
.into_iter()
.map(|(x, y)| build_svg_point_object_with_values(scope, x, y))
.collect(),
}
}
@@ -2160,6 +2237,14 @@ pub(super) fn serialize_svg_value_list_item<'s>(
let value = svg_number_slot(scope, item, SVG_NUMBER_VALUE_SLOT)?;
Some(svg_geometry::serialize_number(value))
}
SvgListKind::Point => {
let point = dom_point_init_from_object(scope, item);
Some(format!(
"{} {}",
svg_geometry::serialize_number(point.x),
svg_geometry::serialize_number(point.y)
))
}
}
}
@@ -119,6 +119,11 @@ const SVG_TEXT_POSITIONING_LIST_ATTRIBUTES: &[(&str, &str, SvgListKind)] = &[
),
];
const SVG_ANIMATED_POINTS_PROPERTIES: &[(&str, &str, bool)] = &[
("points", SVG_POINTS_SLOT, false),
("animatedPoints", SVG_ANIMATED_POINTS_SLOT, true),
];
fn require_svg_receiver<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
@@ -954,6 +959,56 @@ pub(super) fn svg_geometry_path_length_getter<'s>(
rv.set(value.into());
}
pub(super) fn svg_animated_points_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some((name, cache_slot, read_only)) = callback_data_item(
scope,
&args,
SVG_ANIMATED_POINTS_PROPERTIES,
"SVGAnimatedPoints properties",
) else {
rv.set_undefined();
return;
};
let owner = args.this();
let Ok((runtime_ptr, handle)) =
crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner)
else {
webidl::throw_type_error(
scope,
&format!("SVGAnimatedPoints.{name} called on incompatible receiver."),
);
return;
};
let is_animated_points = unsafe { &*runtime_ptr }
.dom_host()
.node(handle)
.and_then(|node| node.as_element())
.is_some_and(|element| {
element.is_svg_element("polygon") || element.is_svg_element("polyline")
});
if !is_animated_points {
webidl::throw_type_error(
scope,
&format!("SVGAnimatedPoints.{name} called on incompatible receiver."),
);
return;
}
if let Some(value) = get_private_value(scope, owner, cache_slot) {
if let Ok(list) = v8::Local::<v8::Object>::try_from(value) {
sync_svg_value_list_from_owner_attribute(scope, list, SvgListKind::Point);
}
rv.set(value);
return;
}
let list = build_svg_point_list_for_attribute(scope, owner, read_only);
set_private_value(scope, owner, cache_slot, list.into());
rv.set(list.into());
}
pub(super) fn svg_text_content_text_length_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -2458,6 +2513,7 @@ fn svg_value_list_metadata(kind: SvgListKind) -> (&'static str, &'static str) {
match kind {
SvgListKind::Length => (SVG_LENGTH_LIST_ITEMS_SLOT, "SVGLengthList"),
SvgListKind::Number => (SVG_NUMBER_LIST_ITEMS_SLOT, "SVGNumberList"),
SvgListKind::Point => (SVG_POINT_LIST_ITEMS_SLOT, "SVGPointList"),
}
}
@@ -2469,6 +2525,8 @@ fn svg_value_list_kind<'s>(
Some(SvgListKind::Length)
} else if web_api_interfaces::SVGNumberList::is_instance(scope, list) {
Some(SvgListKind::Number)
} else if get_private_value(scope, list, SVG_POINT_LIST_ITEMS_SLOT).is_some() {
Some(SvgListKind::Point)
} else {
None
}
@@ -2498,6 +2556,45 @@ fn require_svg_value_list_items<'s>(
Some(svg_value_list_items(scope, list, kind))
}
macro_rules! define_svg_point_list_callback {
($name:ident, $callback:ident) => {
pub(super) fn $name<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'_, v8::Value>,
) {
$callback(scope, args, rv, SvgListKind::Point);
}
};
}
define_svg_point_list_callback!(svg_point_list_length_getter, svg_value_list_length_getter);
define_svg_point_list_callback!(svg_point_list_clear_callback, svg_value_list_clear_callback);
define_svg_point_list_callback!(
svg_point_list_initialize_callback,
svg_value_list_initialize_callback
);
define_svg_point_list_callback!(
svg_point_list_get_item_callback,
svg_value_list_get_item_callback
);
define_svg_point_list_callback!(
svg_point_list_insert_item_before_callback,
svg_value_list_insert_item_before_callback
);
define_svg_point_list_callback!(
svg_point_list_replace_item_callback,
svg_value_list_replace_item_callback
);
define_svg_point_list_callback!(
svg_point_list_remove_item_callback,
svg_value_list_remove_item_callback
);
define_svg_point_list_callback!(
svg_point_list_append_item_callback,
svg_value_list_append_item_callback
);
pub(super) fn svg_length_list_length_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -1,5 +1,10 @@
use super::{
build_dom_point_object, build_dom_rect_object, optional_dom_point_init_arg,
build_dom_point_object, build_dom_rect_object,
geometry_runtime::{
build_svg_point_object_with_values, dom_point_clone_data, dom_point_init_from_object,
set_svg_point_read_only,
},
optional_dom_point_init_arg,
selection::{selection_clear, selection_dispatch_change, selection_has_range},
selection_value_for_window,
};
@@ -85,6 +90,9 @@ const SVG_ANIMATED_INTEGER_PROPERTY_INDEX_SLOT: &str = "__moliSvgAnimatedInteger
const SVG_ANIMATED_NUMBER_LIST_BASE_VAL_SLOT: &str = "__moliSvgAnimatedNumberListBaseVal";
const SVG_ANIMATED_NUMBER_LIST_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedNumberListAnimVal";
const SVG_NUMBER_LIST_ITEMS_SLOT: &str = "__moliSvgNumberListItems";
const SVG_POINT_LIST_ITEMS_SLOT: &str = "__moliSvgPointListItems";
const SVG_POINTS_SLOT: &str = "__moliSvgPoints";
const SVG_ANIMATED_POINTS_SLOT: &str = "__moliSvgAnimatedPoints";
const SVG_STRING_LIST_ITEMS_SLOT: &str = "__moliSvgStringListItems";
const SVG_STRING_LIST_OWNER_ELEMENT_SLOT: &str = "__moliSvgStringListOwnerElement";
const SVG_STRING_LIST_OWNER_ATTRIBUTE_SLOT: &str = "__moliSvgStringListOwnerAttribute";
@@ -163,6 +171,7 @@ const SVG_TEST_STRING_LIST_ATTRIBUTES: &[(&str, &str)] = &[
enum SvgListKind {
Length,
Number,
Point,
}
#[derive(Clone, Copy)]
@@ -1289,6 +1298,7 @@ pub(in crate::context_bootstrap) fn install_svg_template_bindings<'s>(
"SVGAnimatedNumber" => bindings::install_svg_animated_number_bindings(scope, template),
"SVGAnimatedInteger" => bindings::install_svg_animated_integer_bindings(scope, template),
"SVGNumberList" => bindings::install_svg_number_list_bindings(scope, template),
"SVGPointList" => bindings::install_svg_point_list_bindings(scope, template),
"SVGStringList" => bindings::install_svg_string_list_bindings(scope, template),
"SVGAnimatedNumberList" => {
bindings::install_svg_animated_number_list_bindings(scope, template)
@@ -1316,3 +1326,10 @@ pub(in crate::context_bootstrap) fn install_svg_template_bindings<'s>(
_ => {}
}
}
pub(in crate::context_bootstrap) fn reflect_svg_point_mutation<'s>(
scope: &mut v8::PinScope<'s, '_>,
point: v8::Local<'s, v8::Object>,
) {
builders::reflect_svg_value_list_item_to_owner_list(scope, point, SvgListKind::Point);
}
@@ -3102,3 +3102,151 @@ fn svg_value_lists_enforce_item_types_indices_and_read_only_anim_values() {
assert_eq!(result, "ok");
}
#[test]
fn svg_point_lists_are_live_mutable_and_clear_invalid_content() {
let mut vm = new_storage_test_vm("https://svg-point-list-semantics.test/");
let result = vm
.eval(
r#"
(() => {
const assert = (condition, message) => {
if (!condition) throw new Error(message);
};
const errorName = callback => {
try {
callback();
return "none";
} catch (error) {
return error.name;
}
};
const ns = "http://www.w3.org/2000/svg";
const polygon = document.createElementNS(ns, "polygon");
const polyline = document.createElementNS(ns, "polyline");
const svg = document.createElementNS(ns, "svg");
assert(typeof SVGPointList === "function", "constructor exposed");
assert(errorName(() => new SVGPointList()) === "TypeError",
"illegal constructor");
for (const [constructor, element] of [
[SVGPolygonElement, polygon],
[SVGPolylineElement, polyline],
]) {
const pointsDescriptor = Object.getOwnPropertyDescriptor(
constructor.prototype,
"points",
);
const animatedDescriptor = Object.getOwnPropertyDescriptor(
constructor.prototype,
"animatedPoints",
);
assert(typeof pointsDescriptor.get === "function" &&
pointsDescriptor.set === undefined, `${constructor.name}.points descriptor`);
assert(typeof animatedDescriptor.get === "function" &&
animatedDescriptor.set === undefined,
`${constructor.name}.animatedPoints descriptor`);
assert(pointsDescriptor.enumerable && pointsDescriptor.configurable &&
animatedDescriptor.enumerable && animatedDescriptor.configurable,
`${constructor.name} descriptor flags`);
assert(element.points instanceof SVGPointList &&
element.animatedPoints instanceof SVGPointList,
`${constructor.name} point list interfaces`);
assert(element.points === element.points &&
element.animatedPoints === element.animatedPoints &&
element.points !== element.animatedPoints,
`${constructor.name} SameObject lists`);
}
polygon.setAttribute("points", "0,0 100,0 100,100 0,100");
const points = polygon.points;
const animatedPoints = polygon.animatedPoints;
assert(Object.prototype.toString.call(points) === "[object SVGPointList]",
"point list tag");
assert(points.length === 4 && points.numberOfItems === 4,
"valid content points");
assert(points.getItem(1) === points[1] && points[1] instanceof DOMPoint &&
points[1] instanceof SVGPoint && points[1].x === 100 && points[1].y === 0,
"indexed point identity and values");
polygon.setAttribute("points", "0,0 100,0 INVALID");
assert(points.numberOfItems === 0,
"invalid token clears the whole point list");
polygon.setAttribute("points", "0,0 100,0 20");
assert(points.numberOfItems === 2,
"missing final y coordinate truncates the point list");
polygon.setAttribute("points", "0,0 100,0 20,");
assert(points.numberOfItems === 2,
"trailing comma with missing y truncates the point list");
polygon.setAttribute("points", "0,0 10,20");
const first = points[0];
first.x = 2;
assert(polygon.getAttribute("points") === "2 0 10 20",
"point coordinate mutation reflects to content");
const point = svg.createSVGPoint();
point.x = 5;
point.y = 6;
points.clear();
assert(points.length === 0 && polygon.getAttribute("points") === "",
"clear reflects an empty list");
assert(points.initialize(point) === point && points[0] === point,
"initialize keeps point identity");
assert(polygon.getAttribute("points") === "5 6", "initialize reflection");
point.x = 7;
assert(polygon.getAttribute("points") === "7 6", "owned point stays live");
const second = svg.createSVGPoint();
second.x = 8;
second.y = 9;
assert(points.appendItem(second) === second && points.length === 2,
"appendItem");
const third = svg.createSVGPoint();
third.x = 10;
third.y = 11;
assert(points.insertItemBefore(third, 1) === third && points.length === 3,
"insertItemBefore");
const replacement = svg.createSVGPoint();
replacement.x = 12;
replacement.y = 13;
assert(points.replaceItem(replacement, 0) === replacement &&
points[0] === replacement,
"replaceItem");
assert(points.removeItem(1) === third && points.length === 2,
"removeItem");
points[0] = point;
assert(points[0] === point, "indexed setter");
for (const invalid of [1, "point", polygon, null]) {
assert(errorName(() => points.appendItem(invalid)) === "TypeError",
"point item type enforcement");
}
assert(errorName(() => points.getItem(99)) === "IndexSizeError",
"getItem bounds");
polygon.setAttribute("points", "1,2 3,4");
assert(animatedPoints.length === 2 && animatedPoints[1].x === 3,
"animatedPoints live synchronization");
assert(errorName(() => animatedPoints.clear()) === "NoModificationAllowedError",
"animatedPoints list is read-only");
assert(errorName(() => { animatedPoints[0].x = 9; }) ===
"NoModificationAllowedError", "animated point is read-only");
assert(errorName(() => SVGPointList.prototype.clear.call({})) === "TypeError",
"point list receiver brand");
const pointsDescriptor = Object.getOwnPropertyDescriptor(
SVGPolygonElement.prototype,
"points",
);
assert(errorName(() => pointsDescriptor.get.call(svg)) === "TypeError",
"animated points receiver brand");
return "ok";
})()
"#,
)
.expect("SVG point list semantics probe should evaluate");
assert_eq!(result, "ok");
}
@@ -459,6 +459,7 @@ interfaces! {
SVGTextPositioningElement: SVGTextContentElement;
SVGTitleElement: SVGElement;
SVGUnitTypes;
SVGPointList;
SVGStringList;
SVGTransform;
SVGTransformList;
+13
View File
@@ -82,6 +82,19 @@ pub fn parse_number_list(raw: &str) -> Option<Vec<f64>> {
.collect()
}
pub fn parse_point_list(raw: &str) -> Option<Vec<(f64, f64)>> {
let values = parse_number_list(raw).or_else(|| {
let raw = raw.trim_end();
parse_number_list(raw.strip_suffix(',')?)
})?;
Some(
values
.chunks_exact(2)
.map(|point| (point[0], point[1]))
.collect(),
)
}
fn svg_length(length: SvgTypesLength) -> SvgLength {
SvgLength {
value: length.number,
+20 -2
View File
@@ -12,6 +12,7 @@ pub use geometry::{
};
pub use length::{
SvgLength, SvgLengthUnit, parse_length, parse_length_list, parse_number, parse_number_list,
parse_point_list,
};
pub use matrix::{SvgMatrixComponents, serialize_number};
pub use transform::{
@@ -26,14 +27,31 @@ mod tests {
SvgMatrixComponents, SvgTransform, SvgTransformKind, bounding_box_for_element,
bounding_box_for_segments, bounding_box_for_transformed_element,
consolidate_transform_matrices, is_point_in_fill, parse_length, parse_length_list,
parse_number, parse_number_list, parse_transform_attribute, point_at_length,
segments_for_element, serialize_number, serialize_transform_list,
parse_number, parse_number_list, parse_point_list, parse_transform_attribute,
point_at_length, segments_for_element, serialize_number, serialize_transform_list,
};
fn path_segments(raw: &str) -> Vec<SvgGeometrySegment> {
segments_for_element(SvgGeometryElement::Path { d: raw.to_owned() })
}
#[test]
fn point_list_parser_rejects_invalid_tokens_and_truncates_incomplete_pairs() {
assert_eq!(
parse_point_list("0,0 100,0 100,100 0,100"),
Some(vec![(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)])
);
assert_eq!(
parse_point_list("0,0 100,0 20"),
Some(vec![(0.0, 0.0), (100.0, 0.0)])
);
assert_eq!(
parse_point_list("0,0 100,0 20,"),
Some(vec![(0.0, 0.0), (100.0, 0.0)])
);
assert_eq!(parse_point_list("0,0 100,0 INVALID"), None);
}
fn polyline_segments(raw: &str) -> Vec<SvgGeometrySegment> {
segments_for_element(SvgGeometryElement::Polyline {
points: raw.to_owned(),