fix(geometry): implement DOMQuad

This commit is contained in:
ldm0
2026-10-02 03:27:49 +08:00
parent 1f87ef36e5
commit d36e282361
11 changed files with 520 additions and 12 deletions
@@ -2468,12 +2468,7 @@ css/cssom/getComputedStyle-layout-dependent-replaced-into-ib-split.html
css/cssom/idlharness.html
css/cssom/style-sheet-interfaces-001.html
css/geometry/DOMMatrix2DInit-validate-fixup.html
css/geometry/DOMQuad-001.html
css/geometry/DOMQuad-002.html
css/geometry/DOMQuad-nan.html
css/geometry/DOMRectList.html
css/geometry/historical.html
css/geometry/spec-examples.html
css/geometry/structured-serialization.html
css/mediaqueries/at-custom-media-cssom.html
css/mediaqueries/at-custom-media-parsing.html
@@ -3356,10 +3356,15 @@ css/geometry/DOMMatrix-stringifier.html
css/geometry/DOMMatrixInit-validate-fixup.html
css/geometry/DOMPoint-001.html
css/geometry/DOMPoint-002.html
css/geometry/DOMQuad-001.html
css/geometry/DOMQuad-002.html
css/geometry/DOMQuad-nan.html
css/geometry/DOMRect-001.html
css/geometry/DOMRect-002.html
css/geometry/DOMRect-nan.html
css/geometry/WebKitCSSMatrix.html
css/geometry/historical.html
css/geometry/spec-examples.html
css/mediaqueries/aspect-ratio-serialization.html
css/mediaqueries/duplicate-media-stylesheet-crash.html
css/mediaqueries/dynamic-range.html
@@ -13,6 +13,7 @@ mod crypto;
mod css_fontface_runtime;
mod css_runtime;
pub(crate) mod css_stylesheet_runtime;
mod dom_quad;
mod dom_rect;
mod event_document;
mod event_legacy;
@@ -12,6 +12,7 @@ use super::super::{
css_fontface_runtime::font_face_constructor_callback,
css_runtime::{css_keyword_value_constructor_callback, css_unit_value_constructor_callback},
css_stylesheet_runtime::css_style_sheet_constructor_callback,
dom_quad::dom_quad_constructor_callback,
events::{
EventSubclassKind, ValueEventKind, build_event_subclass_template,
build_value_event_template, event_constructor_callback,
@@ -527,6 +528,14 @@ pub(in crate::context_bootstrap) fn build_constructor_template<'s>(
.length(0)
.build(scope)
}
ConstructorKind::DomQuad => {
v8::FunctionTemplate::builder(moli_webapi_declare::web_api_constructor!(
web_api_interfaces::DOMQuad,
dom_quad_constructor_callback
))
.length(0)
.build(scope)
}
ConstructorKind::DomMatrix => {
v8::FunctionTemplate::builder(dom_matrix_constructor_callback)
.length(0)
@@ -14,6 +14,7 @@ use super::super::{
crypto::install_crypto_template_bindings,
css_runtime::install_css_typed_om_template_bindings,
css_stylesheet_runtime::install_css_stylesheet_template_bindings,
dom_quad::install_dom_quad_template_bindings,
dom_rect::install_dom_rect_template_bindings,
event_template::install_event_template_bindings,
file_api::install_file_api_template_bindings,
@@ -408,6 +409,7 @@ pub(super) fn install_constructor_template_bindings<'s>(
install_css_style_declaration_template_accessors(scope, template, spec.interface.name());
install_abort_template_bindings(scope, template, spec.interface.name());
install_attr_template_bindings(scope, template, spec.interface.name());
install_dom_quad_template_bindings(scope, template, spec.interface.name());
install_dom_rect_template_bindings(scope, template, spec.interface.name());
install_dom_exception_template_bindings(scope, template, spec.interface.name());
install_dom_implementation_template_bindings(scope, template, spec.interface.name());
@@ -0,0 +1,346 @@
use super::*;
use crate::{
util::{callback_data_index_value, get_private_object},
web_api_interfaces, webidl,
};
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
const DOM_QUAD_P1_SLOT: &str = "__moliDomQuadP1";
const DOM_QUAD_P2_SLOT: &str = "__moliDomQuadP2";
const DOM_QUAD_P3_SLOT: &str = "__moliDomQuadP3";
const DOM_QUAD_P4_SLOT: &str = "__moliDomQuadP4";
#[derive(WebApiObject)]
#[webapi(interface = web_api_interfaces::DOMQuad, fallback_to_string_tag = "DOMQuad")]
struct DomQuadObjectDeclaration<'scope> {
#[webapi(slot = DOM_QUAD_P1_SLOT)]
p1: v8::Local<'scope, v8::Object>,
#[webapi(slot = DOM_QUAD_P2_SLOT)]
p2: v8::Local<'scope, v8::Object>,
#[webapi(slot = DOM_QUAD_P3_SLOT)]
p3: v8::Local<'scope, v8::Object>,
#[webapi(slot = DOM_QUAD_P4_SLOT)]
p4: v8::Local<'scope, v8::Object>,
}
#[derive(WebApiObject)]
#[webapi(plain, data_properties, enumerable)]
struct DomQuadJsonDeclaration<'scope> {
p1: v8::Local<'scope, v8::Object>,
p2: v8::Local<'scope, v8::Object>,
p3: v8::Local<'scope, v8::Object>,
p4: v8::Local<'scope, v8::Object>,
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::DOMQuad)]
struct DomQuadPrototypeDeclaration {
#[webapi(
accessor_property,
getter = dom_quad_point_getter_callback,
data = callback_data_index_value(scope, 0),
enumerable
)]
p1: (),
#[webapi(
accessor_property,
getter = dom_quad_point_getter_callback,
data = callback_data_index_value(scope, 1),
enumerable
)]
p2: (),
#[webapi(
accessor_property,
getter = dom_quad_point_getter_callback,
data = callback_data_index_value(scope, 2),
enumerable
)]
p3: (),
#[webapi(
accessor_property,
getter = dom_quad_point_getter_callback,
data = callback_data_index_value(scope, 3),
enumerable
)]
p4: (),
#[webapi(
method = "getBounds",
callback = dom_quad_get_bounds_callback,
length = 0,
enumerable
)]
get_bounds: (),
#[webapi(
method = "toJSON",
callback = dom_quad_to_json_callback,
length = 0,
enumerable
)]
to_json: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::DOMQuad)]
struct DomQuadConstructorDeclaration {
#[webapi(
static_method = "fromRect",
callback = dom_quad_from_rect_callback,
length = 0
)]
from_rect: (),
#[webapi(
static_method = "fromQuad",
callback = dom_quad_from_quad_callback,
length = 0
)]
from_quad: (),
}
#[derive(Default, webidl::WebIdlDictionary)]
#[webidl(prefix = "DOMQuadInit")]
struct DomQuadInit {
#[webidl(with = optional_dom_point_member)]
p1: Option<geometry_runtime::DomPointInit>,
#[webidl(with = optional_dom_point_member)]
p2: Option<geometry_runtime::DomPointInit>,
#[webidl(with = optional_dom_point_member)]
p3: Option<geometry_runtime::DomPointInit>,
#[webidl(with = optional_dom_point_member)]
p4: Option<geometry_runtime::DomPointInit>,
}
pub(super) fn dom_quad_constructor_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !args.is_construct_call() {
throw_type_error(
scope,
"Failed to construct 'DOMQuad': Please use the 'new' operator.",
);
return;
}
let mut points = [geometry_runtime::DomPointInit::default(); 4];
for (index, point) in points.iter_mut().enumerate() {
let Some(init) =
geometry_runtime::optional_dom_point_init_arg(scope, &args, index as i32, "DOMQuad")
else {
return;
};
*point = init;
}
let [p1, p2, p3, p4] = points.map(|point| build_dom_point(scope, point));
DomQuadObjectDeclaration::new(p1, p2, p3, p4)
.initialize(scope, args.this())
.expect("DOMQuad declaration should initialize object");
rv.set(args.this().into());
}
pub(in crate::context_bootstrap) fn install_dom_quad_template_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
interface_name: &str,
) {
if interface_name != "DOMQuad" {
return;
}
DomQuadConstructorDeclaration::initialize_template(scope, template);
DomQuadPrototypeDeclaration::initialize_prototype_template(
scope,
template.prototype_template(scope),
);
}
fn dom_quad_from_rect_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(rect) = dom_rect::dom_rect_init_arg(scope, &args, "DOMQuad.fromRect") else {
return;
};
let p1 = point(rect.x, rect.y);
let p2 = point(rect.x + rect.width, rect.y);
let p3 = point(rect.x + rect.width, rect.y + rect.height);
let p4 = point(rect.x, rect.y + rect.height);
rv.set(build_dom_quad(scope, [p1, p2, p3, p4]).into());
}
fn dom_quad_from_quad_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(init) = dom_quad_init_arg(scope, &args) else {
return;
};
rv.set(
build_dom_quad(
scope,
[
init.p1.unwrap_or_default(),
init.p2.unwrap_or_default(),
init.p3.unwrap_or_default(),
init.p4.unwrap_or_default(),
],
)
.into(),
);
}
fn dom_quad_point_getter_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(slot) = callback_data_item(scope, &args, DOM_QUAD_POINT_SLOTS, "DOMQuad point slots")
else {
rv.set_undefined();
return;
};
if !dom_quad_receiver_branded(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
let Some(point) = get_private_object(scope, args.this(), slot) else {
rv.set_undefined();
return;
};
rv.set(point.into());
}
fn dom_quad_get_bounds_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(points) = dom_quad_points(scope, args.this()) else {
throw_type_error(scope, "Illegal invocation");
return;
};
let points = points.map(|point| geometry_runtime::dom_point_init_from_object(scope, point));
let (x, width) = coordinate_bounds(points.map(|point| point.x));
let (y, height) = coordinate_bounds(points.map(|point| point.y));
rv.set(dom_rect::build_dom_rect_object(scope, x, y, width, height).into());
}
fn dom_quad_to_json_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some([p1, p2, p3, p4]) = dom_quad_points(scope, args.this()) else {
throw_type_error(scope, "Illegal invocation");
return;
};
let Ok(object) = DomQuadJsonDeclaration { p1, p2, p3, p4 }.bind(scope) else {
return;
};
rv.set(object.into());
}
fn build_dom_quad<'s>(
scope: &mut v8::PinScope<'s, '_>,
points: [geometry_runtime::DomPointInit; 4],
) -> v8::Local<'s, v8::Object> {
let [p1, p2, p3, p4] = points.map(|point| build_dom_point(scope, point));
DomQuadObjectDeclaration::new(p1, p2, p3, p4)
.bind(scope)
.expect("DOMQuad declaration should bind")
}
fn build_dom_point<'s>(
scope: &mut v8::PinScope<'s, '_>,
point: geometry_runtime::DomPointInit,
) -> v8::Local<'s, v8::Object> {
geometry_runtime::build_dom_point_object(scope, point.x, point.y, point.z, point.w)
}
fn point(x: f64, y: f64) -> geometry_runtime::DomPointInit {
geometry_runtime::DomPointInit {
x,
y,
..Default::default()
}
}
fn dom_quad_init_arg<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
) -> Option<DomQuadInit> {
if args.length() == 0 || args.get(0).is_undefined() {
return Some(DomQuadInit::default());
}
match webidl::parse_dictionary::<DomQuadInit>(
scope,
args.get(0),
webidl::Context::argument("DOMQuad.fromQuad", 1),
) {
Ok(Some(init)) => Some(init),
Ok(None) => Some(DomQuadInit::default()),
Err(error) => {
webidl::throw_error(scope, &error);
None
}
}
}
fn optional_dom_point_member<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
name: &'static str,
) -> Result<Option<geometry_runtime::DomPointInit>, webidl::WebIdlError> {
let context = webidl::Context::member("DOMQuadInit", name);
let Some(value) = webidl::property_result(scope, object, name, context)? else {
return Ok(None);
};
if value.is_undefined() {
return Ok(None);
}
webidl::parse_dictionary::<geometry_runtime::DomPointInit>(scope, value, context)
.map(|init| Some(init.unwrap_or_default()))
}
fn dom_quad_points<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<[v8::Local<'s, v8::Object>; 4]> {
if !dom_quad_receiver_branded(scope, receiver) {
return None;
}
Some([
get_private_object(scope, receiver, DOM_QUAD_P1_SLOT)?,
get_private_object(scope, receiver, DOM_QUAD_P2_SLOT)?,
get_private_object(scope, receiver, DOM_QUAD_P3_SLOT)?,
get_private_object(scope, receiver, DOM_QUAD_P4_SLOT)?,
])
}
fn dom_quad_receiver_branded<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> bool {
web_api_interfaces::DOMQuad::is_instance(scope, receiver)
}
fn coordinate_bounds(values: [f64; 4]) -> (f64, f64) {
if values.iter().any(|value| value.is_nan()) {
return (f64::NAN, f64::NAN);
}
let minimum = values.into_iter().fold(f64::INFINITY, f64::min);
let maximum = values.into_iter().fold(f64::NEG_INFINITY, f64::max);
(minimum, maximum - minimum)
}
const DOM_QUAD_POINT_SLOTS: &[&str] = &[
DOM_QUAD_P1_SLOT,
DOM_QUAD_P2_SLOT,
DOM_QUAD_P3_SLOT,
DOM_QUAD_P4_SLOT,
];
@@ -214,15 +214,15 @@ struct DomRectReadOnlyConstructorArgs {
#[derive(Clone, Copy, Default, webidl::WebIdlDictionary)]
#[webidl(prefix = "DOMRectInit")]
struct DomRectInit {
pub(super) struct DomRectInit {
#[webidl(default = 0.0)]
x: f64,
pub(super) x: f64,
#[webidl(default = 0.0)]
y: f64,
pub(super) y: f64,
#[webidl(default = 0.0)]
width: f64,
pub(super) width: f64,
#[webidl(default = 0.0)]
height: f64,
pub(super) height: f64,
}
pub(super) fn dom_rect_readonly_constructor_callback<'s>(
@@ -368,7 +368,7 @@ fn dom_rect_from_rect_callback<'s>(
rv.set(build_dom_rect_object(scope, init.x, init.y, init.width, init.height).into());
}
fn dom_rect_init_arg<'s>(
pub(super) fn dom_rect_init_arg<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
prefix: &'static str,
@@ -1156,6 +1156,18 @@ fn dom_point_slot<'s>(
.unwrap_or(default)
}
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),
}
}
fn dom_point_receiver_branded<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
@@ -1097,7 +1097,7 @@ const CONSTRUCTOR_SPECS_AFTER_STREAMS: &[ConstructorSpec] = &[
},
ConstructorSpec {
interface: web_api_interfaces::DOMQuad::DESCRIPTOR,
kind: ConstructorKind::Illegal,
kind: ConstructorKind::DomQuad,
},
ConstructorSpec {
interface: web_api_interfaces::DOMMatrixReadOnly::DESCRIPTOR,
@@ -115,6 +115,7 @@ pub(in crate::context_bootstrap) enum ConstructorKind {
DomRect,
DomPointReadOnly,
DomPoint,
DomQuad,
DomMatrix,
ResizeObserver,
PerformanceObserver,
@@ -518,3 +518,140 @@ fn dom_point_matrix_transform_validates_matrix_init_and_returns_in_the_function_
"0|true|false|30,52,12,10|TypeError|TypeError|no throw|no throw"
);
}
#[test]
fn dom_quad_uses_live_same_object_points_and_geometry_dictionary_factories() {
let mut vm = new_storage_test_vm("https://domquad-geometry.test/");
let result = vm
.eval(
r#"
(() => {
const probe = callback => {
try {
return String(callback());
} catch (error) {
return error.name;
}
};
const q = new DOMQuad(
{x: 1, y: 2, z: 3, w: 4},
{x: 11, y: 2},
{x: 11, y: 22},
{x: 1, y: 22}
);
const initial = [q.p1.x, q.p1.y, q.p1.z, q.p1.w,
q.getBounds().x, q.getBounds().y, q.getBounds().width,
q.getBounds().height].join(",");
const copy = DOMQuad.fromQuad(q);
const copyInitial = [copy.p1.x, copy.p1.y, copy.p1.z, copy.p1.w,
copy.p4.x, copy.p4.y].join(",");
const originalP1 = q.p1;
q.p1.x = -5;
q.p4.y = 30;
q.p1 = new DOMPoint(999, 999);
const live = [q.p1 === originalP1, q.p1.x, q.p4.y,
q.getBounds().x, q.getBounds().y, q.getBounds().width,
q.getBounds().height].join(",");
const copyAfterMutation = [copy.p1.x, copy.p4.y, copy.p1 !== q.p1].join(",");
const negative = DOMQuad.fromRect({x: 10, y: 20, width: -100, height: -200});
const negativeValues = [negative.p1.x, negative.p1.y, negative.p2.x,
negative.p3.y, negative.getBounds().x, negative.getBounds().y,
negative.getBounds().width, negative.getBounds().height].join(",");
const nanBounds = new DOMQuad({x: 0, y: 0}, {x: 0, y: 0},
{x: NaN, y: 0}, {x: 0, y: 0}).getBounds();
const nanValues = [nanBounds.x, nanBounds.y, nanBounds.width,
nanBounds.height].join(",");
const json = q.toJSON();
const pointDescriptor = Object.getOwnPropertyDescriptor(DOMQuad.prototype, "p1");
const fake = Object.assign(Object.create(DOMQuad.prototype), {
__moliDomQuadBrand: true,
__moliDomQuadP1: new DOMPoint()
});
return JSON.stringify({
interfaceShape: [DOMQuad.length, DOMQuad.fromRect.length,
DOMQuad.fromQuad.length, q.getBounds.length, q.toJSON.length,
Object.prototype.toString.call(q), "bounds" in q].join("|"),
prototypeKeys: Object.keys(DOMQuad.prototype).join(","),
pointDescriptor: [typeof pointDescriptor.get, pointDescriptor.get.name,
pointDescriptor.get.length, pointDescriptor.set === undefined,
pointDescriptor.enumerable, pointDescriptor.configurable].join("|"),
sameObject: q.p1 === q.p1,
initial,
live,
copyInitial,
copyAfterMutation,
negativeValues,
nanValues,
jsonShape: [Object.getPrototypeOf(json) === Object.prototype,
Object.keys(json).join(","), json.p1 === q.p1,
JSON.stringify(json)].join("|"),
forged: [probe(() => pointDescriptor.get.call(fake)),
probe(() => DOMQuad.prototype.getBounds.call(fake)),
probe(() => DOMQuad.prototype.toJSON.call(fake))].join(","),
visibleSlots: Object.getOwnPropertyNames(q)
.filter(name => name.startsWith("__moliDomQuad")).join(",")
});
})()
"#,
)
.expect("DOMQuad geometry probe should evaluate");
assert_eq!(
result,
concat!(
r#"{"interfaceShape":"0|0|0|0|0|[object DOMQuad]|false","prototypeKeys":"p1,p2,p3,p4,getBounds,toJSON","pointDescriptor":"function|get p1|0|true|true|true","sameObject":true,"initial":"1,2,3,4,1,2,10,20","live":"true,-5,30,-5,2,16,28","copyInitial":"1,2,3,4,1,22","copyAfterMutation":"1,22,true","negativeValues":"10,20,-90,-180,-90,-180,100,200","nanValues":"NaN,0,NaN,0","jsonShape":"true|p1,p2,p3,p4|true|"#,
r#"{\"p1\":{\"x\":-5,\"y\":2,\"z\":3,\"w\":4},\"p2\":{\"x\":11,\"y\":2,\"z\":0,\"w\":1},\"p3\":{\"x\":11,\"y\":22,\"z\":0,\"w\":1},\"p4\":{\"x\":1,\"y\":30,\"z\":0,\"w\":1}}","forged":"TypeError,TypeError,TypeError","visibleSlots":""}"#
)
);
}
#[test]
fn dom_quad_factories_and_default_to_json_use_the_function_realm() {
let mut vm = new_storage_test_vm("https://domquad-function-realm.test/");
vm.eval(
r#"
(() => {
const root = document.documentElement ||
document.appendChild(document.createElement("html"));
const body = document.body || root.appendChild(document.createElement("body"));
const frame = document.createElement("iframe");
frame.id = "domquad-function-realm";
body.appendChild(frame);
})()
"#,
)
.expect("DOMQuad factory child frame should be created");
materialize_single_child_default_realm_for_test(&mut vm, "DOMQuad factory child Realm");
let result = vm
.eval(
r#"
(() => {
const other = document.getElementById("domquad-function-realm").contentWindow;
const quad = other.DOMQuad.fromRect({x: 1, y: 2, width: 3, height: 4});
const source = new DOMQuad({x: 7, y: 8});
const json = other.DOMQuad.prototype.toJSON.call(source);
return [
quad instanceof other.DOMQuad,
quad instanceof DOMQuad,
quad.p1 instanceof other.DOMPoint,
quad.p1 instanceof DOMPoint,
quad.getBounds() instanceof other.DOMRect,
quad.getBounds() instanceof DOMRect,
Object.getPrototypeOf(json) === other.Object.prototype,
json.p1 === source.p1,
json.p1 instanceof DOMPoint,
json.p1 instanceof other.DOMPoint
].join("|");
})()
"#,
)
.expect("DOMQuad function realm probe should evaluate");
assert_eq!(
result,
"true|false|true|false|true|false|true|true|true|false"
);
}