Track owning documents for nodes which aren't the default document

Track this in a lookup on the page, to avoid having to store a pointer for
_every_ node, given that most nodes _are_ owned by the document.

This helps us ensure nodes can be properly adopted.
This commit is contained in:
Karl Seguin
2026-01-07 17:46:09 +08:00
parent a010684ce9
commit 408d3f0a53
4 changed files with 122 additions and 30 deletions

View File

@@ -95,6 +95,7 @@ _element_datasets: Element.DatasetLookup = .{},
_element_class_lists: Element.ClassListLookup = .{}, _element_class_lists: Element.ClassListLookup = .{},
_element_rel_lists: Element.RelListLookup = .{}, _element_rel_lists: Element.RelListLookup = .{},
_element_shadow_roots: Element.ShadowRootLookup = .{}, _element_shadow_roots: Element.ShadowRootLookup = .{},
_node_owner_documents: Node.OwnerDocumentLookup = .{},
_element_assigned_slots: Element.AssignedSlotLookup = .{}, _element_assigned_slots: Element.AssignedSlotLookup = .{},
_script_manager: ScriptManager, _script_manager: ScriptManager,
@@ -266,6 +267,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self._element_class_lists = .{}; self._element_class_lists = .{};
self._element_rel_lists = .{}; self._element_rel_lists = .{};
self._element_shadow_roots = .{}; self._element_shadow_roots = .{};
self._node_owner_documents = .{};
self._element_assigned_slots = .{}; self._element_assigned_slots = .{};
self._notified_network_idle = .init; self._notified_network_idle = .init;
self._notified_network_almost_idle = .init; self._notified_network_almost_idle = .init;
@@ -1289,6 +1291,26 @@ pub fn nodeComplete(self: *Page, node: *Node) !void {
return self.nodeIsReady(true, node); return self.nodeIsReady(true, node);
} }
// Sets the owner document for a node. Only stores entries for nodes whose owner
// is NOT page.document to minimize memory overhead.
pub fn setNodeOwnerDocument(self: *Page, node: *Node, owner: *Document) !void {
if (owner == self.document) {
// No need to store if it's the main document - remove if present
_ = self._node_owner_documents.remove(node);
} else {
try self._node_owner_documents.put(self.arena, node, owner);
}
}
// Recursively sets the owner document for a node and all its descendants
pub fn adoptNodeTree(self: *Page, node: *Node, new_owner: *Document) !void {
try self.setNodeOwnerDocument(node, new_owner);
var it = node.childrenIterator();
while (it.next()) |child| {
try self.adoptNodeTree(child, new_owner);
}
}
pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_iterator: anytype) !*Node { pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_iterator: anytype) !*Node {
const namespace: Element.Namespace = blk: { const namespace: Element.Namespace = blk: {
const ns = ns_ orelse break :blk .html; const ns = ns_ orelse break :blk .html;

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<body></body>
<script id="adoptNode">
const old = document.implementation.createHTMLDocument("");
const div = old.createElement("div");
div.appendChild(old.createTextNode("text"));
testing.expectEqual(old, div.ownerDocument);
testing.expectEqual(old, div.firstChild.ownerDocument);
document.body.appendChild(div);
testing.expectEqual(document, div.ownerDocument);
testing.expectEqual(document, div.firstChild.ownerDocument);
</script>

View File

@@ -121,10 +121,15 @@ const CreateElementOptions = struct {
is: ?[]const u8 = null, is: ?[]const u8 = null,
}; };
pub fn createElement(_: *const Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element { pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element {
const node = try page.createElement(null, name, null); const node = try page.createElement(null, name, null);
const element = node.as(Element); const element = node.as(Element);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
const options = options_ orelse return element; const options = options_ orelse return element;
if (options.is) |is_value| { if (options.is) |is_value| {
try element.setAttribute("is", is_value, page); try element.setAttribute("is", is_value, page);
@@ -134,8 +139,13 @@ pub fn createElement(_: *const Document, name: []const u8, options_: ?CreateElem
return element; return element;
} }
pub fn createElementNS(_: *const Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element { pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element {
const node = try page.createElement(namespace, name, null); const node = try page.createElement(namespace, name, null);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
return node.as(Element); return node.as(Element);
} }
@@ -254,28 +264,53 @@ pub fn getImplementation(_: *const Document) DOMImplementation {
return .{}; return .{};
} }
pub fn createDocumentFragment(_: *const Document, page: *Page) !*Node.DocumentFragment { pub fn createDocumentFragment(self: *Document, page: *Page) !*Node.DocumentFragment {
return Node.DocumentFragment.init(page); const frag = try Node.DocumentFragment.init(page);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(frag.asNode(), self);
}
return frag;
} }
pub fn createComment(_: *const Document, data: []const u8, page: *Page) !*Node { pub fn createComment(self: *Document, data: []const u8, page: *Page) !*Node {
return page.createComment(data); const node = try page.createComment(data);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
return node;
} }
pub fn createTextNode(_: *const Document, data: []const u8, page: *Page) !*Node { pub fn createTextNode(self: *Document, data: []const u8, page: *Page) !*Node {
return page.createTextNode(data); const node = try page.createTextNode(data);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
return node;
} }
pub fn createCDATASection(self: *const Document, data: []const u8, page: *Page) !*Node { pub fn createCDATASection(self: *Document, data: []const u8, page: *Page) !*Node {
switch (self._type) { const node = switch (self._type) {
.html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument .html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument
.xml => return page.createCDATASection(data), .xml => try page.createCDATASection(data),
.generic => return page.createCDATASection(data), .generic => try page.createCDATASection(data),
};
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
} }
return node;
} }
pub fn createProcessingInstruction(_: *const Document, target: []const u8, data: []const u8, page: *Page) !*Node { pub fn createProcessingInstruction(self: *Document, target: []const u8, data: []const u8, page: *Page) !*Node {
return page.createProcessingInstruction(target, data); const node = try page.createProcessingInstruction(target, data);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
return node;
} }
const Range = @import("Range.zig"); const Range = @import("Range.zig");

View File

@@ -46,6 +46,9 @@ _parent: ?*Node = null,
_children: ?*Children = null, _children: ?*Children = null,
_child_link: LinkedList.Node = .{}, _child_link: LinkedList.Node = .{},
// Lookup for nodes that have a different owner document than page.document
pub const OwnerDocumentLookup = std.AutoHashMapUnmanaged(*Node, *Document);
pub const Type = union(enum) { pub const Type = union(enum) {
cdata: *CData, cdata: *CData,
element: *Element, element: *Element,
@@ -205,7 +208,6 @@ fn validateNodeInsertion(parent: *Node, node: *Node) !void {
if (node._type == .attribute) { if (node._type == .attribute) {
return error.HierarchyError; return error.HierarchyError;
} }
} }
pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node { pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node {
@@ -222,10 +224,11 @@ pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node {
// then we can remove + add a bit more efficiently (we don't have to fully // then we can remove + add a bit more efficiently (we don't have to fully
// disconnect then reconnect) // disconnect then reconnect)
const child_connected = child.isConnected(); const child_connected = child.isConnected();
// Check if we're adopting the node to a different document // Check if we're adopting the node to a different document
const child_root = child.getRootNode(null); const child_owner = child.ownerDocument(page);
const parent_root = self.getRootNode(null); const parent_owner = self.ownerDocument(page) orelse self.as(Document);
const adopting_to_new_document = child_connected and child_root != parent_root; const adopting_to_new_document = child_owner != null and child_owner.? != parent_owner;
if (child._parent) |parent| { if (child._parent) |parent| {
// we can signal removeNode that the child will remain connected // we can signal removeNode that the child will remain connected
@@ -233,6 +236,11 @@ pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node {
page.removeNode(parent, child, .{ .will_be_reconnected = self.isConnected() }); page.removeNode(parent, child, .{ .will_be_reconnected = self.isConnected() });
} }
// Adopt the node tree if moving between documents
if (adopting_to_new_document) {
try page.adoptNodeTree(child, parent_owner);
}
try page.appendNode(self, child, .{ try page.appendNode(self, child, .{
.child_already_connected = child_connected, .child_already_connected = child_connected,
.adopting_to_new_document = adopting_to_new_document, .adopting_to_new_document = adopting_to_new_document,
@@ -432,8 +440,13 @@ pub fn ownerDocument(self: *const Node, page: *const Page) ?*Document {
return current._type.document; return current._type.document;
} }
// Otherwise, this is a detached node. The owner is the document that // Otherwise, this is a detached node. Check if it has a specific owner
// created it. For now, we only have one document. // document registered (for nodes created via non-main documents).
if (page._node_owner_documents.get(@constCast(self))) |owner| {
return owner;
}
// Default to the main document for detached nodes without a specific owner.
return page.document; return page.document;
} }
@@ -489,10 +502,11 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, page: *Page
try validateNodeInsertion(self, new_node); try validateNodeInsertion(self, new_node);
const child_already_connected = new_node.isConnected(); const child_already_connected = new_node.isConnected();
// Check if we're adopting the node to a different document // Check if we're adopting the node to a different document
const child_root = new_node.getRootNode(null); const child_owner = new_node.ownerDocument(page);
const parent_root = self.getRootNode(null); const parent_owner = self.ownerDocument(page) orelse self.as(Document);
const adopting_to_new_document = child_already_connected and child_root != parent_root; const adopting_to_new_document = child_owner != null and child_owner.? != parent_owner;
page.domChanged(); page.domChanged();
const will_be_reconnected = self.isConnected(); const will_be_reconnected = self.isConnected();
@@ -500,6 +514,11 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, page: *Page
page.removeNode(parent, new_node, .{ .will_be_reconnected = will_be_reconnected }); page.removeNode(parent, new_node, .{ .will_be_reconnected = will_be_reconnected });
} }
// Adopt the node tree if moving between documents
if (adopting_to_new_document) {
try page.adoptNodeTree(new_node, parent_owner);
}
try page.insertNodeRelative( try page.insertNodeRelative(
self, self,
new_node, new_node,