fix(html): enumerate document named properties

This commit is contained in:
ldm0
2026-09-27 19:23:03 +08:00
parent 06f67e8071
commit 41bf70614a
2 changed files with 181 additions and 25 deletions
@@ -1,5 +1,6 @@
use crate::document_runtime::DomHandle;
use moli_dom::native::DomHost;
use crate::{document_runtime::DomHandle, util::v8_string};
use indexmap::IndexSet;
use moli_dom::native::{DomHost, Element};
use super::{
JsContextHost, collections,
@@ -55,8 +56,7 @@ pub(crate) fn document_all_named_item_handles(dom: &DomHost, name: &str) -> Vec<
pub(crate) fn document_named_item_handles(dom: &DomHost, name: &str) -> Vec<DomHandle> {
// Document named access deliberately has narrower legacy matching than
// Window or HTMLCollection: form/embed/iframe match by name, object by
// name or id, and img by id only while it also has a non-empty name.
// Window or HTMLCollection.
// https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem
dom.element_handles_by_id_or_name_matching(name, |_| true)
.into_iter()
@@ -67,22 +67,65 @@ pub(crate) fn document_named_item_handles(dom: &DomHost, name: &str) -> Vec<DomH
else {
return false;
};
let name_matches = element.name_attribute() == Some(name);
if dom.is_html_element_named(*handle, "img") {
return name_matches
|| (element.id() == Some(name) && element.name_attribute().is_some());
}
if dom.is_html_element_named(*handle, "object") {
return name_matches || element.id() == Some(name);
}
name_matches
&& ["embed", "form", "iframe"]
.into_iter()
.any(|local_name| dom.is_html_element_named(*handle, local_name))
document_named_element_property_names(dom, *handle, element)
.into_iter()
.flatten()
.any(|candidate| candidate == name)
})
.collect()
}
fn document_named_element_property_names<'a>(
dom: &DomHost,
handle: DomHandle,
element: &'a Element,
) -> [Option<&'a str>; 2] {
let name = element.name_attribute().filter(|name| !name.is_empty());
let id = element.id().filter(|id| !id.is_empty());
if dom.is_html_element_named(handle, "object") {
return [id, name];
}
if dom.is_html_element_named(handle, "img") && name.is_some() {
return [id, name];
}
if ["embed", "form", "iframe"]
.into_iter()
.any(|local_name| dom.is_html_element_named(handle, local_name))
{
return [None, name];
}
[None, None]
}
fn document_supported_property_names(dom: &DomHost, document_handle: DomHandle) -> Vec<String> {
if !dom
.node(document_handle)
.is_some_and(moli_dom::native::Node::is_document)
{
return Vec::new();
}
let mut names = IndexSet::new();
let mut stack = dom
.child_handles_reversed(document_handle)
.collect::<Vec<_>>();
while let Some(handle) = stack.pop() {
if let Some(element) = dom
.node(handle)
.and_then(moli_dom::native::Node::as_element)
{
for name in document_named_element_property_names(dom, handle, element)
.into_iter()
.flatten()
{
names.insert(name.to_owned());
}
}
stack.extend(dom.child_handles_reversed(handle));
}
names.into_iter().collect()
}
pub(crate) fn build_window_named_items_collection<'s>(
scope: &mut v8::PinScope<'s, '_>,
runtime_ptr: *mut JsContextHost,
@@ -127,6 +170,18 @@ fn is_document_legacy_unforgeable_property(name: &str) -> bool {
name == "location"
}
fn document_runtime_and_handle(
scope: &mut v8::PinScope<'_, '_>,
holder: v8::Local<'_, v8::Object>,
) -> Option<(*mut JsContextHost, DomHandle)> {
let (runtime_ptr, document_handle) = node_runtime_and_handle_from_object(scope, holder).ok()?;
unsafe { &*runtime_ptr }
.dom_host()
.node(document_handle)
.is_some_and(moli_dom::native::Node::is_document)
.then_some((runtime_ptr, document_handle))
}
fn document_named_access_context_for_name(
scope: &mut v8::PinScope<'_, '_>,
name: String,
@@ -135,15 +190,8 @@ fn document_named_access_context_for_name(
if name.is_empty() || is_document_legacy_unforgeable_property(&name) {
return None;
}
let (runtime_ptr, document_handle) = node_runtime_and_handle_from_object(scope, holder).ok()?;
let (runtime_ptr, document_handle) = document_runtime_and_handle(scope, holder)?;
let runtime = unsafe { &*runtime_ptr };
if !runtime
.dom_host()
.node(document_handle)
.is_some_and(moli_dom::native::Node::is_document)
{
return None;
}
let handles = document_named_item_handles(runtime.dom_host(), &name);
(!handles.is_empty()).then_some((runtime_ptr, document_handle, name, handles))
}
@@ -242,18 +290,58 @@ fn document_indexed_property_query<'s>(
v8::Intercepted::kYes
}
fn document_indexed_property_enumerator(
scope: &mut v8::PinScope<'_, '_>,
args: v8::PropertyCallbackArguments<'_>,
mut rv: v8::ReturnValue<'_, v8::Array>,
) {
let Some((runtime_ptr, document_handle)) = document_runtime_and_handle(scope, args.holder())
else {
rv.set(v8::Array::new(scope, 0));
return;
};
let keys =
document_supported_property_names(unsafe { &*runtime_ptr }.dom_host(), document_handle)
.into_iter()
.filter_map(|name| collections::array_index_property_name(&name))
.map(|index| v8::Integer::new_from_unsigned(scope, index).into())
.collect::<Vec<_>>();
rv.set(v8::Array::new_with_elements(scope, &keys));
}
fn document_named_property_enumerator(
scope: &mut v8::PinScope<'_, '_>,
args: v8::PropertyCallbackArguments<'_>,
mut rv: v8::ReturnValue<'_, v8::Array>,
) {
let Some((runtime_ptr, document_handle)) = document_runtime_and_handle(scope, args.holder())
else {
rv.set(v8::Array::new(scope, 0));
return;
};
let keys =
document_supported_property_names(unsafe { &*runtime_ptr }.dom_host(), document_handle)
.into_iter()
.filter(|name| collections::array_index_property_name(name).is_none())
.filter_map(|name| v8_string(scope, &name).map(Into::into))
.collect::<Vec<_>>();
rv.set(v8::Array::new_with_elements(scope, &keys));
}
pub(in crate::native_bridge) fn install_document_named_property_handler(
template: v8::Local<'_, v8::ObjectTemplate>,
) {
template.set_indexed_property_handler(
v8::IndexedPropertyHandlerConfiguration::new()
.getter(document_indexed_property_getter)
.query(document_indexed_property_query),
.query(document_indexed_property_query)
.enumerator(document_indexed_property_enumerator),
);
template.set_named_property_handler(
v8::NamedPropertyHandlerConfiguration::new()
.getter(document_named_property_getter)
.query(document_named_property_query)
.enumerator(document_named_property_enumerator)
.flags(v8::PropertyHandlerFlags::ONLY_INTERCEPT_STRINGS),
);
}
@@ -6737,6 +6737,74 @@ fn live_document_named_properties_follow_html_candidate_and_liveness_rules() {
);
}
#[test]
fn live_document_enumerates_supported_property_names_in_document_order() {
let mut vm = new_parsed_test_vm(
"https://example.com/",
r#"<!doctype html><html><body>
<embed name="embedName">
<form id="formId" name="formName"></form>
<iframe id="frameId" name="frameName"></iframe>
<img name="imageName">
<object id="objectId" name="objectName">
<object id="nestedObjectId" name="nestedObjectName"></object>
</object>
<img id="imageId" name="imageWithIdName">
<img id="imageIdOnly">
<img name="duplicateName">
<form name="duplicateName"></form>
<img name="42">
<template id="templateId"><img name="templateImage"></template>
</body></html>"#,
);
let result = vm
.eval(
r#"
(() => {
const supported = [
"embedName", "formName", "frameName", "imageName",
"objectId", "objectName", "nestedObjectId", "nestedObjectName",
"imageId", "imageWithIdName", "duplicateName", "42"
];
const ownNames = Object.getOwnPropertyNames(document);
const initial = {
includesEverySupportedName: supported.every(name => ownNames.includes(name)),
nonIndexOrder: ownNames.filter(name => supported.includes(name) && name !== "42"),
duplicateCount: ownNames.filter(name => name === "duplicateName").length,
numericNamePresent: ownNames.includes("42"),
excludedNamesAbsent: [
"formId", "frameId", "imageIdOnly", "templateId", "templateImage"
].every(name => !ownNames.includes(name))
};
const image = document.querySelector('[name="imageName"]');
image.id = "dynamicImageId";
image.name = "dynamicImageName";
const changedNames = Object.getOwnPropertyNames(document);
const changed = [
!changedNames.includes("imageName"),
changedNames.includes("dynamicImageId"),
changedNames.includes("dynamicImageName")
];
image.remove();
const removedNames = Object.getOwnPropertyNames(document);
const removed = [
!removedNames.includes("dynamicImageId"),
!removedNames.includes("dynamicImageName")
];
return JSON.stringify({ initial, changed, removed });
})()
"#,
)
.expect("Document supported-property-name enumeration should evaluate");
assert_eq!(
result,
r#"{"initial":{"includesEverySupportedName":true,"nonIndexOrder":["embedName","formName","frameName","imageName","objectId","objectName","nestedObjectId","nestedObjectName","imageId","imageWithIdName","duplicateName"],"duplicateCount":1,"numericNamePresent":true,"excludedNamesAbsent":true},"changed":[true,true,true],"removed":[true,true]}"#
);
}
#[test]
fn live_document_named_iframe_singletons_return_child_windows() {
let mut vm = new_parsed_test_vm(