mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-02-04 06:23:45 +00:00
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:
@@ -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;
|
||||
|
||||
16
src/browser/tests/node/adoption.html
Normal file
16
src/browser/tests/node/adoption.html
Normal 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>
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user