fix(dom): derive element creation options before validation

This commit is contained in:
ldm0
2026-10-15 18:11:39 +08:00
committed by Donough Liu
parent 38d1425b89
commit bb7fdb41bc
17 changed files with 448 additions and 322 deletions
@@ -360,7 +360,7 @@ pub(in crate::context_bootstrap) fn custom_elements_initialize_callback<'s>(
scope.throw_exception(exception);
return;
};
if !custom_elements::registry_association_matches_document_default(
if !custom_elements::registry_association_matches_document(
unsafe { &*host_ptr },
root_document,
registry_association,
+3 -6
View File
@@ -36,9 +36,7 @@ pub(crate) use definition_upgrade::{
upgrade_existing_definition_for_child, upgrade_existing_definition_for_registry,
};
mod element_creation;
pub(crate) use element_creation::{
create_element_for_document_local_name_is_and_registry, is_name_from_create_options_value,
};
pub(crate) use element_creation::create_element_for_document_local_name_is_and_registry;
mod element_state;
use element_state::{
definition_name_for_handle, set_dom_custom_element_is_name, set_dom_custom_element_state,
@@ -136,9 +134,8 @@ pub(crate) use registry_install::{
};
mod registry_runtime;
pub(crate) use registry_runtime::{
mark_scoped_custom_elements_registry, registry_association_from_create_options_value,
registry_association_from_value, registry_association_matches_document_default,
registry_store_key,
mark_scoped_custom_elements_registry, registry_association_from_value,
registry_association_matches_document, registry_store_key,
};
mod registry_initializer;
pub(crate) use registry_initializer::initialize_registry_for_subtree;
@@ -5,26 +5,7 @@ use super::element_state::{
set_dom_custom_element_state, set_dom_element_prefix,
};
use crate::dom::native::CustomElementState;
use crate::{document_runtime::DomHandle, native_bridge::JsContextHost, util::v8str};
pub(crate) fn is_name_from_create_options_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
) -> Option<String> {
if value.is_null_or_undefined() {
return None;
}
if value.is_string() {
return value
.to_string(scope)
.map(|value| value.to_rust_string_lossy(scope));
}
let options = value.to_object(scope)?;
options
.get(scope, v8str(scope, "is").into())
.and_then(|value| value.to_string(scope))
.map(|value| value.to_rust_string_lossy(scope))
}
use crate::{document_runtime::DomHandle, native_bridge::JsContextHost};
pub(crate) fn create_element_for_document_local_name_is_and_registry<'s>(
scope: &mut v8::PinScope<'s, '_>,
@@ -3,7 +3,7 @@ use crate::web_api_interfaces;
use crate::{
document_runtime::DomHandle,
native_bridge::JsContextHost,
util::{get_private_value, set_private_value, v8str},
util::{get_private_value, set_private_value},
};
pub(super) const CUSTOM_ELEMENTS_REGISTRY_CHILD_HANDLE_SLOT: &str =
@@ -58,19 +58,7 @@ pub(crate) fn registry_association_from_value<'s>(
))
}
pub(crate) fn registry_association_from_create_options_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
) -> Option<CustomElementRegistryAssociation> {
if value.is_null_or_undefined() || value.is_string() {
return None;
}
let options = value.to_object(scope)?;
let registry = options.get(scope, v8str(scope, "customElementRegistry").into())?;
registry_association_from_value(scope, registry)
}
pub(crate) fn registry_association_matches_document_default(
pub(crate) fn registry_association_matches_document(
host: &JsContextHost,
document_handle: DomHandle,
association: CustomElementRegistryAssociation,
@@ -86,12 +86,6 @@ struct NativeBridgeDetachedCreationHelpersDeclaration {
)]
detached_create_cdata_section: (),
#[webapi(
method = "__detachedCreateElement",
callback = document::bridge_detached_create_element_callback
)]
detached_create_element: (),
#[webapi(
method = "__adoptNodeIntoDocument",
callback = document::bridge_adopt_node_into_document_callback
@@ -145,8 +145,11 @@ pub(crate) use css_state::{
node_document_adopted_style_sheets_getter_function,
node_document_adopted_style_sheets_setter_function, node_document_style_sheets_getter_function,
};
pub(in crate::native_bridge::document) use custom_element_options::parse_import_node_options;
pub(in crate::native_bridge) use custom_element_options::validate_registry_association_for_document;
pub(in crate::native_bridge::document) use custom_element_options::{
CreateElementOptions, DocumentCreateElementArgs, DocumentCreateElementNsArgs,
parse_import_node_options,
};
pub(crate) use detached_install::detached_iframe_current_content_document_handle;
pub(super) use detached_install::install_detached_bridge_methods;
pub(in crate::native_bridge) use detached_install::{
@@ -207,6 +210,7 @@ pub(in crate::native_bridge) use detached_objects::{
pub(in crate::native_bridge) use detached_objects::{
detached_shadow_root_active_element_value, detached_shadow_root_selection_value,
};
pub(in crate::native_bridge::document) use detached_surface::create_detached_element_for_document;
pub(in crate::native_bridge::document) use detached_surface::set_detached_document_content_type;
pub(super) use detached_surface::{
bridge_adopt_node_into_document_callback, bridge_clone_node_into_document_callback,
@@ -220,7 +224,7 @@ pub(super) use detached_surface::{
bridge_detached_child_element_count_callback, bridge_detached_child_nodes_callback,
bridge_detached_children_callback, bridge_detached_clone_node_callback,
bridge_detached_create_cdata_section_callback, bridge_detached_create_comment_callback,
bridge_detached_create_document_fragment_callback, bridge_detached_create_element_callback,
bridge_detached_create_document_fragment_callback,
bridge_detached_create_processing_instruction_callback, bridge_detached_create_text_callback,
bridge_detached_doctype_name_callback, bridge_detached_doctype_public_id_callback,
bridge_detached_doctype_system_id_callback, bridge_detached_document_base_uri_callback,
@@ -1,7 +1,4 @@
use super::super::super::{
custom_elements,
util::{call_global_bridge_method, v8str},
};
use super::super::super::{custom_elements, util::call_global_bridge_method};
use super::super::node::{
node_arg_handle, node_is_document, node_or_foreign_arg_handle_allow_detached,
node_runtime_and_handle_from_args, node_runtime_and_handle_from_args_or_detached,
@@ -12,8 +9,9 @@ use super::super::{
validate_qualified_element_name_and_namespace, validate_qualified_name_and_namespace,
};
use super::{
XHTML_NS, clone_js_node_like_into_document_object, is_html_document, is_valid_pi_target,
new_attr_object, normalize_namespace,
DocumentCreateElementArgs, DocumentCreateElementNsArgs, XHTML_NS,
clone_js_node_like_into_document_object, is_html_document, is_valid_pi_target, new_attr_object,
normalize_namespace,
};
use super::{
detached_adopt_node_method_callback, detached_create_attribute_method_callback,
@@ -26,22 +24,6 @@ use super::{
};
use crate::webidl;
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "Document.createElement")]
struct DocumentCreateElementArgs {
#[webidl(required)]
local_name: String,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "Document.createElementNS")]
struct DocumentCreateElementNsArgs {
#[webidl(required, nullable)]
namespace: Option<String>,
#[webidl(required)]
qualified_name: String,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "Document.createTextNode")]
struct DocumentCreateTextNodeArgs {
@@ -1,5 +1,5 @@
use super::helpers::{
create_element_ns_for_document, create_element_options, create_element_wrapper_for_document,
create_element_ns_for_document, create_element_wrapper_for_document,
registry_association_for_create_element, registry_association_has_autonomous_definition,
validate_create_element_name, validate_create_element_ns_name,
};
@@ -22,11 +22,9 @@ pub(in crate::native_bridge) fn bridge_create_element_callback<'s>(
rv.set_null();
return;
};
let mut options = create_element_options(scope, &args, 1);
if options.registry_association.is_none() {
options.registry_association =
unsafe { &*runtime_ptr }.custom_element_registry_association(document_handle);
}
let Some(options) = parsed.options.flatten(scope, runtime_ptr, document_handle) else {
return;
};
match create_element_wrapper_for_document(
scope,
args.this(),
@@ -60,11 +58,9 @@ pub(in crate::native_bridge) fn bridge_create_element_ns_callback<'s>(
rv.set_null();
return;
};
let mut options = create_element_options(scope, &args, 2);
if options.registry_association.is_none() {
options.registry_association =
unsafe { &*runtime_ptr }.custom_element_registry_association(document_handle);
}
let Some(options) = parsed.options.flatten(scope, runtime_ptr, document_handle) else {
return;
};
if namespace.as_deref() == Some(XHTML_NS) {
let (prefix, local_name) = parsed
.qualified_name
@@ -127,6 +123,7 @@ pub(in crate::native_bridge) fn bridge_create_element_ns_callback<'s>(
rv.set_null();
return;
};
options.initialize_non_html_element(runtime_ptr, handle);
unsafe { &mut *runtime_ptr }.capture_node_creation_stack_trace(scope, handle);
match unsafe { &mut *runtime_ptr }
.native_bridge_mut()
@@ -1,11 +1,10 @@
use super::helpers::{
create_element_ns_for_document, create_element_options, create_element_with_parts_for_document,
create_element_ns_for_document, create_element_with_parts_for_document,
create_element_wrapper_for_document, registry_association_for_create_element,
registry_association_has_autonomous_definition, validate_create_element_name,
validate_create_element_ns_name,
};
use super::*;
use crate::native_bridge::document::validate_registry_association_for_document;
use crate::native_bridge::set_wrapped_handle_or_null_for_receiver;
pub(in crate::native_bridge) fn node_create_element_callback<'s>(
@@ -24,11 +23,11 @@ pub(in crate::native_bridge) fn node_create_element_callback<'s>(
}
return;
}
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else {
rv.set_null();
let Some(parsed) = webidl::parse_args::<DocumentCreateElementArgs>(scope, &args) else {
return;
};
let Some(parsed) = webidl::parse_args::<DocumentCreateElementArgs>(scope, &args) else {
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else {
rv.set_null();
return;
};
if !validate_create_element_name(scope, &parsed.local_name) {
@@ -39,19 +38,9 @@ pub(in crate::native_bridge) fn node_create_element_callback<'s>(
rv.set_null();
return;
}
let mut options = create_element_options(scope, &args, 1);
if options.registry_association.is_none() {
options.registry_association =
unsafe { &*runtime_ptr }.custom_element_registry_association(handle);
}
if !validate_registry_association_for_document(
scope,
runtime_ptr,
handle,
options.registry_association,
) {
let Some(options) = parsed.options.flatten(scope, runtime_ptr, handle) else {
return;
}
};
if !is_html_document(runtime, handle) {
let namespace = runtime
.dom_host()
@@ -68,6 +57,7 @@ pub(in crate::native_bridge) fn node_create_element_callback<'s>(
rv.set_null();
return;
};
options.initialize_non_html_element(runtime_ptr, created_handle);
unsafe { &mut *runtime_ptr }.capture_node_creation_stack_trace(scope, created_handle);
set_wrapped_handle_or_null_for_receiver(
scope,
@@ -109,30 +99,20 @@ pub(in crate::native_bridge) fn node_create_element_ns_callback<'s>(
}
return;
}
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else {
rv.set_null();
let Some(parsed) = webidl::parse_args::<DocumentCreateElementNsArgs>(scope, &args) else {
return;
};
let Some(parsed) = webidl::parse_args::<DocumentCreateElementNsArgs>(scope, &args) else {
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else {
rv.set_null();
return;
};
let namespace = normalize_namespace(parsed.namespace);
if !validate_create_element_ns_name(scope, namespace.as_deref(), &parsed.qualified_name) {
return;
}
let mut options = create_element_options(scope, &args, 2);
if options.registry_association.is_none() {
options.registry_association =
unsafe { &*runtime_ptr }.custom_element_registry_association(handle);
}
if !validate_registry_association_for_document(
scope,
runtime_ptr,
handle,
options.registry_association,
) {
let Some(options) = parsed.options.flatten(scope, runtime_ptr, handle) else {
return;
}
};
if namespace.as_deref() == Some(XHTML_NS) {
let (prefix, local_name) = parsed
.qualified_name
@@ -195,6 +175,7 @@ pub(in crate::native_bridge) fn node_create_element_ns_callback<'s>(
rv.set_null();
return;
};
options.initialize_non_html_element(runtime_ptr, created_handle);
unsafe { &mut *runtime_ptr }.capture_node_creation_stack_trace(scope, created_handle);
set_wrapped_handle_or_null_for_receiver(
scope,
@@ -85,11 +85,6 @@ pub(super) fn registry_association_has_autonomous_definition(
}
}
pub(super) struct CreateElementOptions {
pub(super) is_name: Option<String>,
pub(super) registry_association: Option<custom_elements::CustomElementRegistryAssociation>,
}
pub(super) fn validate_create_element_name(
scope: &mut v8::PinScope<'_, '_>,
local_name: &str,
@@ -119,42 +114,3 @@ pub(super) fn validate_create_element_ns_name(
}
}
}
pub(super) fn create_element_options(
scope: &mut v8::PinScope<'_, '_>,
args: &v8::FunctionCallbackArguments<'_>,
index: i32,
) -> CreateElementOptions {
let value = args.get(index);
if value.is_null_or_undefined() {
return CreateElementOptions {
is_name: None,
registry_association: None,
};
}
if value.is_string() {
// Legacy string createElement/createElementNS options are ignored by
// current DOM custom-elements semantics; only dictionary `is` is used.
return CreateElementOptions {
is_name: None,
registry_association: None,
};
}
let Some(options) = value.to_object(scope) else {
return CreateElementOptions {
is_name: None,
registry_association: None,
};
};
let is_name = options
.get(scope, v8str(scope, "is").into())
.and_then(|value| value.to_string(scope))
.map(|value| value.to_rust_string_lossy(scope));
let registry_association = options
.get(scope, v8str(scope, "customElementRegistry").into())
.and_then(|value| custom_elements::registry_association_from_value(scope, value));
CreateElementOptions {
is_name,
registry_association,
}
}
@@ -2,6 +2,163 @@ use crate::custom_elements;
use crate::document_runtime::DomHandle;
use crate::native_bridge::{JsContextHost, throw_dom_exception};
use crate::util::{throw_type_error, v8str};
use crate::web_api_interfaces;
use crate::webidl;
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "Document.createElement")]
pub(in crate::native_bridge::document) struct DocumentCreateElementArgs {
#[webidl(required)]
pub(in crate::native_bridge::document) local_name: String,
#[webidl(with = create_element_options_argument)]
pub(in crate::native_bridge::document) options: CreateElementOptions,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "Document.createElementNS")]
pub(in crate::native_bridge::document) struct DocumentCreateElementNsArgs {
#[webidl(required, nullable)]
pub(in crate::native_bridge::document) namespace: Option<String>,
#[webidl(required)]
pub(in crate::native_bridge::document) qualified_name: String,
#[webidl(with = create_element_options_argument)]
pub(in crate::native_bridge::document) options: CreateElementOptions,
}
#[derive(Default, webidl::WebIdlDictionary)]
#[webidl(prefix = "ElementCreationOptions")]
pub(in crate::native_bridge::document) struct CreateElementOptions {
// An omitted nullable member and an explicitly null registry have different
// meanings. Keep its presence while reusing native interface conversion.
#[webidl(name = "customElementRegistry", with = nullable_creation_registry)]
pub(in crate::native_bridge::document) registry_association:
Option<custom_elements::CustomElementRegistryAssociation>,
#[webidl(name = "is")]
pub(in crate::native_bridge::document) is_name: Option<String>,
}
impl<'s> webidl::WebIdlConverter<'s> for CreateElementOptions {
type Options = ();
fn convert(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
context: webidl::Context,
_options: &(),
) -> Result<Self, webidl::WebIdlError> {
// Objects select the dictionary even if they are boxed strings or have
// a toString method. Nullish values select the empty dictionary.
if value.is_object() || value.is_null_or_undefined() {
return webidl::parse_dictionary::<Self>(scope, value, context)
.map(Option::unwrap_or_default);
}
// Other primitives select the legacy DOMString member, which the DOM
// algorithm ignores. Conversion still rejects Symbols.
webidl::convert::<webidl::DomString16>(scope, value, context)?;
Ok(Self::default())
}
}
fn create_element_options_argument<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
) -> Result<CreateElementOptions, webidl::WebIdlError> {
let prefix = if index == 1 {
"Document.createElement"
} else {
"Document.createElementNS"
};
webidl::argument(
scope,
args,
index,
webidl::Context::argument(prefix, index as usize + 1),
)
}
fn nullable_creation_registry<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
member: &'static str,
) -> Result<Option<custom_elements::CustomElementRegistryAssociation>, webidl::WebIdlError> {
let context = webidl::Context::member("ElementCreationOptions", member);
let Some(value) = webidl::property_result(scope, object, member, context)? else {
return Ok(None);
};
if value.is_undefined() {
return Ok(None);
}
if value.is_null() {
return Ok(Some(
custom_elements::CustomElementRegistryAssociation::Null,
));
}
let registry = webidl::convert_with_options::<webidl::InterfaceObject>(
scope,
value,
context,
&webidl::InterfaceOptions {
name: web_api_interfaces::CustomElementRegistry::NAME,
brand_check: web_api_interfaces::CustomElementRegistry::is_instance,
},
)?;
let target = moli_webapi_declare::web_api_object_target(scope, registry.0)
.expect("converted CustomElementRegistry has native identity");
Ok(Some(
custom_elements::CustomElementRegistryAssociation::Registry(
custom_elements::registry_store_key(scope, target),
),
))
}
impl CreateElementOptions {
pub(in crate::native_bridge::document) fn initialize_non_html_element(
&self,
runtime_ptr: *mut JsContextHost,
handle: DomHandle,
) {
let runtime = unsafe { &mut *runtime_ptr };
if let Some(registry) = self.registry_association {
runtime.set_custom_element_registry_association(handle, registry);
}
if let Some(is_name) = &self.is_name {
runtime
.dom_host_mut()
.set_custom_element_is_name(handle, Some(is_name.clone()));
}
}
pub(in crate::native_bridge::document) fn flatten(
mut self,
scope: &mut v8::PinScope<'_, '_>,
runtime_ptr: *mut JsContextHost,
document_handle: DomHandle,
) -> Option<Self> {
if self.registry_association.is_some() && self.is_name.is_some() {
throw_dom_exception(
scope,
"NotSupportedError",
9,
"An explicit CustomElementRegistry cannot be combined with is.",
);
return None;
}
if !validate_registry_association_for_document(
scope,
runtime_ptr,
document_handle,
self.registry_association,
) {
return None;
}
if self.registry_association.is_none() {
self.registry_association =
unsafe { &*runtime_ptr }.custom_element_registry_association(document_handle);
}
Some(self)
}
}
pub(in crate::native_bridge) fn validate_registry_association_for_document(
scope: &mut v8::PinScope<'_, '_>,
@@ -12,7 +169,7 @@ pub(in crate::native_bridge) fn validate_registry_association_for_document(
let Some(registry_association) = registry_association else {
return true;
};
if custom_elements::registry_association_matches_document_default(
if custom_elements::registry_association_matches_document(
unsafe { &*runtime_ptr },
document_handle,
registry_association,
@@ -544,9 +544,6 @@ pub(in crate::native_bridge::document) fn build_detached_element_object<'s>(
v8_string(scope, document_kind)?.into(),
);
let attributes = new_map_object(scope);
if let Some(is_name) = is_name {
detached_map_set(scope, attributes, "is", is_name);
}
let _ = state.set(scope, v8str(scope, "attributes").into(), attributes.into());
let namespace_attributes = new_map_object(scope);
let _ = state.set(
@@ -573,10 +570,13 @@ pub(in crate::native_bridge::document) fn build_detached_element_object<'s>(
);
if let Some(is_name) = is_name {
let runtime = unsafe { &mut *runtime_ptr };
let _ = runtime.dom_host_mut().set_attribute(handle, "is", is_name);
if runtime
let _ = runtime
.dom_host_mut()
.set_custom_element_state(handle, CustomElementState::Undefined)
.set_custom_element_is_name(handle, Some(is_name.to_owned()));
if namespace_uri.as_deref() == Some(XHTML_NS)
&& runtime
.dom_host_mut()
.set_custom_element_state(handle, CustomElementState::Undefined)
{
runtime.note_style_subtree_context_change(handle);
}
@@ -1,6 +1,9 @@
use super::*;
use crate::detached_event_target::dispatch_detached_focus_event;
use crate::dom_parser::map_live_value_to_foreign;
use crate::native_bridge::document::{
DocumentCreateElementArgs, DocumentCreateElementNsArgs, create_detached_element_for_document,
};
use crate::util::{
context_host_ptr_from_global_bridge, get_private_object, get_private_value, set_private_value,
};
@@ -35,22 +38,6 @@ struct DetachedDomImplementationDeclaration<'scope> {
owner_document: v8::Local<'scope, v8::Object>,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "Document.createElement")]
struct DetachedForwardCreateElementArgs {
#[webidl(required)]
local_name: String,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "Document.createElementNS")]
struct DetachedForwardCreateElementNsArgs {
#[webidl(required, nullable)]
namespace: Option<String>,
#[webidl(required)]
qualified_name: String,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "Document.createCDATASection")]
struct DetachedForwardCreateCdataSectionArgs {
@@ -769,26 +756,19 @@ pub(in crate::native_bridge::document) fn detached_create_html_element_method_ca
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(parsed) = webidl::parse_args::<DetachedForwardCreateElementArgs>(scope, &args) else {
let Some(parsed) = webidl::parse_args::<DocumentCreateElementArgs>(scope, &args) else {
return;
};
let Some(local_name) = v8_string(scope, &parsed.local_name) else {
rv.set_null();
return;
};
match call_global_bridge_method(
match create_detached_element_for_document(
scope,
"__detachedCreateElement",
&[
args.this().into(),
local_name.into(),
v8str(scope, XHTML_NS).into(),
v8str(scope, "html").into(),
v8str(scope, "name").into(),
args.get(1),
],
args.this(),
&parsed.local_name,
Some(XHTML_NS.to_owned()),
"html",
false,
parsed.options,
) {
Some(value) => rv.set(value),
Some(value) => rv.set(value.into()),
None => rv.set_null(),
}
}
@@ -798,26 +778,19 @@ pub(in crate::native_bridge::document) fn detached_create_xml_element_method_cal
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(parsed) = webidl::parse_args::<DetachedForwardCreateElementArgs>(scope, &args) else {
let Some(parsed) = webidl::parse_args::<DocumentCreateElementArgs>(scope, &args) else {
return;
};
let Some(local_name) = v8_string(scope, &parsed.local_name) else {
rv.set_null();
return;
};
match call_global_bridge_method(
match create_detached_element_for_document(
scope,
"__detachedCreateElement",
&[
args.this().into(),
local_name.into(),
v8::null(scope).into(),
v8str(scope, "xml").into(),
v8str(scope, "name").into(),
args.get(1),
],
args.this(),
&parsed.local_name,
None,
"xml",
false,
parsed.options,
) {
Some(value) => rv.set(value),
Some(value) => rv.set(value.into()),
None => rv.set_null(),
}
}
@@ -827,32 +800,19 @@ pub(in crate::native_bridge::document) fn detached_create_html_element_ns_method
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(parsed) = webidl::parse_args::<DetachedForwardCreateElementNsArgs>(scope, &args)
else {
let Some(parsed) = webidl::parse_args::<DocumentCreateElementNsArgs>(scope, &args) else {
return;
};
let Some(qualified_name) = v8_string(scope, &parsed.qualified_name) else {
rv.set_null();
return;
};
let namespace = parsed
.namespace
.as_deref()
.and_then(|namespace| v8_string(scope, namespace).map(Into::into))
.unwrap_or_else(|| v8::null(scope).into());
match call_global_bridge_method(
match create_detached_element_for_document(
scope,
"__detachedCreateElement",
&[
args.this().into(),
qualified_name.into(),
namespace,
v8str(scope, "html").into(),
v8str(scope, "qualified").into(),
args.get(2),
],
args.this(),
&parsed.qualified_name,
parsed.namespace,
"html",
true,
parsed.options,
) {
Some(value) => rv.set(value),
Some(value) => rv.set(value.into()),
None => rv.set_null(),
}
}
@@ -862,32 +822,19 @@ pub(in crate::native_bridge::document) fn detached_create_xml_element_ns_method_
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(parsed) = webidl::parse_args::<DetachedForwardCreateElementNsArgs>(scope, &args)
else {
let Some(parsed) = webidl::parse_args::<DocumentCreateElementNsArgs>(scope, &args) else {
return;
};
let Some(qualified_name) = v8_string(scope, &parsed.qualified_name) else {
rv.set_null();
return;
};
let namespace = parsed
.namespace
.as_deref()
.and_then(|namespace| v8_string(scope, namespace).map(Into::into))
.unwrap_or_else(|| v8::null(scope).into());
match call_global_bridge_method(
match create_detached_element_for_document(
scope,
"__detachedCreateElement",
&[
args.this().into(),
qualified_name.into(),
namespace,
v8str(scope, "xml").into(),
v8str(scope, "qualified").into(),
args.get(2),
],
args.this(),
&parsed.qualified_name,
parsed.namespace,
"xml",
true,
parsed.options,
) {
Some(value) => rv.set(value),
Some(value) => rv.set(value.into()),
None => rv.set_null(),
}
}
@@ -7,10 +7,11 @@ pub(in crate::native_bridge) use self::clone_adopt::{
bridge_adopt_node_into_document_callback, bridge_clone_node_into_document_callback,
bridge_detached_clone_node_callback,
};
pub(in crate::native_bridge::document) use self::document_methods::create_detached_element_for_document;
pub(in crate::native_bridge) use self::document_methods::{
bridge_create_cdata_section_not_supported_callback,
bridge_detached_create_cdata_section_callback, bridge_detached_create_comment_callback,
bridge_detached_create_document_fragment_callback, bridge_detached_create_element_callback,
bridge_detached_create_document_fragment_callback,
bridge_detached_create_processing_instruction_callback, bridge_detached_create_text_callback,
};
pub(in crate::native_bridge) use self::dom_implementation::{
@@ -1,6 +1,5 @@
use super::super::*;
use crate::custom_elements;
use crate::native_bridge::document::validate_registry_association_for_document;
use crate::native_bridge::document::CreateElementOptions;
use crate::webidl;
#[derive(webidl::WebIdlArgs)]
@@ -40,19 +39,6 @@ struct DetachedDocumentCreateCdataSectionArgs {
data: String,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "Document.createElement")]
struct DetachedDocumentCreateElementBridgeArgs {
#[webidl(index = 1, required)]
qualified_name: String,
#[webidl(index = 2, required, nullable)]
namespace_uri: Option<String>,
#[webidl(index = 3, required)]
document_kind: String,
#[webidl(index = 4)]
namespace_validation: Option<String>,
}
pub(in crate::native_bridge) fn bridge_detached_create_text_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -193,29 +179,20 @@ pub(in crate::native_bridge) fn bridge_create_cdata_section_not_supported_callba
);
}
pub(in crate::native_bridge) fn bridge_detached_create_element_callback<'s>(
pub(in crate::native_bridge::document) fn create_detached_element_for_document<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Ok(document) = v8::Local::<v8::Object>::try_from(args.get(0)) else {
rv.set_null();
return;
};
let Some(parsed) = webidl::parse_args::<DetachedDocumentCreateElementBridgeArgs>(scope, &args)
else {
return;
};
let mut namespace_uri = normalize_namespace(parsed.namespace_uri);
let document_kind = parsed.document_kind;
let validation = parsed.namespace_validation.as_deref();
let validation_error = if validation == Some("qualified") {
validate_qualified_element_name_and_namespace(
namespace_uri.as_deref(),
&parsed.qualified_name,
)
.err()
} else if !validate_element_name(&parsed.qualified_name) {
document: v8::Local<'s, v8::Object>,
qualified_name: &str,
namespace_uri: Option<String>,
document_kind: &str,
split_qualified_name: bool,
options: CreateElementOptions,
) -> Option<v8::Local<'s, v8::Object>> {
let mut namespace_uri = normalize_namespace(namespace_uri);
let validation_error = if split_qualified_name {
validate_qualified_element_name_and_namespace(namespace_uri.as_deref(), qualified_name)
.err()
} else if !validate_element_name(qualified_name) {
Some((
"InvalidCharacterError",
5,
@@ -226,42 +203,31 @@ pub(in crate::native_bridge) fn bridge_detached_create_element_callback<'s>(
};
if let Some((name, code, message)) = validation_error {
throw_dom_exception(scope, name, code, message);
return;
return None;
}
let (runtime_ptr, document_handle) =
node_runtime_and_handle_from_object_or_detached(scope, document).ok()?;
let options = options.flatten(scope, runtime_ptr, document_handle)?;
// Conversion and DOM exceptions use the binding's realm. Allocation uses
// the Document's realm so native ownership does not depend on the caller.
let context = document.get_creation_context(scope)?;
let scope = &mut v8::ContextScope::new(scope, context);
if namespace_uri.is_none()
&& validation != Some("qualified")
&& !split_qualified_name
&& document_kind == "xml"
&& detached_document_content_type_value(scope, document).as_deref()
== Some("application/xhtml+xml")
{
namespace_uri = Some(XHTML_NS.to_owned());
}
let registry_association =
custom_elements::registry_association_from_create_options_value(scope, args.get(5));
let is_name = custom_elements::is_name_from_create_options_value(scope, args.get(5));
if let Some(registry_association) = registry_association
&& let Some(runtime_ptr) = crate::util::context_host_ptr_from_global_bridge(scope)
&& let Some(document_handle) = detached_native_handle(scope, document)
&& !validate_registry_association_for_document(
scope,
runtime_ptr,
document_handle,
Some(registry_association),
)
{
return;
}
match build_detached_element_object(
build_detached_element_object(
scope,
document,
&parsed.qualified_name,
qualified_name,
namespace_uri,
&document_kind,
validation == Some("qualified"),
is_name.as_deref(),
registry_association,
) {
Some(element) => rv.set(element.into()),
None => rv.set_null(),
}
document_kind,
split_qualified_name,
options.is_name.as_deref(),
options.registry_association,
)
}
@@ -0,0 +1,146 @@
(async () => {
const checks = [];
const observe = action => {try {return action();} catch (error) {return {exception: error.name, message: String(error)};}};
const check = (name, actual, expected) => checks.push({name, actual, expected, passed: JSON.stringify(actual) === JSON.stringify(expected)});
const capture = (action, sentinel, callee) => {
try {action(); return null;} catch (error) {
if (error === sentinel) return 'sentinel';
if (callee && error.name === 'TypeError' && Object.getPrototypeOf(error) !== callee.TypeError.prototype) return 'wrong-realm-TypeError';
return error.name;
}
};
const htmlNS = 'http://www.w3.org/1999/xhtml';
const frame = document.createElement('iframe'); document.body.appendChild(frame);
let popup;
try {
const realms = [['main', window], ['iframe', frame.contentWindow]];
if (globalThis.__elementOptionsIncludePopup !== false) {
popup = window.open('', '_blank');
if (!popup) throw new Error('popup unavailable');
await new Promise(resolve => setTimeout(resolve, 0));
realms.push(['popup', popup]);
}
for (const [ownerName, owner] of realms) {
const docs = [
['live', owner.document],
['html', owner.document.implementation.createHTMLDocument('')],
['xml', owner.document.implementation.createDocument(null, 'root')],
['parsed-html', new owner.DOMParser().parseFromString('<!doctype html><body>', 'text/html')],
['parsed-xml', new owner.DOMParser().parseFromString('<root/>', 'application/xml')],
['new-document', new owner.Document()],
];
const scoped = new owner.CustomElementRegistry();
for (const [docName, doc] of docs) {
for (const [calleeName, callee] of realms) {
for (const ns of [false, true]) {
const prefix = `${ownerName}/${docName}/${calleeName}/${ns ? 'createElementNS' : 'createElement'}`;
const method = callee.Document.prototype[ns ? 'createElementNS' : 'createElement'];
const create = (tag, options, namespace = htmlNS) => ns ? method.call(doc, namespace, tag, options) : method.call(doc, tag, options);
for (const valid of [false, true]) {
for (const stop of [-1, 0, 1, 2, 3, 4]) {
const log = [], sentinel = {};
const argument = (text, label, position) => ({toString() {log.push(label); if (stop === position) throw sentinel; return text;}});
const options = {
get customElementRegistry() {log.push('registry'); if (stop === 2) throw sentinel; return undefined;},
get is() {log.push('is'); if (stop === 3) throw sentinel; return argument('x-order', 'is-string', 4);},
};
const tag = argument(valid ? 'div' : 'a b', 'name', 1);
const namespace = argument(htmlNS, 'namespace', 0);
const labels = ns ? ['namespace', 'name', 'registry', 'is', 'is-string'] : ['name', 'registry', 'is', 'is-string'];
const stopLabel = ['namespace', 'name', 'registry', 'is', 'is-string'][stop];
const stopIndex = labels.indexOf(stopLabel);
const expectedLog = stopIndex < 0 ? labels : labels.slice(0, stopIndex + 1);
check(prefix + `/order-${valid}-${stop}`, [capture(() => create(tag, options, namespace), sentinel, callee), log], [stopIndex < 0 ? (valid ? null : 'InvalidCharacterError') : 'sentinel', expectedLog]);
}
}
const plain = observe(() => create('button', undefined).outerHTML);
for (const value of [undefined, null, 'x-ignored', 0, 1, false, true, 1n]) {
check(prefix + `/legacy-${typeof value}-${String(value)}`, observe(() => create('button', value).outerHTML), plain);
}
for (const valid of [false, true]) check(prefix + `/symbol-${valid}`, capture(() => create(valid ? 'div' : 'a b', Symbol()), undefined, callee), 'TypeError');
for (const value of [{}, {is: undefined}, {customElementRegistry: undefined, is: undefined}]) {
check(prefix + `/missing-is-${checks.length}`, observe(() => [create('button', value).outerHTML, create('button', value).getAttribute('is')]), [plain, null]);
}
for (const value of [null, '', 'x-built-in', 1, false]) {
const log = [], options = {get customElementRegistry() {log.push('registry'); return undefined;}, get is() {log.push('is'); return value;}};
check(prefix + `/is-${String(value)}`, observe(() => [create('button', options).getAttribute('is'), log]), [null, ['registry', 'is']]);
}
for (const make of [() => new owner.String('x-ignored'), () => function options() {}, () => Object.create({is: 'x-inherited'})]) {
const options = make(), log = [];
Object.defineProperty(options, 'customElementRegistry', {get() {log.push('registry'); return undefined;}});
Object.defineProperty(options, 'is', {get() {log.push('is'); return 'x-boxed';}});
Object.defineProperty(options, 'toString', {value() {throw new Error('dictionary toString must not run');}});
check(prefix + `/dictionary-object-${checks.length}`, [capture(() => create('button', options), undefined, callee), log], [null, ['registry', 'is']]);
}
for (const registry of [null, scoped]) {
for (const value of [null, '', 'x-built-in']) {
for (const valid of [false, true]) {
const log = [], options = {get customElementRegistry() {log.push('registry'); return registry;}, get is() {log.push('is'); return value;}};
check(prefix + `/registry-is-${checks.length}`, [capture(() => create(valid ? 'div' : 'a b', options), undefined, callee), log], [valid ? 'NotSupportedError' : 'InvalidCharacterError', ['registry', 'is']]);
}
}
check(prefix + `/registry-identity-${checks.length}`, observe(() => create('div', {customElementRegistry: registry, is: undefined}).customElementRegistry === registry), true);
}
const revoked = owner.Proxy.revocable(scoped, {}); revoked.revoke();
let traps = 0;
const invalidRegistries = [{}, Object.create(scoped), Object.create(owner.CustomElementRegistry.prototype), new owner.Proxy(scoped, {get() {traps++; throw 42;}, getPrototypeOf() {traps++; throw 42;}}), revoked.proxy, false, 1, 'registry', Symbol()];
for (const registry of invalidRegistries) {
const log = [], options = {get customElementRegistry() {log.push('registry'); return registry;}, get is() {log.push('unexpected'); return 'x-built-in';}};
check(prefix + `/invalid-registry-${checks.length}`, [capture(() => create('a b', options), undefined, callee), log, traps], ['TypeError', ['registry'], 0]);
}
const optionsProxy = new owner.Proxy({}, {get(target, key) {return key === 'customElementRegistry' ? undefined : key === 'is' ? 'x-proxy-options' : Reflect.get(target, key);}});
check(prefix + '/options-author-proxy', capture(() => create('div', optionsProxy), undefined, callee), null);
const revokedOptions = owner.Proxy.revocable({}, {}); revokedOptions.revoke();
check(prefix + '/options-revoked-proxy', capture(() => create('a b', revokedOptions.proxy), undefined, callee), 'TypeError');
for (let count = 0; count < (ns ? 2 : 1); count++) {
let conversions = 0;
const args = Array.from({length: count}, () => ({toString() {conversions++; throw 42;}}));
check(prefix + `/arity-${count}`, [capture(() => method.call(doc, ...args), undefined, callee), conversions], ['TypeError', 0]);
}
const receivers = [{}, Object.create(doc), new owner.Proxy(doc, {})];
const revokedDocument = owner.Proxy.revocable(doc, {}); revokedDocument.revoke(); receivers.push(revokedDocument.proxy);
for (const receiver of receivers) {
let conversions = 0;
const value = {toString() {conversions++; throw 42;}};
const args = ns ? [value, value, value] : [value, value];
check(prefix + `/receiver-${checks.length}`, [capture(() => method.call(receiver, ...args), undefined, callee), conversions], ['TypeError', 0]);
}
if (ns) {
const log = [], sentinel = {};
check(prefix + '/namespace-before-flatten', [capture(() => create('p:div', {customElementRegistry: null, is: 'x-built-in'}, null), undefined, callee)], ['NamespaceError']);
const options = {get customElementRegistry() {log.push('registry'); return undefined;}, get is() {log.push('is'); throw sentinel;}};
check(prefix + '/options-before-namespace', [capture(() => create('p:div', options, null), sentinel, callee), log], ['sentinel', ['registry', 'is']]);
}
}
}
}
}
for (const [name, w] of realms) {
for (const ns of [false, true]) {
const label = `${name}/definition-reentrancy/${ns}`;
const registry = new w.CustomElementRegistry();
let constructed = 0;
class Created extends w.HTMLElement {constructor() {super(); constructed++;}}
const options = {customElementRegistry: registry, get is() {registry.define('x-options-' + ns, Created); return undefined;}};
check(label, observe(() => {
const element = ns ? w.document.createElementNS(htmlNS, 'x-options-' + ns, options) : w.document.createElement('x-options-' + ns, options);
return [element instanceof Created, element.ownerDocument === w.document, element.customElementRegistry === registry, constructed];
}), [true, true, true, 1]);
const sentinel = {};
const badOptions = {customElementRegistry: registry, get is() {throw sentinel;}};
check(label + '/no-construction-after-throw', [capture(() => ns ? w.document.createElementNS(htmlNS, 'x-options-' + ns, badOptions) : w.document.createElement('x-options-' + ns, badOptions), sentinel), constructed], ['sentinel', 1]);
}
}
// Genuine cross-realm registries within one Page share the native host.
const foreign = new frame.contentWindow.CustomElementRegistry();
for (const ns of [false, true]) {
check(`cross-realm-registry/${ns}`, observe(() => {
const element = ns ? document.createElementNS(null, 'foreign', {customElementRegistry: foreign}) : document.createElement('foreign', {customElementRegistry: foreign});
return element.customElementRegistry === foreign;
}), true);
}
} catch (error) {checks.push({name: 'uncaught', actual: String(error), expected: 'no exception', passed: false});}
finally { frame.remove(); if (popup) popup.close(); }
globalThis.__uiEventResults = {complete: true, includePopup: Boolean(popup), total: checks.length, passed: checks.filter(c => c.passed).length, checks};
return checks.every(c => c.passed);
})()
@@ -520,3 +520,32 @@ async fn dom_implementation_derives_receiver_and_argument_conversion_in_each_doc
assert_eq!(vm.eval("__uiEventResults.includePopup").unwrap(), "false");
assert_eq!(vm.eval("__uiEventResults.total").unwrap(), "4344");
}
#[tokio::test(flavor = "current_thread")]
async fn element_creation_options_convert_before_dom_validation_in_each_document_realm() {
let loader = static_http_loader(std::iter::empty::<String>());
let mut vm = new_page_task_executor_test_vm_with_loader(
"https://element-creation-options.test/",
&loader,
);
// Real popup Pages run the same fixture over CDP.
vm.exec("globalThis.__elementOptionsIncludePopup = false;", None)
.unwrap();
vm.exec(include_str!("element_creation_options.js"), None)
.unwrap();
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(globalThis.__uiEventResults?.complete === true)",
"true",
"element creation options conversion",
)
.await;
assert_eq!(
vm.eval("JSON.stringify(__uiEventResults.checks.filter(row => !row.passed))")
.unwrap(),
"[]",
);
assert_eq!(vm.eval("__uiEventResults.includePopup").unwrap(), "false");
assert_eq!(vm.eval("__uiEventResults.total").unwrap(), "3106");
}