From f12a527ae37f3ada0f419b1a1c3287443787f0ea Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 30 Apr 2025 09:01:56 +0200 Subject: [PATCH 01/10] cdp: add ParentId to Node.Writer --- src/cdp/Node.zig | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig index a7a6b658..6b49d4db 100644 --- a/src/cdp/Node.zig +++ b/src/cdp/Node.zig @@ -218,7 +218,7 @@ pub const Writer = struct { fn toJSON(self: *const Writer, w: anytype) !void { try w.beginObject(); - try writeCommon(self.node, false, w); + try self.writeCommon(self.node, false, w); { var registry = self.registry; @@ -232,7 +232,7 @@ pub const Writer = struct { const child = (try parser.nodeListItem(child_nodes, @intCast(i))) orelse break; const child_node = try registry.register(child); try w.beginObject(); - try writeCommon(child_node, true, w); + try self.writeCommon(child_node, true, w); try w.endObject(); i += 1; } @@ -245,7 +245,7 @@ pub const Writer = struct { try w.endObject(); } - fn writeCommon(node: *const Node, include_child_count: bool, w: anytype) !void { + fn writeCommon(self: *const Writer, node: *const Node, include_child_count: bool, w: anytype) !void { try w.objectField("nodeId"); try w.write(node.id); @@ -254,9 +254,11 @@ pub const Writer = struct { const n = node._node; - // TODO: - // try w.objectField("parentId"); - // try w.write(pid); + if (try parser.nodeParentNode(n)) |p| { + const parent_node = try self.registry.register(p); + try w.objectField("parentId"); + try w.write(parent_node.id); + } try w.objectField("nodeType"); try w.write(@intFromEnum(try parser.nodeType(n))); @@ -461,6 +463,7 @@ test "cdp Node: Writer" { .xmlVersion = "", .compatibilityMode = "NoQuirksMode", .isScrollable = false, + .parentId = 1, }, .{ .nodeId = 3, .backendNodeId = 3, @@ -474,6 +477,7 @@ test "cdp Node: Writer" { .xmlVersion = "", .compatibilityMode = "NoQuirksMode", .isScrollable = false, + .parentId = 1, } }, }, json); } From 88f768764637fb66c8dd84ae9b098a5c75e82529 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 30 Apr 2025 09:19:59 +0200 Subject: [PATCH 02/10] cdp: dispatch DOM.setChildNodes on performSearch --- src/cdp/domains/dom.zig | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index ceb9bc93..7353b19b 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -76,6 +76,23 @@ fn performSearch(cmd: anytype) !void { const search = try bc.node_search_list.create(list.nodes.items); + // dispatch setChildNodesEvents to inform the client of the subpart of node + // tree covering the results. + for (list.nodes.items) |n| { + // retrieve the node's parent + if (try parser.nodeParentNode(n)) |p| { + // Register the parent and send the node. + const parent_node = try bc.node_registry.register(p); + const node = bc.node_registry.lookup_by_node.get(n) orelse unreachable; + // Should-we return one DOM.setChildNodes event per parentId + // containing all its children in the nodes array? + try cmd.sendEvent("DOM.setChildNodes", .{ + .parentId = parent_node.id, + .nodes = .{bc.nodeWriter(node, .{})}, + }, .{}); + } + } + return cmd.sendResult(.{ .searchId = search.name, .resultCount = @as(u32, @intCast(search.node_ids.len)), From 09fbbc1e17e8b594fcfa89a4fdb4a2c0b1bb2001 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 30 Apr 2025 15:55:34 +0200 Subject: [PATCH 03/10] netsurf: node's attributes can be null --- src/browser/dom/element.zig | 3 ++- src/browser/dump.zig | 24 ++++++++++++------------ src/browser/netsurf.zig | 4 ++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig index 3aeac4bd..d6cff74d 100644 --- a/src/browser/dom/element.zig +++ b/src/browser/dom/element.zig @@ -99,7 +99,8 @@ pub const Element = struct { } pub fn get_attributes(self: *parser.Element) !*parser.NamedNodeMap { - return try parser.nodeGetAttributes(parser.elementToNode(self)); + // An element must have non-nil attributes. + return try parser.nodeGetAttributes(parser.elementToNode(self)) orelse unreachable; } pub fn get_innerHTML(self: *parser.Element, state: *SessionState) ![]const u8 { diff --git a/src/browser/dump.zig b/src/browser/dump.zig index 4c4e7996..01669036 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -38,18 +38,18 @@ pub fn writeNode(node: *parser.Node, writer: anytype) anyerror!void { try writer.writeAll(tag); // write the attributes - const map = try parser.nodeGetAttributes(node); - const ln = try parser.namedNodeMapGetLength(map); - var i: u32 = 0; - while (i < ln) { - const attr = try parser.namedNodeMapItem(map, i) orelse break; - try writer.writeAll(" "); - try writer.writeAll(try parser.attributeGetName(attr)); - try writer.writeAll("=\""); - const attribute_value = try parser.attributeGetValue(attr) orelse ""; - try writeEscapedAttributeValue(writer, attribute_value); - try writer.writeAll("\""); - i += 1; + const _map = try parser.nodeGetAttributes(node); + if (_map) |map| { + const ln = try parser.namedNodeMapGetLength(map); + for (0..ln) |i| { + const attr = try parser.namedNodeMapItem(map, @intCast(i)) orelse break; + try writer.writeAll(" "); + try writer.writeAll(try parser.attributeGetName(attr)); + try writer.writeAll("=\""); + const attribute_value = try parser.attributeGetValue(attr) orelse ""; + try writeEscapedAttributeValue(writer, attribute_value); + try writer.writeAll("\""); + } } try writer.writeAll(">"); diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index df5ab147..f7050d60 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -1338,11 +1338,11 @@ pub fn nodeHasAttributes(node: *Node) !bool { return res; } -pub fn nodeGetAttributes(node: *Node) !*NamedNodeMap { +pub fn nodeGetAttributes(node: *Node) !?*NamedNodeMap { var res: ?*NamedNodeMap = undefined; const err = nodeVtable(node).dom_node_get_attributes.?(node, &res); try DOMErr(err); - return res.?; + return res; } pub fn nodeGetNamespace(node: *Node) !?[]const u8 { From d2a68e62e947c25d629a782f56a621217bfe6a5a Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 30 Apr 2025 15:56:06 +0200 Subject: [PATCH 04/10] cdp: add attributes to the node's writer --- src/cdp/Node.zig | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig index 6b49d4db..ddec7418 100644 --- a/src/cdp/Node.zig +++ b/src/cdp/Node.zig @@ -260,6 +260,19 @@ pub const Writer = struct { try w.write(parent_node.id); } + const _map = try parser.nodeGetAttributes(n); + if (_map) |map| { + const attr_count = try parser.namedNodeMapGetLength(map); + try w.objectField("attributes"); + try w.beginArray(); + for (0..attr_count) |i| { + const attr = try parser.namedNodeMapItem(map, @intCast(i)) orelse continue; + try w.write(try parser.attributeGetName(attr)); + try w.write(try parser.attributeGetValue(attr) orelse continue); + } + try w.endArray(); + } + try w.objectField("nodeType"); try w.write(@intFromEnum(try parser.nodeType(n))); From 1146453dc21b7794d713369a8d8b3e0c65293656 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 1 May 2025 16:51:02 +0200 Subject: [PATCH 05/10] cdp: add session to setChildNodes event --- src/cdp/domains/dom.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 7353b19b..6870ce76 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -89,7 +89,9 @@ fn performSearch(cmd: anytype) !void { try cmd.sendEvent("DOM.setChildNodes", .{ .parentId = parent_node.id, .nodes = .{bc.nodeWriter(node, .{})}, - }, .{}); + }, .{ + .session_id = bc.session_id.?, + }); } } From 8b9084cb73a8b81f803966655635d6596fb8b918 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 1 May 2025 19:42:04 +0200 Subject: [PATCH 06/10] cdp: dispatch the correct dom hierarchy wit setChildNodes --- src/cdp/domains/dom.zig | 89 +++++++++++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 16 deletions(-) diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 6870ce76..91b4a0d5 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -78,22 +78,7 @@ fn performSearch(cmd: anytype) !void { // dispatch setChildNodesEvents to inform the client of the subpart of node // tree covering the results. - for (list.nodes.items) |n| { - // retrieve the node's parent - if (try parser.nodeParentNode(n)) |p| { - // Register the parent and send the node. - const parent_node = try bc.node_registry.register(p); - const node = bc.node_registry.lookup_by_node.get(n) orelse unreachable; - // Should-we return one DOM.setChildNodes event per parentId - // containing all its children in the nodes array? - try cmd.sendEvent("DOM.setChildNodes", .{ - .parentId = parent_node.id, - .nodes = .{bc.nodeWriter(node, .{})}, - }, .{ - .session_id = bc.session_id.?, - }); - } - } + try dispatchSetChildNodes(cmd, list.nodes.items); return cmd.sendResult(.{ .searchId = search.name, @@ -101,6 +86,78 @@ fn performSearch(cmd: anytype) !void { }, .{}); } +// dispatchSetChildNodes send the setChildNodes event for the whole DOM tree +// hierarchy of each nodes. +// If a parent has already been registred, we don't re-send a setChildNodes +// event anymore. +// We dispatch event in the reverse order: from the top level to the direct parents. +fn dispatchSetChildNodes(cmd: anytype, nodes: []*parser.Node) !void { + const arena = cmd.arena; + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + + var parents: std.ArrayListUnmanaged(*parser.Node) = .{}; + for (nodes) |_n| { + var n = _n; + while (true) { + const p = try parser.nodeParentNode(n) orelse break; + // If the parent exists in he registry, don't send the event. + // In this case we stop browsing the tree, b/c parents must have + // been sent already. + if (bc.node_registry.lookup_by_node.contains(p)) { + break; + } + try parents.append(arena, p); + + n = p; + } + } + + const plen = parents.items.len; + if (plen == 0) return; + + var i: usize = plen; + while (i > 0) { + i -= 1; + const n = parents.items[i]; + // If the parent exists in he registry, don't send the event. + // Indeed the parent can be twice in the array. + if (bc.node_registry.lookup_by_node.contains(n)) { + continue; + } + + // If the node has no parent, it's the root node. + // We don't dispatch event for it because we assume the root node is + // dispatched via the DOM.getDocument command. + const p = try parser.nodeParentNode(n) orelse break; + // Register the node. + const node = try bc.node_registry.register(n); + // Retrieve the parent from the registry. + const parent_node = bc.node_registry.lookup_by_node.get(p) orelse unreachable; + + try cmd.sendEvent("DOM.setChildNodes", .{ + .parentId = parent_node.id, + .nodes = .{bc.nodeWriter(node, .{})}, + }, .{ + .session_id = bc.session_id.?, + }); + } + + // now dispatch the event for the node list. + for (nodes) |n| { + const node = bc.node_registry.lookup_by_node.get(n) orelse unreachable; + const p = try parser.nodeParentNode(n) orelse continue; + // Retrieve the parent from the registry. + const parent_node = bc.node_registry.lookup_by_node.get(p) orelse unreachable; + + try cmd.sendEvent("DOM.setChildNodes", .{ + .parentId = parent_node.id, + .nodes = .{bc.nodeWriter(node, .{})}, + }, .{ + .session_id = bc.session_id.?, + }); + } +} + // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults fn discardSearchResults(cmd: anytype) !void { const params = (try cmd.params(struct { From f04030904e5a9cdf4d2e382c78ca4cf05b7a8fe2 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 2 May 2025 15:55:49 +0200 Subject: [PATCH 07/10] cdp: fix tests for setchildnodes --- src/cdp/domains/dom.zig | 23 +++++++++++++++-------- src/cdp/testing.zig | 1 + 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 91b4a0d5..b2735383 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -94,6 +94,7 @@ fn performSearch(cmd: anytype) !void { fn dispatchSetChildNodes(cmd: anytype, nodes: []*parser.Node) !void { const arena = cmd.arena; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + const session_id = bc.session_id orelse return error.SessionIdNotLoaded; var parents: std.ArrayListUnmanaged(*parser.Node) = .{}; for (nodes) |_n| { @@ -125,35 +126,41 @@ fn dispatchSetChildNodes(cmd: anytype, nodes: []*parser.Node) !void { continue; } + // Register the node. + const node = try bc.node_registry.register(n); // If the node has no parent, it's the root node. // We don't dispatch event for it because we assume the root node is // dispatched via the DOM.getDocument command. - const p = try parser.nodeParentNode(n) orelse break; - // Register the node. - const node = try bc.node_registry.register(n); + const p = try parser.nodeParentNode(n) orelse continue; + // Retrieve the parent from the registry. - const parent_node = bc.node_registry.lookup_by_node.get(p) orelse unreachable; + const parent_node = try bc.node_registry.register(p); try cmd.sendEvent("DOM.setChildNodes", .{ .parentId = parent_node.id, .nodes = .{bc.nodeWriter(node, .{})}, }, .{ - .session_id = bc.session_id.?, + .session_id = session_id, }); } // now dispatch the event for the node list. for (nodes) |n| { - const node = bc.node_registry.lookup_by_node.get(n) orelse unreachable; + // Register the node. + const node = try bc.node_registry.register(n); + // If the node has no parent, it's the root node. + // We don't dispatch event for it because we assume the root node is + // dispatched via the DOM.getDocument command. const p = try parser.nodeParentNode(n) orelse continue; + // Retrieve the parent from the registry. - const parent_node = bc.node_registry.lookup_by_node.get(p) orelse unreachable; + const parent_node = try bc.node_registry.register(p); try cmd.sendEvent("DOM.setChildNodes", .{ .parentId = parent_node.id, .nodes = .{bc.nodeWriter(node, .{})}, }, .{ - .session_id = bc.session_id.?, + .session_id = session_id, }); } } diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index 045fa7ee..fc19d3af 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -120,6 +120,7 @@ const TestContext = struct { } if (opts.html) |html| { + if (bc.session_id == null) bc.session_id = "SID-X"; parser.deinit(); try parser.init(); const page = try bc.session.createPage(); From 9373cf9cf678e9d4e3a4ca1d3599621a41363540 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 2 May 2025 21:55:14 +0200 Subject: [PATCH 08/10] cdp: refacto sendChildNodes --- src/cdp/domains/dom.zig | 58 ++++++++++++----------------------------- 1 file changed, 16 insertions(+), 42 deletions(-) diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index b2735383..1ea5b9d9 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -88,27 +88,22 @@ fn performSearch(cmd: anytype) !void { // dispatchSetChildNodes send the setChildNodes event for the whole DOM tree // hierarchy of each nodes. -// If a parent has already been registred, we don't re-send a setChildNodes -// event anymore. // We dispatch event in the reverse order: from the top level to the direct parents. +// TODO we should dispatch a node only if it has never been sent. fn dispatchSetChildNodes(cmd: anytype, nodes: []*parser.Node) !void { const arena = cmd.arena; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const session_id = bc.session_id orelse return error.SessionIdNotLoaded; - var parents: std.ArrayListUnmanaged(*parser.Node) = .{}; + var parents: std.ArrayListUnmanaged(*Node) = .{}; for (nodes) |_n| { var n = _n; while (true) { const p = try parser.nodeParentNode(n) orelse break; - // If the parent exists in he registry, don't send the event. - // In this case we stop browsing the tree, b/c parents must have - // been sent already. - if (bc.node_registry.lookup_by_node.contains(p)) { - break; - } - try parents.append(arena, p); + // Register the node. + const node = try bc.node_registry.register(p); + try parents.append(arena, node); n = p; } } @@ -116,42 +111,21 @@ fn dispatchSetChildNodes(cmd: anytype, nodes: []*parser.Node) !void { const plen = parents.items.len; if (plen == 0) return; + var uniq: std.AutoHashMapUnmanaged(Node.Id, bool) = .{}; var i: usize = plen; while (i > 0) { i -= 1; - const n = parents.items[i]; - // If the parent exists in he registry, don't send the event. - // Indeed the parent can be twice in the array. - if (bc.node_registry.lookup_by_node.contains(n)) { + const node = parents.items[i]; + + if (uniq.contains(node.id)) continue; + try uniq.putNoClobber(arena, node.id, true); + + // If the node has no parent, it's the root node. + // We don't dispatch event for it because we assume the root node is + // dispatched via the DOM.getDocument command. + const p = try parser.nodeParentNode(node._node) orelse { continue; - } - - // Register the node. - const node = try bc.node_registry.register(n); - // If the node has no parent, it's the root node. - // We don't dispatch event for it because we assume the root node is - // dispatched via the DOM.getDocument command. - const p = try parser.nodeParentNode(n) orelse continue; - - // Retrieve the parent from the registry. - const parent_node = try bc.node_registry.register(p); - - try cmd.sendEvent("DOM.setChildNodes", .{ - .parentId = parent_node.id, - .nodes = .{bc.nodeWriter(node, .{})}, - }, .{ - .session_id = session_id, - }); - } - - // now dispatch the event for the node list. - for (nodes) |n| { - // Register the node. - const node = try bc.node_registry.register(n); - // If the node has no parent, it's the root node. - // We don't dispatch event for it because we assume the root node is - // dispatched via the DOM.getDocument command. - const p = try parser.nodeParentNode(n) orelse continue; + }; // Retrieve the parent from the registry. const parent_node = try bc.node_registry.register(p); From f8846279274b79c58a37d712fa9e438162a5f656 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 2 May 2025 22:10:26 +0200 Subject: [PATCH 09/10] cdp: sent setchildnodes once per node --- src/cdp/Node.zig | 2 ++ src/cdp/domains/dom.zig | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig index ddec7418..44b8c9f2 100644 --- a/src/cdp/Node.zig +++ b/src/cdp/Node.zig @@ -29,6 +29,7 @@ const Node = @This(); id: Id, _node: *parser.Node, +set_child_nodes_event: bool, // Whenever we send a node to the client, we register it here for future lookup. // We maintain a node -> id and id -> node lookup. @@ -85,6 +86,7 @@ pub const Registry = struct { node.* = .{ ._node = n, .id = id, + .set_child_nodes_event = false, }; node_lookup_gop.value_ptr.* = node; diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 1ea5b9d9..e17aa94e 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -89,7 +89,7 @@ fn performSearch(cmd: anytype) !void { // dispatchSetChildNodes send the setChildNodes event for the whole DOM tree // hierarchy of each nodes. // We dispatch event in the reverse order: from the top level to the direct parents. -// TODO we should dispatch a node only if it has never been sent. +// We should dispatch a node only if it has never been sent. fn dispatchSetChildNodes(cmd: anytype, nodes: []*parser.Node) !void { const arena = cmd.arena; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; @@ -103,6 +103,7 @@ fn dispatchSetChildNodes(cmd: anytype, nodes: []*parser.Node) !void { // Register the node. const node = try bc.node_registry.register(p); + if (node.set_child_nodes_event) break; try parents.append(arena, node); n = p; } @@ -111,14 +112,13 @@ fn dispatchSetChildNodes(cmd: anytype, nodes: []*parser.Node) !void { const plen = parents.items.len; if (plen == 0) return; - var uniq: std.AutoHashMapUnmanaged(Node.Id, bool) = .{}; var i: usize = plen; while (i > 0) { i -= 1; const node = parents.items[i]; + if (node.set_child_nodes_event) continue; - if (uniq.contains(node.id)) continue; - try uniq.putNoClobber(arena, node.id, true); + node.set_child_nodes_event = true; // If the node has no parent, it's the root node. // We don't dispatch event for it because we assume the root node is From 2402443dcc8a1f716c7fdb97e19ccae3baa1e4e8 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 5 May 2025 08:48:04 +0200 Subject: [PATCH 10/10] cdp: add comments on setChildNodes event Co-authored-by: Karl Seguin --- src/cdp/domains/dom.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index e17aa94e..c37f59cb 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -113,9 +113,14 @@ fn dispatchSetChildNodes(cmd: anytype, nodes: []*parser.Node) !void { if (plen == 0) return; var i: usize = plen; + // We're going to iterate in reverse order from how we added them. + // This ensures that we're emitting the tree of nodes top-down. while (i > 0) { i -= 1; const node = parents.items[i]; + // Although our above loop won't add an already-sent node to `parents` + // this can still be true because two nodes can share the same parent node + // so we might have just sent the node a previous iteration of this loop if (node.set_child_nodes_event) continue; node.set_child_nodes_event = true;