fix(geometry): return DOMRectList from geometry APIs

This commit is contained in:
ldm0
2026-09-22 21:43:34 +08:00
parent 32fa6c93e1
commit 853bf46a36
12 changed files with 338 additions and 28 deletions
@@ -2349,7 +2349,6 @@ css/css-variables/variable-reference-perspective-origin.html
css/css-variables/variable-reference.html
css/css-variables/variable-substitution-variable-declaration.html
css/css-variables/variables-animation-math-functions.html
css/cssom-view/DOMRectList.html
css/cssom-view/HTMLBody-ScrollArea_quirksmode.html
css/cssom-view/MediaQueryList-addListener-handleEvent.html
css/cssom-view/MediaQueryList-addListener-removeListener.html
@@ -2470,7 +2469,6 @@ 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/DOMRectList.html
css/geometry/structured-serialization.html
css/mediaqueries/at-custom-media-cssom.html
css/mediaqueries/at-custom-media-parsing.html
@@ -3076,6 +3076,7 @@ css/css-will-change/parsing/will-change-valid.html
css/css-will-change/will-change-contents-crash.html
css/css-will-change/will-change-inherit-dynamic.html
css/cssom-view/CaretPosition-001.html
css/cssom-view/DOMRectList.html
css/cssom-view/Element-currentCSSZoom.html
css/cssom-view/GetBoundingRect.html
css/cssom-view/HTMLImageElement-x-and-y-ignore-transforms.html
@@ -3359,6 +3360,7 @@ css/geometry/DOMQuad-nan.html
css/geometry/DOMRect-001.html
css/geometry/DOMRect-002.html
css/geometry/DOMRect-nan.html
css/geometry/DOMRectList.html
css/geometry/WebKitCSSMatrix.html
css/geometry/historical.html
css/geometry/spec-examples.html
@@ -11,6 +11,7 @@ mod css_runtime;
pub(crate) mod css_stylesheet_runtime;
mod dom_quad;
mod dom_rect;
mod dom_rect_list;
mod event_document;
mod event_legacy;
mod event_template;
@@ -231,6 +232,7 @@ pub(crate) use self::css_stylesheet_runtime::{
sync_css_style_sheet_shadow_root_adopted_owner_tracking,
};
pub(crate) use self::dom_rect::build_dom_rect_object;
pub(crate) use self::dom_rect_list::{build_dom_rect_list_object};
pub(crate) use self::events::{
EVENT_DISPATCHING_SLOT, EVENT_PASSIVE_SLOT, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT,
EVENT_STOP_PROPAGATION_SLOT, EventHandlerType, apply_event_handler_return_value,
@@ -16,6 +16,7 @@ use super::super::{
css_stylesheet_runtime::install_css_stylesheet_template_bindings,
dom_quad::install_dom_quad_template_bindings,
dom_rect::install_dom_rect_template_bindings,
dom_rect_list::install_dom_rect_list_template_bindings,
event_template::install_event_template_bindings,
file_api::install_file_api_template_bindings,
geometry_runtime::install_geometry_template_bindings,
@@ -411,6 +412,7 @@ pub(super) fn install_constructor_template_bindings<'s>(
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_rect_list_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());
install_custom_element_registry_template_bindings(scope, template, spec.interface.name());
@@ -0,0 +1,229 @@
use crate::{
util::{get_private_object, serialize_v8_array, throw_type_error},
web_api_interfaces, webidl,
};
use moli_webapi_declare::{
DataPropertyDescriptorDeclaration, WebApiFunctionTemplate, WebApiObject,
};
const DOM_RECT_LIST_VALUES_SLOT: &str = "__moliDomRectListValues";
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::DOMRectList, enumerable)]
struct DomRectListPrototypeDeclaration {
#[webapi(accessor_property, getter = dom_rect_list_length_getter)]
length: (),
#[webapi(method, length = 1, callback = dom_rect_list_item_callback)]
item: (),
}
#[derive(WebApiObject)]
#[webapi(
interface = web_api_interfaces::DOMRectList,
require_prototype,
fallback_to_string_tag = "DOMRectList"
)]
struct DomRectListObjectDeclaration<'scope> {
#[webapi(slot = DOM_RECT_LIST_VALUES_SLOT)]
values: v8::Local<'scope, v8::Array>,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "DOMRectList.item")]
struct DomRectListItemArgs {
#[webidl(required)]
index: u32,
}
pub(crate) fn build_dom_rect_list_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
rects: &[v8::Local<'s, v8::Object>],
) -> v8::Local<'s, v8::Object> {
let values = serialize_v8_array(scope, rects).unwrap_or_else(|| v8::Array::new(scope, 0));
let template = v8::ObjectTemplate::new(scope);
template.set_indexed_property_handler(
v8::IndexedPropertyHandlerConfiguration::new()
.getter(dom_rect_list_indexed_getter)
.setter(dom_rect_list_indexed_setter)
.query(dom_rect_list_indexed_query)
.deleter(dom_rect_list_indexed_deleter)
.enumerator(dom_rect_list_indexed_enumerator)
.definer(dom_rect_list_indexed_definer)
.descriptor(dom_rect_list_indexed_descriptor),
);
let object = template
.new_instance(scope)
.expect("DOMRectList object template should instantiate");
DomRectListObjectDeclaration::new(values)
.bind_into(scope, object)
.expect("DOMRectList declaration should bind");
object
}
pub(in crate::context_bootstrap) fn install_dom_rect_list_template_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
interface_name: &str,
) {
if interface_name == "DOMRectList" {
DomRectListPrototypeDeclaration::initialize_prototype_template(
scope,
template.prototype_template(scope),
);
}
}
fn dom_rect_list_values<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Array>> {
get_private_object(scope, object, DOM_RECT_LIST_VALUES_SLOT)
.and_then(|values| v8::Local::<v8::Array>::try_from(values).ok())
}
fn require_dom_rect_list_values<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
member: &str,
) -> Option<v8::Local<'s, v8::Array>> {
dom_rect_list_values(scope, object).or_else(|| {
throw_type_error(
scope,
&format!("Failed to execute '{member}' on 'DOMRectList': Illegal invocation."),
);
None
})
}
fn dom_rect_list_length_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(values) = require_dom_rect_list_values(scope, args.this(), "get length") else {
return;
};
rv.set_uint32(values.length());
}
fn dom_rect_list_item_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(values) = require_dom_rect_list_values(scope, args.this(), "item") else {
return;
};
let Some(parsed) = webidl::parse_args::<DomRectListItemArgs>(scope, &args) else {
return;
};
match values.get_index(scope, parsed.index) {
Some(value) if !value.is_null_or_undefined() => rv.set(value),
_ => rv.set(v8::null(scope).into()),
}
}
fn dom_rect_list_indexed_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
index: u32,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) -> v8::Intercepted {
let Some(value) = dom_rect_list_values(scope, args.holder())
.filter(|values| index < values.length())
.and_then(|values| values.get_index(scope, index))
else {
return v8::Intercepted::kNo;
};
rv.set(value);
v8::Intercepted::kYes
}
fn dom_rect_list_indexed_query<'s>(
scope: &mut v8::PinScope<'s, '_>,
index: u32,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Integer>,
) -> v8::Intercepted {
let Some(values) = dom_rect_list_values(scope, args.holder()) else {
return v8::Intercepted::kNo;
};
if index >= values.length() {
return v8::Intercepted::kNo;
}
rv.set_int32(v8::PropertyAttribute::READ_ONLY.as_u32() as i32);
v8::Intercepted::kYes
}
fn dom_rect_list_indexed_setter(
_scope: &mut v8::PinScope<'_, '_>,
_index: u32,
_value: v8::Local<'_, v8::Value>,
_args: v8::PropertyCallbackArguments<'_>,
mut rv: v8::ReturnValue<'_, v8::Boolean>,
) -> v8::Intercepted {
rv.set_bool(false);
v8::Intercepted::kYes
}
fn dom_rect_list_indexed_deleter<'s>(
scope: &mut v8::PinScope<'s, '_>,
index: u32,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Boolean>,
) -> v8::Intercepted {
let Some(values) = dom_rect_list_values(scope, args.holder()) else {
return v8::Intercepted::kNo;
};
if index >= values.length() {
return v8::Intercepted::kNo;
}
rv.set_bool(false);
v8::Intercepted::kYes
}
fn dom_rect_list_indexed_definer(
_scope: &mut v8::PinScope<'_, '_>,
_index: u32,
_descriptor: &v8::PropertyDescriptor,
_args: v8::PropertyCallbackArguments<'_>,
mut rv: v8::ReturnValue<'_, v8::Boolean>,
) -> v8::Intercepted {
rv.set_bool(true);
v8::Intercepted::kYes
}
fn dom_rect_list_indexed_enumerator<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Array>,
) {
let length = dom_rect_list_values(scope, args.holder())
.map(|values| values.length())
.unwrap_or(0);
let keys = (0..length)
.map(|index| v8::Integer::new_from_unsigned(scope, index).into())
.collect::<Vec<_>>();
rv.set(v8::Array::new_with_elements(scope, &keys));
}
fn dom_rect_list_indexed_descriptor<'s>(
scope: &mut v8::PinScope<'s, '_>,
index: u32,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) -> v8::Intercepted {
let Some(value) = dom_rect_list_values(scope, args.holder())
.filter(|values| index < values.length())
.and_then(|values| values.get_index(scope, index))
else {
return v8::Intercepted::kNo;
};
let Ok(descriptor) = DataPropertyDescriptorDeclaration::new(value, false, true).bind(scope)
else {
return v8::Intercepted::kNo;
};
rv.set(descriptor.into());
v8::Intercepted::kYes
}
@@ -1,5 +1,5 @@
use super::*;
use crate::util::{call_object_method, object_string_property, serialize_v8_array};
use crate::util::{call_object_method, object_string_property};
enum NativeRangeGeometryQuery {
Text,
@@ -330,7 +330,7 @@ pub(in crate::context_bootstrap) fn range_geometry_dom_rect<'s>(
pub(in crate::context_bootstrap) fn range_geometry_client_rects<'s>(
scope: &mut v8::PinScope<'s, '_>,
range: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Array>> {
) -> Option<v8::Local<'s, v8::Object>> {
if let Some(rects) = native_range_client_rects(scope, range) {
return match rects {
Ok(rects) => {
@@ -338,7 +338,7 @@ pub(in crate::context_bootstrap) fn range_geometry_client_rects<'s>(
.into_iter()
.filter_map(|rect| new_dom_rect_from_client_rect(scope, rect))
.collect::<Vec<_>>();
serialize_v8_array(scope, rects)
Some(dom_rect_list::build_dom_rect_list_object(scope, &rects))
}
Err(error) => {
throw_range_layout_error(scope, error);
@@ -348,12 +348,12 @@ pub(in crate::context_bootstrap) fn range_geometry_client_rects<'s>(
}
if let Some(rect) = collapsed_text_range_client_rect(scope, range) {
let rect = new_dom_rect_from_client_rect(scope, rect)?;
return serialize_v8_array(scope, [rect]);
return Some(dom_rect_list::build_dom_rect_list_object(scope, &[rect]));
}
let target = range_geometry_target(scope, range)?;
let rect = client_rect_for_object(scope, target)?;
let rect = new_dom_rect_from_client_rect(scope, rect)?;
serialize_v8_array(scope, [rect])
Some(dom_rect_list::build_dom_rect_list_object(scope, &[rect]))
}
pub(in crate::context_bootstrap) fn new_dom_rect_zero<'s>(
@@ -18,11 +18,10 @@ pub(super) fn range_get_client_rects_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let rects =
range_geometry_client_rects(scope, args.this()).or_else(|| Some(v8::Array::new(scope, 0)));
if let Some(rects) = rects {
rv.set(rects.into());
} else {
rv.set(v8::undefined(scope).into());
}
// Build the no-geometry fallback before querying layout. The query may
// throw, and allocating a wrapper after that would disturb V8's pending
// exception state.
let empty = dom_rect_list::build_dom_rect_list_object(scope, &[]);
let rects = range_geometry_client_rects(scope, args.this()).unwrap_or(empty);
rv.set(rects.into());
}
@@ -40,6 +40,10 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[
interface: web_api_interfaces::DOMStringList::DESCRIPTOR,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
interface: web_api_interfaces::DOMRectList::DESCRIPTOR,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
interface: web_api_interfaces::PluginArray::DESCRIPTOR,
kind: ConstructorKind::Illegal,
@@ -3,8 +3,8 @@ use super::super::super::node::{
throw_incompatible_method_receiver,
};
use super::{ClientRect, observable_bounding_client_rect, observable_client_rects};
use crate::context_bootstrap::build_dom_rect_object;
use crate::util::{serialize_v8_array, v8_string};
use crate::context_bootstrap::{build_dom_rect_list_object, build_dom_rect_object};
use crate::util::v8_string;
pub(crate) fn client_rect_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
@@ -22,12 +22,12 @@ pub(crate) fn client_rect_object<'s>(
fn client_rect_list<'s>(
scope: &mut v8::PinScope<'s, '_>,
rects: impl IntoIterator<Item = ClientRect>,
) -> Option<v8::Local<'s, v8::Array>> {
) -> v8::Local<'s, v8::Object> {
let rects = rects
.into_iter()
.filter_map(|rect| client_rect_object(scope, rect))
.collect::<Vec<_>>();
serialize_v8_array(scope, rects)
build_dom_rect_list_object(scope, &rects)
}
fn throw_layout_error(scope: &mut v8::PinScope<'_, '_>, error: moli_layout::LayoutError) {
@@ -83,7 +83,6 @@ pub(in crate::native_bridge) fn node_get_client_rects_callback(
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args_or_detached(scope, &args)
else {
throw_incompatible_method_receiver(scope, "Element", "getClientRects");
rv.set(v8::Array::new(scope, 0).into());
return;
};
if !require_element_method_receiver(scope, unsafe { &*runtime_ptr }, handle, "getClientRects") {
@@ -97,12 +96,8 @@ pub(in crate::native_bridge) fn node_get_client_rects_callback(
Ok(rects) => rects,
Err(error) => {
throw_layout_error(scope, error);
rv.set(v8::Array::new(scope, 0).into());
return;
}
};
let value = client_rect_list(scope, rects)
.map(Into::into)
.unwrap_or_else(|| v8::Array::new(scope, 0).into());
rv.set(value);
rv.set(client_rect_list(scope, rects).into());
}
@@ -681,7 +681,7 @@ fn detached_interaction_brand_checks_accept_standard_prototype_methods() {
rootSame: root === host.shadowRoot,
rectType: Object.prototype.toString.call(rect),
rectWidthType: typeof rect.width,
rectsArray: Array.isArray(rects),
rectsType: Object.prototype.toString.call(rects),
activeAfterFocus,
activeAfterBlur,
events: events.join(","),
@@ -703,7 +703,7 @@ fn detached_interaction_brand_checks_accept_standard_prototype_methods() {
assert_eq!(
result,
r#"{"rootType":"[object ShadowRoot]","rootSame":true,"rectType":"[object DOMRect]","rectWidthType":"number","rectsArray":true,"activeAfterFocus":true,"activeAfterBlur":true,"events":"focus,click,blur","attachOwn":false,"boundingOwn":false,"rectsOwn":false,"focusOwn":false,"blurOwn":false,"clickOwn":false,"elementOwners":"Element,Element,Element,Element,Element,Element,Element,Element","elementShapes":"true:function:1:true:true:true|true:function:0:true:true:true|true:function:0:true:true:true|true:function:1:true:true:true|true:function:1:true:true:true|true:function:1:true:true:true|true:function:2:true:true:true|true:function:1:true:true:true","actionOwners":"HTMLElement,HTMLElement,HTMLElement","actionShapes":"true:function:0:true:true:true|true:function:0:true:true:true|true:function:0:true:true:true"}"#
r#"{"rootType":"[object ShadowRoot]","rootSame":true,"rectType":"[object DOMRect]","rectWidthType":"number","rectsType":"[object DOMRectList]","activeAfterFocus":true,"activeAfterBlur":true,"events":"focus,click,blur","attachOwn":false,"boundingOwn":false,"rectsOwn":false,"focusOwn":false,"blurOwn":false,"clickOwn":false,"elementOwners":"Element,Element,Element,Element,Element,Element,Element,Element","elementShapes":"true:function:1:true:true:true|true:function:0:true:true:true|true:function:0:true:true:true|true:function:1:true:true:true|true:function:1:true:true:true|true:function:1:true:true:true|true:function:2:true:true:true|true:function:1:true:true:true","actionOwners":"HTMLElement,HTMLElement,HTMLElement","actionShapes":"true:function:0:true:true:true|true:function:0:true:true:true|true:function:0:true:true:true"}"#
);
}
@@ -1667,7 +1667,7 @@ fn child_content_document_created_elements_expose_geometry_methods() {
assert_eq!(result, "function|function|0|true");
}
#[test]
fn geometry_rect_objects_expose_domrect_to_json() {
fn geometry_rect_objects_and_lists_expose_their_webidl_shapes() {
let mut vm = new_parsed_test_vm(
"https://geometry-domrect-shape.test/",
"<!doctype html><body><div id='node'>text</div></body>",
@@ -1698,12 +1698,16 @@ fn geometry_rect_objects_expose_domrect_to_json() {
[
'x', 'y', 'width', 'height', 'top', 'right', 'bottom', 'left'
].every(name => json[name] === rect[name]),
rects instanceof DOMRectList,
Array.isArray(rects),
Object.prototype.toString.call(rects),
rects.length,
rects[0] instanceof DOMRect,
rects.item(0) === rects[0],
typeof rects[0].toJSON,
rangeRect instanceof DOMRect,
typeof rangeRect.toJSON,
rangeRects instanceof DOMRectList,
rangeRects[0] instanceof DOMRect,
contentRect instanceof DOMRect,
typeof contentRect.toJSON,
@@ -1717,7 +1721,81 @@ fn geometry_rect_objects_expose_domrect_to_json() {
assert_eq!(
result,
"true|true||function|x,y,width,height,top,right,bottom,left|true|true|1|true|function|true|function|true|true|function|true|false"
concat!(
"true|true||function|x,y,width,height,top,right,bottom,left|true|",
"true|false|[object DOMRectList]|1|true|true|function|true|function|",
"true|true|true|function|true|false"
)
);
}
#[test]
fn dom_rect_list_has_readonly_indexed_semantics_and_rejects_structured_clone() {
let mut vm = new_parsed_test_vm(
"https://domrect-list.test/",
"<!doctype html><body><div id='node'>text</div></body>",
);
let result = vm
.eval(
r#"
(() => {
const probe = callback => {
try {
callback();
return "ok";
} catch (error) {
return error.name;
}
};
const node = document.getElementById("node");
const list = node.getClientRects();
const range = document.createRange();
range.selectNodeContents(node);
const rangeList = range.getClientRects();
const indexDescriptor = Object.getOwnPropertyDescriptor(list, "0");
const lengthDescriptor = Object.getOwnPropertyDescriptor(
DOMRectList.prototype,
"length"
);
const itemDescriptor = Object.getOwnPropertyDescriptor(
DOMRectList.prototype,
"item"
);
const fake = Object.create(DOMRectList.prototype);
return JSON.stringify({
constructor: [typeof DOMRectList, DOMRectList.length,
probe(() => DOMRectList()), probe(() => new DOMRectList())].join("|"),
instance: [list instanceof DOMRectList, list instanceof Array,
Object.getPrototypeOf(list) === DOMRectList.prototype,
Object.prototype.toString.call(list), list.constructor === DOMRectList].join("|"),
values: [list.length, list[-1], list[1], list.item(-1), list.item(1),
list.item(0) === list[0], list[0] instanceof DOMRect,
Object.keys(list).join(",")].join("|"),
indexDescriptor: [indexDescriptor.value === list[0], indexDescriptor.writable,
indexDescriptor.enumerable, indexDescriptor.configurable].join("|"),
prototypeDescriptors: [typeof lengthDescriptor.get, lengthDescriptor.get.name,
lengthDescriptor.get.length, lengthDescriptor.set === undefined,
lengthDescriptor.enumerable, lengthDescriptor.configurable,
itemDescriptor.value.name, itemDescriptor.value.length,
itemDescriptor.enumerable, itemDescriptor.writable,
itemDescriptor.configurable].join("|"),
brands: [probe(() => lengthDescriptor.get.call(fake)),
probe(() => itemDescriptor.value.call(fake, 0))].join("|"),
range: [rangeList instanceof DOMRectList,
Object.prototype.toString.call(rangeList), rangeList.item(0) === rangeList[0]].join("|"),
clone: probe(() => structuredClone(list)),
visibleSlots: Object.getOwnPropertyNames(list)
.filter(name => name.startsWith("__moliDomRectList")).join(",")
});
})()
"#,
)
.expect("DOMRectList WebIDL probe should evaluate");
assert_eq!(
result,
r#"{"constructor":"function|0|TypeError|TypeError","instance":"true|false|true|[object DOMRectList]|true","values":"1|||||true|true|0","indexDescriptor":"true|false|true|true","prototypeDescriptors":"function|get length|0|true|true|true|item|1|true|true|true","brands":"TypeError|TypeError","range":"true|[object DOMRectList]|true","clone":"DataCloneError","visibleSlots":""}"#
);
}
@@ -113,6 +113,7 @@ interfaces! {
DOMPointReadOnly;
DOMQuad;
DOMRect: DOMRectReadOnly;
DOMRectList;
DOMRectReadOnly;
DOMStringList;
DOMStringMap;