Merge pull request #587 from lightpanda-io/dom-setchildnodes

cdp: dispatch DOM.setChildNodes event for search results
This commit is contained in:
Pierre Tachoire
2025-05-05 08:56:04 +02:00
committed by GitHub
6 changed files with 104 additions and 21 deletions

View File

@@ -99,7 +99,8 @@ pub const Element = struct {
} }
pub fn get_attributes(self: *parser.Element) !*parser.NamedNodeMap { 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 { pub fn get_innerHTML(self: *parser.Element, state: *SessionState) ![]const u8 {

View File

@@ -38,18 +38,18 @@ pub fn writeNode(node: *parser.Node, writer: anytype) anyerror!void {
try writer.writeAll(tag); try writer.writeAll(tag);
// write the attributes // write the attributes
const map = try parser.nodeGetAttributes(node); const _map = try parser.nodeGetAttributes(node);
if (_map) |map| {
const ln = try parser.namedNodeMapGetLength(map); const ln = try parser.namedNodeMapGetLength(map);
var i: u32 = 0; for (0..ln) |i| {
while (i < ln) { const attr = try parser.namedNodeMapItem(map, @intCast(i)) orelse break;
const attr = try parser.namedNodeMapItem(map, i) orelse break;
try writer.writeAll(" "); try writer.writeAll(" ");
try writer.writeAll(try parser.attributeGetName(attr)); try writer.writeAll(try parser.attributeGetName(attr));
try writer.writeAll("=\""); try writer.writeAll("=\"");
const attribute_value = try parser.attributeGetValue(attr) orelse ""; const attribute_value = try parser.attributeGetValue(attr) orelse "";
try writeEscapedAttributeValue(writer, attribute_value); try writeEscapedAttributeValue(writer, attribute_value);
try writer.writeAll("\""); try writer.writeAll("\"");
i += 1; }
} }
try writer.writeAll(">"); try writer.writeAll(">");

View File

@@ -1250,11 +1250,11 @@ pub fn nodeHasAttributes(node: *Node) !bool {
return res; return res;
} }
pub fn nodeGetAttributes(node: *Node) !*NamedNodeMap { pub fn nodeGetAttributes(node: *Node) !?*NamedNodeMap {
var res: ?*NamedNodeMap = undefined; var res: ?*NamedNodeMap = undefined;
const err = nodeVtable(node).dom_node_get_attributes.?(node, &res); const err = nodeVtable(node).dom_node_get_attributes.?(node, &res);
try DOMErr(err); try DOMErr(err);
return res.?; return res;
} }
pub fn nodeGetNamespace(node: *Node) !?[]const u8 { pub fn nodeGetNamespace(node: *Node) !?[]const u8 {

View File

@@ -29,6 +29,7 @@ const Node = @This();
id: Id, id: Id,
_node: *parser.Node, _node: *parser.Node,
set_child_nodes_event: bool,
// Whenever we send a node to the client, we register it here for future lookup. // Whenever we send a node to the client, we register it here for future lookup.
// We maintain a node -> id and id -> node lookup. // We maintain a node -> id and id -> node lookup.
@@ -85,6 +86,7 @@ pub const Registry = struct {
node.* = .{ node.* = .{
._node = n, ._node = n,
.id = id, .id = id,
.set_child_nodes_event = false,
}; };
node_lookup_gop.value_ptr.* = node; node_lookup_gop.value_ptr.* = node;
@@ -218,7 +220,7 @@ pub const Writer = struct {
fn toJSON(self: *const Writer, w: anytype) !void { fn toJSON(self: *const Writer, w: anytype) !void {
try w.beginObject(); try w.beginObject();
try writeCommon(self.node, false, w); try self.writeCommon(self.node, false, w);
{ {
var registry = self.registry; var registry = self.registry;
@@ -232,7 +234,7 @@ pub const Writer = struct {
const child = (try parser.nodeListItem(child_nodes, @intCast(i))) orelse break; const child = (try parser.nodeListItem(child_nodes, @intCast(i))) orelse break;
const child_node = try registry.register(child); const child_node = try registry.register(child);
try w.beginObject(); try w.beginObject();
try writeCommon(child_node, true, w); try self.writeCommon(child_node, true, w);
try w.endObject(); try w.endObject();
i += 1; i += 1;
} }
@@ -245,7 +247,7 @@ pub const Writer = struct {
try w.endObject(); 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.objectField("nodeId");
try w.write(node.id); try w.write(node.id);
@@ -254,9 +256,24 @@ pub const Writer = struct {
const n = node._node; const n = node._node;
// TODO: if (try parser.nodeParentNode(n)) |p| {
// try w.objectField("parentId"); const parent_node = try self.registry.register(p);
// try w.write(pid); try w.objectField("parentId");
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.objectField("nodeType");
try w.write(@intFromEnum(try parser.nodeType(n))); try w.write(@intFromEnum(try parser.nodeType(n)));
@@ -461,6 +478,7 @@ test "cdp Node: Writer" {
.xmlVersion = "", .xmlVersion = "",
.compatibilityMode = "NoQuirksMode", .compatibilityMode = "NoQuirksMode",
.isScrollable = false, .isScrollable = false,
.parentId = 1,
}, .{ }, .{
.nodeId = 3, .nodeId = 3,
.backendNodeId = 3, .backendNodeId = 3,
@@ -474,6 +492,7 @@ test "cdp Node: Writer" {
.xmlVersion = "", .xmlVersion = "",
.compatibilityMode = "NoQuirksMode", .compatibilityMode = "NoQuirksMode",
.isScrollable = false, .isScrollable = false,
.parentId = 1,
} }, } },
}, json); }, json);
} }

View File

@@ -76,12 +76,74 @@ fn performSearch(cmd: anytype) !void {
const search = try bc.node_search_list.create(list.nodes.items); 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.
try dispatchSetChildNodes(cmd, list.nodes.items);
return cmd.sendResult(.{ return cmd.sendResult(.{
.searchId = search.name, .searchId = search.name,
.resultCount = @as(u32, @intCast(search.node_ids.len)), .resultCount = @as(u32, @intCast(search.node_ids.len)),
}, .{}); }, .{});
} }
// 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.
// 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(*Node) = .{};
for (nodes) |_n| {
var n = _n;
while (true) {
const p = try parser.nodeParentNode(n) orelse break;
// 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;
}
}
const plen = parents.items.len;
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;
// 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;
};
// 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,
});
}
}
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults
fn discardSearchResults(cmd: anytype) !void { fn discardSearchResults(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {

View File

@@ -120,6 +120,7 @@ const TestContext = struct {
} }
if (opts.html) |html| { if (opts.html) |html| {
if (bc.session_id == null) bc.session_id = "SID-X";
parser.deinit(); parser.deinit();
try parser.init(); try parser.init();
const page = try bc.session.createPage(); const page = try bc.session.createPage();