changes for <template> content

Template content belongs to an inert "template contents owner document" with no browsing context, related changes for #3328.
This commit is contained in:
Halil Durak committed 2026-09-23 17:04:15 +03:00
1 parent 1dd33ee7ec
commit 7a4d2fec71
7 files changed
+182 -31

No files matched your search

+7 -4
View File
@@ -3295,10 +3295,13 @@ fn nodeIsReady(self: *Frame, comptime from_parser: bool, node: *Node) !void {
// Scripts, iframes, links and styles activate on becoming connected;
// appending them to a detached parent does nothing (they run/load later
// if the subtree gets inserted into the document).
if (comptime from_parser == false) {
switch (node._type) {
.element => if (!node.isConnected()) return,
else => {},
if (node._type == .element) {
if (comptime from_parser) {
if (node.getDocument(self)._frame == null) {
return;
}
} else if (!node.isConnected()) {
return;
}
}
+10 -2
View File
@@ -38,7 +38,7 @@ const String = lp.String;
const IFrame = Element.Html.IFrame;
pub fn createElementNS(document: *const Node.Document, namespace: Element.Namespace, name: []const u8, attribute_iterator: anytype) !*Node {
const from_parser = @TypeOf(attribute_iterator) == Parser.AttributeIterator;
const from_parser = @TypeOf(attribute_iterator) == Parser.AttributeIterator or @TypeOf(attribute_iterator) == Parser.RebuiltAttributes;
const from_clone = @TypeOf(attribute_iterator) == *Element.Attribute.List or @TypeOf(attribute_iterator) == *const Element.Attribute.List;
const frame = frameOf(document);
@@ -849,6 +849,7 @@ pub fn createElementNS(document: *const Node.Document, namespace: Element.Namesp
const node = try createHtmlElementT(document, Element.Html.Custom, namespace, attribute_iterator, .{
._tag_name = tag_name,
._definition = definition,
._upgrade_candidate = creation == .construct,
});
if (creation == .construct) {
try realm._undefined_custom_elements.append(realm.arena, node.as(Element).is(Element.Html.Custom).?);
@@ -1070,7 +1071,7 @@ fn createHtmlElementT(document: *const Node.Document, comptime E: type, namespac
const node = element.asNode();
if (@hasDecl(E, "Build") and @hasDecl(E.Build, "created")) {
if (comptime @TypeOf(attribute_iterator) == Parser.AttributeIterator and @hasDecl(E.Build, "parser_created_on_insert")) {
if (comptime (@TypeOf(attribute_iterator) == Parser.AttributeIterator or @TypeOf(attribute_iterator) == Parser.RebuiltAttributes) and @hasDecl(E.Build, "parser_created_on_insert")) {
// The element wants its parent, it'll do this work when inserted.
return node;
}
@@ -1120,6 +1121,13 @@ fn populateElementAttributes(frame: *Frame, element: *Element, list: anytype) !v
return;
}
if (@TypeOf(list) == Parser.RebuiltAttributes) {
// from the parser, rebuilding an element in another document
try element._attributes.cloneFrom(list.list, frame);
element.noteStyleAttribute();
return;
}
// from the parser
if (@TypeOf(list) == @TypeOf(null)) {
return;
+124 -22
View File
@@ -42,6 +42,17 @@ pub const ParsedNode = struct {
// html5ever should never ask us for this data on a non-element, and we'll
// assert that, with this optional, to make sure our assumption is correct.
data: ?*anyopaque,
// Set once html5ever has put the node in the tree or given it children.
// Until then the node is unobservable and its document is only a guess.
placed: bool = false,
};
// Attributes of an element the parser rebuilds (see settleDocument): the ones
// html5ever gave the original, read back from it. node_factory treats this
// like AttributeIterator, i.e. as a creation by the parser.
pub const RebuiltAttributes = struct {
list: *const Element.Attribute.List,
};
// html5ever's tokenizer flushes the script-data character buffer on every '<'
@@ -63,6 +74,13 @@ err: ?Error,
arena: Allocator,
container: ParsedNode,
document: *Node.Document,
// The document new nodes are created in. The spec creates an element for a
// token in its intended parent's document, and a <template>'s content belongs
// to the inert template contents owner document (Document.templateContentsOwner)
// where custom elements are never constructed and nothing runs or loads.
// html5ever creates an element before it says where it goes, so this is the
// document of the last insertion point it did tell us about.
creation_document: *Node.Document,
strings: std.StringHashMapUnmanaged(void),
pending_text: ?PendingText,
// One buffer reused across every text run in this parser. clearRetainingCapacity
@@ -98,15 +116,18 @@ pub const Options = struct {
};
pub fn init(arena: Allocator, node: *Node, frame: *Frame, opts: Options) Parser {
const document = node.getDocument(frame);
return .{
.err = null,
.frame = frame,
.document = node.getDocument(frame),
.document = document,
.creation_document = document,
.strings = .empty,
.arena = arena,
.container = ParsedNode{
.data = null,
.node = node,
.placed = true,
},
.pending_text = null,
.buf = .empty,
@@ -161,7 +182,7 @@ fn appendTextChunk(self: *Parser, parent: *Node, txt: []const u8) !void {
// Fresh text run: the first chunk lives on _data only. buf stays empty
// until (and unless) a second chunk arrives.
const new_text = try Frame.node_factory.createTextNode(self.document, txt);
const new_text = try Frame.node_factory.createTextNode(parent.getDocument(self.frame), txt);
try self.frame.appendNew(parent, new_text);
self.inserted_since_checkpoint +|= 1;
self.pending_text = .{
@@ -530,7 +551,7 @@ fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName,
} else local;
const namespace_string = qname.ns.slice();
const namespace = if (namespace_string.len == 0) default_namespace else Element.Namespace.parse(namespace_string);
const node = try Frame.node_factory.createElementNS(self.document, namespace, name, attributes);
const node = try Frame.node_factory.createElementNS(self.creation_document, namespace, name, attributes);
if (namespace == .unknown and namespace_string.len > 0) {
// Same as Document.createElementNS: keep the URI so namespaceURI and
// lookupNamespaceURI can return it.
@@ -556,7 +577,7 @@ fn createCommentCallback(ctx: *anyopaque, str: h5e.StringSlice) callconv(.c) ?*a
};
}
fn _createCommentCallback(self: *Parser, str: []const u8) !*anyopaque {
const node = try Frame.node_factory.createComment(self.document, str);
const node = try Frame.node_factory.createComment(self.creation_document, str);
const pn = try self.arena.create(ParsedNode);
pn.* = .{
.data = null,
@@ -575,7 +596,7 @@ fn createProcessingInstruction(ctx: *anyopaque, target: h5e.StringSlice, data: h
};
}
fn _createProcessingInstruction(self: *Parser, target: []const u8, data: []const u8) !*anyopaque {
const node = try Frame.node_factory.createProcessingInstruction(self.document, target, data);
const node = try Frame.node_factory.createProcessingInstruction(self.creation_document, target, data);
const pn = try self.arena.create(ParsedNode);
pn.* = .{
.data = null,
@@ -649,11 +670,15 @@ fn _getTemplateContentsCallback(self: *Parser, node: *Node) !*anyopaque {
const template = element.subtype(Element.Html).is(Element.Html.Template) orelse unreachable;
const content_node = template.getContent().asNode();
// html5ever asks for this to insert there next.
self.creation_document = content_node.getDocument(self.frame);
// Create a ParsedNode wrapper for the content DocumentFragment
const pn = try self.arena.create(ParsedNode);
pn.* = .{
.data = null,
.node = content_node,
.placed = true,
};
return pn;
}
@@ -708,11 +733,11 @@ fn appendCallback(ctx: *anyopaque, parent_ref: *anyopaque, node_or_text: h5e.Nod
const cp = self.frame._ce_reactions.push();
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
self._appendCallback(getNode(parent_ref), node_or_text) catch |err| {
self._appendCallback(getParsed(parent_ref), node_or_text) catch |err| {
self.err = .{ .err = err, .source = .append };
};
}
fn _appendCallback(self: *Parser, parent: *Node, node_or_text: h5e.NodeOrText) !void {
fn _appendCallback(self: *Parser, parent_pn: *ParsedNode, node_or_text: h5e.NodeOrText) !void {
// child node is guaranteed not to belong to another parent
switch (node_or_text.toUnion()) {
.node => |cpn| {
@@ -722,7 +747,8 @@ fn _appendCallback(self: *Parser, parent: *Node, node_or_text: h5e.NodeOrText) !
try self.flushPendingText();
self.maybeCheckpoint();
self.inserted_since_checkpoint +|= 1;
const child = getNode(cpn);
const child = try self.settleDocument(parent_pn, getParsed(cpn));
const parent = parent_pn.node;
if (child._parent) |previous_parent| {
// html5ever says this can't happen, but we might be screwing up
// the node on our side. We shouldn't be, but we're seeing this
@@ -736,11 +762,66 @@ fn _appendCallback(self: *Parser, parent: *Node, node_or_text: h5e.NodeOrText) !
}
try self.frame.appendNew(parent, child);
},
.text => |txt| try self.appendTextChunk(parent, txt),
.text => |txt| {
const parent = parent_pn.node;
self.creation_document = parent.getDocument(self.frame);
try self.appendTextChunk(parent, txt);
},
.failed => {},
}
}
fn settleDocument(self: *Parser, parent_pn: *ParsedNode, child_pn: *ParsedNode) !*Node {
const frame = self.frame;
const parent = parent_pn.node;
const child = child_pn.node;
// A custom element constructor may have put the child in the tree
// itself; that is as observable as being placed by html5ever.
if (child._parent != null) {
child_pn.placed = true;
}
var document = parent.getDocument(frame);
const child_document = child.getDocument(frame);
if (child_document != document) {
if (child_pn.placed == false) {
child_pn.node = try self.rebuildIn(child, document);
} else if (parent_pn.placed == false) {
try frame.adoptNodeTree(parent, document, child_document);
document = child_document;
} else {
try frame.adoptNodeTree(child, child_document, document);
}
}
parent_pn.placed = true;
child_pn.placed = true;
self.creation_document = document;
return child_pn.node;
}
// A copy of the never-placed `node` in `document`, created like the parser
// created `node`, in the wrong document.
fn rebuildIn(self: *Parser, node: *Node, document: *Node.Document) !*Node {
const frame = self.frame;
const element = node.is(Element) orelse {
// A comment or processing instruction: nothing to construct or load.
try frame.adoptNodeTree(node, node.getDocument(frame), document);
return node;
};
const copy = try Frame.node_factory.createElementNS(document, element._namespace, element.getTagNameDump(), RebuiltAttributes{ .list = &element._attributes });
if (element._namespace == .unknown) {
// The URI lives in a side table, see _createElementCallback.
const page = self.document._page;
if (page.element_namespace_uris.fetchRemove(element)) |entry| {
try page.element_namespace_uris.put(page.frame_arena, copy.as(Element), entry.value);
}
}
return copy;
}
fn removeFromParentCallback(ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) void {
const self: *Parser = @ptrCast(@alignCast(ctx));
if (self.terminated) {
@@ -769,15 +850,28 @@ fn reparentChildrenCallback(ctx: *anyopaque, node_ref: *anyopaque, new_parent_re
}
const cp = self.frame._ce_reactions.push();
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
self._reparentChildrenCallback(getNode(node_ref), getNode(new_parent_ref)) catch |err| {
self._reparentChildrenCallback(getParsed(node_ref), getParsed(new_parent_ref)) catch |err| {
self.err = .{ .err = err, .source = .reparent_children };
};
}
fn _reparentChildrenCallback(self: *Parser, node: *Node, new_parent: *Node) !void {
fn _reparentChildrenCallback(self: *Parser, node_pn: *ParsedNode, new_parent_pn: *ParsedNode) !void {
// Reparenting can move the pending text node out from under us — the
// node's _parent changes but pending_text.parent does not. Flush so the
// accumulator commits before the tree is rearranged.
try self.flushPendingText();
// The new parent is a fresh element (adoption agency algorithm); the
// children it takes settle its document, not the other way around.
const node = node_pn.node;
const new_parent = new_parent_pn.node;
const document = node.getDocument(self.frame);
if (new_parent_pn.placed == false) {
const new_parent_document = new_parent.getDocument(self.frame);
if (new_parent_document != document) {
try self.frame.adoptNodeTree(new_parent, new_parent_document, document);
}
new_parent_pn.placed = true;
}
try self.frame.appendAllChildren(node, new_parent);
}
@@ -789,19 +883,21 @@ fn appendBeforeSiblingCallback(ctx: *anyopaque, sibling_ref: *anyopaque, node_or
const cp = self.frame._ce_reactions.push();
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
self._appendBeforeSiblingCallback(getNode(sibling_ref), node_or_text) catch |err| {
self._appendBeforeSiblingCallback(getParsed(sibling_ref), node_or_text) catch |err| {
self.err = .{ .err = err, .source = .append_before_sibling };
};
}
fn _appendBeforeSiblingCallback(self: *Parser, sibling: *Node, node_or_text: h5e.NodeOrText) !void {
fn _appendBeforeSiblingCallback(self: *Parser, sibling_pn: *ParsedNode, node_or_text: h5e.NodeOrText) !void {
// Foster parenting / before-sibling insertions interrupt any pending text
// run (the new node lands at a different position from the pending text's
// tail). Flush before reading the parent's structure.
try self.flushPendingText();
const sibling = sibling_pn.node;
const parent = sibling.parentNode() orelse return error.NoParent;
const node: *Node = switch (node_or_text.toUnion()) {
.node => |cpn| blk: {
const child = getNode(cpn);
var parent_pn = ParsedNode{ .node = parent, .data = null, .placed = true };
const child = try self.settleDocument(&parent_pn, getParsed(cpn));
if (child._parent) |previous_parent| {
// A custom element constructor may have inserted the node into the
// DOM before the parser officially places it (e.g. via foster
@@ -810,7 +906,10 @@ fn _appendBeforeSiblingCallback(self: *Parser, sibling: *Node, node_or_text: h5e
}
break :blk child;
},
.text => |txt| try Frame.node_factory.createTextNode(self.document, txt),
.text => |txt| blk: {
self.creation_document = parent.getDocument(self.frame);
break :blk try Frame.node_factory.createTextNode(self.creation_document, txt);
},
.failed => return,
};
try self.frame.insertNodeRelative(parent, node, .{ .before = sibling }, .{});
@@ -824,21 +923,24 @@ fn appendBasedOnParentNodeCallback(ctx: *anyopaque, element_ref: *anyopaque, pre
const cp = self.frame._ce_reactions.push();
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
self._appendBasedOnParentNodeCallback(getNode(element_ref), getNode(prev_element_ref), node_or_text) catch |err| {
self._appendBasedOnParentNodeCallback(getParsed(element_ref), getParsed(prev_element_ref), node_or_text) catch |err| {
self.err = .{ .err = err, .source = .append_based_on_parent_node };
};
}
fn _appendBasedOnParentNodeCallback(self: *Parser, element: *Node, prev_element: *Node, node_or_text: h5e.NodeOrText) !void {
if (element.parentNode()) |_| {
try self._appendBeforeSiblingCallback(element, node_or_text);
fn _appendBasedOnParentNodeCallback(self: *Parser, element_pn: *ParsedNode, prev_element_pn: *ParsedNode, node_or_text: h5e.NodeOrText) !void {
if (element_pn.node.parentNode()) |_| {
try self._appendBeforeSiblingCallback(element_pn, node_or_text);
} else {
try self._appendCallback(prev_element, node_or_text);
try self._appendCallback(prev_element_pn, node_or_text);
}
}
fn getParsed(ref: *anyopaque) *ParsedNode {
return @ptrCast(@alignCast(ref));
}
fn getNode(ref: *anyopaque) *Node {
const pn: *ParsedNode = @ptrCast(@alignCast(ref));
return pn.node;
return getParsed(ref).node;
}
fn asUint(comptime string: anytype) std.meta.Int(
@@ -186,6 +186,10 @@ fn upgradeNode(self: *CustomElementRegistry, node: *Node, frame: *Frame) !void {
}
fn upgradeElement(self: *CustomElementRegistry, element: *Element, frame: *Frame) !void {
if (element.asNode().getDocument(frame)._frame == null) {
return;
}
const custom = element.is(Custom) orelse {
return Custom.checkAndAttachBuiltIn(element, frame);
};
+27
View File
@@ -58,6 +58,7 @@ _proto: *Node,
_page: *Page,
_index: u32, // browser.documents index
_frame: ?*Frame = null,
_template_contents_owner: ?*Document = null,
_url: ?[:0]const u8 = null, // URL for documents created via DOMImplementation (about:blank)
// content type override for documents created via DOMImplementation.createDocument
_content_type: ?[]const u8 = null,
@@ -517,6 +518,32 @@ fn createDocumentFragment(self: *Document, frame: *Frame) !*Node.DocumentFragmen
return Node.DocumentFragment.init(self, frame);
}
// https://html.spec.whatwg.org/multipage/scripting.html#appropriate-template-contents-owner-document
// A <template>'s content lives in a document with no browsing context, shared
// by every template of this document (and by the templates nested in that
// content). Nothing in there is connected, scripts never run and, having no
// custom element registry, custom elements are never constructed.
//
// Only a copy stamped into a real document gets upgraded.
pub fn templateContentsOwner(self: *Document, frame: *Frame) !*Document {
if (self._template_contents_owner) |owner| {
return owner;
}
const owner: *Document = if (self._type == .html)
(try frame._factory.document(HTMLDocument{ ._proto = undefined })).asDocument()
else
try frame._factory.genericDocument(.{});
owner._url = "about:blank";
owner._charset = "UTF-8";
owner._ready_state = .complete;
// Its own templates' content stays in it.
owner._template_contents_owner = owner;
self._template_contents_owner = owner;
return owner;
}
pub fn createComment(self: *Document, data: []const u8) !*Node {
return Frame.node_factory.createComment(self, data);
}
+6 -1
View File
@@ -45,6 +45,7 @@ _connected_callback_invoked: bool = false,
_disconnected_callback_invoked: bool = false,
_upgrade_failed: bool = false, // a failed upgrade is never retried
_upgrade_in_progress: bool = false,
_upgrade_candidate: bool = false, // listed in a frame's _undefined_custom_elements
pub fn asElement(self: *Custom) *Element {
return Factory.protoOf(self).asElement();
@@ -85,7 +86,11 @@ pub fn enqueueConnectedCallbackOnElement(comptime from_parser: bool, element: *E
CustomElementRegistry.upgradeCustomElement(custom, definition, frame) catch {};
return;
}
// Element is undefined and no definition exists yet — nothing to queue.
if (!custom._upgrade_candidate) {
custom._upgrade_candidate = true;
try frame._undefined_custom_elements.append(frame.arena, custom);
}
return;
}
+4 -2
View File
@@ -157,8 +157,10 @@ pub const JsApi = struct {
pub const Build = struct {
pub fn created(node: *Node, frame: *Frame) !void {
const self = node.as(Template);
// Create the template content DocumentFragment
self._content = try DocumentFragment.init(node.getDocument(frame), frame);
// The content DocumentFragment belongs to the inert template contents
// owner document, not to the template's own document.
const owner = try node.getDocument(frame).templateContentsOwner(frame);
self._content = try DocumentFragment.init(owner, frame);
}
// Per the HTML spec's cloning steps for <template>, a deep clone must