diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig
index 3f8f069a..f609be49 100644
--- a/src/cdp/Node.zig
+++ b/src/cdp/Node.zig
@@ -22,79 +22,19 @@ const Allocator = std.mem.Allocator;
pub const Id = u32;
+const log = std.log.scoped(.cdp_node);
+
const Node = @This();
id: Id,
-parent_id: ?Id = null,
-node_type: u32,
-backend_node_id: Id,
-node_name: []const u8,
-local_name: []const u8,
-node_value: []const u8,
-child_node_count: u32,
-children: []const *Node,
-document_url: ?[]const u8,
-base_url: ?[]const u8,
-xml_version: []const u8,
-compatibility_mode: CompatibilityMode,
-is_scrollable: bool,
_node: *parser.Node,
-const CompatibilityMode = enum {
- NoQuirksMode,
-};
-
-pub fn jsonStringify(self: *const Node, writer: anytype) !void {
- try writer.beginObject();
- try writer.objectField("nodeId");
- try writer.write(self.id);
-
- try writer.objectField("parentId");
- try writer.write(self.parent_id);
-
- try writer.objectField("backendNodeId");
- try writer.write(self.backend_node_id);
-
- try writer.objectField("nodeType");
- try writer.write(self.node_type);
-
- try writer.objectField("nodeName");
- try writer.write(self.node_name);
-
- try writer.objectField("localName");
- try writer.write(self.local_name);
-
- try writer.objectField("nodeValue");
- try writer.write(self.node_value);
-
- try writer.objectField("childNodeCount");
- try writer.write(self.child_node_count);
-
- try writer.objectField("children");
- try writer.write(self.children);
-
- try writer.objectField("documentURL");
- try writer.write(self.document_url);
-
- try writer.objectField("baseURL");
- try writer.write(self.base_url);
-
- try writer.objectField("xmlVersion");
- try writer.write(self.xml_version);
-
- try writer.objectField("compatibilityMode");
- try writer.write(self.compatibility_mode);
-
- try writer.objectField("isScrollable");
- try writer.write(self.is_scrollable);
- try writer.endObject();
-}
-
// Whenever we send a node to the client, we register it here for future lookup.
// We maintain a node -> id and id -> node lookup.
pub const Registry = struct {
node_id: u32,
allocator: Allocator,
+ arena: std.heap.ArenaAllocator,
node_pool: std.heap.MemoryPool(Node),
lookup_by_id: std.AutoHashMapUnmanaged(Id, *Node),
lookup_by_node: std.HashMapUnmanaged(*parser.Node, *Node, NodeContext, std.hash_map.default_max_load_percentage),
@@ -102,9 +42,10 @@ pub const Registry = struct {
pub fn init(allocator: Allocator) Registry {
return .{
.node_id = 0,
- .allocator = allocator,
.lookup_by_id = .{},
.lookup_by_node = .{},
+ .allocator = allocator,
+ .arena = std.heap.ArenaAllocator.init(allocator),
.node_pool = std.heap.MemoryPool(Node).init(allocator),
};
}
@@ -114,12 +55,14 @@ pub const Registry = struct {
self.lookup_by_id.deinit(allocator);
self.lookup_by_node.deinit(allocator);
self.node_pool.deinit();
+ self.arena.deinit();
}
pub fn reset(self: *Registry) void {
self.lookup_by_id.clearRetainingCapacity();
self.lookup_by_node.clearRetainingCapacity();
- _ = self.node_pool.reset(.{ .retain_capacity = {} });
+ _ = self.arena.reset(.{ .retain_with_limit = 1024 });
+ _ = self.node_pool.reset(.{ .retain_with_limit = 1024 });
}
pub fn register(self: *Registry, n: *parser.Node) !*Node {
@@ -132,38 +75,17 @@ pub const Registry = struct {
// but, just in case, let's try to keep things tidy.
errdefer _ = self.lookup_by_node.remove(n);
- const children = try parser.nodeGetChildNodes(n);
- const children_count = try parser.nodeListLength(children);
-
- const id = self.node_id;
- defer self.node_id = id + 1;
-
const node = try self.node_pool.create();
errdefer self.node_pool.destroy(node);
+ const id = self.node_id;
+ self.node_id = id + 1;
+
node.* = .{
._node = n,
.id = id,
- .parent_id = null, // TODO
- .backend_node_id = id, // ??
- .node_name = try parser.nodeName(n),
- .local_name = try parser.nodeLocalName(n),
- .node_value = try parser.nodeValue(n) orelse "",
- .node_type = @intFromEnum(try parser.nodeType(n)),
- .child_node_count = children_count,
- .children = &.{}, // TODO
- .document_url = null,
- .base_url = null,
- .xml_version = "",
- .compatibility_mode = .NoQuirksMode,
- .is_scrollable = false,
};
- // if (try parser.nodeParentNode(n)) |pn| {
- // _ = pn;
- // // TODO
- // }
-
node_lookup_gop.value_ptr.* = node;
try self.lookup_by_id.putNoClobber(self.allocator, id, node);
return node;
@@ -271,8 +193,107 @@ pub const Search = struct {
};
};
+// Need a custom writer, because we can't just serialize the node as-is.
+// Sometimes we want to serializ the node without chidren, sometimes with just
+// its direct children, and sometimes the entire tree.
+// (For now, we only support direct children)
+
+pub const Writer = struct {
+ opts: Opts,
+ node: *const Node,
+ registry: *Registry,
+
+ pub const Opts = struct {};
+
+ 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("json stringify: {}", .{err});
+ return error.OutOfMemory;
+ };
+ }
+
+ fn toJSON(self: *const Writer, w: anytype) !void {
+ try w.beginObject();
+ try writeCommon(self.node, false, w);
+
+ {
+ var registry = self.registry;
+ const child_nodes = try parser.nodeGetChildNodes(self.node._node);
+ const child_count = try parser.nodeListLength(child_nodes);
+
+ 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.beginObject();
+ try writeCommon(child_node, true, w);
+ try w.endObject();
+ i += 1;
+ }
+ try w.endArray();
+
+ try w.objectField("childNodeCount");
+ try w.write(i);
+ }
+
+ try w.endObject();
+ }
+
+ fn writeCommon(node: *const Node, include_child_count: bool, w: anytype) !void {
+ try w.objectField("nodeId");
+ try w.write(node.id);
+
+ try w.objectField("backendNodeId");
+ try w.write(node.id);
+
+ const n = node._node;
+
+ // TODO:
+ // try w.objectField("parentId");
+ // try w.write(pid);
+
+ try w.objectField("nodeType");
+ try w.write(@intFromEnum(try parser.nodeType(n)));
+
+ try w.objectField("nodeName");
+ try w.write(try parser.nodeName(n));
+
+ try w.objectField("localName");
+ try w.write(try parser.nodeLocalName(n));
+
+ try w.objectField("nodeValue");
+ try w.write((try parser.nodeValue(n)) orelse "");
+
+ if (include_child_count) {
+ try w.objectField("childNodeCount");
+ const child_nodes = try parser.nodeGetChildNodes(n);
+ try w.write(try parser.nodeListLength(child_nodes));
+ }
+
+ try w.objectField("documentURL");
+ try w.write(null);
+
+ try w.objectField("baseURL");
+ try w.write(null);
+
+ try w.objectField("xmlVersion");
+ try w.write("");
+
+ try w.objectField("compatibilityMode");
+ try w.write("NoQuirksMode");
+
+ try w.objectField("isScrollable");
+ try w.write(false);
+ }
+};
+
const testing = @import("testing.zig");
-test "CDP Node: Registry register" {
+test "cdp Node: Registry register" {
var registry = Registry.init(testing.allocator);
defer registry.deinit();
@@ -291,19 +312,6 @@ test "CDP Node: Registry register" {
try testing.expectEqual(node, n1c);
try testing.expectEqual(0, node.id);
- try testing.expectEqual(null, node.parent_id);
- try testing.expectEqual(1, node.node_type);
- try testing.expectEqual(0, node.backend_node_id);
- try testing.expectEqual("A", node.node_name);
- try testing.expectEqual("a", node.local_name);
- try testing.expectEqual("", node.node_value);
- try testing.expectEqual(1, node.child_node_count);
- try testing.expectEqual(0, node.children.len);
- try testing.expectEqual(null, node.document_url);
- try testing.expectEqual(null, node.base_url);
- try testing.expectEqual("", node.xml_version);
- try testing.expectEqual(.NoQuirksMode, node.compatibility_mode);
- try testing.expectEqual(false, node.is_scrollable);
try testing.expectEqual(n, node._node);
}
@@ -316,24 +324,11 @@ test "CDP Node: Registry register" {
try testing.expectEqual(node, n1c);
try testing.expectEqual(1, node.id);
- try testing.expectEqual(null, node.parent_id);
- try testing.expectEqual(1, node.node_type);
- try testing.expectEqual(1, node.backend_node_id);
- try testing.expectEqual("P", node.node_name);
- try testing.expectEqual("p", node.local_name);
- try testing.expectEqual("", node.node_value);
- try testing.expectEqual(1, node.child_node_count);
- try testing.expectEqual(0, node.children.len);
- try testing.expectEqual(null, node.document_url);
- try testing.expectEqual(null, node.base_url);
- try testing.expectEqual("", node.xml_version);
- try testing.expectEqual(.NoQuirksMode, node.compatibility_mode);
- try testing.expectEqual(false, node.is_scrollable);
try testing.expectEqual(n, node._node);
}
}
-test "CDP Node: search list" {
+test "cdp Node: search list" {
var registry = Registry.init(testing.allocator);
defer registry.deinit();
@@ -383,3 +378,102 @@ test "CDP Node: search list" {
try testing.expectEqual(2, registry.lookup_by_node.count());
}
}
+
+test "cdp Node: Writer" {
+ var registry = Registry.init(testing.allocator);
+ defer registry.deinit();
+
+ 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 = .{},
+ .registry = ®istry,
+ }, .{});
+ defer testing.allocator.free(json);
+
+ 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);
+ }
+
+ {
+ const node = registry.lookup_by_id.get(1).?;
+ const json = try std.json.stringifyAlloc(testing.allocator, Writer{
+ .node = node,
+ .opts = .{},
+ .registry = ®istry,
+ }, .{});
+ defer testing.allocator.free(json);
+
+ 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);
+ }
+}
diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig
index 763d1b4d..253442bb 100644
--- a/src/cdp/cdp.zig
+++ b/src/cdp/cdp.zig
@@ -325,6 +325,14 @@ pub fn BrowserContext(comptime CDP_T: type) type {
self.node_search_list.reset();
}
+ pub fn nodeWriter(self: *Self, node: *const Node, opts: Node.Writer.Opts) Node.Writer {
+ return .{
+ .node = node,
+ .opts = opts,
+ .registry = &self.node_registry,
+ };
+ }
+
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
if (std.log.defaultLogEnabled(.debug)) {
// msg should be {"id":,...
diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig
index 3c97b078..97cdc40d 100644
--- a/src/cdp/domains/dom.zig
+++ b/src/cdp/domains/dom.zig
@@ -51,9 +51,7 @@ fn getDocument(cmd: anytype) !void {
const doc = page.doc orelse return error.DocumentNotLoaded;
const node = try bc.node_registry.register(parser.documentToNode(doc));
- return cmd.sendResult(.{
- .root = node,
- }, .{});
+ return cmd.sendResult(.{ .root = bc.nodeWriter(node, .{}) }, .{});
}
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch
@@ -118,6 +116,7 @@ fn getSearchResults(cmd: anytype) !void {
}
const testing = @import("../testing.zig");
+
test "cdp.dom: getSearchResults unknown search id" {
var ctx = testing.context();
defer ctx.deinit();
diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig
index 759c3a0e..a8b0e7d9 100644
--- a/src/cdp/testing.zig
+++ b/src/cdp/testing.zig
@@ -26,11 +26,12 @@ const main = @import("cdp.zig");
const parser = @import("netsurf");
const App = @import("../app.zig").App;
-pub const allocator = @import("../testing.zig").allocator;
-
-pub const expectEqual = @import("../testing.zig").expectEqual;
-pub const expectError = @import("../testing.zig").expectError;
-pub const expectEqualSlices = @import("../testing.zig").expectEqualSlices;
+const base = @import("../testing.zig");
+pub const allocator = base.allocator;
+pub const expectJson = base.expectJson;
+pub const expectEqual = base.expectEqual;
+pub const expectError = base.expectError;
+pub const expectEqualSlices = base.expectEqualSlices;
pub const Document = @import("../testing.zig").Document;
@@ -284,6 +285,7 @@ const TestContext = struct {
_ = self.client.?.serialized.orderedRemove(i);
return;
}
+
std.debug.print("Error not found. Expecting:\n{s}\n\nGot:\n", .{serialized});
for (self.client.?.serialized.items, 0..) |sent, i| {
std.debug.print("#{d}\n{s}\n\n", .{ i, sent });
@@ -310,47 +312,5 @@ pub fn context() TestContext {
fn compareExpectedToSent(expected: []const u8, actual: json.Value) !bool {
const expected_value = try std.json.parseFromSlice(json.Value, std.testing.allocator, expected, .{});
defer expected_value.deinit();
- return compareJsonValues(expected_value.value, actual);
-}
-
-fn compareJsonValues(a: std.json.Value, b: std.json.Value) bool {
- if (!std.mem.eql(u8, @tagName(a), @tagName(b))) {
- return false;
- }
-
- switch (a) {
- .null => return true,
- .bool => return a.bool == b.bool,
- .integer => return a.integer == b.integer,
- .float => return a.float == b.float,
- .number_string => return std.mem.eql(u8, a.number_string, b.number_string),
- .string => return std.mem.eql(u8, a.string, b.string),
- .array => {
- const a_len = a.array.items.len;
- const b_len = b.array.items.len;
- if (a_len != b_len) {
- return false;
- }
- for (a.array.items, b.array.items) |a_item, b_item| {
- if (compareJsonValues(a_item, b_item) == false) {
- return false;
- }
- }
- return true;
- },
- .object => {
- var it = a.object.iterator();
- while (it.next()) |entry| {
- const key = entry.key_ptr.*;
- if (b.object.get(key)) |b_item| {
- if (compareJsonValues(entry.value_ptr.*, b_item) == false) {
- return false;
- }
- } else {
- return false;
- }
- }
- return true;
- },
- }
+ return base.isEqualJson(expected_value.value, actual);
}
diff --git a/src/testing.zig b/src/testing.zig
index 309938c6..633d9087 100644
--- a/src/testing.zig
+++ b/src/testing.zig
@@ -17,14 +17,15 @@
// along with this program. If not, see .
const std = @import("std");
-const parser = @import("netsurf");
+const parser = @import("netsurf");
pub const allocator = std.testing.allocator;
pub const expectError = std.testing.expectError;
pub const expectString = std.testing.expectEqualStrings;
pub const expectEqualSlices = std.testing.expectEqualSlices;
const App = @import("app.zig").App;
+const Allocator = std.mem.Allocator;
// Merged std.testing.expectEqual and std.testing.expectString
// can be useful when testing fields of an anytype an you don't know
@@ -217,12 +218,135 @@ pub const Document = struct {
pub fn querySelectorAll(self: *Document, selector: []const u8) ![]const *parser.Node {
const css = @import("dom/css.zig");
- const node_list = try css.querySelectorAll(self.arena.allocator(), parser.documentToNode(self.doc), selector);
+ const node_list = try css.querySelectorAll(self.arena.allocator(), self.asNode(), selector);
return node_list.nodes.items;
}
pub fn querySelector(self: *Document, selector: []const u8) !?*parser.Node {
const css = @import("dom/css.zig");
- return css.querySelector(self.arena.allocator(), parser.documentToNode(self.doc), selector);
+ return css.querySelector(self.arena.allocator(), self.asNode(), selector);
+ }
+
+ pub fn asNode(self: *const Document) *parser.Node {
+ return parser.documentToNode(self.doc);
}
};
+
+pub fn expectJson(a: anytype, b: anytype) !void {
+ var arena = std.heap.ArenaAllocator.init(allocator);
+ defer arena.deinit();
+
+ const aa = arena.allocator();
+
+ const a_value = try convertToJson(aa, a);
+ const b_value = try convertToJson(aa, b);
+
+ errdefer {
+ const a_json = std.json.stringifyAlloc(aa, a_value, .{ .whitespace = .indent_2 }) catch unreachable;
+ const b_json = std.json.stringifyAlloc(aa, b_value, .{ .whitespace = .indent_2 }) catch unreachable;
+ std.debug.print("== Expected ==\n{s}\n\n== Actual ==\n{s}", .{ a_json, b_json });
+ }
+
+ try expectJsonValue(a_value, b_value);
+}
+
+pub fn isEqualJson(a: anytype, b: anytype) !bool {
+ var arena = std.heap.ArenaAllocator.init(allocator);
+ defer arena.deinit();
+
+ const aa = arena.allocator();
+ const a_value = try convertToJson(aa, a);
+ const b_value = try convertToJson(aa, b);
+ return isJsonValue(a_value, b_value);
+}
+
+fn convertToJson(arena: Allocator, value: anytype) !std.json.Value {
+ const T = @TypeOf(value);
+ if (T == std.json.Value) {
+ return value;
+ }
+
+ var str: []const u8 = undefined;
+ if (T == []u8 or T == []const u8 or comptime isStringArray(T)) {
+ str = value;
+ } else {
+ str = try std.json.stringifyAlloc(arena, value, .{});
+ }
+ return std.json.parseFromSliceLeaky(std.json.Value, arena, str, .{});
+}
+
+fn expectJsonValue(a: std.json.Value, b: std.json.Value) !void {
+ try expectEqual(@tagName(a), @tagName(b));
+
+ // at this point, we know that if a is an int, b must also be an int
+ switch (a) {
+ .null => return,
+ .bool => try expectEqual(a.bool, b.bool),
+ .integer => try expectEqual(a.integer, b.integer),
+ .float => try expectEqual(a.float, b.float),
+ .number_string => try expectEqual(a.number_string, b.number_string),
+ .string => try expectEqual(a.string, b.string),
+ .array => {
+ const a_len = a.array.items.len;
+ const b_len = b.array.items.len;
+ try expectEqual(a_len, b_len);
+ for (a.array.items, b.array.items) |a_item, b_item| {
+ try expectJsonValue(a_item, b_item);
+ }
+ },
+ .object => {
+ var it = a.object.iterator();
+ while (it.next()) |entry| {
+ const key = entry.key_ptr.*;
+ if (b.object.get(key)) |b_item| {
+ try expectJsonValue(entry.value_ptr.*, b_item);
+ } else {
+ return error.MissingKey;
+ }
+ }
+ },
+ }
+}
+
+fn isJsonValue(a: std.json.Value, b: std.json.Value) bool {
+ if (std.mem.eql(u8, @tagName(a), @tagName(b)) == false) {
+ return false;
+ }
+
+ // at this point, we know that if a is an int, b must also be an int
+ switch (a) {
+ .null => return true,
+ .bool => return a.bool == b.bool,
+ .integer => return a.integer == b.integer,
+ .float => return a.float == b.float,
+ .number_string => return std.mem.eql(u8, a.number_string, b.number_string),
+ .string => return std.mem.eql(u8, a.string, b.string),
+ .array => {
+ const a_len = a.array.items.len;
+ const b_len = b.array.items.len;
+ if (a_len != b_len) {
+ return false;
+ }
+ for (a.array.items, b.array.items) |a_item, b_item| {
+ if (isJsonValue(a_item, b_item) == false) {
+ return false;
+ }
+ }
+ return true;
+ },
+ .object => {
+ var it = a.object.iterator();
+ while (it.next()) |entry| {
+ const key = entry.key_ptr.*;
+ if (b.object.get(key)) |b_item| {
+ if (isJsonValue(entry.value_ptr.*, b_item) == false) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ return true;
+ },
+ }
+}