From 09ca0e6ef054106ac6ecbfcaa41be3f2603395fc Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 14 Jul 2025 15:13:01 +0800 Subject: [PATCH] Add support for CDP's DOM.requestChildNodes https://github.com/lightpanda-io/browser/issues/866 --- src/cdp/Node.zig | 143 +++++++++++++++++++++++++++++++--------- src/cdp/cdp.zig | 7 +- src/cdp/domains/dom.zig | 25 +++++++ 3 files changed, 140 insertions(+), 35 deletions(-) diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig index 81ddc00d..2e987c7e 100644 --- a/src/cdp/Node.zig +++ b/src/cdp/Node.zig @@ -201,49 +201,69 @@ pub const Search = struct { // (For now, we only support direct children) pub const Writer = struct { - opts: Opts, - node: *const Node, + depth: i32, + exclude_root: bool, + root: *const Node, registry: *Registry, - pub const Opts = struct {}; + pub const Opts = struct { + depth: i32 = 0, + exclude_root: bool = false, + }; pub fn jsonStringify(self: *const Writer, w: anytype) !void { - self.toJSON(w) catch |err| { - // The only error our jsonStringify method can return is - // @TypeOf(w).Error. In other words, our code can't return its own - // error, we can only return a writer error. Kinda sucks. - log.err(.cdp, "json stringify", .{ .err = err }); - return error.OutOfMemory; - }; + if (self.exclude_root) { + _ = self.writeChildren(self.root, 0, w) catch |err| { + log.err(.cdp, "node writeChildren", .{ .err = err }); + return error.OutOfMemory; + }; + } else { + self.toJSON(self.root, 0, w) catch |err| { + // The only error our jsonStringify method can return is + // @TypeOf(w).Error. In other words, our code can't return its own + // error, we can only return a writer error. Kinda sucks. + log.err(.cdp, "node toJSON stringify", .{ .err = err }); + return error.OutOfMemory; + }; + } } - fn toJSON(self: *const Writer, w: anytype) !void { + fn toJSON(self: *const Writer, node: *const Node, depth: usize, w: anytype) !void { try w.beginObject(); - try self.writeCommon(self.node, false, w); + try self.writeCommon(node, false, w); - { - var registry = self.registry; - const child_nodes = try parser.nodeGetChildNodes(self.node._node); - const child_count = try parser.nodeListLength(child_nodes); + try w.objectField("children"); + const child_count = try self.writeChildren(node, depth, w); + try w.objectField("childNodeCount"); + try w.write(child_count); - var i: usize = 0; - try w.objectField("children"); - try w.beginArray(); - for (0..child_count) |_| { - const child = (try parser.nodeListItem(child_nodes, @intCast(i))) orelse break; - const child_node = try registry.register(child); + try w.endObject(); + } + + fn writeChildren(self: *const Writer, node: *const Node, depth: usize, w: anytype) anyerror!usize { + var registry = self.registry; + const child_nodes = try parser.nodeGetChildNodes(node._node); + const child_count = try parser.nodeListLength(child_nodes); + const full_child = self.depth < 0 or self.depth < depth; + + var i: usize = 0; + try w.beginArray(); + for (0..child_count) |_| { + const child = (try parser.nodeListItem(child_nodes, @intCast(i))) orelse break; + const child_node = try registry.register(child); + if (full_child) { + try self.toJSON(child_node, depth + 1, w); + } else { try w.beginObject(); try self.writeCommon(child_node, true, w); try w.endObject(); - i += 1; } - try w.endArray(); - try w.objectField("childNodeCount"); - try w.write(i); + i += 1; } + try w.endArray(); - try w.endObject(); + return i; } fn writeCommon(self: *const Writer, node: *const Node, include_child_count: bool, w: anytype) !void { @@ -400,14 +420,15 @@ test "cdp Node: Writer" { var registry = Registry.init(testing.allocator); defer registry.deinit(); - var doc = try testing.Document.init(""); + var doc = try testing.Document.init("
"); defer doc.deinit(); { const node = try registry.register(doc.asNode()); const json = try std.json.stringifyAlloc(testing.allocator, Writer{ - .node = node, - .opts = .{}, + .root = node, + .depth = 0, + .exclude_root = false, .registry = ®istry, }, .{}); defer testing.allocator.free(json); @@ -445,8 +466,9 @@ test "cdp Node: Writer" { { const node = registry.lookup_by_id.get(1).?; const json = try std.json.stringifyAlloc(testing.allocator, Writer{ - .node = node, - .opts = .{}, + .root = node, + .depth = 0, + .exclude_root = false, .registry = ®istry, }, .{}); defer testing.allocator.free(json); @@ -495,4 +517,61 @@ test "cdp Node: Writer" { } }, }, json); } + + { + const node = registry.lookup_by_id.get(1).?; + const json = try std.json.stringifyAlloc(testing.allocator, Writer{ + .root = node, + .depth = -1, + .exclude_root = true, + .registry = ®istry, + }, .{}); + defer testing.allocator.free(json); + + try testing.expectJson(&.{ .{ + .nodeId = 2, + .backendNodeId = 2, + .nodeType = 1, + .nodeName = "HEAD", + .localName = "head", + .nodeValue = "", + .childNodeCount = 0, + .documentURL = null, + .baseURL = null, + .xmlVersion = "", + .compatibilityMode = "NoQuirksMode", + .isScrollable = false, + .parentId = 1, + }, .{ + .nodeId = 3, + .backendNodeId = 3, + .nodeType = 1, + .nodeName = "BODY", + .localName = "body", + .nodeValue = "", + .childNodeCount = 2, + .documentURL = null, + .baseURL = null, + .xmlVersion = "", + .compatibilityMode = "NoQuirksMode", + .isScrollable = false, + .children = &.{ .{ + .nodeId = 4, + .localName = "a", + .childNodeCount = 0, + .parentId = 3, + }, .{ + .nodeId = 5, + .localName = "div", + .childNodeCount = 1, + .parentId = 3, + .children = &.{ .{ + .nodeId = 6, + .localName = "a", + .childNodeCount = 0, + .parentId = 5, + }} + } + } } }, json); + } } diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 2472481a..25ccb730 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -402,10 +402,11 @@ pub fn BrowserContext(comptime CDP_T: type) type { return &self.isolated_world.?; } - pub fn nodeWriter(self: *Self, node: *const Node, opts: Node.Writer.Opts) Node.Writer { + pub fn nodeWriter(self: *Self, root: *const Node, opts: Node.Writer.Opts) Node.Writer { return .{ - .node = node, - .opts = opts, + .root = root, + .depth = opts.depth, + .exclude_root = opts.exclude_root, .registry = &self.node_registry, }; } diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 89a077f6..a352328d 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -38,6 +38,7 @@ pub fn processMessage(cmd: anytype) !void { scrollIntoViewIfNeeded, getContentQuads, getBoxModel, + requestChildNodes, }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { @@ -53,6 +54,7 @@ pub fn processMessage(cmd: anytype) !void { .scrollIntoViewIfNeeded => return scrollIntoViewIfNeeded(cmd), .getContentQuads => return getContentQuads(cmd), .getBoxModel => return getBoxModel(cmd), + .requestChildNodes => return requestChildNodes(cmd), } } @@ -433,6 +435,29 @@ fn getBoxModel(cmd: anytype) !void { } }, .{}); } +fn requestChildNodes(cmd: anytype) !void { + const params = (try cmd.params(struct { + nodeId: Node.Id, + depth: i32 = 1, + pierce: bool = false, + })) orelse return error.InvalidParams; + + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + const session_id = bc.session_id orelse return error.SessionIdNotLoaded; + const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse { + return error.InvalidNode; + }; + + try cmd.sendEvent("DOM.setChildNodes", .{ + .parentId = node.id, + .nodes = bc.nodeWriter(node, .{.depth = params.depth, .exclude_root = true}), + }, .{ + .session_id = session_id, + }); + + return cmd.sendResult(null, .{}); +} + const testing = @import("../testing.zig"); test "cdp.dom: getSearchResults unknown search id" {