Skip to content

Commit f095411

Browse files
karlseguinsjorsdonkers
authored andcommitted
Better CDP node serialization
Include direct descendant, with hooks for other serialization options. Don't include parentId if null.
1 parent 309d70c commit f095411

File tree

4 files changed

+239
-119
lines changed

4 files changed

+239
-119
lines changed

src/cdp/Node.zig

Lines changed: 143 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -44,67 +44,27 @@ const CompatibilityMode = enum {
4444
NoQuirksMode,
4545
};
4646

47-
pub fn jsonStringify(self: *const Node, writer: anytype) !void {
48-
try writer.beginObject();
49-
try writer.objectField("nodeId");
50-
try writer.write(self.id);
51-
52-
try writer.objectField("parentId");
53-
try writer.write(self.parent_id);
54-
55-
try writer.objectField("backendNodeId");
56-
try writer.write(self.backend_node_id);
57-
58-
try writer.objectField("nodeType");
59-
try writer.write(self.node_type);
60-
61-
try writer.objectField("nodeName");
62-
try writer.write(self.node_name);
63-
64-
try writer.objectField("localName");
65-
try writer.write(self.local_name);
66-
67-
try writer.objectField("nodeValue");
68-
try writer.write(self.node_value);
69-
70-
try writer.objectField("childNodeCount");
71-
try writer.write(self.child_node_count);
72-
73-
try writer.objectField("children");
74-
try writer.write(self.children);
75-
76-
try writer.objectField("documentURL");
77-
try writer.write(self.document_url);
78-
79-
try writer.objectField("baseURL");
80-
try writer.write(self.base_url);
81-
82-
try writer.objectField("xmlVersion");
83-
try writer.write(self.xml_version);
84-
85-
try writer.objectField("compatibilityMode");
86-
try writer.write(self.compatibility_mode);
87-
88-
try writer.objectField("isScrollable");
89-
try writer.write(self.is_scrollable);
90-
try writer.endObject();
47+
pub fn writer(self: *const Node, opts: Writer.Opts) Writer {
48+
return .{ .node = self, .opts = opts };
9149
}
9250

9351
// Whenever we send a node to the client, we register it here for future lookup.
9452
// We maintain a node -> id and id -> node lookup.
9553
pub const Registry = struct {
9654
node_id: u32,
9755
allocator: Allocator,
56+
arena: std.heap.ArenaAllocator,
9857
node_pool: std.heap.MemoryPool(Node),
9958
lookup_by_id: std.AutoHashMapUnmanaged(Id, *Node),
10059
lookup_by_node: std.HashMapUnmanaged(*parser.Node, *Node, NodeContext, std.hash_map.default_max_load_percentage),
10160

10261
pub fn init(allocator: Allocator) Registry {
10362
return .{
10463
.node_id = 0,
105-
.allocator = allocator,
10664
.lookup_by_id = .{},
10765
.lookup_by_node = .{},
66+
.allocator = allocator,
67+
.arena = std.heap.ArenaAllocator.init(allocator),
10868
.node_pool = std.heap.MemoryPool(Node).init(allocator),
10969
};
11070
}
@@ -114,12 +74,14 @@ pub const Registry = struct {
11474
self.lookup_by_id.deinit(allocator);
11575
self.lookup_by_node.deinit(allocator);
11676
self.node_pool.deinit();
77+
self.arena.deinit();
11778
}
11879

11980
pub fn reset(self: *Registry) void {
12081
self.lookup_by_id.clearRetainingCapacity();
12182
self.lookup_by_node.clearRetainingCapacity();
122-
_ = self.node_pool.reset(.{ .retain_capacity = {} });
83+
_ = self.arena.reset(.{ .retain_with_limit = 1024 });
84+
_ = self.node_pool.reset(.{ .retain_with_limit = 1024 });
12385
}
12486

12587
pub fn register(self: *Registry, n: *parser.Node) !*Node {
@@ -132,11 +94,10 @@ pub const Registry = struct {
13294
// but, just in case, let's try to keep things tidy.
13395
errdefer _ = self.lookup_by_node.remove(n);
13496

135-
const children = try parser.nodeGetChildNodes(n);
136-
const children_count = try parser.nodeListLength(children);
137-
13897
const id = self.node_id;
139-
defer self.node_id = id + 1;
98+
self.node_id = id + 1;
99+
100+
const child_nodes = try self.registerChildNodes(n);
140101

141102
const node = try self.node_pool.create();
142103
errdefer self.node_pool.destroy(node);
@@ -146,12 +107,12 @@ pub const Registry = struct {
146107
.id = id,
147108
.parent_id = null, // TODO
148109
.backend_node_id = id, // ??
149-
.node_name = try parser.nodeName(n),
150-
.local_name = try parser.nodeLocalName(n),
151-
.node_value = try parser.nodeValue(n) orelse "",
152-
.node_type = @intFromEnum(try parser.nodeType(n)),
153-
.child_node_count = children_count,
154-
.children = &.{}, // TODO
110+
.node_name = parser.nodeName(n) catch return error.NodeNameError,
111+
.local_name = parser.nodeLocalName(n) catch return error.NodeLocalNameError,
112+
.node_value = (parser.nodeValue(n) catch return error.NameValueError) orelse "",
113+
.node_type = @intFromEnum(parser.nodeType(n) catch return error.NodeTypeError),
114+
.child_node_count = @intCast(child_nodes.len),
115+
.children = child_nodes,
155116
.document_url = null,
156117
.base_url = null,
157118
.xml_version = "",
@@ -168,6 +129,31 @@ pub const Registry = struct {
168129
try self.lookup_by_id.putNoClobber(self.allocator, id, node);
169130
return node;
170131
}
132+
133+
pub fn registerChildNodes(self: *Registry, n: *parser.Node) RegisterError![]*Node {
134+
const node_list = parser.nodeGetChildNodes(n) catch return error.GetChildNodeError;
135+
const count = parser.nodeListLength(node_list) catch return error.NodeListLengthError;
136+
137+
var arr = try self.arena.allocator().alloc(*Node, count);
138+
var i: usize = 0;
139+
for (0..count) |_| {
140+
const child = (parser.nodeListItem(node_list, @intCast(i)) catch return error.NodeListItemError) orelse continue;
141+
arr[i] = try self.register(child);
142+
i += 1;
143+
}
144+
return arr[0..i];
145+
}
146+
};
147+
148+
const RegisterError = error{
149+
OutOfMemory,
150+
GetChildNodeError,
151+
NodeListLengthError,
152+
NodeListItemError,
153+
NodeNameError,
154+
NodeLocalNameError,
155+
NameValueError,
156+
NodeTypeError,
171157
};
172158

173159
const NodeContext = struct {
@@ -271,8 +257,76 @@ pub const Search = struct {
271257
};
272258
};
273259

260+
// Need a custom writer, because we can't just serialize the node as-is.
261+
// Sometimes we want to serializ the node without chidren, sometimes with just
262+
// its direct children, and sometimes the entire tree.
263+
// (For now, we only support direct children)
264+
pub const Writer = struct {
265+
opts: Opts,
266+
node: *const Node,
267+
268+
pub const Opts = struct {};
269+
270+
pub fn jsonStringify(self: *const Writer, w: anytype) !void {
271+
try w.beginObject();
272+
try writeCommon(self.node, w);
273+
try w.objectField("children");
274+
try w.beginArray();
275+
for (self.node.children) |node| {
276+
try w.beginObject();
277+
try writeCommon(node, w);
278+
try w.endObject();
279+
}
280+
try w.endArray();
281+
try w.endObject();
282+
}
283+
284+
fn writeCommon(node: *const Node, w: anytype) !void {
285+
try w.objectField("nodeId");
286+
try w.write(node.id);
287+
288+
if (node.parent_id) |pid| {
289+
try w.objectField("parentId");
290+
try w.write(pid);
291+
}
292+
293+
try w.objectField("backendNodeId");
294+
try w.write(node.backend_node_id);
295+
296+
try w.objectField("nodeType");
297+
try w.write(node.node_type);
298+
299+
try w.objectField("nodeName");
300+
try w.write(node.node_name);
301+
302+
try w.objectField("localName");
303+
try w.write(node.local_name);
304+
305+
try w.objectField("nodeValue");
306+
try w.write(node.node_value);
307+
308+
try w.objectField("childNodeCount");
309+
try w.write(node.child_node_count);
310+
311+
try w.objectField("documentURL");
312+
try w.write(node.document_url);
313+
314+
try w.objectField("baseURL");
315+
try w.write(node.base_url);
316+
317+
try w.objectField("xmlVersion");
318+
try w.write(node.xml_version);
319+
320+
try w.objectField("compatibilityMode");
321+
try w.write(node.compatibility_mode);
322+
323+
try w.objectField("isScrollable");
324+
try w.write(node.is_scrollable);
325+
}
326+
};
327+
274328
const testing = @import("testing.zig");
275-
test "CDP Node: Registry register" {
329+
test "cdp Node: Registry register" {
276330
var registry = Registry.init(testing.allocator);
277331
defer registry.deinit();
278332

@@ -298,7 +352,8 @@ test "CDP Node: Registry register" {
298352
try testing.expectEqual("a", node.local_name);
299353
try testing.expectEqual("", node.node_value);
300354
try testing.expectEqual(1, node.child_node_count);
301-
try testing.expectEqual(0, node.children.len);
355+
try testing.expectEqual(1, node.children.len);
356+
try testing.expectEqual(1, node.children[0].id);
302357
try testing.expectEqual(null, node.document_url);
303358
try testing.expectEqual(null, node.base_url);
304359
try testing.expectEqual("", node.xml_version);
@@ -310,20 +365,21 @@ test "CDP Node: Registry register" {
310365
{
311366
const n = (try doc.querySelector("p")).?;
312367
const node = try registry.register(n);
313-
const n1b = registry.lookup_by_id.get(1).?;
368+
const n1b = registry.lookup_by_id.get(2).?;
314369
const n1c = registry.lookup_by_node.get(node._node).?;
315370
try testing.expectEqual(node, n1b);
316371
try testing.expectEqual(node, n1c);
317372

318-
try testing.expectEqual(1, node.id);
373+
try testing.expectEqual(2, node.id);
319374
try testing.expectEqual(null, node.parent_id);
320375
try testing.expectEqual(1, node.node_type);
321-
try testing.expectEqual(1, node.backend_node_id);
376+
try testing.expectEqual(2, node.backend_node_id);
322377
try testing.expectEqual("P", node.node_name);
323378
try testing.expectEqual("p", node.local_name);
324379
try testing.expectEqual("", node.node_value);
325380
try testing.expectEqual(1, node.child_node_count);
326-
try testing.expectEqual(0, node.children.len);
381+
try testing.expectEqual(1, node.children.len);
382+
try testing.expectEqual(3, node.children[0].id);
327383
try testing.expectEqual(null, node.document_url);
328384
try testing.expectEqual(null, node.base_url);
329385
try testing.expectEqual("", node.xml_version);
@@ -333,7 +389,7 @@ test "CDP Node: Registry register" {
333389
}
334390
}
335391

336-
test "CDP Node: search list" {
392+
test "cdp Node: search list" {
337393
var registry = Registry.init(testing.allocator);
338394
defer registry.deinit();
339395

@@ -383,3 +439,27 @@ test "CDP Node: search list" {
383439
try testing.expectEqual(2, registry.lookup_by_node.count());
384440
}
385441
}
442+
443+
test "cdp Node: Writer" {
444+
var registry = Registry.init(testing.allocator);
445+
defer registry.deinit();
446+
447+
var doc = try testing.Document.init("<a id=a1></a><a id=a2></a>");
448+
defer doc.deinit();
449+
450+
{
451+
const node = try registry.register(doc.asNode());
452+
const json = try std.json.stringifyAlloc(testing.allocator, node.writer(.{}), .{});
453+
defer testing.allocator.free(json);
454+
455+
try testing.expectJson(.{ .nodeId = 0, .backendNodeId = 0, .nodeType = 9, .nodeName = "#document", .localName = "", .nodeValue = "", .documentURL = null, .baseURL = null, .xmlVersion = "", .isScrollable = false, .compatibilityMode = "NoQuirksMode", .childNodeCount = 1, .children = &.{.{ .nodeId = 1, .backendNodeId = 1, .nodeType = 1, .nodeName = "HTML", .localName = "html", .nodeValue = "", .childNodeCount = 2, .documentURL = null, .baseURL = null, .xmlVersion = "", .compatibilityMode = "NoQuirksMode", .isScrollable = false }} }, json);
456+
}
457+
458+
{
459+
const node = registry.lookup_by_id.get(1).?;
460+
const json = try std.json.stringifyAlloc(testing.allocator, node.writer(.{}), .{});
461+
defer testing.allocator.free(json);
462+
463+
try testing.expectJson(.{ .nodeId = 1, .backendNodeId = 1, .nodeType = 1, .nodeName = "HTML", .localName = "html", .nodeValue = "", .childNodeCount = 2, .documentURL = null, .baseURL = null, .xmlVersion = "", .compatibilityMode = "NoQuirksMode", .isScrollable = false, .children = &.{ .{ .nodeId = 2, .backendNodeId = 2, .nodeType = 1, .nodeName = "HEAD", .localName = "head", .nodeValue = "", .childNodeCount = 0, .documentURL = null, .baseURL = null, .xmlVersion = "", .compatibilityMode = "NoQuirksMode", .isScrollable = false }, .{ .nodeId = 3, .backendNodeId = 3, .nodeType = 1, .nodeName = "BODY", .localName = "body", .nodeValue = "", .childNodeCount = 2, .documentURL = null, .baseURL = null, .xmlVersion = "", .compatibilityMode = "NoQuirksMode", .isScrollable = false } } }, json);
464+
}
465+
}

src/cdp/domains/dom.zig

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,7 @@ fn getDocument(cmd: anytype) !void {
5151
const doc = page.doc orelse return error.DocumentNotLoaded;
5252

5353
const node = try bc.node_registry.register(parser.documentToNode(doc));
54-
return cmd.sendResult(.{
55-
.root = node,
56-
}, .{});
54+
return cmd.sendResult(.{ .root = node.writer(.{}) }, .{});
5755
}
5856

5957
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch
@@ -118,6 +116,7 @@ fn getSearchResults(cmd: anytype) !void {
118116
}
119117

120118
const testing = @import("../testing.zig");
119+
121120
test "cdp.dom: getSearchResults unknown search id" {
122121
var ctx = testing.context();
123122
defer ctx.deinit();
@@ -149,15 +148,15 @@ test "cdp.dom: search flow" {
149148
.method = "DOM.getSearchResults",
150149
.params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 2 },
151150
});
152-
try ctx.expectSentResult(.{ .nodeIds = &.{ 0, 1 } }, .{ .id = 13 });
151+
try ctx.expectSentResult(.{ .nodeIds = &.{ 0, 2 } }, .{ .id = 13 });
153152

154153
// different fromIndex
155154
try ctx.processMessage(.{
156155
.id = 14,
157156
.method = "DOM.getSearchResults",
158157
.params = .{ .searchId = "0", .fromIndex = 1, .toIndex = 2 },
159158
});
160-
try ctx.expectSentResult(.{ .nodeIds = &.{1} }, .{ .id = 14 });
159+
try ctx.expectSentResult(.{ .nodeIds = &.{2} }, .{ .id = 14 });
161160

162161
// different toIndex
163162
try ctx.processMessage(.{

0 commit comments

Comments
 (0)