fix(html): share template contents owner documents

This commit is contained in:
ldm0
2026-09-22 21:43:34 +08:00
parent 15512ddd37
commit 0611caed62
6 changed files with 215 additions and 32 deletions
+62
View File
@@ -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();
@@ -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));
}
}
}
}
}
+66 -12
View File
@@ -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<NativeNodeId, NativeNodeId>,
stylesheet_candidate_registries: StylesheetCandidateRegistries,
parse_errors: Vec<String>,
}
@@ -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!(
@@ -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"
@@ -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
@@ -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/");