diff --git a/moli-renderer-v8/src/custom_elements/registry.rs b/moli-renderer-v8/src/custom_elements/registry.rs index 27d33b151c..90ece0b1b8 100644 --- a/moli-renderer-v8/src/custom_elements/registry.rs +++ b/moli-renderer-v8/src/custom_elements/registry.rs @@ -56,21 +56,6 @@ pub(crate) struct CustomElementAdoptionPlan { pub(crate) registry_retargets: Vec, } -impl CustomElementAdoptionPlan { - pub(crate) fn has_targets(&self) -> bool { - !self.targets.is_empty() - } - - pub(crate) fn has_registry_retargets_without_adoption(&self) -> bool { - self.targets.is_empty() && !self.registry_retargets.is_empty() - } - - pub(crate) fn extend(&mut self, other: Self) { - self.targets.extend(other.targets); - self.registry_retargets.extend(other.registry_retargets); - } -} - #[cfg(test)] mod tests { use super::{CustomElementRegistryAssociation, CustomElementRegistryKey}; diff --git a/moli-renderer-v8/src/custom_elements/registry_adoption_retarget.rs b/moli-renderer-v8/src/custom_elements/registry_adoption_retarget.rs index 59d943dc9c..46f337a157 100644 --- a/moli-renderer-v8/src/custom_elements/registry_adoption_retarget.rs +++ b/moli-renderer-v8/src/custom_elements/registry_adoption_retarget.rs @@ -8,32 +8,37 @@ use super::{ }; use crate::{document_runtime::DomHandle, dom::native::Node, native_bridge::JsContextHost}; -fn adoption_plan_before_adoption( - host_ptr: *mut JsContextHost, - root: DomHandle, - new_document: DomHandle, -) -> CustomElementAdoptionPlan { - let host = unsafe { &*host_ptr }; - CustomElementAdoptionPlan { - targets: adoption_callback_targets(host.dom_host(), root, new_document), - registry_retargets: registry_association_retargets_before_adoption( - host_ptr, - root, - new_document, - ), - } -} - pub(crate) fn adoption_plan_for_roots_before_adoption( host_ptr: *mut JsContextHost, roots: &[DomHandle], new_document: DomHandle, + crosses_documents: bool, ) -> CustomElementAdoptionPlan { - let mut plan = CustomElementAdoptionPlan::default(); + let host = unsafe { &*host_ptr }; + if !crosses_documents && !host.has_explicit_custom_element_registry_associations() { + return CustomElementAdoptionPlan::default(); + } + + let mut targets = Vec::new(); + let mut registry_retargets = Vec::new(); for &root in roots { - plan.extend(adoption_plan_before_adoption(host_ptr, root, new_document)); + if crosses_documents { + targets.extend(adoption_callback_targets( + host.dom_host(), + root, + new_document, + )); + } + registry_retargets.extend(registry_association_retargets_before_adoption( + host_ptr, + root, + new_document, + )); + } + CustomElementAdoptionPlan { + targets, + registry_retargets, } - plan } fn registry_association_retargets_before_adoption( diff --git a/moli-renderer-v8/src/document_runtime/dom_facade.rs b/moli-renderer-v8/src/document_runtime/dom_facade.rs index b8b3af578e..9b23874527 100644 --- a/moli-renderer-v8/src/document_runtime/dom_facade.rs +++ b/moli-renderer-v8/src/document_runtime/dom_facade.rs @@ -712,33 +712,23 @@ impl DocumentRuntime { document_handle: DomHandle, handle: DomHandle, ) -> Option { - let plan = self.tree_adoption_plan(host_ptr, document_handle, handle); - self.apply_native_adoption_plan(scope, host_ptr, &plan) - } - - fn tree_adoption_plan( - &self, - host_ptr: *mut JsContextHost, - document_handle: DomHandle, - handle: DomHandle, - ) -> TreeAdoptionPlan { - TreeAdoptionPlan::before_adoption( + let plan = TreeAdoptionPlan::before_standalone_adoption( &self.dom_host, host_ptr, - std::slice::from_ref(&handle), + handle, document_handle, - true, - ) + ); + self.apply_native_adoption_plan(scope, host_ptr, handle, &plan) } fn apply_native_adoption_plan( &mut self, scope: &mut v8::PinScope<'_, '_>, host_ptr: *mut JsContextHost, + root: DomHandle, plan: &TreeAdoptionPlan, ) -> Option { - let (root, previous_owner_document) = plan.root_with_previous_owner_document()?; - let new_document = plan.new_document()?; + let (previous_owner_document, new_document) = plan.documents()?; let (adopted, stylesheet_owner_changes) = self .dom_host .adopt_node_with_stylesheet_owner_changes(new_document, root)?; @@ -746,7 +736,7 @@ impl DocumentRuntime { host_ptr, &plan.custom_elements().registry_retargets, ); - if previous_owner_document.is_some_and(|owner| owner != new_document) { + if previous_owner_document != new_document { self.queue_image_loads_after_owner_document_change(scope, host_ptr, root); } custom_elements::enqueue_adopted_callbacks( @@ -770,11 +760,11 @@ impl DocumentRuntime { scope: &mut v8::PinScope<'_, '_>, host_ptr: *mut JsContextHost, handle: DomHandle, - previous_owner_document: Option, + previous_owner_document: DomHandle, document_handle: DomHandle, stylesheet_owner_changes: &[crate::dom::native::DomStylesheetOwnerChange], ) { - if !previous_owner_document.is_some_and(|owner| owner != document_handle) { + if previous_owner_document == document_handle { return; } let runtime = unsafe { &mut *host_ptr }; diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/adoption.rs b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/adoption.rs index 00d7a2f424..b1986953cf 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/adoption.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/adoption.rs @@ -2,67 +2,95 @@ use super::insertion_plan::TreeInsertionPlan; use crate::{ custom_elements, document_runtime::{DocumentRuntime, DomHandle}, - dom::native::{DomHost, Node}, + dom::native::DomHost, native_bridge::JsContextHost, }; #[derive(Clone, Debug, Default)] pub(in crate::document_runtime) struct TreeAdoptionPlan { - root: Option, - previous_owner_document: Option, - new_document: Option, - roots_with_owner_document_change: Vec, + transition: Option, custom_elements: custom_elements::CustomElementAdoptionPlan, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct TreeDocumentTransition { + previous_document: DomHandle, + new_document: DomHandle, +} + +impl TreeDocumentTransition { + fn documents(self) -> (DomHandle, DomHandle) { + (self.previous_document, self.new_document) + } + + fn crosses_documents(self) -> bool { + self.previous_document != self.new_document + } + + fn cross_document(self) -> Option<(DomHandle, DomHandle)> { + self.crosses_documents().then_some(self.documents()) + } + + fn for_root(dom_host: &DomHost, root: DomHandle, new_document: DomHandle) -> Option { + Some(Self { + previous_document: dom_host.owner_document_handle(root)?, + new_document, + }) + } +} + impl TreeAdoptionPlan { - pub(in crate::document_runtime) fn before_adoption( + pub(in crate::document_runtime) fn before_standalone_adoption( + dom_host: &DomHost, + host_ptr: *mut JsContextHost, + root: DomHandle, + new_document: DomHandle, + ) -> Self { + Self::before_adoption( + dom_host, + host_ptr, + std::slice::from_ref(&root), + new_document, + ) + } + + fn before_adoption( dom_host: &DomHost, host_ptr: *mut JsContextHost, roots: &[DomHandle], new_document: DomHandle, - collect_custom_elements: bool, ) -> Self { - let root = roots.first().copied(); - let previous_owner_document = - root.and_then(|root| dom_host.node(root).and_then(Node::owner_document)); - let roots_with_owner_document_change = roots - .iter() - .copied() - .filter(|root| { - dom_host - .node(*root) - .and_then(Node::owner_document) - .is_some_and(|previous| previous != new_document) - }) - .collect(); - let custom_elements = if collect_custom_elements { - custom_elements::adoption_plan_for_roots_before_adoption(host_ptr, roots, new_document) - } else { - custom_elements::CustomElementAdoptionPlan::default() - }; + // The roots of one DOM insertion share a node document, including + // children hoisted from a DocumentFragment. One lookup classifies the + // whole batch. + let transition = roots + .first() + .and_then(|root| TreeDocumentTransition::for_root(dom_host, *root, new_document)); + let custom_elements = transition.map_or_else(Default::default, |transition| { + custom_elements::adoption_plan_for_roots_before_adoption( + host_ptr, + roots, + new_document, + transition.crosses_documents(), + ) + }); Self { - root, - previous_owner_document, - new_document: Some(new_document), - roots_with_owner_document_change, + transition, custom_elements, } } - pub(in crate::document_runtime) fn root_with_previous_owner_document( - &self, - ) -> Option<(DomHandle, Option)> { - self.root.map(|root| (root, self.previous_owner_document)) + pub(in crate::document_runtime) fn documents(&self) -> Option<(DomHandle, DomHandle)> { + self.transition.map(TreeDocumentTransition::documents) } - pub(super) fn has_targets(&self) -> bool { - self.custom_elements.has_targets() + pub(super) fn crosses_documents(&self) -> bool { + self.transition + .is_some_and(TreeDocumentTransition::crosses_documents) } - pub(super) fn has_registry_retargets_without_adoption(&self) -> bool { - self.custom_elements - .has_registry_retargets_without_adoption() + fn cross_document(&self) -> Option<(DomHandle, DomHandle)> { + self.transition?.cross_document() } pub(in crate::document_runtime) fn custom_elements( @@ -70,14 +98,6 @@ impl TreeAdoptionPlan { ) -> &custom_elements::CustomElementAdoptionPlan { &self.custom_elements } - - pub(in crate::document_runtime) fn roots_with_owner_document_change(&self) -> &[DomHandle] { - &self.roots_with_owner_document_change - } - - pub(in crate::document_runtime) fn new_document(&self) -> Option { - self.new_document - } } impl DocumentRuntime { @@ -87,17 +107,10 @@ impl DocumentRuntime { roots: &[DomHandle], parent: DomHandle, ) -> TreeAdoptionPlan { - let Some(new_document) = self.document_for_insertion_parent(parent) else { + let Some(new_document) = self.dom_host.owner_document_handle(parent) else { return TreeAdoptionPlan::default(); }; - TreeAdoptionPlan::before_adoption(&self.dom_host, host_ptr, roots, new_document, true) - } - - fn document_for_insertion_parent(&self, parent: DomHandle) -> Option { - if self.dom_host.node(parent).is_some_and(Node::is_document) { - return Some(parent); - } - self.dom_host.owner_document_handle(parent) + TreeAdoptionPlan::before_adoption(&self.dom_host, host_ptr, roots, new_document) } pub(super) fn sync_shadow_root_adopted_style_sheets_after_insertion_adoption( @@ -106,7 +119,7 @@ impl DocumentRuntime { host_ptr: *mut JsContextHost, insertion_plan: &TreeInsertionPlan<'_>, ) { - let Some(new_document) = insertion_plan.adoption.new_document() else { + let Some((_, new_document)) = insertion_plan.adoption.cross_document() else { return; }; if unsafe { &*host_ptr } @@ -116,7 +129,7 @@ impl DocumentRuntime { return; } let runtime = unsafe { &mut *host_ptr }; - for &root in insertion_plan.adoption.roots_with_owner_document_change() { + for &root in insertion_plan.insertion_roots { for shadow_root in runtime.shadow_roots_in_subtree(root) { crate::native_bridge::element::clear_shadow_root_adopted_style_sheets( scope, @@ -136,40 +149,51 @@ mod tests { use url::Url; #[test] - fn adoption_plan_records_only_roots_that_change_owner_document() { + fn fragment_insertion_roots_share_one_document_transition() { let mut dom_host = DomHost::from_dom(NativeDom::new( Url::parse("https://example.test/").expect("test URL parses"), )); let target_document = dom_host.document_handle(); let other_document = dom_host.create_detached_html_document(); - let same_document_root = dom_host.create_parser_element_without_attributes_for_document( - target_document, + let foreign_fragment = dom_host.create_document_fragment_for_document(other_document); + let first_root = dom_host.create_parser_element_without_attributes_for_document( + other_document, "div".to_owned(), "http://www.w3.org/1999/xhtml".to_owned(), None, ); - let other_document_root = dom_host.create_parser_element_without_attributes_for_document( + let second_root = dom_host.create_parser_element_without_attributes_for_document( other_document, "span".to_owned(), "http://www.w3.org/1999/xhtml".to_owned(), None, ); - - let plan = TreeAdoptionPlan::before_adoption( - &dom_host, - std::ptr::null_mut(), - &[same_document_root, other_document_root], - target_document, - false, - ); - + assert!(dom_host.append_child(foreign_fragment, first_root)); + assert!(dom_host.append_child(foreign_fragment, second_root)); assert_eq!( - plan.root_with_previous_owner_document(), - Some((same_document_root, Some(target_document))) + dom_host.owner_document_handle(first_root), + Some(other_document) ); assert_eq!( - plan.roots_with_owner_document_change(), - &[other_document_root] + dom_host.owner_document_handle(second_root), + Some(other_document) + ); + + // A DocumentFragment adopts each appended child into its own node + // document, so either root classifies the whole insertion batch. + assert_eq!( + TreeDocumentTransition::for_root(&dom_host, first_root, target_document), + Some(TreeDocumentTransition { + previous_document: other_document, + new_document: target_document, + }) + ); + assert_eq!( + TreeDocumentTransition::for_root(&dom_host, first_root, other_document), + Some(TreeDocumentTransition { + previous_document: other_document, + new_document: other_document, + }) ); } } diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/context_followups.rs b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/context_followups.rs index e6c741e434..46f3460b78 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/context_followups.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/context_followups.rs @@ -12,8 +12,10 @@ impl DocumentRuntime { insertion_plan: &TreeInsertionPlan<'_>, ) { let runtime = unsafe { &mut *host_ptr }; - for &root in insertion_plan.adoption.roots_with_owner_document_change() { - runtime.migrate_inline_style_metadata_in_subtree(root); + if insertion_plan.adoption.crosses_documents() { + for &root in insertion_plan.insertion_roots { + runtime.migrate_inline_style_metadata_in_subtree(root); + } } for &root in insertion_plan.insertion_roots { runtime.clear_disconnected_shadow_roots_in_subtree(root); diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/insertion_followups.rs b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/insertion_followups.rs index 3f36c07f92..6f1afacf50 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/insertion_followups.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/insertion_followups.rs @@ -152,7 +152,7 @@ impl DocumentRuntime { sync_upgrade_connected_subtrees: bool, ) { let was_connected = insertion_plan.was_lifecycle_connected_before_insert(); - let adopted_across_documents = insertion_plan.adopted_across_documents(); + let adopted_across_documents = insertion_plan.adoption.crosses_documents(); if was_connected && adopted_across_documents && !insertion_plan.inserting_fragment_children { self.enqueue_adoption_disconnected_callbacks_in_subtrees_unless_pending( diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/insertion_plan.rs b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/insertion_plan.rs index 8f1511e3f7..2389c4dc2f 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/insertion_plan.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/insertion_plan.rs @@ -48,10 +48,6 @@ impl TreeInsertionPlan<'_> { pub(super) fn was_lifecycle_connected_before_insert(&self) -> bool { !self.lifecycle_connected_roots_before_insert.is_empty() } - - pub(super) fn adopted_across_documents(&self) -> bool { - self.adoption.has_targets() - } } impl TreeInsertionLiveRangeMode { diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/parser.rs b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/parser.rs index da83dba1c8..00105ae72f 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/parser.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/parser.rs @@ -195,10 +195,7 @@ impl DocumentRuntime { insertion_plan, profile.subresource_request_initiator_type(), ); - if insertion_plan - .adoption - .has_registry_retargets_without_adoption() - { + if !insertion_plan.adoption.crosses_documents() { custom_elements::apply_registry_association_retargets( host_ptr, &insertion_plan.adoption.custom_elements().registry_retargets, @@ -227,7 +224,7 @@ impl DocumentRuntime { ) { let lifecycle_quiescent = unsafe { &*host_ptr }.custom_elements_subtree_lifecycle_quiescent(); - let adopted_across_documents = insertion_plan.adoption.has_targets(); + let adopted_across_documents = insertion_plan.adoption.crosses_documents(); let mut connected_roots = Vec::new(); let mut removed_from_lifecycle_roots = Vec::new(); let mut form_state_roots = Vec::new(); diff --git a/moli-renderer-v8/src/native_bridge/context_host/core.rs b/moli-renderer-v8/src/native_bridge/context_host/core.rs index a4d060102d..e2697badc7 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -1034,6 +1034,10 @@ impl JsContextHost { .message_port_delivery() } + pub(crate) fn has_explicit_custom_element_registry_associations(&self) -> bool { + !self.custom_element_registry_associations.is_empty() + } + #[cfg(test)] pub(crate) fn bridge_ref_count_for_test(&self) -> usize { self.bridge_ref_count.get()