script: Set host for template content (#42276)

A template element should set the host of a DocumentFragment. However,
it didn't have a host yet. That's because ShadowRoot declares a host
attribute which returns Element, but its property is declared on
DocumentFragment.

Therefore, define the host on DocumentFragment instead and use it in all
relevant points for shadow roots. Then, update the pre-insert validity
check to use the correct host-including variant of inclusive ancestors.
Lastly, set the host of the template to wire it all up.

---------

Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
This commit is contained in:
Tim van der Lippe
2026-02-02 09:25:54 +01:00
committed by GitHub
parent 17f7c87088
commit e8aa7d51ba
7 changed files with 92 additions and 40 deletions

View File

@@ -14,7 +14,7 @@ use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
use crate::dom::bindings::codegen::UnionTypes::NodeOrString;
use crate::dom::bindings::error::{ErrorResult, Fallible};
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom, MutNullableDom};
use crate::dom::bindings::str::DOMString;
use crate::dom::document::Document;
use crate::dom::element::Element;
@@ -31,14 +31,18 @@ pub(crate) struct DocumentFragment {
node: Node,
/// Caches for the getElement methods
id_map: DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>>,
/// <https://dom.spec.whatwg.org/#concept-documentfragment-host>
host: MutNullableDom<Element>,
}
impl DocumentFragment {
/// Creates a new DocumentFragment.
pub(crate) fn new_inherited(document: &Document) -> DocumentFragment {
pub(crate) fn new_inherited(document: &Document, host: Option<&Element>) -> DocumentFragment {
DocumentFragment {
node: Node::new_inherited(document),
id_map: DomRefCell::new(HashMapTracedValues::new_fx()),
host: MutNullableDom::new(host),
}
}
@@ -52,7 +56,7 @@ impl DocumentFragment {
can_gc: CanGc,
) -> DomRoot<DocumentFragment> {
Node::reflect_node_with_proto(
Box::new(DocumentFragment::new_inherited(document)),
Box::new(DocumentFragment::new_inherited(document, None)),
document,
proto,
can_gc,
@@ -64,6 +68,33 @@ impl DocumentFragment {
) -> &DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>> {
&self.id_map
}
pub(crate) fn host(&self) -> Option<DomRoot<Element>> {
self.host.get()
}
pub(crate) fn set_host(&self, host: &Element) {
self.host.set(Some(host));
}
}
pub(crate) trait LayoutDocumentFragmentHelpers<'dom> {
fn shadowroot_host_for_layout(self) -> LayoutDom<'dom, Element>;
}
impl<'dom> LayoutDocumentFragmentHelpers<'dom> for LayoutDom<'dom, DocumentFragment> {
#[inline]
fn shadowroot_host_for_layout(self) -> LayoutDom<'dom, Element> {
#[expect(unsafe_code)]
unsafe {
// https://dom.spec.whatwg.org/#shadowroot
// > Shadow rootss associated host is never null.
self.unsafe_get()
.host
.get_inner_as_layout()
.expect("Shadow roots's associated host is never null")
}
}
}
impl DocumentFragmentMethods<crate::DomTypeHolder> for DocumentFragment {

View File

@@ -99,9 +99,16 @@ impl HTMLTemplateElementMethods<crate::DomTypeHolder> for HTMLTemplateElement {
/// <https://html.spec.whatwg.org/multipage/#dom-template-content>
fn Content(&self, can_gc: CanGc) -> DomRoot<DocumentFragment> {
self.contents.or_init(|| {
// https://html.spec.whatwg.org/multipage/#template-contents
// Step 1. Let document be the template element's node document's appropriate template contents owner document.
let doc = self.owner_document();
doc.appropriate_template_contents_owner_document(can_gc)
.CreateDocumentFragment(can_gc)
// Step 2. Create a DocumentFragment object whose node document is document and host is the template element.
let document_fragment = doc
.appropriate_template_contents_owner_document(can_gc)
.CreateDocumentFragment(can_gc);
document_fragment.set_host(self.upcast());
// Step 3. Set the template element's template contents to the newly created DocumentFragment object.
document_fragment
})
}
}

View File

@@ -929,11 +929,15 @@ impl Node {
})
}
/// <https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor>
pub(crate) fn is_inclusive_ancestor_of(&self, child: &Node) -> bool {
// > An inclusive ancestor is an object or one of its ancestors.
self == child || self.is_ancestor_of(child)
}
/// <https://dom.spec.whatwg.org/#concept-tree-ancestor>
pub(crate) fn is_ancestor_of(&self, possible_descendant: &Node) -> bool {
// > An object A is called an ancestor of an object B if and only if B is a descendant of A.
let mut current = &possible_descendant.parent_node;
let mut done = false;
@@ -949,6 +953,19 @@ impl Node {
done
}
/// <https://dom.spec.whatwg.org/#concept-tree-host-including-inclusive-ancestor>
fn is_host_including_inclusive_ancestor(&self, child: &Node) -> bool {
// An object A is a host-including inclusive ancestor of an object B, if either A is an inclusive ancestor of B,
// or if Bs root has a non-null host and A is a host-including inclusive ancestor of Bs roots host.
self.is_inclusive_ancestor_of(child) ||
child
.GetRootNode(&GetRootNodeOptions::empty())
.downcast::<DocumentFragment>()
.and_then(|fragment| fragment.host())
.is_some_and(|host| self.is_host_including_inclusive_ancestor(host.upcast()))
}
/// <https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor>
pub(crate) fn is_shadow_including_inclusive_ancestor_of(&self, node: &Node) -> bool {
node.inclusive_ancestors(ShadowIncluding::Yes)
.any(|ancestor| &*ancestor == self)
@@ -2369,27 +2386,29 @@ impl Node {
parent: &Node,
child: Option<&Node>,
) -> ErrorResult {
// Step 1.
// Step 1. If parent is not a Document, DocumentFragment, or Element node, then throw a "HierarchyRequestError" DOMException.
match parent.type_id() {
NodeTypeId::Document(_) | NodeTypeId::DocumentFragment(_) | NodeTypeId::Element(..) => {
},
_ => return Err(Error::HierarchyRequest(None)),
}
// Step 2.
if node.is_inclusive_ancestor_of(parent) {
// Step 2. If node is a host-including inclusive ancestor of parent, then throw a "HierarchyRequestError" DOMException.
if node.is_host_including_inclusive_ancestor(parent) {
return Err(Error::HierarchyRequest(None));
}
// Step 3.
// Step 3. If child is non-null and its parent is not parent, then throw a "NotFoundError" DOMException.
if let Some(child) = child {
if !parent.is_parent_of(child) {
return Err(Error::NotFound(None));
}
}
// Step 4-5.
// Step 4. If node is not a DocumentFragment, DocumentType, Element, or CharacterData node, then throw a "HierarchyRequestError" DOMException.
match node.type_id() {
// Step 5. If either node is a Text node and parent is a document,
// or node is a doctype and parent is not a document, then throw a "HierarchyRequestError" DOMException.
NodeTypeId::CharacterData(CharacterDataTypeId::Text(_)) => {
if parent.is::<Document>() {
return Err(Error::HierarchyRequest(None));
@@ -2409,18 +2428,19 @@ impl Node {
},
}
// Step 6.
// Step 6. If parent is a document, and any of the statements below, switched on the interface node implements,
// are true, then throw a "HierarchyRequestError" DOMException.
if parent.is::<Document>() {
match node.type_id() {
// Step 6.1
NodeTypeId::DocumentFragment(_) => {
// Step 6.1.1(b)
// Step 6."DocumentFragment". If node has more than one element child or has a Text node child.
if node.children().any(|c| c.is::<Text>()) {
return Err(Error::HierarchyRequest(None));
}
match node.child_elements().count() {
0 => (),
// Step 6.1.2
// Step 6."DocumentFragment". Otherwise, if node has one element child and either parent has an element child,
// child is a doctype, or child is non-null and a doctype is following child.
1 => {
if parent.child_elements().next().is_some() {
return Err(Error::HierarchyRequest(None));
@@ -2434,12 +2454,11 @@ impl Node {
}
}
},
// Step 6.1.1(a)
_ => return Err(Error::HierarchyRequest(None)),
}
},
// Step 6.2
NodeTypeId::Element(_) => {
// Step 6."Element". parent has an element child, child is a doctype, or child is non-null and a doctype is following child.
if parent.child_elements().next().is_some() {
return Err(Error::HierarchyRequest(None));
}
@@ -2452,8 +2471,9 @@ impl Node {
}
}
},
// Step 6.3
NodeTypeId::DocumentType => {
// Step 6."DocumentType". parent has a doctype child, child is non-null and an element is preceding child,
// or child is null and parent has an element child.
if parent.children().any(|c| c.is_doctype()) {
return Err(Error::HierarchyRequest(None));
}
@@ -2490,12 +2510,13 @@ impl Node {
child: Option<&Node>,
can_gc: CanGc,
) -> Fallible<DomRoot<Node>> {
// Step 1.
// Step 1. Ensure pre-insert validity of node into parent before child.
Node::ensure_pre_insertion_validity(node, parent, child)?;
// Steps 2-3.
// Step 2. Let referenceChild be child.
let reference_child_root;
let reference_child = match child {
// Step 3. If referenceChild is node, then set referenceChild to nodes next sibling.
Some(child) if child == node => {
reference_child_root = node.GetNextSibling();
reference_child_root.as_deref()
@@ -2503,7 +2524,7 @@ impl Node {
_ => child,
};
// Step 4.
// Step 4. Insert node into parent before referenceChild.
Node::insert(
node,
parent,
@@ -2512,7 +2533,7 @@ impl Node {
can_gc,
);
// Step 5.
// Step 5. Return node.
Ok(DomRoot::from_ref(node))
}

View File

@@ -35,12 +35,12 @@ use crate::dom::bindings::frozenarray::CachedFrozenArray;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::num::Finite;
use crate::dom::bindings::reflector::reflect_dom_object;
use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom, MutNullableDom, ToLayout};
use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom, MutNullableDom};
use crate::dom::bindings::str::DOMString;
use crate::dom::css::cssstylesheet::CSSStyleSheet;
use crate::dom::css::stylesheetlist::{StyleSheetList, StyleSheetListOwner};
use crate::dom::document::Document;
use crate::dom::documentfragment::DocumentFragment;
use crate::dom::documentfragment::{DocumentFragment, LayoutDocumentFragmentHelpers};
use crate::dom::documentorshadowroot::{
DocumentOrShadowRoot, ServoStylesheetInDocument, StylesheetSource,
};
@@ -71,7 +71,6 @@ pub(crate) struct ShadowRoot {
document_fragment: DocumentFragment,
document_or_shadow_root: DocumentOrShadowRoot,
document: Dom<Document>,
host: Dom<Element>,
/// List of author styles associated with nodes in this shadow tree.
#[custom_trace]
author_styles: DomRefCell<AuthorStyles<ServoStylesheetInDocument>>,
@@ -124,7 +123,7 @@ impl ShadowRoot {
clonable: bool,
is_user_agent_widget: IsUserAgentWidget,
) -> ShadowRoot {
let document_fragment = DocumentFragment::new_inherited(document);
let document_fragment = DocumentFragment::new_inherited(document, Some(host));
let node = document_fragment.upcast::<Node>();
node.set_flag(NodeFlags::IS_IN_SHADOW_TREE, true);
node.set_flag(
@@ -136,7 +135,6 @@ impl ShadowRoot {
document_fragment,
document_or_shadow_root: DocumentOrShadowRoot::new(document.window()),
document: Dom::from_ref(document),
host: Dom::from_ref(host),
author_styles: DomRefCell::new(AuthorStyles::new()),
stylesheet_list: MutNullableDom::new(None),
window: Dom::from_ref(document.window()),
@@ -279,11 +277,11 @@ impl ShadowRoot {
self.document.invalidate_shadow_roots_stylesheets();
self.author_styles.borrow_mut().stylesheets.force_dirty();
// Mark the host element dirty so a reflow will be performed.
self.host.upcast::<Node>().dirty(NodeDamage::Style);
self.Host().upcast::<Node>().dirty(NodeDamage::Style);
// Also mark the host element with `RestyleHint::restyle_subtree` so a reflow
// can traverse into the shadow tree.
let mut restyle = self.document.ensure_pending_restyle(&self.host);
let mut restyle = self.document.ensure_pending_restyle(&self.Host());
restyle.hint.insert(RestyleHint::restyle_subtree());
}
@@ -452,7 +450,9 @@ impl ShadowRootMethods<crate::DomTypeHolder> for ShadowRoot {
/// <https://dom.spec.whatwg.org/#dom-shadowroot-host>
fn Host(&self) -> DomRoot<Element> {
self.host.as_rooted()
self.upcast::<DocumentFragment>()
.host()
.expect("ShadowRoot always has an element as host")
}
/// <https://drafts.csswg.org/cssom/#dom-document-stylesheets>
@@ -640,9 +640,9 @@ pub(crate) trait LayoutShadowRootHelpers<'dom> {
impl<'dom> LayoutShadowRootHelpers<'dom> for LayoutDom<'dom, ShadowRoot> {
#[inline]
#[expect(unsafe_code)]
fn get_host_for_layout(self) -> LayoutDom<'dom, Element> {
unsafe { self.unsafe_get().host.to_layout() }
self.upcast::<DocumentFragment>()
.shadowroot_host_for_layout()
}
#[inline]