mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
feat(svg): expose native value and element interfaces
Completes native SVG value and element interface exposure in one independently mergeable PR against main, refreshing the former SVG constructor PR. Animated scalar values, angles, preserve-aspect-ratio values and live point/string/number/length/transform lists provide the shared native DOM foundation for the added elements. Adds the remaining component-transfer, flood, image, merge, merge-node and tile filter interfaces; SVGSwitchElement and SVGMPathElement; and the animation, animate, set, animate-motion and animate-transform interfaces. Native factories, inheritance, SVGTests lists, attribute reflection, receiver validation and animation event handlers use native objects and the existing DOM backend. Tests cover main/iframe realms, live and windowless documents, native identity, value/list mutation, units, Trusted Types, animation target resolution, handlers and timing argument validation. All internal SVG dependencies are included here, with no external PR merge order. Filter raster rendering and SMIL scheduling remain unimplemented. Timing controls reject NotSupportedError, getStartTime reports no interval, and getCurrentTime remains zero. The separate SVG path parser changes and animated-string UTF-16 storage migration are excluded. The retained length synchronization is included; a separate ignored regression records the lone-surrogate limitation in the current DOM attribute storage. SVG event-handler properties reuse main's existing shared event backend. Tests verify replacement, clearing, canonical event types, cancellation and realm/receiver identity; complete handler/listener registration-order semantics remain part of the separate event-handler backend migration.
This commit is contained in:
@@ -94,23 +94,46 @@ pub fn svg_element_interface_name(local_name: &str) -> &'static str {
|
||||
"animate" => "SVGAnimateElement",
|
||||
"animateMotion" => "SVGAnimateMotionElement",
|
||||
"animateTransform" => "SVGAnimateTransformElement",
|
||||
"set" => "SVGSetElement",
|
||||
"circle" => "SVGCircleElement",
|
||||
"clipPath" => "SVGClipPathElement",
|
||||
"defs" => "SVGDefsElement",
|
||||
"desc" => "SVGDescElement",
|
||||
"ellipse" => "SVGEllipseElement",
|
||||
"feBlend" => "SVGFEBlendElement",
|
||||
"feColorMatrix" => "SVGFEColorMatrixElement",
|
||||
"feComponentTransfer" => "SVGFEComponentTransferElement",
|
||||
"feComposite" => "SVGFECompositeElement",
|
||||
"feConvolveMatrix" => "SVGFEConvolveMatrixElement",
|
||||
"feDiffuseLighting" => "SVGFEDiffuseLightingElement",
|
||||
"feDisplacementMap" => "SVGFEDisplacementMapElement",
|
||||
"feDistantLight" => "SVGFEDistantLightElement",
|
||||
"feDropShadow" => "SVGFEDropShadowElement",
|
||||
"feFlood" => "SVGFEFloodElement",
|
||||
"feFuncA" => "SVGFEFuncAElement",
|
||||
"feFuncB" => "SVGFEFuncBElement",
|
||||
"feFuncG" => "SVGFEFuncGElement",
|
||||
"feFuncR" => "SVGFEFuncRElement",
|
||||
"feGaussianBlur" => "SVGFEGaussianBlurElement",
|
||||
"feImage" => "SVGFEImageElement",
|
||||
"feMerge" => "SVGFEMergeElement",
|
||||
"feMergeNode" => "SVGFEMergeNodeElement",
|
||||
"feMorphology" => "SVGFEMorphologyElement",
|
||||
"feOffset" => "SVGFEOffsetElement",
|
||||
"fePointLight" => "SVGFEPointLightElement",
|
||||
"feSpecularLighting" => "SVGFESpecularLightingElement",
|
||||
"feSpotLight" => "SVGFESpotLightElement",
|
||||
"feTile" => "SVGFETileElement",
|
||||
"feTurbulence" => "SVGFETurbulenceElement",
|
||||
"filter" => "SVGFilterElement",
|
||||
"foreignObject" => "SVGForeignObjectElement",
|
||||
"g" => "SVGGElement",
|
||||
"image" => "SVGImageElement",
|
||||
"line" => "SVGLineElement",
|
||||
"linearGradient" => "SVGLinearGradientElement",
|
||||
"marker" => "SVGMarkerElement",
|
||||
"mask" => "SVGMaskElement",
|
||||
"metadata" => "SVGMetadataElement",
|
||||
"mpath" => "SVGMPathElement",
|
||||
"path" => "SVGPathElement",
|
||||
"pattern" => "SVGPatternElement",
|
||||
"polygon" => "SVGPolygonElement",
|
||||
@@ -118,15 +141,18 @@ pub fn svg_element_interface_name(local_name: &str) -> &'static str {
|
||||
"radialGradient" => "SVGRadialGradientElement",
|
||||
"rect" => "SVGRectElement",
|
||||
"script" => "SVGScriptElement",
|
||||
"set" => "SVGSetElement",
|
||||
"stop" => "SVGStopElement",
|
||||
"svg" => "SVGSVGElement",
|
||||
"style" => "SVGStyleElement",
|
||||
"switch" => "SVGSwitchElement",
|
||||
"mpath" => "SVGMPathElement",
|
||||
"symbol" => "SVGSymbolElement",
|
||||
"text" => "SVGTextElement",
|
||||
"textPath" => "SVGTextPathElement",
|
||||
"title" => "SVGTitleElement",
|
||||
"tspan" => "SVGTSpanElement",
|
||||
"use" => "SVGUseElement",
|
||||
"view" => "SVGViewElement",
|
||||
_ => "SVGElement",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,9 +221,69 @@ fn html_element_interface_name_covers_replay_tags() {
|
||||
fn svg_element_interface_name_specializes_standard_elements() {
|
||||
assert_eq!(svg_element_interface_name("a"), "SVGAElement");
|
||||
assert_eq!(svg_element_interface_name("circle"), "SVGCircleElement");
|
||||
assert_eq!(svg_element_interface_name("clipPath"), "SVGClipPathElement");
|
||||
assert_eq!(svg_element_interface_name("defs"), "SVGDefsElement");
|
||||
assert_eq!(svg_element_interface_name("desc"), "SVGDescElement");
|
||||
assert_eq!(svg_element_interface_name("ellipse"), "SVGEllipseElement");
|
||||
assert_eq!(svg_element_interface_name("feBlend"), "SVGFEBlendElement");
|
||||
assert_eq!(
|
||||
svg_element_interface_name("feColorMatrix"),
|
||||
"SVGFEColorMatrixElement"
|
||||
);
|
||||
assert_eq!(
|
||||
svg_element_interface_name("feComposite"),
|
||||
"SVGFECompositeElement"
|
||||
);
|
||||
assert_eq!(
|
||||
svg_element_interface_name("feConvolveMatrix"),
|
||||
"SVGFEConvolveMatrixElement"
|
||||
);
|
||||
assert_eq!(
|
||||
svg_element_interface_name("feDiffuseLighting"),
|
||||
"SVGFEDiffuseLightingElement"
|
||||
);
|
||||
assert_eq!(
|
||||
svg_element_interface_name("feDisplacementMap"),
|
||||
"SVGFEDisplacementMapElement"
|
||||
);
|
||||
assert_eq!(
|
||||
svg_element_interface_name("feDistantLight"),
|
||||
"SVGFEDistantLightElement"
|
||||
);
|
||||
assert_eq!(
|
||||
svg_element_interface_name("feDropShadow"),
|
||||
"SVGFEDropShadowElement"
|
||||
);
|
||||
assert_eq!(svg_element_interface_name("feFuncA"), "SVGFEFuncAElement");
|
||||
assert_eq!(svg_element_interface_name("feFuncB"), "SVGFEFuncBElement");
|
||||
assert_eq!(svg_element_interface_name("feFuncG"), "SVGFEFuncGElement");
|
||||
assert_eq!(svg_element_interface_name("feFuncR"), "SVGFEFuncRElement");
|
||||
assert_eq!(
|
||||
svg_element_interface_name("feGaussianBlur"),
|
||||
"SVGFEGaussianBlurElement"
|
||||
);
|
||||
assert_eq!(
|
||||
svg_element_interface_name("feMorphology"),
|
||||
"SVGFEMorphologyElement"
|
||||
);
|
||||
assert_eq!(svg_element_interface_name("feOffset"), "SVGFEOffsetElement");
|
||||
assert_eq!(
|
||||
svg_element_interface_name("fePointLight"),
|
||||
"SVGFEPointLightElement"
|
||||
);
|
||||
assert_eq!(
|
||||
svg_element_interface_name("feSpecularLighting"),
|
||||
"SVGFESpecularLightingElement"
|
||||
);
|
||||
assert_eq!(
|
||||
svg_element_interface_name("feSpotLight"),
|
||||
"SVGFESpotLightElement"
|
||||
);
|
||||
assert_eq!(
|
||||
svg_element_interface_name("feTurbulence"),
|
||||
"SVGFETurbulenceElement"
|
||||
);
|
||||
assert_eq!(svg_element_interface_name("filter"), "SVGFilterElement");
|
||||
assert_eq!(
|
||||
svg_element_interface_name("foreignObject"),
|
||||
"SVGForeignObjectElement"
|
||||
@@ -236,6 +296,8 @@ fn svg_element_interface_name_specializes_standard_elements() {
|
||||
"SVGLinearGradientElement"
|
||||
);
|
||||
assert_eq!(svg_element_interface_name("metadata"), "SVGMetadataElement");
|
||||
assert_eq!(svg_element_interface_name("marker"), "SVGMarkerElement");
|
||||
assert_eq!(svg_element_interface_name("mask"), "SVGMaskElement");
|
||||
assert_eq!(svg_element_interface_name("path"), "SVGPathElement");
|
||||
assert_eq!(svg_element_interface_name("pattern"), "SVGPatternElement");
|
||||
assert_eq!(svg_element_interface_name("polygon"), "SVGPolygonElement");
|
||||
@@ -246,12 +308,15 @@ fn svg_element_interface_name_specializes_standard_elements() {
|
||||
);
|
||||
assert_eq!(svg_element_interface_name("rect"), "SVGRectElement");
|
||||
assert_eq!(svg_element_interface_name("script"), "SVGScriptElement");
|
||||
assert_eq!(svg_element_interface_name("stop"), "SVGStopElement");
|
||||
assert_eq!(svg_element_interface_name("svg"), "SVGSVGElement");
|
||||
assert_eq!(svg_element_interface_name("symbol"), "SVGSymbolElement");
|
||||
assert_eq!(svg_element_interface_name("text"), "SVGTextElement");
|
||||
assert_eq!(svg_element_interface_name("textPath"), "SVGTextPathElement");
|
||||
assert_eq!(svg_element_interface_name("title"), "SVGTitleElement");
|
||||
assert_eq!(svg_element_interface_name("tspan"), "SVGTSpanElement");
|
||||
assert_eq!(svg_element_interface_name("use"), "SVGUseElement");
|
||||
assert_eq!(svg_element_interface_name("view"), "SVGViewElement");
|
||||
assert_eq!(svg_element_interface_name("style"), "SVGStyleElement");
|
||||
assert_eq!(svg_element_interface_name("custom"), "SVGElement");
|
||||
}
|
||||
|
||||
@@ -100,14 +100,6 @@ const NODE_BRIDGE_DESCRIPTORS: &[BridgeDescriptor] = &[
|
||||
),
|
||||
descriptor(web_api_interfaces::Element::DESCRIPTOR, ELEMENT_GROUPS),
|
||||
descriptor(web_api_interfaces::SVGElement::DESCRIPTOR, ELEMENT_GROUPS),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGGraphicsElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGGeometryElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGAnimationElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
@@ -128,7 +120,31 @@ const NODE_BRIDGE_DESCRIPTORS: &[BridgeDescriptor] = &[
|
||||
web_api_interfaces::SVGSetElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGGraphicsElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGGeometryElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(web_api_interfaces::SVGAElement::DESCRIPTOR, ELEMENT_GROUPS),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGClipPathElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFilterElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGMaskElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGViewElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
specialized_descriptor(
|
||||
web_api_interfaces::SVGCircleElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
@@ -149,6 +165,34 @@ const NODE_BRIDGE_DESCRIPTORS: &[BridgeDescriptor] = &[
|
||||
SpecializedTemplateInstaller::None,
|
||||
SVG_GEOMETRY_RUNTIME_INSTALL_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGComponentTransferFunctionElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFEFuncAElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFEFuncBElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFEFuncGElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFEFuncRElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFEBlendElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFEColorMatrixElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFEComponentTransferElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
@@ -173,6 +217,58 @@ const NODE_BRIDGE_DESCRIPTORS: &[BridgeDescriptor] = &[
|
||||
web_api_interfaces::SVGFETileElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFECompositeElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFEConvolveMatrixElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFEDiffuseLightingElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFEDisplacementMapElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFEDistantLightElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFEDropShadowElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFEGaussianBlurElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFEMorphologyElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFEOffsetElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFEPointLightElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFESpecularLightingElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFESpotLightElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGFETurbulenceElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGForeignObjectElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
@@ -204,6 +300,10 @@ const NODE_BRIDGE_DESCRIPTORS: &[BridgeDescriptor] = &[
|
||||
svg_gradient_transform: true,
|
||||
},
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGMarkerElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGMetadataElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
@@ -212,6 +312,10 @@ const NODE_BRIDGE_DESCRIPTORS: &[BridgeDescriptor] = &[
|
||||
web_api_interfaces::SVGScriptElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGStopElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGStyleElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
@@ -278,6 +382,10 @@ const NODE_BRIDGE_DESCRIPTORS: &[BridgeDescriptor] = &[
|
||||
web_api_interfaces::SVGTextContentElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGTextPathElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::SVGTextPositioningElement::DESCRIPTOR,
|
||||
ELEMENT_GROUPS,
|
||||
@@ -715,10 +823,6 @@ const NODE_BRIDGE_DESCRIPTORS: &[BridgeDescriptor] = &[
|
||||
SpecializedTemplateInstaller::HtmlTableSectionElement,
|
||||
NONE_RUNTIME_INSTALL_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::HTMLTitleElement::DESCRIPTOR,
|
||||
HTML_ELEMENT_GROUPS,
|
||||
),
|
||||
descriptor(
|
||||
web_api_interfaces::HTMLUListElement::DESCRIPTOR,
|
||||
HTML_ELEMENT_GROUPS,
|
||||
|
||||
@@ -13,6 +13,8 @@ const DOM_POINT_X_SLOT: &str = "__moliDomPointX";
|
||||
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";
|
||||
@@ -561,7 +563,73 @@ pub(in crate::context_bootstrap) fn build_dom_point_object<'s>(
|
||||
.expect("DOMPoint declaration should bind")
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn build_dom_matrix_identity_object<'s>(
|
||||
pub(super) fn dom_point_init_from_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> DomPointInit {
|
||||
DomPointInit {
|
||||
x: dom_point_slot(scope, object, DOM_POINT_X_SLOT, 0.0),
|
||||
y: dom_point_slot(scope, object, DOM_POINT_Y_SLOT, 0.0),
|
||||
z: dom_point_slot(scope, object, DOM_POINT_Z_SLOT, 0.0),
|
||||
w: dom_point_slot(scope, object, DOM_POINT_W_SLOT, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn dom_point_clone_data<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> Option<(bool, [f64; 4])> {
|
||||
if !dom_point_receiver_branded(scope, object) {
|
||||
return None;
|
||||
}
|
||||
let mutable = web_api_interfaces::DOMPoint::is_instance(scope, object);
|
||||
Some((
|
||||
mutable,
|
||||
[
|
||||
dom_point_slot(scope, object, DOM_POINT_X_SLOT, 0.0),
|
||||
dom_point_slot(scope, object, DOM_POINT_Y_SLOT, 0.0),
|
||||
dom_point_slot(scope, object, DOM_POINT_Z_SLOT, 0.0),
|
||||
dom_point_slot(scope, object, DOM_POINT_W_SLOT, 1.0),
|
||||
],
|
||||
))
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn build_svg_point_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
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,
|
||||
object,
|
||||
DOM_POINT_RESTRICTED_NUMBER_SLOT,
|
||||
restricted.into(),
|
||||
);
|
||||
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_matrix_identity_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
build_dom_matrix_object(scope, DomMatrixComponents::identity())
|
||||
@@ -647,12 +715,33 @@ 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 {
|
||||
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))
|
||||
{
|
||||
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,
|
||||
@@ -660,6 +749,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();
|
||||
}
|
||||
|
||||
@@ -2193,3 +2283,76 @@ const DOM_MATRIX_ATTRIBUTES: &[DomMatrixAttribute] = &[
|
||||
DOM_MATRIX_READONLY_ATTRIBUTES[0],
|
||||
DOM_MATRIX_READONLY_ATTRIBUTES[1],
|
||||
];
|
||||
|
||||
#[derive(Default, webidl::WebIdlDictionary)]
|
||||
#[webidl(prefix = "DOMMatrix2DInit")]
|
||||
struct DomMatrix2DInit {
|
||||
a: Option<f64>,
|
||||
b: Option<f64>,
|
||||
c: Option<f64>,
|
||||
d: Option<f64>,
|
||||
e: Option<f64>,
|
||||
f: Option<f64>,
|
||||
m11: Option<f64>,
|
||||
m12: Option<f64>,
|
||||
m21: Option<f64>,
|
||||
m22: Option<f64>,
|
||||
m41: Option<f64>,
|
||||
m42: Option<f64>,
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn dom_matrix_2d_init_arg<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: &v8::FunctionCallbackArguments<'s>,
|
||||
index: i32,
|
||||
prefix: &'static str,
|
||||
) -> Option<[f64; 6]> {
|
||||
if index >= args.length() || args.get(index).is_undefined() {
|
||||
return Some([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
|
||||
}
|
||||
let init = match webidl::parse_dictionary::<DomMatrix2DInit>(
|
||||
scope,
|
||||
args.get(index),
|
||||
webidl::Context::argument(prefix, (index + 1) as usize),
|
||||
) {
|
||||
Ok(Some(init)) => init,
|
||||
Ok(None) => DomMatrix2DInit::default(),
|
||||
Err(error) => {
|
||||
webidl::throw_error(scope, &error);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some([
|
||||
validated_dom_matrix_2d_alias(scope, "DOMMatrix2DInit", "a", init.a, "m11", init.m11, 1.0)?,
|
||||
validated_dom_matrix_2d_alias(scope, "DOMMatrix2DInit", "b", init.b, "m12", init.m12, 0.0)?,
|
||||
validated_dom_matrix_2d_alias(scope, "DOMMatrix2DInit", "c", init.c, "m21", init.m21, 0.0)?,
|
||||
validated_dom_matrix_2d_alias(scope, "DOMMatrix2DInit", "d", init.d, "m22", init.m22, 1.0)?,
|
||||
validated_dom_matrix_2d_alias(scope, "DOMMatrix2DInit", "e", init.e, "m41", init.m41, 0.0)?,
|
||||
validated_dom_matrix_2d_alias(scope, "DOMMatrix2DInit", "f", init.f, "m42", init.m42, 0.0)?,
|
||||
])
|
||||
}
|
||||
|
||||
fn validated_dom_matrix_2d_alias(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
dictionary_name: &'static str,
|
||||
alias_name: &'static str,
|
||||
alias: Option<f64>,
|
||||
matrix_name: &'static str,
|
||||
matrix: Option<f64>,
|
||||
default: f64,
|
||||
) -> Option<f64> {
|
||||
if let (Some(alias), Some(matrix)) = (alias, matrix)
|
||||
&& !same_dom_matrix_2d_number(alias, matrix)
|
||||
{
|
||||
throw_type_error(
|
||||
scope,
|
||||
&format!("{dictionary_name} {alias_name} and {matrix_name} values differ."),
|
||||
);
|
||||
return None;
|
||||
}
|
||||
Some(matrix.or(alias).unwrap_or(default))
|
||||
}
|
||||
|
||||
fn same_dom_matrix_2d_number(lhs: f64, rhs: f64) -> bool {
|
||||
lhs == rhs || (lhs.is_nan() && rhs.is_nan())
|
||||
}
|
||||
|
||||
@@ -1787,14 +1787,27 @@ pub(crate) fn finish_context_bootstrap(
|
||||
("XPathExpression", "XPathExpression"),
|
||||
("XPathResult", "XPathResult"),
|
||||
("SVGLength", "SVGLength"),
|
||||
("SVGAngle", "SVGAngle"),
|
||||
("SVGNumber", "SVGNumber"),
|
||||
("SVGRect", "SVGRect"),
|
||||
("SVGAnimatedString", "SVGAnimatedString"),
|
||||
("SVGAnimatedLength", "SVGAnimatedLength"),
|
||||
("SVGAnimatedAngle", "SVGAnimatedAngle"),
|
||||
("SVGAnimatedRect", "SVGAnimatedRect"),
|
||||
("SVGPreserveAspectRatio", "SVGPreserveAspectRatio"),
|
||||
(
|
||||
"SVGAnimatedPreserveAspectRatio",
|
||||
"SVGAnimatedPreserveAspectRatio",
|
||||
),
|
||||
("SVGLengthList", "SVGLengthList"),
|
||||
("SVGAnimatedLengthList", "SVGAnimatedLengthList"),
|
||||
("SVGAnimatedNumber", "SVGAnimatedNumber"),
|
||||
("SVGAnimatedInteger", "SVGAnimatedInteger"),
|
||||
("SVGNumberList", "SVGNumberList"),
|
||||
("SVGPointList", "SVGPointList"),
|
||||
("SVGStringList", "SVGStringList"),
|
||||
("SVGAnimatedNumberList", "SVGAnimatedNumberList"),
|
||||
("SVGAnimatedBoolean", "SVGAnimatedBoolean"),
|
||||
("SVGAnimatedEnumeration", "SVGAnimatedEnumeration"),
|
||||
("SVGUnitTypes", "SVGUnitTypes"),
|
||||
("SVGAnimatedTransformList", "SVGAnimatedTransformList"),
|
||||
|
||||
@@ -100,6 +100,10 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[
|
||||
interface: web_api_interfaces::SVGLength::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::SVGAngle::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::SVGNumber::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
@@ -108,10 +112,30 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[
|
||||
interface: web_api_interfaces::SVGRect::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::SVGAnimatedString::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::SVGAnimatedLength::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::SVGAnimatedAngle::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::SVGAnimatedRect::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::SVGPreserveAspectRatio::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::SVGAnimatedPreserveAspectRatio::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::SVGLengthList::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
@@ -124,14 +148,30 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[
|
||||
interface: web_api_interfaces::SVGAnimatedNumber::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::SVGAnimatedInteger::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
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,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::SVGAnimatedNumberList::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::SVGAnimatedBoolean::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::SVGAnimatedEnumeration::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
use moli_webapi_declare::WebApiFunctionTemplate;
|
||||
|
||||
use super::callbacks::svg_test_string_list_getter;
|
||||
use crate::{
|
||||
document_runtime::DomHandle,
|
||||
native_bridge::{
|
||||
JsContextHost,
|
||||
document::{SVG_NS, detached_native_object_for_handle},
|
||||
element::{node_event_handler_getter_function, node_event_handler_setter_function},
|
||||
node_runtime_and_handle_from_object_or_detached, throw_dom_exception,
|
||||
},
|
||||
util::{callback_data_index_value, v8str},
|
||||
web_api_interfaces, webidl,
|
||||
};
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::SVGAnimationElement, enumerable, receiver)]
|
||||
struct SvgAnimationDeclaration {
|
||||
#[webapi(accessor_property = "targetElement", getter = target_element_getter)]
|
||||
target_element: (),
|
||||
#[webapi(accessor_property = "onbegin", getter = node_event_handler_getter_function,
|
||||
setter = node_event_handler_setter_function, data = v8str(scope, "onbegin"))]
|
||||
onbegin: (),
|
||||
#[webapi(accessor_property = "onend", getter = node_event_handler_getter_function,
|
||||
setter = node_event_handler_setter_function, data = v8str(scope, "onend"))]
|
||||
onend: (),
|
||||
#[webapi(accessor_property = "onrepeat", getter = node_event_handler_getter_function,
|
||||
setter = node_event_handler_setter_function, data = v8str(scope, "onrepeat"))]
|
||||
onrepeat: (),
|
||||
#[webapi(accessor_property = "requiredExtensions", getter = svg_test_string_list_getter,
|
||||
data = callback_data_index_value(scope, 0))]
|
||||
required_extensions: (),
|
||||
#[webapi(accessor_property = "systemLanguage", getter = svg_test_string_list_getter,
|
||||
data = callback_data_index_value(scope, 1))]
|
||||
system_language: (),
|
||||
#[webapi(method = "getStartTime", length = 0, callback = get_start_time)]
|
||||
get_start_time: (),
|
||||
#[webapi(method = "getCurrentTime", length = 0, callback = get_current_time)]
|
||||
get_current_time: (),
|
||||
#[webapi(method = "getSimpleDuration", length = 0, callback = get_simple_duration)]
|
||||
get_simple_duration: (),
|
||||
#[webapi(method = "beginElement", length = 0, callback = unsupported_timing)]
|
||||
begin_element: (),
|
||||
#[webapi(method = "beginElementAt", length = 1, callback = unsupported_timing_at)]
|
||||
begin_element_at: (),
|
||||
#[webapi(method = "endElement", length = 0, callback = unsupported_timing)]
|
||||
end_element: (),
|
||||
#[webapi(method = "endElementAt", length = 1, callback = unsupported_timing_at)]
|
||||
end_element_at: (),
|
||||
}
|
||||
|
||||
pub(super) fn install_bindings<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_, ()>,
|
||||
template: v8::Local<'s, v8::FunctionTemplate>,
|
||||
name: &str,
|
||||
) {
|
||||
if name == "SVGAnimationElement" {
|
||||
let prototype = template.prototype_template(scope);
|
||||
SvgAnimationDeclaration::initialize_prototype_template(scope, prototype);
|
||||
}
|
||||
}
|
||||
|
||||
fn target_element(runtime: &JsContextHost, animation: DomHandle) -> Option<DomHandle> {
|
||||
let dom = runtime.dom_host();
|
||||
if !dom.is_connected_to_document(animation) {
|
||||
return None;
|
||||
}
|
||||
let href = dom
|
||||
.get_attribute_ns(animation, None, "href")
|
||||
.or_else(|| dom.get_attribute_ns(animation, Some("http://www.w3.org/1999/xlink"), "href"))
|
||||
.unwrap_or_default();
|
||||
let target = if href.is_empty() {
|
||||
dom.node(animation)?.parent_node()?
|
||||
} else {
|
||||
let fragment = if let Some(fragment) = href.strip_prefix('#') {
|
||||
fragment.to_owned()
|
||||
} else {
|
||||
let document = dom.owner_document_handle(animation)?;
|
||||
let mut url = runtime
|
||||
.document_base_url_for_handle(document)
|
||||
.join(&href)
|
||||
.ok()?;
|
||||
let fragment = url.fragment()?.to_owned();
|
||||
url.set_fragment(None);
|
||||
let mut document_url = runtime.document_url_for_handle(document);
|
||||
document_url.set_fragment(None);
|
||||
if url != document_url {
|
||||
return None;
|
||||
}
|
||||
fragment
|
||||
};
|
||||
if fragment.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let id = percent_encoding::percent_decode_str(&fragment).decode_utf8_lossy();
|
||||
let root = dom.root_node_handle(animation)?;
|
||||
dom.element_handle_by_id_in_subtree(root, &id)?
|
||||
};
|
||||
dom.node(target)
|
||||
.is_some_and(|node| node.namespace() == Some(SVG_NS))
|
||||
.then_some(target)
|
||||
}
|
||||
|
||||
fn target_element_getter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
let Ok((runtime_ptr, handle)) =
|
||||
node_runtime_and_handle_from_object_or_detached(scope, args.this())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let target = target_element(unsafe { &*runtime_ptr }, handle);
|
||||
let Some(target) =
|
||||
target.and_then(|handle| detached_native_object_for_handle(scope, runtime_ptr, handle))
|
||||
else {
|
||||
rv.set_null();
|
||||
return;
|
||||
};
|
||||
rv.set(target.into());
|
||||
}
|
||||
|
||||
// Native DOM, interface identity and handler dispatch are supported. A SMIL
|
||||
// time container is not yet scheduled, so no current interval exists. Timing
|
||||
// controls fail explicitly instead of pretending to start an animation.
|
||||
fn get_start_time<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
_args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
throw_dom_exception(
|
||||
scope,
|
||||
"InvalidStateError",
|
||||
11,
|
||||
"The animation has no current interval.",
|
||||
);
|
||||
}
|
||||
|
||||
fn get_current_time<'s>(
|
||||
_scope: &mut v8::PinScope<'s, '_>,
|
||||
_args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
rv.set_double(0.0);
|
||||
}
|
||||
|
||||
fn get_simple_duration<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
_args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
throw_dom_exception(
|
||||
scope,
|
||||
"NotSupportedError",
|
||||
9,
|
||||
"SVG animation durations are not implemented.",
|
||||
);
|
||||
}
|
||||
|
||||
fn unsupported_timing<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
_args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
throw_dom_exception(
|
||||
scope,
|
||||
"NotSupportedError",
|
||||
9,
|
||||
"SVG animation timing is not implemented.",
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "SVGAnimationElement")]
|
||||
struct TimingOffsetArgs {
|
||||
#[webidl(required, converter = "double")]
|
||||
offset: f64,
|
||||
}
|
||||
|
||||
fn unsupported_timing_at<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
let Some(parsed) = webidl::parse_args::<TimingOffsetArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
if !(parsed.offset as f32).is_finite() {
|
||||
webidl::throw_type_error(
|
||||
scope,
|
||||
"SVG animation offset is outside the finite float range.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
unsupported_timing(scope, args, rv);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,129 @@
|
||||
use moli_webapi_declare::WebApiFunctionTemplate;
|
||||
|
||||
use super::SVG_URI_HREF_SLOT;
|
||||
use super::callbacks::{
|
||||
svg_animated_string_attribute_getter, svg_filter_primitive_animated_length_getter,
|
||||
svg_fit_to_view_box_getter,
|
||||
};
|
||||
use crate::{
|
||||
util::{callback_data_index_value, callback_data_item},
|
||||
web_api_interfaces,
|
||||
};
|
||||
|
||||
// A mixin's attributes are installed on each concrete interface. Generate its
|
||||
// receiver checks there so, for example, a flood getter cannot accept a tile.
|
||||
macro_rules! filter_primitive {
|
||||
($declaration:ident, $interface:ident, {
|
||||
$(($field:ident, $name:literal, $getter:ident, $index:literal)),* $(,)?
|
||||
}) => {
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::$interface, enumerable, receiver)]
|
||||
struct $declaration {
|
||||
#[webapi(accessor_property = "x", getter = animated_length_getter, data = callback_data_index_value(scope, 0))]
|
||||
x: (),
|
||||
#[webapi(accessor_property = "y", getter = animated_length_getter, data = callback_data_index_value(scope, 1))]
|
||||
y: (),
|
||||
#[webapi(accessor_property = "width", getter = animated_length_getter, data = callback_data_index_value(scope, 2))]
|
||||
width: (),
|
||||
#[webapi(accessor_property = "height", getter = animated_length_getter, data = callback_data_index_value(scope, 3))]
|
||||
height: (),
|
||||
#[webapi(accessor_property = "result", getter = animated_string_getter, data = callback_data_index_value(scope, 0))]
|
||||
result: (),
|
||||
$(
|
||||
#[webapi(accessor_property = $name, getter = $getter, data = callback_data_index_value(scope, $index))]
|
||||
$field: (),
|
||||
)*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
filter_primitive!(ComponentTransferDeclaration, SVGFEComponentTransferElement, {
|
||||
(in1, "in1", animated_string_getter, 1),
|
||||
});
|
||||
filter_primitive!(FloodDeclaration, SVGFEFloodElement, {});
|
||||
filter_primitive!(ImageDeclaration, SVGFEImageElement, {
|
||||
(href, "href", animated_string_getter, 2),
|
||||
(preserve_aspect_ratio, "preserveAspectRatio", preserve_aspect_ratio_getter, 1),
|
||||
});
|
||||
filter_primitive!(MergeDeclaration, SVGFEMergeElement, {});
|
||||
filter_primitive!(TileDeclaration, SVGFETileElement, {
|
||||
(in1, "in1", animated_string_getter, 1),
|
||||
});
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::SVGFEMergeNodeElement, enumerable, receiver)]
|
||||
struct MergeNodeDeclaration {
|
||||
#[webapi(accessor_property = "in1", getter = animated_string_getter, data = callback_data_index_value(scope, 1))]
|
||||
in1: (),
|
||||
}
|
||||
|
||||
pub(super) fn install_bindings<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_, ()>,
|
||||
template: v8::Local<'s, v8::FunctionTemplate>,
|
||||
name: &str,
|
||||
) {
|
||||
let prototype = template.prototype_template(scope);
|
||||
match name {
|
||||
"SVGFEComponentTransferElement" => {
|
||||
ComponentTransferDeclaration::initialize_prototype_template(scope, prototype)
|
||||
}
|
||||
"SVGFEFloodElement" => FloodDeclaration::initialize_prototype_template(scope, prototype),
|
||||
"SVGFEImageElement" => ImageDeclaration::initialize_prototype_template(scope, prototype),
|
||||
"SVGFEMergeElement" => MergeDeclaration::initialize_prototype_template(scope, prototype),
|
||||
"SVGFEMergeNodeElement" => {
|
||||
MergeNodeDeclaration::initialize_prototype_template(scope, prototype)
|
||||
}
|
||||
"SVGFETileElement" => TileDeclaration::initialize_prototype_template(scope, prototype),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn animated_length_getter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
// Tear-off objects belong to the element's realm, including when the
|
||||
// accessor was obtained from a different Window.
|
||||
let Some(context) = args.this().get_creation_context(scope) else {
|
||||
return;
|
||||
};
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
svg_filter_primitive_animated_length_getter(scope, args, rv);
|
||||
}
|
||||
|
||||
fn animated_string_getter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Some((slot, attribute)) = callback_data_item(
|
||||
scope,
|
||||
&args,
|
||||
&[
|
||||
("__moliSvgFilterResult", "result"),
|
||||
("__moliSvgFilterInput", "in"),
|
||||
(SVG_URI_HREF_SLOT, "href"),
|
||||
],
|
||||
"SVG filter string attributes",
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
let Some(context) = args.this().get_creation_context(scope) else {
|
||||
return;
|
||||
};
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
svg_animated_string_attribute_getter(scope, args, rv, slot, attribute);
|
||||
}
|
||||
|
||||
fn preserve_aspect_ratio_getter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Some(context) = args.this().get_creation_context(scope) else {
|
||||
return;
|
||||
};
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
svg_fit_to_view_box_getter(scope, args, rv);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
//! 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::web_api_interfaces;
|
||||
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,
|
||||
};
|
||||
@@ -15,6 +15,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)]
|
||||
@@ -82,6 +84,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>,
|
||||
@@ -101,6 +157,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);
|
||||
}
|
||||
@@ -114,6 +171,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),
|
||||
@@ -131,10 +199,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());
|
||||
}
|
||||
|
||||
@@ -226,7 +226,11 @@ fn invoke_event_handler_property<'s>(
|
||||
event_type: &str,
|
||||
event: v8::Local<'s, v8::Object>,
|
||||
) {
|
||||
let handler_name = format!("on{event_type}");
|
||||
let Some(handler_name) =
|
||||
crate::native_bridge::element::event_handler_content_attribute_name(event_type)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let (callback_id, temporary) = match registry.event_handler_property_value(target, event_type) {
|
||||
Some(EventHandlerPropertyValue::Callback(callback_id)) => (callback_id, false),
|
||||
Some(EventHandlerPropertyValue::Null) => {
|
||||
|
||||
@@ -297,6 +297,7 @@ pub(in crate::native_bridge::document) use structure::set_document_body_for_nati
|
||||
|
||||
pub(crate) const XHTML_NS: &str = "http://www.w3.org/1999/xhtml";
|
||||
pub(crate) const SVG_NS: &str = "http://www.w3.org/2000/svg";
|
||||
pub(crate) const XLINK_NS: &str = "http://www.w3.org/1999/xlink";
|
||||
pub(crate) const MATHML_NS: &str = "http://www.w3.org/1998/Math/MathML";
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
|
||||
@@ -100,6 +100,7 @@ mod trusted_types;
|
||||
pub(crate) use script_execution::{
|
||||
inline_script_source_for_execution, prepare_inline_classic_frame_script_job_for_execution,
|
||||
};
|
||||
pub(crate) use trusted_types::set_svg_animated_string_base_value;
|
||||
use trusted_types::{
|
||||
TrustedScriptElementSink, trusted_script_element_sink_string, trusted_script_url_sink_string,
|
||||
};
|
||||
@@ -259,7 +260,8 @@ pub(crate) use event_handlers::compile_window_body_onmessageerror_attribute;
|
||||
use event_handlers::install_global_event_handler_template_bindings as install_global_event_handler_templates_for_owner;
|
||||
pub(crate) use event_handlers::{
|
||||
EventAttributeHandlerScope, GlobalEventHandlerOwner, compile_event_attribute_handler_for_owner,
|
||||
initialize_parser_inserted_body_window_event_handlers,
|
||||
event_handler_content_attribute_name, initialize_parser_inserted_body_window_event_handlers,
|
||||
node_event_handler_getter_function, node_event_handler_setter_function,
|
||||
};
|
||||
use event_handlers::{
|
||||
body_onerror_getter_function, body_onerror_setter_function, body_onload_getter_function,
|
||||
|
||||
@@ -508,6 +508,12 @@ fn event_handler_slot_name(name: &str) -> String {
|
||||
fn event_handler_event_type(name: &str) -> Option<&str> {
|
||||
name.strip_prefix("on")
|
||||
.filter(|event_type| !event_type.is_empty())
|
||||
.map(|event_type| match event_type {
|
||||
"begin" => "beginEvent",
|
||||
"end" => "endEvent",
|
||||
"repeat" => "repeatEvent",
|
||||
event_type => event_type,
|
||||
})
|
||||
}
|
||||
|
||||
fn legacy_lenient_this_event_handler(name: &str) -> bool {
|
||||
@@ -537,3 +543,14 @@ pub(super) fn invalidate_node_event_attribute_handler(
|
||||
};
|
||||
runtime.clear_event_handler_property(EventTargetHandle::Node(handle), event_type);
|
||||
}
|
||||
|
||||
pub(crate) fn event_handler_content_attribute_name(event_type: &str) -> Option<String> {
|
||||
let event_type = match event_type {
|
||||
"begin" | "end" | "repeat" => return None,
|
||||
"beginEvent" => "begin",
|
||||
"endEvent" => "end",
|
||||
"repeatEvent" => "repeat",
|
||||
event_type => event_type,
|
||||
};
|
||||
Some(format!("on{event_type}"))
|
||||
}
|
||||
|
||||
@@ -12,7 +12,11 @@ pub(crate) use body_window::compile_window_body_onmessageerror_attribute;
|
||||
pub(super) use body_window::{
|
||||
body_onmessageerror_getter_function, body_onmessageerror_setter_function,
|
||||
};
|
||||
pub(crate) use generic::{GlobalEventHandlerOwner, install_global_event_handler_template_bindings};
|
||||
pub(crate) use generic::{
|
||||
GlobalEventHandlerOwner, event_handler_content_attribute_name,
|
||||
install_global_event_handler_template_bindings, node_event_handler_getter_function,
|
||||
node_event_handler_setter_function,
|
||||
};
|
||||
pub(crate) use shared::{EventAttributeHandlerScope, compile_event_attribute_handler_for_owner};
|
||||
|
||||
fn is_body_or_frameset_element(
|
||||
|
||||
@@ -156,3 +156,88 @@ pub(crate) fn trusted_script_source_for_execution(
|
||||
scope, source, sink,
|
||||
)
|
||||
}
|
||||
|
||||
fn svg_animated_string_attribute_namespace(
|
||||
runtime: &JsContextHost,
|
||||
handle: DomHandle,
|
||||
attribute: &str,
|
||||
) -> Option<&'static str> {
|
||||
(attribute == "href"
|
||||
&& !runtime.dom_host().has_attribute_ns(handle, None, attribute)
|
||||
&& runtime.dom_host().has_attribute_ns(
|
||||
handle,
|
||||
Some(crate::native_bridge::document::XLINK_NS),
|
||||
attribute,
|
||||
))
|
||||
.then_some(crate::native_bridge::document::XLINK_NS)
|
||||
}
|
||||
|
||||
pub(crate) fn set_svg_animated_string_base_value<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
attribute: &str,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
) -> Option<String> {
|
||||
let (runtime_ptr, handle) =
|
||||
crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner).ok()?;
|
||||
let script_href = {
|
||||
let element = unsafe { &*runtime_ptr }
|
||||
.dom_host()
|
||||
.node(handle)?
|
||||
.as_element()?;
|
||||
element.namespace() == "http://www.w3.org/2000/svg"
|
||||
&& element.local_name() == "script"
|
||||
&& attribute == "href"
|
||||
};
|
||||
let value = if script_href {
|
||||
let requirements = unsafe { &*runtime_ptr }.trusted_types_for_script_requirements(scope);
|
||||
crate::context_bootstrap::trusted_script_url_string_or_throw(
|
||||
scope,
|
||||
value,
|
||||
requirements,
|
||||
"SVGScriptElement href",
|
||||
"baseVal",
|
||||
)?
|
||||
} else {
|
||||
match crate::webidl::convert::<crate::webidl::DomString>(
|
||||
scope,
|
||||
value,
|
||||
crate::webidl::Context::member("SVGAnimatedString", "baseVal"),
|
||||
) {
|
||||
Ok(value) => value.0,
|
||||
Err(error) => {
|
||||
crate::webidl::throw_error(scope, &error);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
};
|
||||
let (runtime_ptr, handle) =
|
||||
crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, owner).ok()?;
|
||||
let namespace =
|
||||
svg_animated_string_attribute_namespace(unsafe { &*runtime_ptr }, handle, attribute);
|
||||
if attribute == "href" {
|
||||
let _ = unsafe { &mut *runtime_ptr }.set_attribute_ns(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
handle,
|
||||
namespace,
|
||||
namespace.map(|_| "xlink"),
|
||||
attribute,
|
||||
if namespace.is_some() {
|
||||
"xlink:href"
|
||||
} else {
|
||||
attribute
|
||||
},
|
||||
&value,
|
||||
);
|
||||
} else {
|
||||
let _ = unsafe { &mut *runtime_ptr }.set_attribute(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
handle,
|
||||
attribute,
|
||||
&value,
|
||||
);
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
|
||||
@@ -1606,3 +1606,114 @@ fn service_worker_register_converts_options_before_the_default_policy() {
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_script_href_base_val_uses_the_owner_reflected_trusted_script_url_sink() {
|
||||
let mut vm = new_storage_test_vm("https://svg-script-href-trusted-types.test/");
|
||||
vm.set_response_content_security_policies(&["require-trusted-types-for 'script'".to_owned()]);
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const violations = [];
|
||||
document.addEventListener("securitypolicyviolation", event => {
|
||||
violations.push({
|
||||
blockedURI: event.blockedURI,
|
||||
effectiveDirective: event.effectiveDirective,
|
||||
sample: event.sample
|
||||
});
|
||||
});
|
||||
globalThis.__svgScriptHrefViolations = violations;
|
||||
|
||||
const svgScript = document.createElementNS("http://www.w3.org/2000/svg", "script");
|
||||
const href = svgScript.href;
|
||||
let illegalConstructor = false;
|
||||
try {
|
||||
new SVGAnimatedString();
|
||||
} catch (error) {
|
||||
illegalConstructor = error instanceof TypeError;
|
||||
}
|
||||
let plainRejected = false;
|
||||
try {
|
||||
href.baseVal = "plain.js";
|
||||
} catch (error) {
|
||||
plainRejected = error instanceof TypeError;
|
||||
}
|
||||
const rejectedAttribute = svgScript.getAttribute("href");
|
||||
|
||||
const explicit = trustedTypes.createPolicy("svg-script-href", {
|
||||
createScriptURL: value => value
|
||||
});
|
||||
href.baseVal = explicit.createScriptURL("trusted.js");
|
||||
const trustedValues = [
|
||||
svgScript.getAttribute("href"),
|
||||
href.baseVal,
|
||||
href.animVal
|
||||
];
|
||||
svgScript.setAttribute("href", explicit.createScriptURL("attribute.js"));
|
||||
const externallySynced = [href.baseVal, href.animVal];
|
||||
const className = svgScript.className;
|
||||
className.baseVal = explicit.createScriptURL("trusted-class");
|
||||
const use = document.createElementNS("http://www.w3.org/2000/svg", "use");
|
||||
const useHref = use.href;
|
||||
useHref.baseVal = explicit.createScriptURL("trusted-use");
|
||||
|
||||
const defaultCalls = [];
|
||||
trustedTypes.createPolicy("default", {
|
||||
createScriptURL: (value, type, sink) => {
|
||||
defaultCalls.push([value, type, sink]);
|
||||
return `safe-${value}`;
|
||||
}
|
||||
});
|
||||
href.baseVal = "default-input";
|
||||
className.baseVal = "plain-class";
|
||||
useHref.baseVal = "plain-use";
|
||||
|
||||
return JSON.stringify({
|
||||
shape: [
|
||||
href instanceof SVGAnimatedString,
|
||||
Object.prototype.toString.call(href),
|
||||
svgScript.href === href,
|
||||
illegalConstructor
|
||||
],
|
||||
plainRejected,
|
||||
rejectedAttribute,
|
||||
trustedValues,
|
||||
externallySynced,
|
||||
ordinaryAnimatedStrings: [
|
||||
className instanceof SVGAnimatedString,
|
||||
svgScript.className === className,
|
||||
className.baseVal,
|
||||
className.animVal,
|
||||
svgScript.getAttribute("class"),
|
||||
useHref instanceof SVGAnimatedString,
|
||||
use.href === useHref,
|
||||
useHref.baseVal,
|
||||
useHref.animVal,
|
||||
use.getAttribute("href")
|
||||
],
|
||||
defaulted: [svgScript.getAttribute("href"), href.baseVal, href.animVal],
|
||||
defaultCalls,
|
||||
violations
|
||||
});
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("SVGScriptElement href Trusted Types sink probe should evaluate");
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"shape":[true,"[object SVGAnimatedString]",true,true],"plainRejected":true,"rejectedAttribute":null,"trustedValues":["trusted.js","trusted.js","trusted.js"],"externallySynced":["attribute.js","attribute.js"],"ordinaryAnimatedStrings":[true,true,"plain-class","plain-class","plain-class",true,true,"plain-use","plain-use","plain-use"],"defaulted":["safe-default-input","safe-default-input","safe-default-input"],"defaultCalls":[["default-input","TrustedScriptURL","SVGScriptElement href"]],"violations":[]}"#
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
drain_pre_domcontentloaded_non_script_page_tasks_for_test(&mut vm),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify(globalThis.__svgScriptHrefViolations)")
|
||||
.expect("queued SVGScriptElement href violation should be observable"),
|
||||
r#"[{"blockedURI":"trusted-types-sink","effectiveDirective":"require-trusted-types-for","sample":"SVGScriptElement href|plain.js"}]"#
|
||||
);
|
||||
}
|
||||
|
||||
+233
@@ -2728,3 +2728,236 @@ fn detached_plain_document_all_matches_chromium_htmldda_surface() {
|
||||
r#"{"ownAll":false,"protoGetter":"function","allType":"undefined","loose":true,"strict":false,"bool":false,"string":"[object HTMLAllCollection]","tag":"[object HTMLAllCollection]","length":0,"noArgNull":true,"itemZeroNull":true,"itemMissNull":true,"itemMethodNull":true,"namedMethodNull":true}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_value_lists_enforce_item_types_indices_and_read_only_anim_values() {
|
||||
let mut vm = new_storage_test_vm("https://svg-value-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 SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const text = document.createElementNS(SVG_NS, "text");
|
||||
const svg = document.createElementNS(SVG_NS, "svg");
|
||||
text.setAttribute("x", "10 20");
|
||||
text.setAttribute("rotate", "15 30");
|
||||
|
||||
const lengths = text.x;
|
||||
const lengthBase = lengths.baseVal;
|
||||
const lengthAnim = lengths.animVal;
|
||||
const numbers = text.rotate;
|
||||
const numberBase = numbers.baseVal;
|
||||
const numberAnim = numbers.animVal;
|
||||
const invalidItems = [30, "invalid", text, null];
|
||||
|
||||
for (const item of invalidItems) {
|
||||
assert(errorName(() => lengthBase.initialize(item)) === "TypeError",
|
||||
"SVGLengthList.initialize item type");
|
||||
assert(errorName(() => lengthBase.insertItemBefore(item, 0)) === "TypeError",
|
||||
"SVGLengthList.insertItemBefore item type");
|
||||
assert(errorName(() => lengthBase.replaceItem(item, 0)) === "TypeError",
|
||||
"SVGLengthList.replaceItem item type");
|
||||
assert(errorName(() => lengthBase.appendItem(item)) === "TypeError",
|
||||
"SVGLengthList.appendItem item type");
|
||||
assert(errorName(() => { lengthBase[0] = item; }) === "TypeError",
|
||||
"SVGLengthList indexed setter item type");
|
||||
}
|
||||
|
||||
const length = svg.createSVGLength();
|
||||
length.value = 42;
|
||||
lengthBase[0] = length;
|
||||
assert(lengthBase[0] === length, "SVGLengthList indexed getter");
|
||||
assert(text.getAttribute("x") === "42 20", "SVGLengthList indexed reflection");
|
||||
|
||||
const number = svg.createSVGNumber();
|
||||
number.value = 7;
|
||||
numberBase[1] = number;
|
||||
assert(numberBase[1] === number, "SVGNumberList indexed getter");
|
||||
assert(text.getAttribute("rotate") === "15 7", "SVGNumberList indexed reflection");
|
||||
assert(errorName(() => lengthBase.appendItem(number)) === "TypeError",
|
||||
"SVGLengthList rejects SVGNumber");
|
||||
assert(errorName(() => numberBase.appendItem(length)) === "TypeError",
|
||||
"SVGNumberList rejects SVGLength");
|
||||
|
||||
text.setAttribute("x", "1 2 3");
|
||||
assert(lengthBase.length === 3 && lengthBase[2].value === 3,
|
||||
"saved baseVal resynchronizes");
|
||||
assert(text.x.animVal.length === 3 && text.x.animVal[2].value === 3,
|
||||
"animVal resynchronizes after direct baseVal access");
|
||||
|
||||
assert(errorName(() => lengthAnim.clear()) === "NoModificationAllowedError",
|
||||
"SVGLengthList animVal clear");
|
||||
assert(errorName(() => { lengthAnim[0] = length; }) === "NoModificationAllowedError",
|
||||
"SVGLengthList animVal indexed setter");
|
||||
assert(errorName(() => numberAnim.appendItem(number)) === "NoModificationAllowedError",
|
||||
"SVGNumberList animVal appendItem");
|
||||
assert(errorName(() => SVGLengthList.prototype.clear.call({})) === "TypeError",
|
||||
"SVGLengthList receiver brand");
|
||||
return "ok";
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("SVG value list semantics probe should evaluate");
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+174
@@ -0,0 +1,174 @@
|
||||
(() => {
|
||||
const ns = 'http://www.w3.org/2000/svg', xlink = 'http://www.w3.org/1999/xlink';
|
||||
const cases = [['animate', 'SVGAnimateElement'], ['set', 'SVGSetElement'],
|
||||
['animateMotion', 'SVGAnimateMotionElement'], ['animateTransform', 'SVGAnimateTransformElement']];
|
||||
const rows = [];
|
||||
const assert = (condition, message) => { if (!condition) throw Error(message); };
|
||||
const check = (name, run) => {
|
||||
try { run(); rows.push({name, pass: true}); }
|
||||
catch (error) { rows.push({name, pass: false, error: error.name, message: error.message}); }
|
||||
};
|
||||
const throws = (run, Constructor, name = Constructor.name) => {
|
||||
let error;
|
||||
try { run(); } catch (caught) { error = caught; }
|
||||
assert(error instanceof Constructor && error.name === name, name + ' required');
|
||||
};
|
||||
const child = document.getElementById('child').contentWindow;
|
||||
const realms = [['main', window], ['child', child]];
|
||||
for (const [owner, realm] of realms) {
|
||||
const Base = realm.SVGAnimationElement;
|
||||
check(owner + '/base', () => {
|
||||
assert(Object.getPrototypeOf(Base) === realm.SVGElement, 'base constructor inheritance');
|
||||
assert(Object.getPrototypeOf(Base.prototype) === realm.SVGElement.prototype, 'base prototype inheritance');
|
||||
throws(() => new Base(), realm.TypeError);
|
||||
throws(() => Base(), realm.TypeError);
|
||||
assert(!('onbegin' in realm.document.createElementNS(ns, 'rect')), 'only animation interfaces expose onbegin');
|
||||
});
|
||||
const documents = [['live', realm.document],
|
||||
['html', realm.document.implementation.createHTMLDocument('')],
|
||||
['xml', realm.document.implementation.createDocument(ns, 'svg')],
|
||||
['parser', new realm.DOMParser().parseFromString('<svg xmlns="'+ns+'"/>', 'image/svg+xml')]];
|
||||
for (const [tag, name] of cases) {
|
||||
const Constructor = realm[name];
|
||||
check(owner+'/'+tag+'/constructor', () => {
|
||||
assert(Constructor.name === name && Constructor.length === 0, 'constructor metadata');
|
||||
assert(Object.getPrototypeOf(Constructor) === Base, 'constructor inheritance');
|
||||
assert(Object.getPrototypeOf(Constructor.prototype) === Base.prototype, 'prototype inheritance');
|
||||
throws(() => new Constructor(), realm.TypeError);
|
||||
throws(() => Constructor(), realm.TypeError);
|
||||
});
|
||||
for (const [kind, doc] of documents) {
|
||||
const label=owner+'/'+kind+'/'+tag, element=doc.createElementNS(ns, tag);
|
||||
check(label+'/factory', () => {
|
||||
assert(Object.getPrototypeOf(element) === Constructor.prototype, 'native prototype');
|
||||
assert(element instanceof Base && element instanceof realm.SVGElement && !(element instanceof realm.SVGGraphicsElement), 'native inheritance');
|
||||
assert(Object.prototype.toString.call(element)==='[object '+name+']', 'native tag');
|
||||
assert(Object.getPrototypeOf(element.cloneNode())===Constructor.prototype, 'clone identity');
|
||||
assert(Object.getPrototypeOf(realm.document.importNode(element))===Constructor.prototype, 'import identity');
|
||||
assert(!(doc.createElementNS('urn:other', tag) instanceof Constructor), 'namespace-sensitive factory');
|
||||
assert(!(doc.createElementNS(ns, tag.toUpperCase()) instanceof Constructor), 'case-sensitive factory');
|
||||
});
|
||||
check(label+'/SVGTests', () => {
|
||||
for (const attr of ['requiredExtensions', 'systemLanguage']) {
|
||||
const list = element[attr];
|
||||
assert(list instanceof realm.SVGStringList && element[attr] === list, 'stable native list');
|
||||
list.appendItem('test');
|
||||
assert(element.getAttribute(attr)==='test', 'list writes native attribute');
|
||||
element.setAttribute(attr, 'replacement');
|
||||
assert(list.numberOfItems===1 && list.getItem(0)==='replacement', 'attribute refreshes cached list');
|
||||
element.removeAttribute(attr);
|
||||
assert(list.numberOfItems===0, 'removal clears list');
|
||||
}
|
||||
});
|
||||
check(label+'/target', () => {
|
||||
const svg=doc.createElementNS(ns,'svg'), parent=doc.createElementNS(ns,'g'), target=doc.createElementNS(ns,'rect');
|
||||
target.id='svg-animation-target id'; parent.append(element); svg.append(parent,target);
|
||||
assert(element.targetElement===null, 'detached subtree has no target');
|
||||
(doc.body || doc.documentElement).append(svg);
|
||||
try {
|
||||
assert(element.targetElement===parent, 'parent target');
|
||||
element.setAttributeNS(xlink,'other:href','#svg-animation-target%20id');
|
||||
assert(element.targetElement===target, 'namespaced fallback and decoded fragment');
|
||||
element.setAttribute('href','#missing');
|
||||
assert(element.targetElement===null, 'href takes precedence');
|
||||
element.setAttribute('href','');
|
||||
assert(element.targetElement===parent, 'empty href targets parent');
|
||||
element.removeAttribute('href');
|
||||
assert(element.targetElement===target, 'remove href restores xlink');
|
||||
element.setAttribute('href','https://external-svg.test/#svg-animation-target%20id');
|
||||
assert(element.targetElement===null, 'external URL does not resolve locally');
|
||||
element.setAttribute('href','#svg-animation-target%20id');
|
||||
target.id='renamed'; assert(element.targetElement===null, 'ID mutations refresh target');
|
||||
target.id='svg-animation-target id'; assert(element.targetElement===target, 'target restored');
|
||||
target.remove(); assert(element.targetElement===null, 'removed reference target');
|
||||
element.removeAttribute('href'); element.removeAttributeNS(xlink,'href');
|
||||
assert(element.targetElement===parent, 'fallback parent restored');
|
||||
} finally {svg.remove();}
|
||||
assert(element.targetElement===null, 'removed animation loses target');
|
||||
});
|
||||
check(label+'/handlers', () => {
|
||||
for (const [name,type] of [['onbegin','beginEvent'],['onend','endEvent'],['onrepeat','repeatEvent']]) {
|
||||
const log=[], before=()=>log.push('before'),after=()=>log.push('after');
|
||||
const first=function(e){assert(this===element && e.type===type,'handler receiver and event');log.push('first');return false;};
|
||||
const replacement=function(){log.push('replacement');};
|
||||
assert(element[name]===null,'initial null');
|
||||
element.addEventListener(type,before); element[name]=first; element.addEventListener(type,after); element[name]=replacement;
|
||||
element.dispatchEvent(new realm.Event(type));
|
||||
assert(log.slice().sort().join(',')==='after,before,replacement','replacement invokes current handler exactly once');log.length=0;
|
||||
element.dispatchEvent(new realm.Event(name.slice(2)));
|
||||
assert(log.length===0,'short event name does not invoke SVG handler');
|
||||
element[name]=null;element.dispatchEvent(new realm.Event(type));
|
||||
assert(log.join(',')==='before,after','null clears handler');log.length=0;
|
||||
element[name]=first;const event=new realm.Event(type,{cancelable:true});
|
||||
assert(!element.dispatchEvent(event) && event.defaultPrevented,'return false cancels event');
|
||||
assert(log.slice().sort().join(',')==='after,before,first','reassignment restores the handler exactly once');
|
||||
element[name]=null;element.removeEventListener(type,before);element.removeEventListener(type,after);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
check(owner+'/parser', () => {
|
||||
const svg=realm.document.createElementNS(ns,'svg');
|
||||
svg.innerHTML='<animate/><set/><animateMotion/><animateTransform/>';
|
||||
for (let i=0;i<cases.length;i++) assert(svg.children[i] instanceof realm[cases[i][1]],'HTML SVG parser interface');
|
||||
});
|
||||
check(owner+'/content-handlers', () => {
|
||||
const svg=realm.document.createElementNS(ns,'svg'), element=realm.document.createElementNS(ns,'animate');
|
||||
svg.append(element);realm.document.body.append(svg);
|
||||
try {
|
||||
for(const [name,type] of [['onbegin','beginEvent'],['onend','endEvent'],['onrepeat','repeatEvent']]) {
|
||||
element.setAttribute(name,'this.setAttribute("data-event", event.type)');
|
||||
element.dispatchEvent(new realm.Event(name.slice(2)));
|
||||
assert(!element.hasAttribute('data-event'),'short name does not compile content handler');
|
||||
element.dispatchEvent(new realm.Event(type));
|
||||
assert(element.getAttribute('data-event')===type,'content handler observes canonical event type');
|
||||
element.removeAttribute(name);element.removeAttribute('data-event');
|
||||
element.dispatchEvent(new realm.Event(type));
|
||||
assert(!element.hasAttribute('data-event'),'removal unregisters content handler');
|
||||
}
|
||||
} finally {svg.remove();}
|
||||
});
|
||||
}
|
||||
for(const [sourceName,source] of realms) for(const [ownerName,owner] of realms) {
|
||||
check(sourceName+'->'+ownerName+'/receiver-and-realm', () => {
|
||||
const element=owner.document.createElementNS(ns,'animate');
|
||||
const Base=source.SVGAnimationElement;
|
||||
const revoked=Proxy.revocable(element,{});revoked.revoke();
|
||||
let traps=0,conversions=0;
|
||||
const trap=()=>{traps++;throw Error('trap');};
|
||||
const bad=[{},Base.prototype,Object.create(element),new Proxy(element,{get:trap,getPrototypeOf:trap}),revoked.proxy,owner.document.createElementNS(ns,'rect')];
|
||||
for(const name of ['targetElement','requiredExtensions','systemLanguage','onbegin','onend','onrepeat']) {
|
||||
const descriptor=Object.getOwnPropertyDescriptor(Base.prototype,name);
|
||||
assert(descriptor.enumerable && descriptor.configurable && descriptor.get.length===0,'accessor descriptor');
|
||||
for(const value of bad) {
|
||||
throws(()=>descriptor.get.call(value),source.TypeError);
|
||||
if(descriptor.set) throws(()=>descriptor.set.call(value,()=>{}),source.TypeError);
|
||||
}
|
||||
}
|
||||
for(const [name,length] of [['getStartTime',0],['getCurrentTime',0],['getSimpleDuration',0],['beginElement',0],['beginElementAt',1],['endElement',0],['endElementAt',1]]) {
|
||||
const descriptor=Object.getOwnPropertyDescriptor(Base.prototype,name), method=descriptor.value;
|
||||
assert(descriptor.enumerable && descriptor.configurable && descriptor.writable && method.length===length && method.name===name,'method descriptor');
|
||||
for(const value of bad) throws(()=>method.call(value,{valueOf(){conversions++;return 1;}}),source.TypeError);
|
||||
if(length) {
|
||||
throws(()=>method.call(element),source.TypeError);
|
||||
for(const value of [NaN,Infinity,-Infinity,1e100,Symbol(),1n]) throws(()=>method.call(element,value),source.TypeError);
|
||||
const sentinel={};let caught;
|
||||
try {method.call(element,{valueOf(){throw sentinel;}});} catch(error){caught=error;}
|
||||
assert(caught===sentinel,'conversion exception propagated');
|
||||
}
|
||||
}
|
||||
assert(traps===0 && conversions===0,'brand checked before any author code');
|
||||
assert(Base.prototype.getCurrentTime.call(element)===0,'detached current time');
|
||||
throws(()=>Base.prototype.getStartTime.call(element),source.DOMException,'InvalidStateError');
|
||||
throws(()=>Base.prototype.getSimpleDuration.call(element),source.DOMException,'NotSupportedError');
|
||||
const listGet=Object.getOwnPropertyDescriptor(Base.prototype,'systemLanguage').get;
|
||||
assert(listGet.call(element) instanceof owner.SVGStringList,'list belongs to element realm');
|
||||
const graphicsGet=Object.getOwnPropertyDescriptor(source.SVGGraphicsElement.prototype,'systemLanguage').get;
|
||||
throws(()=>graphicsGet.call(element),source.TypeError);
|
||||
const handler=Object.getOwnPropertyDescriptor(Base.prototype,'onbegin'),callback=()=>{};
|
||||
handler.set.call(element,callback);assert(handler.get.call(element)===callback,'genuine cross-realm receiver');handler.set.call(element,null);
|
||||
});
|
||||
}
|
||||
globalThis.__nodeReplacementResults={rows,failures:rows.filter(row=>!row.pass),passed:rows.filter(row=>row.pass).length,total:rows.length};
|
||||
return rows.every(row=>row.pass);
|
||||
})()
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
(() => {
|
||||
const check = (value, message) => { if (!value) throw new Error(message); };
|
||||
const throws = (callback, name, Constructor = Error) => {
|
||||
let error;
|
||||
try { callback(); } catch (caught) { error = caught; }
|
||||
check(error && error.name === name && error instanceof Constructor, name + ' required');
|
||||
};
|
||||
const ns = 'http://www.w3.org/2000/svg';
|
||||
const xlink = 'http://www.w3.org/1999/xlink';
|
||||
const cases = [
|
||||
['feComponentTransfer', SVGFEComponentTransferElement, ['in1']],
|
||||
['feFlood', SVGFEFloodElement, []],
|
||||
['feImage', SVGFEImageElement, ['href', 'preserveAspectRatio']],
|
||||
['feMerge', SVGFEMergeElement, []],
|
||||
['feMergeNode', SVGFEMergeNodeElement, ['in1']],
|
||||
['feTile', SVGFETileElement, ['in1']],
|
||||
];
|
||||
const documents = [document, document.implementation.createHTMLDocument(),
|
||||
document.implementation.createDocument(ns, 'svg'),
|
||||
new DOMParser().parseFromString('<svg xmlns="' + ns + '"/>', 'image/svg+xml')];
|
||||
const standard = ['x', 'y', 'width', 'height', 'result'];
|
||||
for (const [tag, Constructor, extra] of cases) {
|
||||
check(Constructor.length === 0, tag + ' constructor length');
|
||||
check(Object.getPrototypeOf(Constructor) === SVGElement, tag + ' constructor inheritance');
|
||||
check(Object.getPrototypeOf(Constructor.prototype) === SVGElement.prototype, tag + ' prototype inheritance');
|
||||
throws(() => Constructor(), 'TypeError', TypeError);
|
||||
throws(() => new Constructor(), 'TypeError', TypeError);
|
||||
const attributes = [...(tag === 'feMergeNode' ? [] : standard), ...extra];
|
||||
for (const doc of documents) {
|
||||
const element = doc.createElementNS(ns, tag);
|
||||
check(Object.getPrototypeOf(element) === Constructor.prototype, tag + ' factory prototype');
|
||||
check(Object.prototype.toString.call(element) === '[object ' + Constructor.name + ']', tag + ' native tag');
|
||||
check(element instanceof SVGElement && !(element instanceof SVGGraphicsElement), tag + ' native inheritance');
|
||||
check(Object.getPrototypeOf(element.cloneNode()) === Constructor.prototype, tag + ' clone interface');
|
||||
check(Object.getPrototypeOf(document.importNode(element)) === Constructor.prototype, tag + ' import interface');
|
||||
check(!(doc.createElementNS(ns, tag.toUpperCase()) instanceof Constructor), tag + ' case-sensitive namespace factory');
|
||||
check(!(doc.createElementNS('urn:other', tag) instanceof Constructor), tag + ' namespace-sensitive factory');
|
||||
let traps = 0;
|
||||
const revoked = Proxy.revocable(element, {});revoked.revoke();
|
||||
for (const name of attributes) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(Constructor.prototype, name);
|
||||
check(descriptor && descriptor.enumerable && descriptor.configurable && !descriptor.set, tag + '.' + name + ' readonly descriptor');
|
||||
check(descriptor.get.name === 'get ' + name && descriptor.get.length === 0, tag + '.' + name + ' getter metadata');
|
||||
const value = element[name];
|
||||
check(value === element[name] && !Object.hasOwn(element, name), tag + '.' + name + ' stable prototype accessor');
|
||||
for (const receiver of [{}, Constructor.prototype, Object.create(element),
|
||||
new Proxy(element, {get() {traps++;throw Error('author trap');}}), revoked.proxy,
|
||||
doc.createElementNS(ns, tag === 'feTile' ? 'feFlood' : 'feTile')]) {
|
||||
throws(() => descriptor.get.call(receiver), 'TypeError', TypeError);
|
||||
}
|
||||
}
|
||||
check(traps === 0, tag + ' brand check does not run Proxy traps');
|
||||
if (tag === 'feMergeNode') {
|
||||
for (const name of standard) check(!(name in element), 'merge node has no primitive region');
|
||||
} else {
|
||||
for (const [name, initial] of [['x','0%'],['y','0%'],['width','100%'],['height','100%']]) {
|
||||
const animated = element[name];
|
||||
check(animated instanceof SVGAnimatedLength, tag + '.' + name + ' animated length');
|
||||
check(animated.baseVal.valueAsString === initial && animated.animVal.valueAsString === initial, tag + '.' + name + ' initial');
|
||||
element.setAttribute(name, '12');
|
||||
check(animated.baseVal.value === 12 && animated.animVal.value === 12, tag + '.' + name + ' live attribute reflection');
|
||||
animated.baseVal.valueAsString = '25%';
|
||||
check(element.getAttribute(name) === '25%' && animated.animVal.valueAsString === '25%', tag + '.' + name + ' reflected setter');
|
||||
throws(() => {animated.animVal.value = 1;}, 'NoModificationAllowedError', DOMException);
|
||||
element.removeAttribute(name);
|
||||
check(animated.baseVal.valueAsString === initial, tag + '.' + name + ' removal restores default');
|
||||
}
|
||||
}
|
||||
for (const name of ['result', 'in1'].filter(name => attributes.includes(name))) {
|
||||
const attribute = name === 'in1' ? 'in' : name;
|
||||
const value = element[name];
|
||||
check(value instanceof SVGAnimatedString && value.baseVal === '' && value.animVal === '', tag + '.' + name + ' initial string');
|
||||
element.setAttribute(attribute, 'SourceGraphic');
|
||||
check(value.baseVal === 'SourceGraphic' && value.animVal === 'SourceGraphic', tag + '.' + name + ' cached reflection');
|
||||
let conversions = 0;
|
||||
value.baseVal = {toString() {conversions++;return 'SourceAlpha';}};
|
||||
check(conversions === 1 && element.getAttribute(attribute) === 'SourceAlpha' && value.animVal === 'SourceAlpha', tag + '.' + name + ' native mutation');
|
||||
element.removeAttribute(attribute);
|
||||
check(value.baseVal === '' && value.animVal === '', tag + '.' + name + ' string removal');
|
||||
}
|
||||
if (tag === 'feImage') {
|
||||
const href = element.href;
|
||||
element.setAttributeNS(xlink, 'xlink:href', '#fallback');
|
||||
check(href.baseVal === '#fallback', 'xlink href fallback');
|
||||
href.baseVal = '#changed';
|
||||
check(element.getAttributeNS(xlink, 'href') === '#changed' && !element.hasAttribute('href'), 'setter reflects existing xlink href');
|
||||
element.setAttribute('href', '#preferred');
|
||||
check(href.animVal === '#preferred', 'unprefixed href has priority');
|
||||
href.baseVal = '#updated';
|
||||
check(element.getAttribute('href') === '#updated' && element.getAttributeNS(xlink, 'href') === '#changed', 'setter follows href precedence');
|
||||
element.removeAttribute('href');
|
||||
check(href.baseVal === '#changed', 'removal restores xlink fallback');
|
||||
const ratio = element.preserveAspectRatio;
|
||||
check(ratio instanceof SVGAnimatedPreserveAspectRatio && ratio.baseVal.align === 6 && ratio.baseVal.meetOrSlice === 1, 'initial preserveAspectRatio');
|
||||
element.setAttribute('preserveAspectRatio', 'xMaxYMax slice');
|
||||
check(ratio.baseVal.align === 10 && ratio.animVal.meetOrSlice === 2, 'preserveAspectRatio live reflection');
|
||||
ratio.baseVal.align = 2;
|
||||
check(element.getAttribute('preserveAspectRatio') === 'xMinYMin slice', 'preserveAspectRatio reflected setter');
|
||||
throws(() => {ratio.animVal.align = 3;}, 'NoModificationAllowedError', DOMException);
|
||||
}
|
||||
}
|
||||
}
|
||||
const parsed = new DOMParser().parseFromString('<svg xmlns="' + ns + '">' + cases.map(([tag]) => '<' + tag + '/>').join('') + '</svg>', 'image/svg+xml');
|
||||
const html = document.createElement('div');
|
||||
html.innerHTML = '<svg>' + cases.map(([tag]) => '<' + tag.toLowerCase() + '/>').join('') + '</svg>';
|
||||
for (const [i, [, Constructor]] of cases.entries()) {
|
||||
check(parsed.documentElement.children[i] instanceof Constructor, 'XML parser interface ' + Constructor.name + ': ' + Object.prototype.toString.call(parsed.documentElement.children[i]) + ' ' + parsed.documentElement.innerHTML);
|
||||
check(html.firstChild.children[i] instanceof Constructor, 'HTML foreign-content case adjustment');
|
||||
}
|
||||
const frame = document.createElement('iframe');document.body.append(frame);
|
||||
const other = frame.contentWindow;
|
||||
for (const [tag, Constructor, extra] of cases) {
|
||||
const foreign = other.document.createElementNS(ns, tag);
|
||||
const attributes = [...(tag === 'feMergeNode' ? [] : standard), ...extra];
|
||||
for (const name of attributes) {
|
||||
const getter = Object.getOwnPropertyDescriptor(Constructor.prototype, name).get;
|
||||
const otherGetter = Object.getOwnPropertyDescriptor(other[Constructor.name].prototype, name).get;
|
||||
const value = getter.call(foreign);
|
||||
const ValueConstructor = ['x','y','width','height'].includes(name) ? 'SVGAnimatedLength' : name === 'preserveAspectRatio' ? 'SVGAnimatedPreserveAspectRatio' : 'SVGAnimatedString';
|
||||
check(value instanceof other[ValueConstructor] && !(value instanceof window[ValueConstructor]), tag + '.' + name + ' owner realm');
|
||||
throws(() => otherGetter.call({}), 'TypeError', other.TypeError);
|
||||
}
|
||||
}
|
||||
const kept = other.document.createElementNS(ns, 'feTile');
|
||||
const input = kept.in1;
|
||||
frame.remove();
|
||||
input.baseVal = 'after-removal';
|
||||
check(kept.getAttribute('in') === 'after-removal', 'retained filter value survives iframe removal');
|
||||
return true;
|
||||
})()
|
||||
@@ -0,0 +1,119 @@
|
||||
(() => {
|
||||
const check = (ok, message) => { if (!ok) throw new Error(message); };
|
||||
const throws = (run, Constructor = TypeError) => {
|
||||
let error;
|
||||
try { run(); } catch (caught) { error = caught; }
|
||||
check(error instanceof Constructor, 'expected ' + Constructor.name + ', got ' + error);
|
||||
};
|
||||
const ns = 'http://www.w3.org/2000/svg';
|
||||
const xlink = 'http://www.w3.org/1999/xlink';
|
||||
const cases = [
|
||||
['switch', SVGSwitchElement, SVGGraphicsElement],
|
||||
['mpath', SVGMPathElement, SVGElement],
|
||||
];
|
||||
const documents = [document, document.implementation.createHTMLDocument(),
|
||||
document.implementation.createDocument(ns, 'svg'),
|
||||
new DOMParser().parseFromString('<svg xmlns="' + ns + '"/>', 'image/svg+xml')];
|
||||
for (const [tag, Constructor, Parent] of cases) {
|
||||
check(Constructor.length === 0 && Constructor.name === 'SVG' + (tag === 'switch' ? 'Switch' : 'MPath') + 'Element', tag + ' constructor metadata');
|
||||
check(Object.getPrototypeOf(Constructor) === Parent, tag + ' constructor inheritance');
|
||||
check(Object.getPrototypeOf(Constructor.prototype) === Parent.prototype, tag + ' prototype inheritance');
|
||||
throws(() => Constructor());
|
||||
throws(() => new Constructor());
|
||||
for (const doc of documents) {
|
||||
const element = doc.createElementNS(ns, 's:' + tag);
|
||||
check(Object.getPrototypeOf(element) === Constructor.prototype, tag + ' factory prototype');
|
||||
check(element instanceof Parent && element instanceof SVGElement, tag + ' native inheritance');
|
||||
check((element instanceof SVGGraphicsElement) === (tag === 'switch'), tag + ' graphics identity');
|
||||
check(Object.prototype.toString.call(element) === '[object ' + Constructor.name + ']', tag + ' native tag');
|
||||
check(element.prefix === 's' && element.localName === tag, tag + ' qualified name');
|
||||
check(Object.getPrototypeOf(element.cloneNode(true)) === Constructor.prototype, tag + ' clone');
|
||||
check(Object.getPrototypeOf(document.importNode(element, true)) === Constructor.prototype, tag + ' import');
|
||||
check(!(doc.createElementNS(ns, tag.toUpperCase()) instanceof Constructor), tag + ' case-sensitive factory');
|
||||
check(!(doc.createElementNS('urn:other', tag) instanceof Constructor), tag + ' namespace-sensitive factory');
|
||||
check(!(doc.createElement(tag) instanceof Constructor), tag + ' HTML factory');
|
||||
let events = 0;
|
||||
element.addEventListener('check', () => events++);
|
||||
element.dispatchEvent(new Event('check'));
|
||||
check(events === 1, tag + ' inherited EventTarget brand');
|
||||
if (tag === 'switch') {
|
||||
element.setAttribute('transform', 'translate(4 7)');
|
||||
check(element.transform.baseVal.getItem(0).matrix.e === 4, 'inherited live transform');
|
||||
element.systemLanguage.appendItem('en');
|
||||
check(element.getAttribute('systemLanguage') === 'en', 'inherited SVGTests list');
|
||||
check(!('href' in element), 'switch has no URI-reference mixin');
|
||||
continue;
|
||||
}
|
||||
const descriptor = Object.getOwnPropertyDescriptor(Constructor.prototype, 'href');
|
||||
check(descriptor.enumerable && descriptor.configurable && !descriptor.set &&
|
||||
descriptor.get.name === 'get href' && descriptor.get.length === 0, 'href descriptor');
|
||||
const href = element.href;
|
||||
check(href instanceof SVGAnimatedString && href === element.href && !Object.hasOwn(element, 'href'), 'cached href object');
|
||||
check(href.baseVal === '' && href.animVal === '', 'missing href');
|
||||
const observer = new MutationObserver(() => {});
|
||||
observer.observe(element, {attributes: true, attributeOldValue: true});
|
||||
element.setAttributeNS(xlink, 'xlink:href', '#fallback');
|
||||
check(href.baseVal === '#fallback', 'xlink fallback');
|
||||
href.baseVal = '#changed';
|
||||
check(element.getAttributeNS(xlink, 'href') === '#changed' && !element.hasAttribute('href'), 'write existing xlink');
|
||||
element.setAttribute('href', '');
|
||||
check(href.animVal === '', 'empty href overrides xlink');
|
||||
let conversions = 0;
|
||||
href.baseVal = {toString() { conversions++; return '#preferred'; }};
|
||||
check(conversions === 1 && href.baseVal === '#preferred' && element.getAttribute('href') === '#preferred', 'convert and reflect href');
|
||||
check(element.getAttributeNS(xlink, 'href') === '#changed', 'preserve fallback attribute');
|
||||
const records = observer.takeRecords();
|
||||
check(records.length === 4 && records[1].attributeNamespace === xlink &&
|
||||
records[1].oldValue === '#fallback' && records[3].attributeNamespace === null &&
|
||||
records[3].oldValue === '', 'native mutation records');
|
||||
observer.disconnect();
|
||||
element.setAttribute('href', '#before-after');
|
||||
check(href.baseVal === '#before-after' && href.animVal === '#before-after', 'native attribute read');
|
||||
element.removeAttribute('href');
|
||||
check(href.baseVal === '#changed', 'removal reveals xlink');
|
||||
element.removeAttributeNS(xlink, 'href');
|
||||
check(href.baseVal === '' && href.animVal === '', 'remove both');
|
||||
check(!Reflect.set(href, 'animVal', '#ignored') && href.animVal === '', 'readonly animated value');
|
||||
const sentinel = {};
|
||||
try { href.baseVal = {toString() {throw sentinel;}}; throw Error('missing conversion exception'); }
|
||||
catch (error) {check(error === sentinel && !element.hasAttribute('href'), 'conversion exception leaves native state unchanged');}
|
||||
let traps = 0;
|
||||
const revoked = Proxy.revocable(element, {}); revoked.revoke();
|
||||
for (const receiver of [{}, Constructor.prototype, Object.create(element),
|
||||
new Proxy(element, {get() {traps++; throw Error('author trap');}}), revoked.proxy,
|
||||
doc.createElementNS(ns, 'use'), doc.createElementNS(ns, 'switch')]) {
|
||||
throws(() => descriptor.get.call(receiver));
|
||||
}
|
||||
check(traps === 0, 'brand check does not run author Proxy traps');
|
||||
Object.setPrototypeOf(element, null);
|
||||
check(descriptor.get.call(element) === href, 'native identity survives prototype mutation');
|
||||
href.baseVal = '#native';
|
||||
check(Element.prototype.getAttribute.call(element, 'href') === '#native', 'native mutation survives prototype mutation');
|
||||
}
|
||||
}
|
||||
const markup = '<svg xmlns="' + ns + '"><switch/><mpath href="#path"/></svg>';
|
||||
const xml = new DOMParser().parseFromString(markup, 'image/svg+xml');
|
||||
const html = document.createElement('div'); html.innerHTML = markup;
|
||||
for (const parent of [xml.documentElement, html.firstChild]) {
|
||||
check(parent.children[0] instanceof SVGSwitchElement && parent.children[1] instanceof SVGMPathElement, 'parser interfaces');
|
||||
check(parent.children[1].href.baseVal === '#path', 'parser href');
|
||||
}
|
||||
const frame = document.createElement('iframe'); document.body.append(frame);
|
||||
const other = frame.contentWindow;
|
||||
const foreign = other.document.createElementNS(ns, 'mpath');
|
||||
const getHref = Object.getOwnPropertyDescriptor(SVGMPathElement.prototype, 'href').get;
|
||||
const otherGetHref = Object.getOwnPropertyDescriptor(other.SVGMPathElement.prototype, 'href').get;
|
||||
const foreignHref = getHref.call(foreign);
|
||||
check(foreignHref instanceof other.SVGAnimatedString && !(foreignHref instanceof SVGAnimatedString), 'cached value belongs to element realm');
|
||||
check(foreign.href === foreignHref, 'cross-realm SameObject');
|
||||
throws(() => otherGetHref.call({}), other.TypeError);
|
||||
throws(() => otherGetHref.call(document.createElementNS(ns, 'use')), other.TypeError);
|
||||
const local = document.createElementNS(ns, 'mpath');
|
||||
check(otherGetHref.call(local) instanceof SVGAnimatedString, 'foreign accessor with local element');
|
||||
const imported = document.importNode(foreign, true);
|
||||
check(imported instanceof SVGMPathElement && !(imported instanceof other.SVGMPathElement), 'import selects destination realm');
|
||||
frame.remove();
|
||||
foreignHref.baseVal = '#retained';
|
||||
check(foreign.getAttribute('href') === '#retained', 'retained value after iframe removal');
|
||||
return true;
|
||||
})()
|
||||
@@ -394,21 +394,56 @@ interfaces! {
|
||||
ResizeObserverSize;
|
||||
Response;
|
||||
SVGAElement: SVGGraphicsElement;
|
||||
SVGAnimatedEnumeration;
|
||||
SVGUnitTypes;
|
||||
SVGAnimatedLength;
|
||||
SVGAnimatedLengthList;
|
||||
SVGAnimatedNumber;
|
||||
SVGAnimatedNumberList;
|
||||
SVGAnimatedTransformList;
|
||||
SVGAnimationElement: SVGElement;
|
||||
SVGAnimateElement: SVGAnimationElement;
|
||||
SVGAnimateMotionElement: SVGAnimationElement;
|
||||
SVGAnimateTransformElement: SVGAnimationElement;
|
||||
SVGSetElement: SVGAnimationElement;
|
||||
SVGAnimatedEnumeration;
|
||||
SVGUnitTypes;
|
||||
SVGAnimatedString;
|
||||
SVGAnimatedLength;
|
||||
SVGAnimatedLengthList;
|
||||
SVGAnimatedBoolean;
|
||||
SVGFEConvolveMatrixElement: SVGElement;
|
||||
SVGAngle;
|
||||
SVGAnimatedAngle;
|
||||
SVGMarkerElement: SVGElement;
|
||||
SVGAnimatedInteger;
|
||||
SVGAnimatedRect;
|
||||
SVGPreserveAspectRatio;
|
||||
SVGAnimatedPreserveAspectRatio;
|
||||
SVGAnimatedNumber;
|
||||
SVGAnimatedNumberList;
|
||||
SVGAnimatedTransformList;
|
||||
SVGCircleElement: SVGGeometryElement;
|
||||
SVGClipPathElement: SVGElement;
|
||||
SVGFilterElement: SVGElement;
|
||||
SVGMaskElement: SVGElement;
|
||||
SVGViewElement: SVGElement;
|
||||
SVGDefsElement: SVGGraphicsElement;
|
||||
SVGDescElement: SVGElement;
|
||||
SVGComponentTransferFunctionElement: SVGElement;
|
||||
SVGFEFuncAElement: SVGComponentTransferFunctionElement;
|
||||
SVGFEFuncBElement: SVGComponentTransferFunctionElement;
|
||||
SVGFEFuncGElement: SVGComponentTransferFunctionElement;
|
||||
SVGFEFuncRElement: SVGComponentTransferFunctionElement;
|
||||
SVGFEBlendElement: SVGElement;
|
||||
SVGFEColorMatrixElement: SVGElement;
|
||||
SVGFECompositeElement: SVGElement;
|
||||
SVGFEDisplacementMapElement: SVGElement;
|
||||
SVGFEMorphologyElement: SVGElement;
|
||||
SVGFETurbulenceElement: SVGElement;
|
||||
SVGTextPathElement: SVGTextContentElement;
|
||||
SVGFEDiffuseLightingElement: SVGElement;
|
||||
SVGFEDistantLightElement: SVGElement;
|
||||
SVGFEDropShadowElement: SVGElement;
|
||||
SVGFEGaussianBlurElement: SVGElement;
|
||||
SVGFEOffsetElement: SVGElement;
|
||||
SVGFEPointLightElement: SVGElement;
|
||||
SVGFESpecularLightingElement: SVGElement;
|
||||
SVGFESpotLightElement: SVGElement;
|
||||
SVGStopElement: SVGElement;
|
||||
SVGElement: Element;
|
||||
SVGEllipseElement: SVGGeometryElement;
|
||||
SVGFEComponentTransferElement: SVGElement;
|
||||
@@ -429,6 +464,7 @@ interfaces! {
|
||||
SVGLinearGradientElement: SVGGradientElement;
|
||||
SVGMatrix;
|
||||
SVGMetadataElement: SVGElement;
|
||||
SVGMPathElement: SVGElement;
|
||||
SVGNumber;
|
||||
SVGNumberList;
|
||||
SVGPathElement: SVGGeometryElement;
|
||||
@@ -442,13 +478,14 @@ interfaces! {
|
||||
SVGScriptElement: SVGElement;
|
||||
SVGStyleElement: SVGElement;
|
||||
SVGSwitchElement: SVGGraphicsElement;
|
||||
SVGMPathElement: SVGElement;
|
||||
SVGSymbolElement: SVGGraphicsElement;
|
||||
SVGTSpanElement: SVGTextPositioningElement;
|
||||
SVGTextContentElement: SVGGraphicsElement;
|
||||
SVGTextElement: SVGTextPositioningElement;
|
||||
SVGTextPositioningElement: SVGTextContentElement;
|
||||
SVGTitleElement: SVGElement;
|
||||
SVGPointList;
|
||||
SVGStringList;
|
||||
SVGTransform;
|
||||
SVGTransformList;
|
||||
SVGUseElement: SVGGraphicsElement;
|
||||
|
||||
+159
-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> {
|
||||
@@ -82,6 +106,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,
|
||||
@@ -99,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)
|
||||
}
|
||||
|
||||
+32
-2
@@ -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,8 +27,8 @@ 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,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -50,6 +51,23 @@ mod tests {
|
||||
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(),
|
||||
@@ -420,6 +438,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