diff --git a/moli-dom/src/native/host/clone.rs b/moli-dom/src/native/host/clone.rs index 587ca84584..bfd869c380 100644 --- a/moli-dom/src/native/host/clone.rs +++ b/moli-dom/src/native/host/clone.rs @@ -722,6 +722,68 @@ mod tests { assert_eq!(element_chain_depth(&host, root), DEEP_TREE_DEPTH); } + #[test] + fn template_adoption_retargets_nested_contents_to_the_shared_inert_document() { + let mut host = test_host(); + let target_document = host.create_detached_html_document(); + let template = host.create_element("template"); + let contents = host + .node(template) + .and_then(Node::as_element) + .and_then(Element::template_contents) + .expect("template contents"); + let source_contents_document = host + .node(contents) + .and_then(Node::owner_document) + .expect("source contents document"); + let nested_template = host.create_element("template"); + let nested_contents = host + .node(nested_template) + .and_then(Node::as_element) + .and_then(Element::template_contents) + .expect("nested template contents"); + + assert!(host.append_child(contents, nested_template)); + assert_eq!( + host.node(nested_template).and_then(Node::owner_document), + Some(source_contents_document) + ); + assert_eq!( + host.node(nested_contents).and_then(Node::owner_document), + Some(source_contents_document) + ); + + assert_eq!(host.adopt_node(target_document, template), Some(template)); + let target_contents_document = host + .node(contents) + .and_then(Node::owner_document) + .expect("target contents document"); + assert_ne!(target_contents_document, source_contents_document); + assert_ne!(target_contents_document, target_document); + assert_eq!( + host.node(nested_template).and_then(Node::owner_document), + Some(target_contents_document) + ); + assert_eq!( + host.node(nested_contents).and_then(Node::owner_document), + Some(target_contents_document) + ); + + let sibling_template = host.create_element("template"); + assert_eq!( + host.adopt_node(target_document, sibling_template), + Some(sibling_template) + ); + let sibling_contents_document = host + .node(sibling_template) + .and_then(Node::as_element) + .and_then(Element::template_contents) + .and_then(|contents| host.node(contents)) + .and_then(Node::owner_document) + .expect("sibling contents document"); + assert_eq!(sibling_contents_document, target_contents_document); + } + #[test] fn deep_normalize_walks_iteratively() { let mut host = test_host(); diff --git a/moli-dom/src/native/host/mutation/owner_lifecycle.rs b/moli-dom/src/native/host/mutation/owner_lifecycle.rs index eade2cc416..a738d5d650 100644 --- a/moli-dom/src/native/host/mutation/owner_lifecycle.rs +++ b/moli-dom/src/native/host/mutation/owner_lifecycle.rs @@ -427,7 +427,17 @@ impl NativeDom { traversal: &mut OwnerLifecycleTraversal, ) { let mut stack = vec![root]; + let mut template_contents_to_adopt = Vec::new(); while let Some(handle) = stack.pop() { + let previous_owner_document = self.node(handle).and_then(Node::owner_document); + let template_contents = (previous_owner_document != owner_document) + .then(|| { + self.node(handle) + .and_then(Node::as_element) + .filter(|element| element.is_html_element("template")) + .and_then(|element| element.template_contents()) + }) + .flatten(); let maintains_candidates = traversal.collect_stylesheet_owners || !matches!( traversal.registry_mutation, @@ -464,6 +474,43 @@ impl NativeDom { let node = self.node_mut(handle).expect("node must exist"); node.set_tree_scope(owner_document, connected, in_document_tree); stack.extend(self.child_ids_reversed(handle)); + if let (Some(template_contents), Some(document)) = (template_contents, owner_document) { + template_contents_to_adopt.push((template_contents, document)); + } + } + self.retarget_adopted_template_contents(template_contents_to_adopt, traversal); + } + + fn retarget_adopted_template_contents( + &mut self, + mut pending: Vec<(NativeNodeId, NativeNodeId)>, + traversal: &mut OwnerLifecycleTraversal, + ) { + while let Some((contents, template_document)) = pending.pop() { + let contents_document = + self.appropriate_template_contents_owner_document(template_document); + let mut stack = vec![contents]; + while let Some(handle) = stack.pop() { + let previous_owner_document = self.node(handle).and_then(Node::owner_document); + let nested_template_contents = (previous_owner_document != Some(contents_document)) + .then(|| { + self.node(handle) + .and_then(Node::as_element) + .filter(|element| element.is_html_element("template")) + .and_then(|element| element.template_contents()) + }) + .flatten(); + if traversal.collect_stylesheet_owners && self.is_stylesheet_candidate(handle) { + traversal.changes.stylesheet_owners.push(handle); + } + self.node_mut(handle) + .expect("template content node must exist") + .set_tree_scope(Some(contents_document), false, false); + stack.extend(self.child_ids_reversed(handle)); + if let Some(nested_template_contents) = nested_template_contents { + pending.push((nested_template_contents, contents_document)); + } + } } } } diff --git a/moli-dom/src/native/mod.rs b/moli-dom/src/native/mod.rs index c8b8ce5f41..a6e9c884e2 100644 --- a/moli-dom/src/native/mod.rs +++ b/moli-dom/src/native/mod.rs @@ -8,7 +8,7 @@ mod node; mod queries; mod scripts; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; pub use document::{ Document, DocumentFragment, DocumentReadyState, DocumentTitleSetterTarget, DocumentType, @@ -143,6 +143,7 @@ impl std::iter::FusedIterator for NativeDomNodes<'_> {} pub struct NativeDom { nodes: NativeNodeStorage, document_node_id: NativeNodeId, + inert_template_documents: HashMap, stylesheet_candidate_registries: StylesheetCandidateRegistries, parse_errors: Vec, } @@ -177,6 +178,7 @@ impl NativeDom { Self { nodes: NativeNodeStorage::from_node(document_node), document_node_id, + inert_template_documents: HashMap::new(), stylesheet_candidate_registries: StylesheetCandidateRegistries::default(), parse_errors: Vec::new(), } @@ -194,6 +196,7 @@ impl NativeDom { Self { nodes: NativeNodeStorage::from_node(document_node), document_node_id, + inert_template_documents: HashMap::new(), stylesheet_candidate_registries: StylesheetCandidateRegistries::default(), parse_errors: Vec::new(), } @@ -504,24 +507,58 @@ impl NativeDom { self.create_template_contents_fragment_for_document(self.document_node_id) } + pub fn is_inert_template_document(&self, document_handle: NativeNodeId) -> bool { + self.inert_template_documents.get(&document_handle) == Some(&document_handle) + } + pub fn create_template_contents_fragment_for_document( &mut self, document_handle: NativeNodeId, ) -> NativeNodeId { - let url = self - .node(document_handle) - .and_then(Node::as_document) - .map(|document| document.url().clone()) - .unwrap_or_else(|| url::Url::parse("about:blank").expect("about:blank is valid")); - let owner_document = self.create_node( - NodeData::Document(Box::new(Document::new_html_with_scripting(url, false))), - None, - false, - false, - ); + let owner_document = self.appropriate_template_contents_owner_document(document_handle); self.create_document_fragment_for_document(owner_document) } + pub(crate) fn appropriate_template_contents_owner_document( + &mut self, + document_handle: NativeNodeId, + ) -> NativeNodeId { + if let Some(owner_document) = self.inert_template_documents.get(&document_handle).copied() { + owner_document + } else { + let (url, is_html_document) = self + .node(document_handle) + .and_then(Node::as_document) + .map(|document| (document.url().clone(), document.is_html_document())) + .unwrap_or_else(|| { + ( + url::Url::parse("about:blank").expect("about:blank is valid"), + true, + ) + }); + let inert_document = if is_html_document { + Document::new_html_with_scripting(url, false) + } else { + let mut document = Document::new_xml(url); + document.set_scripting_enabled(false); + document + }; + let owner_document = self.create_node( + NodeData::Document(Box::new(inert_document)), + None, + false, + false, + ); + self.inert_template_documents + .insert(document_handle, owner_document); + // Documents created by this algorithm reuse themselves for nested + // template contents instead of allocating another inert document. + self.inert_template_documents + .insert(owner_document, owner_document); + owner_document + } + } + pub fn create_processing_instruction(&mut self, target: &str, data: &str) -> NativeNodeId { self.create_processing_instruction_for_document(self.document_node_id, target, data) } @@ -2597,6 +2634,23 @@ mod tests { .map(Document::scripting_enabled), Some(false) ); + assert!(dom.is_inert_template_document(content_owner)); + + let second_template = dom.create_element("template"); + let second_content_owner = dom + .node(second_template) + .and_then(Node::as_element) + .and_then(Element::template_contents) + .and_then(|contents| dom.node(contents)) + .and_then(Node::owner_document) + .expect("second template content owner document"); + assert_eq!(second_content_owner, content_owner); + + let nested_content = dom.create_template_contents_fragment_for_document(content_owner); + assert_eq!( + dom.node(nested_content).and_then(Node::owner_document), + Some(content_owner) + ); let child = dom.create_element("span"); assert_eq!( diff --git a/moli-renderer-v8/src/native_bridge/bindings.rs b/moli-renderer-v8/src/native_bridge/bindings.rs index 63a48a80ec..ddf49bcb49 100644 --- a/moli-renderer-v8/src/native_bridge/bindings.rs +++ b/moli-renderer-v8/src/native_bridge/bindings.rs @@ -80,7 +80,13 @@ fn prototype_name_for_handle(host_ptr: *mut JsContextHost, handle: &BridgeHandle .node(*node_handle) .map(|node| match node.data() { crate::dom::native::NodeData::Document(document) => { - if document.is_html_document() { + if runtime + .dom_host() + .dom() + .is_inert_template_document(*node_handle) + { + "Document" + } else if document.is_html_document() { "HTMLDocument" } else { "XMLDocument" diff --git a/moli-renderer-v8/src/native_bridge/document/detached_objects/builders/elements.rs b/moli-renderer-v8/src/native_bridge/document/detached_objects/builders/elements.rs index b88da42ac5..02c20319ec 100644 --- a/moli-renderer-v8/src/native_bridge/document/detached_objects/builders/elements.rs +++ b/moli-renderer-v8/src/native_bridge/document/detached_objects/builders/elements.rs @@ -624,25 +624,6 @@ pub(in crate::native_bridge::document) fn build_detached_element_object<'s>( } } - // HTMLTemplateElement spec: a template's `content` is a DocumentFragment - // owned by the template's *template contents owner document*, not by the - // surrounding document. The standard `HTMLTemplateElement.prototype.content` - // getter reads this native handle for both live and detached wrappers. - if html_like - && local_name == "template" - && let Some((runtime_ptr, template_handle)) = native_handle - && let Some(contents_owner) = - build_detached_document_object(scope, "plain", None, None, None) - && let Some(fragment) = build_detached_document_fragment_object(scope, contents_owner) - && let Some(fragment_handle) = detached_native_handle(scope, fragment) - && let Some(element) = unsafe { &mut *runtime_ptr } - .dom_host_mut() - .node_mut(template_handle) - .and_then(|node| node.data_mut().as_element_mut()) - { - element.set_template_contents(Some(fragment_handle)); - } - // Run custom-element upgrade only after the detached wrapper has its own // DOM surface installed. The HTML constructor returns this object from the // construction stack, so constructor code must not observe a half-built diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs index a246cea6bc..74171b730d 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs @@ -6980,6 +6980,39 @@ fn detached_html_template_content_uses_separate_owner_document() { ); } +#[test] +fn detached_templates_share_their_inert_owner_document() { + let mut vm = new_storage_test_vm("https://detached-template-shared-owner.test/"); + + let result = vm + .eval( + r#" +(() => { + const doc = document.implementation.createHTMLDocument(''); + const first = doc.createElement('template'); + const second = doc.createElement('template'); + const owner = first.content.ownerDocument; + const nested = owner.createElement('template'); + first.content.append(nested); + return [ + owner !== doc, + owner.defaultView === null, + Object.prototype.toString.call(owner) === '[object Document]', + owner instanceof Document, + !(owner instanceof HTMLDocument), + second.content.ownerDocument === owner, + nested.ownerDocument === owner, + nested.content.ownerDocument === owner, + nested.content.ownerDocument.defaultView === null + ].join('|'); +})() +"#, + ) + .expect("detached templates should share one inert owner document"); + + assert_eq!(result, "true|true|true|true|true|true|true|true|true"); +} + #[test] fn detached_html_image_decode_uses_prototype_method() { let mut vm = new_storage_test_vm("https://detached-image-decode-surface.test/");