fix(dom): expose inert template documents through Document

Track the inert template-document role separately from HTML/XML semantics.
Choose the Document wrapper for that role while preserving the source kind
for HTML name normalization and XML element creation.

Cover parser/createElement/innerHTML paths in main and child documents,
DOMParser and createHTMLDocument sources, and XHTML contents. Ordinary
Document interfaces retain their existing identity.

Validation: cargo fmt --all; strict workspace/all-targets/all-features
Clippy; 86 template-related tests across three stress iterations; full
Nextest 21,167 passed, 19 skipped, without retries.
This commit is contained in:
ldm0
2026-10-28 07:16:00 +08:00
parent 409d5d8842
commit 3dc8bcef3a
4 changed files with 80 additions and 9 deletions
+16
View File
@@ -52,6 +52,7 @@ pub struct Document {
ready_state: DocumentReadyState,
quirks_mode: QuirksMode,
kind: DocumentKind,
is_inert_template_document: bool,
design_mode_enabled: bool,
css_target: Option<NativeNodeId>,
default_language: Option<Box<str>>,
@@ -73,6 +74,7 @@ impl Document {
ready_state: DocumentReadyState::Complete,
quirks_mode: QuirksMode::NoQuirks,
kind: DocumentKind::Html,
is_inert_template_document: false,
design_mode_enabled: false,
css_target: None,
default_language: None,
@@ -89,6 +91,7 @@ impl Document {
ready_state: DocumentReadyState::Complete,
quirks_mode: QuirksMode::NoQuirks,
kind: DocumentKind::Xml,
is_inert_template_document: false,
design_mode_enabled: false,
css_target: None,
default_language: None,
@@ -96,6 +99,15 @@ impl Document {
}
}
pub(super) fn new_inert_template(url: Url, kind: DocumentKind) -> Self {
let mut document = match kind {
DocumentKind::Html => Self::new_html(url),
DocumentKind::Xml => Self::new_xml(url),
};
document.is_inert_template_document = true;
document
}
pub fn url(&self) -> &Url {
&self.url
}
@@ -144,6 +156,10 @@ impl Document {
self.kind == DocumentKind::Html
}
pub fn is_inert_template_document(&self) -> bool {
self.is_inert_template_document
}
pub fn design_mode_enabled(&self) -> bool {
self.design_mode_enabled
}
+5 -8
View File
@@ -13,6 +13,7 @@ mod scripts;
use std::sync::Arc;
use document::DocumentKind;
pub use document::{
Document, DocumentFragment, DocumentReadyState, DocumentTitleSetterTarget, DocumentType,
};
@@ -518,21 +519,17 @@ impl NativeDom {
document_handle: NativeNodeId,
host: Option<NativeNodeId>,
) -> NativeNodeId {
let (url, is_html_document) = self
let (url, kind) = self
.node(document_handle)
.and_then(Node::as_document)
.map(|document| (document.url().clone(), document.is_html_document()))
.map(|document| (document.url().clone(), document.kind()))
.unwrap_or_else(|| {
(
url::Url::parse("about:blank").expect("about:blank is valid"),
true,
DocumentKind::Html,
)
});
let document = if is_html_document {
Document::new_html(url)
} else {
Document::new_xml(url)
};
let document = Document::new_inert_template(url, kind);
let owner_document =
self.create_node(NodeData::Document(Box::new(document)), None, false, false);
self.create_node(
@@ -80,7 +80,9 @@ 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 document.is_inert_template_document() {
"Document"
} else if document.is_html_document() {
"HTMLDocument"
} else {
"XMLDocument"
@@ -907,6 +907,62 @@ fn detached_html_template_contents_preserve_html_element_names() {
);
}
#[test]
fn template_contents_use_the_document_interface_with_source_document_semantics() {
let mut vm = new_parsed_test_vm(
"https://template-content-interface.test/",
r#"<!doctype html><body>
<template id="parsed"><div>main</div></template>
<iframe></iframe>
</body>"#,
);
let result = vm
.eval(
r#"(() => {
const check = (ok, label) => { if (!ok) throw Error(label); };
const child = document.querySelector('iframe').contentDocument;
child.open();
child.write('<!doctype html><body><template id="parsed"><div>child</div></template>');
child.close();
const documents = [
['main', document],
['child', child],
['created', document.implementation.createHTMLDocument('')],
['parsed', new DOMParser().parseFromString('<body></body>', 'text/html')]
];
const checkTemplate = (template, label, html) => {
const owner = template.content.ownerDocument;
check(Object.prototype.toString.call(owner) === '[object Document]', label + ' interface');
check(owner.defaultView === null, label + ' inert');
check(owner.createElement('DIV').localName === (html ? 'div' : 'DIV'), label + ' kind');
};
for (const [label, doc] of documents) {
check(Object.prototype.toString.call(doc) === '[object HTMLDocument]', label + ' normal interface');
checkTemplate(doc.createElement('template'), label + ' created', true);
const host = doc.createElement('div');
host.innerHTML = '<template><div>content</div></template>';
checkTemplate(host.firstElementChild, label + ' fragment parsed', true);
const parsed = doc.querySelector('#parsed');
if (label === 'main' || label === 'child') {
check(parsed !== null, label + ' parsed fixture');
checkTemplate(parsed, label + ' document parsed', true);
}
}
const xml = new DOMParser().parseFromString(
'<template xmlns="http://www.w3.org/1999/xhtml"><title>xml</title></template>',
'application/xhtml+xml'
);
check(Object.prototype.toString.call(xml) === '[object Document]', 'normal XML interface');
checkTemplate(xml.documentElement, 'XML template', false);
return 'ok';
})()"#,
)
.expect("inert document interfaces should be independent of HTML/XML semantics");
assert_eq!(result, "ok");
}
#[test]
fn template_content_host_rejects_insertion_cycles_after_adoption() {
let mut vm = new_storage_test_vm("https://template-content-host.test/");