mirror of
https://github.com/lexmount/moli.git
synced 2026-10-09 00:01:06 +00:00
fix(html): share template contents owner documents
This commit is contained in:
@@ -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
@@ -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/");
|
||||
|
||||
Reference in New Issue
Block a user