mirror of
https://github.com/lexmount/moli.git
synced 2026-09-27 16:01:31 +00:00
fix(html): expose document named properties
This commit is contained in:
@@ -6,7 +6,10 @@ mod element_methods;
|
||||
mod node_accessors;
|
||||
mod node_methods;
|
||||
|
||||
use crate::{context_bootstrap::bridge_descriptor::BridgeDescriptor, native_bridge::element};
|
||||
use crate::{
|
||||
context_bootstrap::bridge_descriptor::BridgeDescriptor,
|
||||
native_bridge::{element, named_access},
|
||||
};
|
||||
|
||||
pub(super) fn build_node_wrapper_template<'s, 'i>(
|
||||
scope: &mut v8::PinScope<'s, 'i, ()>,
|
||||
@@ -15,6 +18,10 @@ pub(super) fn build_node_wrapper_template<'s, 'i>(
|
||||
let template = v8::ObjectTemplate::new(scope);
|
||||
let _ = template.set_internal_field_count(1);
|
||||
|
||||
if descriptor.prototype_name == "HTMLDocument" {
|
||||
named_access::install_document_named_property_handler(template);
|
||||
}
|
||||
|
||||
node_accessors::install_node_accessors(scope, template, descriptor);
|
||||
document_accessors::install_document_accessors(scope, template, descriptor);
|
||||
element_accessors::install_element_accessors(scope, template, descriptor);
|
||||
|
||||
@@ -173,6 +173,7 @@ pub(super) enum LiveCollectionQueryKind {
|
||||
ClassName,
|
||||
Name,
|
||||
WindowNamedItems,
|
||||
DocumentNamedItems,
|
||||
DocumentAllNamedItems,
|
||||
FormControlsByName,
|
||||
Forms,
|
||||
@@ -202,6 +203,7 @@ impl LiveCollectionQueryKind {
|
||||
Self::ClassName => "className",
|
||||
Self::Name => "name",
|
||||
Self::WindowNamedItems => "windowNamedItems",
|
||||
Self::DocumentNamedItems => "documentNamedItems",
|
||||
Self::DocumentAllNamedItems => "documentAllNamedItems",
|
||||
Self::FormControlsByName => "formControlsByName",
|
||||
Self::Forms => "forms",
|
||||
@@ -318,6 +320,11 @@ impl LiveCollectionDescriptor {
|
||||
self.root,
|
||||
self.query.as_deref().unwrap_or_default(),
|
||||
)
|
||||
} else if self.query_kind == LiveCollectionQueryKind::DocumentNamedItems {
|
||||
crate::native_bridge::named_access::document_named_item_handles(
|
||||
host.dom_host(),
|
||||
self.query.as_deref().unwrap_or_default(),
|
||||
)
|
||||
} else if self.query_kind == LiveCollectionQueryKind::DocumentAllNamedItems {
|
||||
crate::native_bridge::named_access::document_all_named_item_handles(
|
||||
host.dom_host(),
|
||||
|
||||
@@ -4,6 +4,7 @@ use moli_dom::native::DomHost;
|
||||
use super::{
|
||||
JsContextHost, collections,
|
||||
identity::{CollectionKind, LiveCollectionQueryKind},
|
||||
node::node_runtime_and_handle_from_object,
|
||||
};
|
||||
|
||||
const WINDOW_NAME_ELEMENTS: &[&str] = &["img", "form", "embed", "object"];
|
||||
@@ -51,6 +52,36 @@ 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.
|
||||
// https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem
|
||||
dom.element_handles_by_id_or_name_matching(name, |_| true)
|
||||
.into_iter()
|
||||
.filter(|handle| {
|
||||
let Some(element) = dom
|
||||
.node(*handle)
|
||||
.and_then(moli_dom::native::Node::as_element)
|
||||
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))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn build_window_named_items_collection<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
@@ -68,6 +99,146 @@ pub(crate) fn build_window_named_items_collection<'s>(
|
||||
))
|
||||
}
|
||||
|
||||
fn build_document_named_items_collection<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
document_handle: DomHandle,
|
||||
name: &str,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
Some(collections::build_live_collection_for_node(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
document_handle,
|
||||
CollectionKind::HtmlCollection,
|
||||
LiveCollectionQueryKind::DocumentNamedItems,
|
||||
Some(name.to_owned()),
|
||||
false,
|
||||
))
|
||||
}
|
||||
|
||||
type DocumentNamedAccessContext = (*mut JsContextHost, DomHandle, String, Vec<DomHandle>);
|
||||
|
||||
fn document_named_access_context_for_name(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
name: String,
|
||||
holder: v8::Local<'_, v8::Object>,
|
||||
) -> Option<DocumentNamedAccessContext> {
|
||||
if name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (runtime_ptr, document_handle) = node_runtime_and_handle_from_object(scope, holder).ok()?;
|
||||
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))
|
||||
}
|
||||
|
||||
fn document_named_access_context(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
key: v8::Local<'_, v8::Name>,
|
||||
holder: v8::Local<'_, v8::Object>,
|
||||
) -> Option<DocumentNamedAccessContext> {
|
||||
let key = v8::Local::<v8::String>::try_from(key).ok()?;
|
||||
document_named_access_context_for_name(scope, key.to_rust_string_lossy(scope), holder)
|
||||
}
|
||||
|
||||
fn document_named_access_value<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
context: DocumentNamedAccessContext,
|
||||
) -> Option<v8::Local<'s, v8::Value>> {
|
||||
let (runtime_ptr, document_handle, name, handles) = context;
|
||||
match handles.as_slice() {
|
||||
[handle] => unsafe { &mut *runtime_ptr }
|
||||
.native_bridge_mut()
|
||||
.wrap_handle(scope, runtime_ptr, *handle)
|
||||
.map(Into::into),
|
||||
_ => build_document_named_items_collection(scope, runtime_ptr, document_handle, &name)
|
||||
.map(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
fn document_named_property_getter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
key: v8::Local<'s, v8::Name>,
|
||||
args: v8::PropertyCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) -> v8::Intercepted {
|
||||
let Some(context) = document_named_access_context(scope, key, args.holder()) else {
|
||||
return v8::Intercepted::kNo;
|
||||
};
|
||||
let Some(value) = document_named_access_value(scope, context) else {
|
||||
return v8::Intercepted::kNo;
|
||||
};
|
||||
rv.set(value);
|
||||
v8::Intercepted::kYes
|
||||
}
|
||||
|
||||
fn document_named_property_query<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
key: v8::Local<'s, v8::Name>,
|
||||
args: v8::PropertyCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Integer>,
|
||||
) -> v8::Intercepted {
|
||||
if document_named_access_context(scope, key, args.holder()).is_none() {
|
||||
return v8::Intercepted::kNo;
|
||||
}
|
||||
rv.set_int32(v8::PropertyAttribute::NONE.as_u32() as i32);
|
||||
v8::Intercepted::kYes
|
||||
}
|
||||
|
||||
fn document_indexed_property_getter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
index: u32,
|
||||
args: v8::PropertyCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) -> v8::Intercepted {
|
||||
let Some(context) =
|
||||
document_named_access_context_for_name(scope, index.to_string(), args.holder())
|
||||
else {
|
||||
return v8::Intercepted::kNo;
|
||||
};
|
||||
let Some(value) = document_named_access_value(scope, context) else {
|
||||
return v8::Intercepted::kNo;
|
||||
};
|
||||
rv.set(value);
|
||||
v8::Intercepted::kYes
|
||||
}
|
||||
|
||||
fn document_indexed_property_query<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
index: u32,
|
||||
args: v8::PropertyCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Integer>,
|
||||
) -> v8::Intercepted {
|
||||
if document_named_access_context_for_name(scope, index.to_string(), args.holder()).is_none() {
|
||||
return v8::Intercepted::kNo;
|
||||
}
|
||||
rv.set_int32(v8::PropertyAttribute::NONE.as_u32() as i32);
|
||||
v8::Intercepted::kYes
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
template.set_named_property_handler(
|
||||
v8::NamedPropertyHandlerConfiguration::new()
|
||||
.getter(document_named_property_getter)
|
||||
.query(document_named_property_query)
|
||||
.flags(v8::PropertyHandlerFlags::ONLY_INTERCEPT_STRINGS),
|
||||
);
|
||||
}
|
||||
|
||||
fn name_elements(kind: LegacyNamedAccessKind) -> &'static [&'static str] {
|
||||
match kind {
|
||||
LegacyNamedAccessKind::Window => WINDOW_NAME_ELEMENTS,
|
||||
|
||||
@@ -5785,6 +5785,76 @@ fn live_document_all_obeys_legacy_named_and_indexed_semantics() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn live_document_named_properties_follow_html_candidate_and_liveness_rules() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://example.com/",
|
||||
r#"<!doctype html><html><body>
|
||||
<img id="imageId" name="imageName">
|
||||
<img id="duplicate" name="duplicate">
|
||||
<img name="duplicate">
|
||||
<img id="idOnly">
|
||||
<img name="42">
|
||||
<form id="formId" name="formName"></form>
|
||||
<object id="objectId"></object>
|
||||
</body></html>"#,
|
||||
);
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r##"
|
||||
(() => {
|
||||
const image = document.getElementById("imageId");
|
||||
const duplicate = document.duplicate;
|
||||
const duplicateImages = document.querySelectorAll("#duplicate, [name=duplicate]");
|
||||
const initial = [
|
||||
document.imageId === image,
|
||||
document.imageName === image,
|
||||
"imageId" in document,
|
||||
document.idOnly === undefined,
|
||||
!("idOnly" in document),
|
||||
document[42] === document.querySelector("[name='42']"),
|
||||
document.formName === document.querySelector("form"),
|
||||
document.formId === undefined,
|
||||
document.objectId === document.querySelector("object"),
|
||||
duplicate instanceof HTMLCollection,
|
||||
duplicate.length,
|
||||
duplicate[0] === duplicateImages[0],
|
||||
duplicate[1] === duplicateImages[1],
|
||||
];
|
||||
|
||||
image.removeAttribute("name");
|
||||
const removedName = [
|
||||
document.imageId === undefined,
|
||||
document.imageName === undefined,
|
||||
];
|
||||
|
||||
duplicateImages[1].name = "other";
|
||||
const narrowed = [
|
||||
duplicate.length,
|
||||
duplicate[0] === duplicateImages[0],
|
||||
document.duplicate === duplicateImages[0],
|
||||
document.other === duplicateImages[1],
|
||||
];
|
||||
|
||||
duplicateImages[0].remove();
|
||||
const removed = [
|
||||
duplicate.length,
|
||||
document.duplicate === undefined,
|
||||
!("duplicate" in document),
|
||||
];
|
||||
return JSON.stringify({ initial, removedName, narrowed, removed });
|
||||
})()
|
||||
"##,
|
||||
)
|
||||
.expect("document named-property probe should evaluate");
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"initial":[true,true,true,true,true,true,true,true,true,true,2,true,true],"removedName":[true,true],"narrowed":[1,true,true,true],"removed":[0,true,true]}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_named_access_filters_name_candidates_by_consumer() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
|
||||
Reference in New Issue
Block a user