diff --git a/src/browser/Page.zig b/src/browser/Page.zig index f2cab39b..30a2fbfc 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -95,6 +95,7 @@ _element_datasets: Element.DatasetLookup = .{}, _element_class_lists: Element.ClassListLookup = .{}, _element_rel_lists: Element.RelListLookup = .{}, _element_shadow_roots: Element.ShadowRootLookup = .{}, +_node_owner_documents: Node.OwnerDocumentLookup = .{}, _element_assigned_slots: Element.AssignedSlotLookup = .{}, _script_manager: ScriptManager, @@ -266,6 +267,7 @@ fn reset(self: *Page, comptime initializing: bool) !void { self._element_class_lists = .{}; self._element_rel_lists = .{}; self._element_shadow_roots = .{}; + self._node_owner_documents = .{}; self._element_assigned_slots = .{}; self._notified_network_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); } +// 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 { const namespace: Element.Namespace = blk: { const ns = ns_ orelse break :blk .html; diff --git a/src/browser/tests/node/adoption.html b/src/browser/tests/node/adoption.html new file mode 100644 index 00000000..cbe084bb --- /dev/null +++ b/src/browser/tests/node/adoption.html @@ -0,0 +1,16 @@ + + + + diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 8b6c9e76..ea9300ca 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -121,10 +121,15 @@ const CreateElementOptions = struct { 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 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; if (options.is) |is_value| { try element.setAttribute("is", is_value, page); @@ -134,8 +139,13 @@ pub fn createElement(_: *const Document, name: []const u8, options_: ?CreateElem 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); + + // Track owner document if it's not the main document + if (self != page.document) { + try page.setNodeOwnerDocument(node, self); + } return node.as(Element); } @@ -254,28 +264,53 @@ pub fn getImplementation(_: *const Document) DOMImplementation { return .{}; } -pub fn createDocumentFragment(_: *const Document, page: *Page) !*Node.DocumentFragment { - return Node.DocumentFragment.init(page); -} - -pub fn createComment(_: *const Document, data: []const u8, page: *Page) !*Node { - return page.createComment(data); -} - -pub fn createTextNode(_: *const Document, data: []const u8, page: *Page) !*Node { - return page.createTextNode(data); -} - -pub fn createCDATASection(self: *const Document, data: []const u8, page: *Page) !*Node { - switch (self._type) { - .html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument - .xml => return page.createCDATASection(data), - .generic => return page.createCDATASection(data), +pub fn createDocumentFragment(self: *Document, page: *Page) !*Node.DocumentFragment { + 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 createProcessingInstruction(_: *const Document, target: []const u8, data: []const u8, page: *Page) !*Node { - return page.createProcessingInstruction(target, data); +pub fn createComment(self: *Document, data: []const u8, page: *Page) !*Node { + 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(self: *Document, data: []const u8, page: *Page) !*Node { + 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: *Document, data: []const u8, page: *Page) !*Node { + const node = switch (self._type) { + .html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument + .xml => try 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(self: *Document, target: []const u8, data: []const u8, page: *Page) !*Node { + 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"); diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 11833184..b168d610 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -46,6 +46,9 @@ _parent: ?*Node = null, _children: ?*Children = null, _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) { cdata: *CData, element: *Element, @@ -205,7 +208,6 @@ fn validateNodeInsertion(parent: *Node, node: *Node) !void { if (node._type == .attribute) { return error.HierarchyError; } - } 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 // disconnect then reconnect) const child_connected = child.isConnected(); + // Check if we're adopting the node to a different document - const child_root = child.getRootNode(null); - const parent_root = self.getRootNode(null); - const adopting_to_new_document = child_connected and child_root != parent_root; + const child_owner = child.ownerDocument(page); + const parent_owner = self.ownerDocument(page) orelse self.as(Document); + const adopting_to_new_document = child_owner != null and child_owner.? != parent_owner; if (child._parent) |parent| { // 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() }); } + // Adopt the node tree if moving between documents + if (adopting_to_new_document) { + try page.adoptNodeTree(child, parent_owner); + } + try page.appendNode(self, child, .{ .child_already_connected = child_connected, .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; } - // Otherwise, this is a detached node. The owner is the document that - // created it. For now, we only have one document. + // Otherwise, this is a detached node. Check if it has a specific owner + // 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; } @@ -489,10 +502,11 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, page: *Page try validateNodeInsertion(self, new_node); const child_already_connected = new_node.isConnected(); + // Check if we're adopting the node to a different document - const child_root = new_node.getRootNode(null); - const parent_root = self.getRootNode(null); - const adopting_to_new_document = child_already_connected and child_root != parent_root; + const child_owner = new_node.ownerDocument(page); + const parent_owner = self.ownerDocument(page) orelse self.as(Document); + const adopting_to_new_document = child_owner != null and child_owner.? != parent_owner; page.domChanged(); 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 }); } + // Adopt the node tree if moving between documents + if (adopting_to_new_document) { + try page.adoptNodeTree(new_node, parent_owner); + } + try page.insertNodeRelative( self, new_node,