From d3973172e8dbb3a7fffeaaa8c5c63ef5e4f3712c Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 28 Oct 2025 18:56:03 +0800 Subject: [PATCH] re-enable minimum viable CDP server --- src/browser/URL.zig | 129 +++ src/browser/js/bridge.zig | 1 + src/browser/session.zig | 2 +- src/browser/webapi/MutationObserver.zig | 23 + src/browser/webapi/Node.zig | 3 + src/browser/webapi/TreeWalker.zig | 22 +- src/browser/webapi/URL.zig | 83 +- src/browser/webapi/storage/storage.zig | 1 + src/cdp/Node.zig | 1137 ++++++++++---------- src/cdp/cdp.zig | 72 +- src/cdp/domains/dom.zig | 1283 ++++++++++++----------- src/cdp/domains/fetch.zig | 2 +- src/cdp/domains/input.zig | 2 +- src/cdp/domains/log.zig | 2 +- src/cdp/domains/network.zig | 59 +- src/cdp/domains/page.zig | 13 +- src/cdp/domains/storage.zig | 6 +- src/cdp/domains/target.zig | 5 +- src/cdp/testing.zig | 1 - src/http/Client.zig | 4 +- src/http/Http.zig | 2 +- src/lightpanda.zig | 2 + src/main.zig | 35 +- src/server.zig | 20 +- src/telemetry/lightpanda.zig | 2 +- 25 files changed, 1512 insertions(+), 1399 deletions(-) create mode 100644 src/browser/webapi/MutationObserver.zig diff --git a/src/browser/URL.zig b/src/browser/URL.zig index a2062d50..da031949 100644 --- a/src/browser/URL.zig +++ b/src/browser/URL.zig @@ -122,6 +122,135 @@ pub fn isCompleteHTTPUrl(url: []const u8) bool { std.ascii.startsWithIgnoreCase(url, "ftp://"); } +pub fn getUsername(raw: [:0]const u8) []const u8 { + const user_info = getUserInfo(raw) orelse return ""; + const pos = std.mem.indexOfScalarPos(u8, user_info, 0, ':') orelse return user_info; + return user_info[0..pos]; +} + +pub fn getPassword(raw: [:0]const u8) []const u8 { + const user_info = getUserInfo(raw) orelse return ""; + const pos = std.mem.indexOfScalarPos(u8, user_info, 0, ':') orelse return ""; + return user_info[pos + 1 ..]; +} + +pub fn getPathname(raw: [:0]const u8) []const u8 { + const protocol_end = std.mem.indexOf(u8, raw, "://") orelse 0; + const path_start = std.mem.indexOfScalarPos(u8, raw, if (protocol_end > 0) protocol_end + 3 else 0, '/') orelse raw.len; + + const query_or_hash_start = std.mem.indexOfAnyPos(u8, raw, path_start, "?#") orelse raw.len; + + if (path_start >= query_or_hash_start) { + if (std.mem.indexOf(u8, raw, "://") != null) return "/"; + return ""; + } + + return raw[path_start..query_or_hash_start]; +} + +pub fn getProtocol(raw: [:0]const u8) []const u8 { + const pos = std.mem.indexOfScalarPos(u8, raw, 0, ':') orelse return ""; + return raw[0 .. pos + 1]; +} + +pub fn getHostname(raw: [:0]const u8) []const u8 { + const host = getHost(raw); + const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return host; + return host[0..pos]; +} + +pub fn getPort(raw: [:0]const u8) []const u8 { + const host = getHost(raw); + const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return ""; + + if (pos + 1 >= host.len) { + return ""; + } + + for (host[pos + 1 ..]) |c| { + if (c < '0' or c > '9') { + return ""; + } + } + + return host[pos + 1 ..]; +} + +pub fn getSearch(raw: [:0]const u8) []const u8 { + const pos = std.mem.indexOfScalarPos(u8, raw, 0, '?') orelse return ""; + const query_part = raw[pos..]; + + if (std.mem.indexOfScalarPos(u8, query_part, 0, '#')) |fragment_start| { + return query_part[0..fragment_start]; + } + + return query_part; +} + +pub fn getHash(raw: [:0]const u8) []const u8 { + const start = std.mem.indexOfScalarPos(u8, raw, 0, '#') orelse return ""; + return raw[start..]; +} + +pub fn getOrigin(allocator: Allocator, raw: [:0]const u8) !?[]const u8 { + const port = getPort(raw); + const protocol = getProtocol(raw); + const hostname = getHostname(raw); + + const p = std.meta.stringToEnum(KnownProtocol, getProtocol(raw)) orelse return null; + + const include_port = blk: { + if (port.len == 0) { + break :blk false; + } + if (p == .@"https:" and std.mem.eql(u8, port, "443")) { + break :blk false; + } + if (p == .@"http:" and std.mem.eql(u8, port, "80")) { + break :blk false; + } + break :blk true; + }; + + if (include_port) { + return try std.fmt.allocPrint(allocator, "{s}//{s}:{s}", .{ protocol, hostname, port }); + } + return try std.fmt.allocPrint(allocator, "{s}//{s}", .{ protocol, hostname }); +} + +fn getUserInfo(raw: [:0]const u8) ?[]const u8 { + const scheme_end = std.mem.indexOf(u8, raw, "://") orelse return null; + const authority_start = scheme_end + 3; + + const pos = std.mem.indexOfScalar(u8, raw[authority_start..], '@') orelse return null; + const path_start = std.mem.indexOfScalarPos(u8, raw, authority_start, '/') orelse raw.len; + + const full_pos = authority_start + pos; + if (full_pos < path_start) { + return raw[authority_start..full_pos]; + } + + return null; +} + +fn getHost(raw: [:0]const u8) []const u8 { + const scheme_end = std.mem.indexOf(u8, raw, "://") orelse return ""; + + var authority_start = scheme_end + 3; + if (std.mem.indexOf(u8, raw[authority_start..], "@")) |pos| { + authority_start += pos + 1; + } + + const authority = raw[authority_start..]; + const path_start = std.mem.indexOfAny(u8, authority, "/?#") orelse return authority; + return authority[0..path_start]; +} + +const KnownProtocol = enum { + @"http:", + @"https:", +}; + const testing = @import("../testing.zig"); test "URL: isCompleteHTTPUrl" { try testing.expectEqual(true, isCompleteHTTPUrl("http://example.com/about")); diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 0e2d5c80..ae2790ea 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -467,4 +467,5 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/storage/storage.zig"), @import("../webapi/URL.zig"), @import("../webapi/Window.zig"), + @import("../webapi/MutationObserver.zig"), }); diff --git a/src/browser/session.zig b/src/browser/session.zig index 41fd795a..0f90a82a 100644 --- a/src/browser/session.zig +++ b/src/browser/session.zig @@ -141,7 +141,7 @@ pub fn wait(self: *Session, wait_ms: u32) WaitResult { return .done; }; - if (self.page) |*page| { + if (self.page) |page| { return page.wait(wait_ms); } return .no_page; diff --git a/src/browser/webapi/MutationObserver.zig b/src/browser/webapi/MutationObserver.zig new file mode 100644 index 00000000..73001ee4 --- /dev/null +++ b/src/browser/webapi/MutationObserver.zig @@ -0,0 +1,23 @@ +const js = @import("../js/js.zig"); + +// @ZIGDOM (haha, bet you wish you hadn't opened this file) +// puppeteer's startup script creates a MutationObserver, even if it doesn't use +// it in simple scripts. This not-even-a-skeleton is required for puppeteer/cdp.js +// to run +const MutationObserver = @This(); + +pub fn init() MutationObserver { + return .{}; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(MutationObserver); + + pub const Meta = struct { + pub const name = "MutationObserver"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_index: u16 = 0; + }; + + pub const constructor = bridge.constructor(MutationObserver.init, .{}); +}; diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index e6f03d98..88a827fa 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -340,6 +340,9 @@ pub fn setNodeValue(self: *const Node, value: ?[]const u8, page: *Page) !void { } pub fn format(self: *Node, writer: *std.Io.Writer) !void { + // // If you need extra debugging: + // return @import("../dump.zig").deep(self, .{}, writer); + return switch (self._type) { .cdata => |cd| cd.format(writer), .element => |el| writer.print("{f}", .{el}), diff --git a/src/browser/webapi/TreeWalker.zig b/src/browser/webapi/TreeWalker.zig index c2b1f39e..cee99ff1 100644 --- a/src/browser/webapi/TreeWalker.zig +++ b/src/browser/webapi/TreeWalker.zig @@ -39,14 +39,22 @@ pub fn TreeWalker(comptime mode: Mode) type { self._next = children.first(); } else if (node._child_link.next) |n| { self._next = Node.linkToNode(n); - } else if (node._parent) |n| { - if (n == self._root) { - self._next = null; - } else { - self._next = Node.linkToNodeOrNull(n._child_link.next); - } } else { - self._next = null; + // No children, no next sibling - walk up until we find a next sibling or hit root + var current = node._parent; + while (current) |parent| { + if (parent == self._root) { + self._next = null; + break; + } + if (parent._child_link.next) |next_sibling| { + self._next = Node.linkToNode(next_sibling); + break; + } + current = parent._parent; + } else { + self._next = null; + } } return node; } diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig index b81c8cb2..d7bf0d7d 100644 --- a/src/browser/webapi/URL.zig +++ b/src/browser/webapi/URL.zig @@ -1,6 +1,7 @@ const std = @import("std"); const js = @import("../js/js.zig"); +const U = @import("../URL.zig"); const Page = @import("../Page.zig"); const URLSearchParams = @import("net/URLSearchParams.zig"); @@ -42,106 +43,42 @@ pub fn init(url: [:0]const u8, base_: ?[:0]const u8, page: *Page) !*URL { } pub fn getUsername(self: *const URL) []const u8 { - const user_info = self.getUserInfo() orelse return ""; - const pos = std.mem.indexOfScalarPos(u8, user_info, 0, ':') orelse return user_info; - return user_info[0..pos]; + return U.getUsername(self._raw); } pub fn getPassword(self: *const URL) []const u8 { - const user_info = self.getUserInfo() orelse return ""; - const pos = std.mem.indexOfScalarPos(u8, user_info, 0, ':') orelse return ""; - return user_info[pos + 1 ..]; + return U.getPassword(self._raw); } pub fn getPathname(self: *const URL) []const u8 { - const raw = self._raw; - const protocol_end = std.mem.indexOf(u8, raw, "://") orelse 0; - const path_start = std.mem.indexOfScalarPos(u8, raw, if (protocol_end > 0) protocol_end + 3 else 0, '/') orelse raw.len; - - const query_or_hash_start = std.mem.indexOfAnyPos(u8, raw, path_start, "?#") orelse raw.len; - - if (path_start >= query_or_hash_start) { - if (std.mem.indexOf(u8, raw, "://") != null) return "/"; - return ""; - } - - return raw[path_start..query_or_hash_start]; + return U.getPathname(self._raw); } pub fn getProtocol(self: *const URL) []const u8 { - const raw = self._raw; - const pos = std.mem.indexOfScalarPos(u8, raw, 0, ':') orelse return ""; - return raw[0 .. pos + 1]; + return U.getProtocol(self._raw); } pub fn getHostname(self: *const URL) []const u8 { - const host = self.getHost(); - const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return host; - return host[0..pos]; + return U.getHostname(self._raw); } pub fn getPort(self: *const URL) []const u8 { - const host = self.getHost(); - const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return ""; - - if (pos + 1 >= host.len) { - return ""; - } - - for (host[pos + 1 ..]) |c| { - if (c < '0' or c > '9') { - return ""; - } - } - - return host[pos + 1 ..]; + return U.getPort(self._raw); } pub fn getOrigin(self: *const URL, page: *const Page) ![]const u8 { - const port = self.getPort(); - const protocol = self.getProtocol(); - const hostname = self.getHostname(); - - const p = std.meta.stringToEnum(KnownProtocol, self.getProtocol()) orelse { + return (try U.getOrigin(page.call_arena, self._raw)) orelse { // yes, a null string, that's what the spec wants return "null"; }; - - const include_port = blk: { - if (port.len == 0) { - break :blk false; - } - if (p == .@"https:" and std.mem.eql(u8, port, "443")) { - break :blk false; - } - if (p == .@"http:" and std.mem.eql(u8, port, "80")) { - break :blk false; - } - break :blk true; - }; - - if (include_port) { - return std.fmt.allocPrint(page.call_arena, "{s}//{s}:{s}", .{ protocol, hostname, port }); - } - return std.fmt.allocPrint(page.call_arena, "{s}//{s}", .{ protocol, hostname }); } pub fn getSearch(self: *const URL) []const u8 { - const raw = self._raw; - const pos = std.mem.indexOfScalarPos(u8, raw, 0, '?') orelse return ""; - const query_part = raw[pos..]; - - if (std.mem.indexOfScalarPos(u8, query_part, 0, '#')) |fragment_start| { - return query_part[0..fragment_start]; - } - - return query_part; + return U.getSearch(self._raw); } pub fn getHash(self: *const URL) []const u8 { - const raw = self._raw; - const start = std.mem.indexOfScalarPos(u8, raw, 0, '#') orelse return ""; - return raw[start..]; + return U.getHash(self._raw); } pub fn getSearchParams(self: *URL, page: *Page) !*URLSearchParams { diff --git a/src/browser/webapi/storage/storage.zig b/src/browser/webapi/storage/storage.zig index 13bbc72f..8813c092 100644 --- a/src/browser/webapi/storage/storage.zig +++ b/src/browser/webapi/storage/storage.zig @@ -9,6 +9,7 @@ pub fn registerTypes() []const type { } pub const Jar = @import("cookie.zig").Jar; +pub const Cookie =@import("cookie.zig").Cookie; pub const Shed = struct { _origins: std.StringHashMapUnmanaged(*Bucket) = .empty, diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig index be18206c..c5109312 100644 --- a/src/cdp/Node.zig +++ b/src/cdp/Node.zig @@ -16,571 +16,572 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -const std = @import("std"); -const Allocator = std.mem.Allocator; - -const log = @import("../log.zig"); -const parser = @import("../browser/netsurf.zig"); - -pub const Id = u32; - -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. -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), - - pub fn init(allocator: Allocator) Registry { - return .{ - .node_id = 1, - .lookup_by_id = .{}, - .lookup_by_node = .{}, - .allocator = allocator, - .arena = std.heap.ArenaAllocator.init(allocator), - .node_pool = std.heap.MemoryPool(Node).init(allocator), - }; - } - - pub fn deinit(self: *Registry) void { - const allocator = self.allocator; - 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.arena.reset(.{ .retain_with_limit = 1024 }); - _ = self.node_pool.reset(.{ .retain_with_limit = 1024 }); - } - - pub fn register(self: *Registry, n: *parser.Node) !*Node { - const node_lookup_gop = try self.lookup_by_node.getOrPut(self.allocator, n); - if (node_lookup_gop.found_existing) { - return node_lookup_gop.value_ptr.*; - } - - // on error, we're probably going to abort the entire browser context - // but, just in case, let's try to keep things tidy. - errdefer _ = self.lookup_by_node.remove(n); - - 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, - .set_child_nodes_event = false, - }; - - node_lookup_gop.value_ptr.* = node; - try self.lookup_by_id.putNoClobber(self.allocator, id, node); - return node; - } -}; - -const NodeContext = struct { - pub fn hash(_: NodeContext, n: *parser.Node) u64 { - return std.hash.Wyhash.hash(0, std.mem.asBytes(&@intFromPtr(n))); - } - - pub fn eql(_: NodeContext, a: *parser.Node, b: *parser.Node) bool { - return @intFromPtr(a) == @intFromPtr(b); - } -}; - -// Searches are a 3 step process: -// 1 - Dom.performSearch -// 2 - Dom.getSearchResults -// 3 - Dom.discardSearchResults -// -// For a given browser context, we can have multiple active searches. I.e. -// performSearch could be called multiple times without getSearchResults or -// discardSearchResults being called. We keep these active searches in the -// browser context's node_search_list, which is a SearchList. Since we don't -// expect many active searches (mostly just 1), a list is fine to scan through. -pub const Search = struct { - name: []const u8, - node_ids: []const Id, - - pub const List = struct { - registry: *Registry, - search_id: u16 = 0, - arena: std.heap.ArenaAllocator, - searches: std.ArrayListUnmanaged(Search) = .{}, - - pub fn init(allocator: Allocator, registry: *Registry) List { - return .{ - .registry = registry, - .arena = std.heap.ArenaAllocator.init(allocator), - }; - } - - pub fn deinit(self: *List) void { - self.arena.deinit(); - } - - pub fn reset(self: *List) void { - self.search_id = 0; - self.searches = .{}; - _ = self.arena.reset(.{ .retain_with_limit = 4096 }); - } - - pub fn create(self: *List, nodes: []const *parser.Node) !Search { - const id = self.search_id; - defer self.search_id = id +% 1; - - const arena = self.arena.allocator(); - - const name = switch (id) { - 0 => "0", - 1 => "1", - 2 => "2", - 3 => "3", - 4 => "4", - 5 => "5", - 6 => "6", - 7 => "7", - 8 => "8", - 9 => "9", - else => try std.fmt.allocPrint(arena, "{d}", .{id}), - }; - - var registry = self.registry; - const node_ids = try arena.alloc(Id, nodes.len); - for (nodes, node_ids) |node, *node_id| { - node_id.* = (try registry.register(node)).id; - } - - const search = Search{ - .name = name, - .node_ids = node_ids, - }; - try self.searches.append(arena, search); - return search; - } - - pub fn remove(self: *List, name: []const u8) void { - for (self.searches.items, 0..) |search, i| { - if (std.mem.eql(u8, name, search.name)) { - _ = self.searches.swapRemove(i); - return; - } - } - } - - pub fn get(self: *const List, name: []const u8) ?Search { - for (self.searches.items) |search| { - if (std.mem.eql(u8, name, search.name)) { - return search; - } - } - return null; - } - }; -}; - -// 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 { - depth: i32, - exclude_root: bool, - root: *const Node, - registry: *Registry, - - pub const Opts = struct { - depth: i32 = 0, - exclude_root: bool = false, - }; - - pub fn jsonStringify(self: *const Writer, w: anytype) error{WriteFailed}!void { - if (self.exclude_root) { - _ = self.writeChildren(self.root, 1, w) catch |err| { - log.err(.cdp, "node writeChildren", .{ .err = err }); - return error.WriteFailed; - }; - } 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.WriteFailed; - }; - } - } - - fn toJSON(self: *const Writer, node: *const Node, depth: usize, w: anytype) !void { - try w.beginObject(); - try self.writeCommon(node, false, w); - - try w.objectField("children"); - const child_count = try self.writeChildren(node, depth, w); - try w.objectField("childNodeCount"); - try w.write(child_count); - - 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 = 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 = (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(); - - return i; - } - - fn writeCommon(self: *const Writer, 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; - - if (parser.nodeParentNode(n)) |p| { - const parent_node = try self.registry.register(p); - 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.write(@intFromEnum(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((parser.nodeValue(n)) orelse ""); - - if (include_child_count) { - try w.objectField("childNodeCount"); - const child_nodes = try parser.nodeGetChildNodes(n); - try w.write(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" { - parser.init(); - defer parser.deinit(); - - var registry = Registry.init(testing.allocator); - defer registry.deinit(); - - try testing.expectEqual(0, registry.lookup_by_id.count()); - try testing.expectEqual(0, registry.lookup_by_node.count()); - - var doc = try testing.Document.init("link1

other

"); - defer doc.deinit(); - - { - const n = (try doc.querySelector("#a1")).?; - const node = try registry.register(n); - const n1b = registry.lookup_by_id.get(1).?; - const n1c = registry.lookup_by_node.get(node._node).?; - try testing.expectEqual(node, n1b); - try testing.expectEqual(node, n1c); - - try testing.expectEqual(1, node.id); - try testing.expectEqual(n, node._node); - } - - { - const n = (try doc.querySelector("p")).?; - const node = try registry.register(n); - const n1b = registry.lookup_by_id.get(2).?; - const n1c = registry.lookup_by_node.get(node._node).?; - try testing.expectEqual(node, n1b); - try testing.expectEqual(node, n1c); - - try testing.expectEqual(2, node.id); - try testing.expectEqual(n, node._node); - } -} - -test "cdp Node: search list" { - parser.init(); - defer parser.deinit(); - - var registry = Registry.init(testing.allocator); - defer registry.deinit(); - - var search_list = Search.List.init(testing.allocator, ®istry); - defer search_list.deinit(); - - { - // empty search list, noops - search_list.remove("0"); - try testing.expectEqual(null, search_list.get("0")); - } - - { - // empty nodes - const s1 = try search_list.create(&.{}); - try testing.expectEqual("0", s1.name); - try testing.expectEqual(0, s1.node_ids.len); - - const s2 = search_list.get("0").?; - try testing.expectEqual("0", s2.name); - try testing.expectEqual(0, s2.node_ids.len); - - search_list.remove("0"); - try testing.expectEqual(null, search_list.get("0")); - } - - { - var doc = try testing.Document.init(""); - defer doc.deinit(); - - const s1 = try search_list.create(try doc.querySelectorAll("a")); - try testing.expectEqual("1", s1.name); - try testing.expectEqualSlices(u32, &.{ 1, 2 }, s1.node_ids); - - try testing.expectEqual(2, registry.lookup_by_id.count()); - try testing.expectEqual(2, registry.lookup_by_node.count()); - - const s2 = try search_list.create(try doc.querySelectorAll("#a1")); - try testing.expectEqual("2", s2.name); - try testing.expectEqualSlices(u32, &.{1}, s2.node_ids); - - const s3 = try search_list.create(try doc.querySelectorAll("#a2")); - try testing.expectEqual("3", s3.name); - try testing.expectEqualSlices(u32, &.{2}, s3.node_ids); - - try testing.expectEqual(2, registry.lookup_by_id.count()); - try testing.expectEqual(2, registry.lookup_by_node.count()); - } -} - -test "cdp Node: Writer" { - parser.init(); - defer parser.deinit(); - - 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.Stringify.valueAlloc(testing.allocator, Writer{ - .root = node, - .depth = 0, - .exclude_root = false, - .registry = ®istry, - }, .{}); - defer testing.allocator.free(json); - - try testing.expectJson(.{ - .nodeId = 1, - .backendNodeId = 1, - .nodeType = 9, - .nodeName = "#document", - .localName = "", - .nodeValue = "", - .documentURL = null, - .baseURL = null, - .xmlVersion = "", - .isScrollable = false, - .compatibilityMode = "NoQuirksMode", - .childNodeCount = 1, - .children = &.{.{ - .nodeId = 2, - .backendNodeId = 2, - .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(2).?; - const json = try std.json.Stringify.valueAlloc(testing.allocator, Writer{ - .root = node, - .depth = 1, - .exclude_root = false, - .registry = ®istry, - }, .{}); - defer testing.allocator.free(json); - - try testing.expectJson(.{ - .nodeId = 2, - .backendNodeId = 2, - .nodeType = 1, - .nodeName = "HTML", - .localName = "html", - .nodeValue = "", - .childNodeCount = 2, - .documentURL = null, - .baseURL = null, - .xmlVersion = "", - .compatibilityMode = "NoQuirksMode", - .isScrollable = false, - .children = &.{ .{ - .nodeId = 3, - .backendNodeId = 3, - .nodeType = 1, - .nodeName = "HEAD", - .localName = "head", - .nodeValue = "", - .childNodeCount = 0, - .documentURL = null, - .baseURL = null, - .xmlVersion = "", - .compatibilityMode = "NoQuirksMode", - .isScrollable = false, - .parentId = 2, - }, .{ - .nodeId = 4, - .backendNodeId = 4, - .nodeType = 1, - .nodeName = "BODY", - .localName = "body", - .nodeValue = "", - .childNodeCount = 2, - .documentURL = null, - .baseURL = null, - .xmlVersion = "", - .compatibilityMode = "NoQuirksMode", - .isScrollable = false, - .parentId = 2, - } }, - }, json); - } - - { - const node = registry.lookup_by_id.get(2).?; - const json = try std.json.Stringify.valueAlloc(testing.allocator, Writer{ - .root = node, - .depth = -1, - .exclude_root = true, - .registry = ®istry, - }, .{}); - defer testing.allocator.free(json); - - try testing.expectJson(&.{ .{ - .nodeId = 3, - .backendNodeId = 3, - .nodeType = 1, - .nodeName = "HEAD", - .localName = "head", - .nodeValue = "", - .childNodeCount = 0, - .documentURL = null, - .baseURL = null, - .xmlVersion = "", - .compatibilityMode = "NoQuirksMode", - .isScrollable = false, - .parentId = 2, - }, .{ - .nodeId = 4, - .backendNodeId = 4, - .nodeType = 1, - .nodeName = "BODY", - .localName = "body", - .nodeValue = "", - .childNodeCount = 2, - .documentURL = null, - .baseURL = null, - .xmlVersion = "", - .compatibilityMode = "NoQuirksMode", - .isScrollable = false, - .children = &.{ .{ - .nodeId = 5, - .localName = "a", - .childNodeCount = 0, - .parentId = 4, - }, .{ - .nodeId = 6, - .localName = "div", - .childNodeCount = 1, - .parentId = 4, - .children = &.{.{ - .nodeId = 7, - .localName = "a", - .childNodeCount = 0, - .parentId = 6, - }}, - } }, - } }, json); - } -} +// @ZIGDOM +// const std = @import("std"); +// const Allocator = std.mem.Allocator; + +// const log = @import("../log.zig"); +// const parser = @import("../browser/netsurf.zig"); + +// pub const Id = u32; + +// 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. +// 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), + +// pub fn init(allocator: Allocator) Registry { +// return .{ +// .node_id = 1, +// .lookup_by_id = .{}, +// .lookup_by_node = .{}, +// .allocator = allocator, +// .arena = std.heap.ArenaAllocator.init(allocator), +// .node_pool = std.heap.MemoryPool(Node).init(allocator), +// }; +// } + +// pub fn deinit(self: *Registry) void { +// const allocator = self.allocator; +// 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.arena.reset(.{ .retain_with_limit = 1024 }); +// _ = self.node_pool.reset(.{ .retain_with_limit = 1024 }); +// } + +// pub fn register(self: *Registry, n: *parser.Node) !*Node { +// const node_lookup_gop = try self.lookup_by_node.getOrPut(self.allocator, n); +// if (node_lookup_gop.found_existing) { +// return node_lookup_gop.value_ptr.*; +// } + +// // on error, we're probably going to abort the entire browser context +// // but, just in case, let's try to keep things tidy. +// errdefer _ = self.lookup_by_node.remove(n); + +// 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, +// .set_child_nodes_event = false, +// }; + +// node_lookup_gop.value_ptr.* = node; +// try self.lookup_by_id.putNoClobber(self.allocator, id, node); +// return node; +// } +// }; + +// const NodeContext = struct { +// pub fn hash(_: NodeContext, n: *parser.Node) u64 { +// return std.hash.Wyhash.hash(0, std.mem.asBytes(&@intFromPtr(n))); +// } + +// pub fn eql(_: NodeContext, a: *parser.Node, b: *parser.Node) bool { +// return @intFromPtr(a) == @intFromPtr(b); +// } +// }; + +// // Searches are a 3 step process: +// // 1 - Dom.performSearch +// // 2 - Dom.getSearchResults +// // 3 - Dom.discardSearchResults +// // +// // For a given browser context, we can have multiple active searches. I.e. +// // performSearch could be called multiple times without getSearchResults or +// // discardSearchResults being called. We keep these active searches in the +// // browser context's node_search_list, which is a SearchList. Since we don't +// // expect many active searches (mostly just 1), a list is fine to scan through. +// pub const Search = struct { +// name: []const u8, +// node_ids: []const Id, + +// pub const List = struct { +// registry: *Registry, +// search_id: u16 = 0, +// arena: std.heap.ArenaAllocator, +// searches: std.ArrayListUnmanaged(Search) = .{}, + +// pub fn init(allocator: Allocator, registry: *Registry) List { +// return .{ +// .registry = registry, +// .arena = std.heap.ArenaAllocator.init(allocator), +// }; +// } + +// pub fn deinit(self: *List) void { +// self.arena.deinit(); +// } + +// pub fn reset(self: *List) void { +// self.search_id = 0; +// self.searches = .{}; +// _ = self.arena.reset(.{ .retain_with_limit = 4096 }); +// } + +// pub fn create(self: *List, nodes: []const *parser.Node) !Search { +// const id = self.search_id; +// defer self.search_id = id +% 1; + +// const arena = self.arena.allocator(); + +// const name = switch (id) { +// 0 => "0", +// 1 => "1", +// 2 => "2", +// 3 => "3", +// 4 => "4", +// 5 => "5", +// 6 => "6", +// 7 => "7", +// 8 => "8", +// 9 => "9", +// else => try std.fmt.allocPrint(arena, "{d}", .{id}), +// }; + +// var registry = self.registry; +// const node_ids = try arena.alloc(Id, nodes.len); +// for (nodes, node_ids) |node, *node_id| { +// node_id.* = (try registry.register(node)).id; +// } + +// const search = Search{ +// .name = name, +// .node_ids = node_ids, +// }; +// try self.searches.append(arena, search); +// return search; +// } + +// pub fn remove(self: *List, name: []const u8) void { +// for (self.searches.items, 0..) |search, i| { +// if (std.mem.eql(u8, name, search.name)) { +// _ = self.searches.swapRemove(i); +// return; +// } +// } +// } + +// pub fn get(self: *const List, name: []const u8) ?Search { +// for (self.searches.items) |search| { +// if (std.mem.eql(u8, name, search.name)) { +// return search; +// } +// } +// return null; +// } +// }; +// }; + +// // 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 { +// depth: i32, +// exclude_root: bool, +// root: *const Node, +// registry: *Registry, + +// pub const Opts = struct { +// depth: i32 = 0, +// exclude_root: bool = false, +// }; + +// pub fn jsonStringify(self: *const Writer, w: anytype) error{WriteFailed}!void { +// if (self.exclude_root) { +// _ = self.writeChildren(self.root, 1, w) catch |err| { +// log.err(.cdp, "node writeChildren", .{ .err = err }); +// return error.WriteFailed; +// }; +// } 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.WriteFailed; +// }; +// } +// } + +// fn toJSON(self: *const Writer, node: *const Node, depth: usize, w: anytype) !void { +// try w.beginObject(); +// try self.writeCommon(node, false, w); + +// try w.objectField("children"); +// const child_count = try self.writeChildren(node, depth, w); +// try w.objectField("childNodeCount"); +// try w.write(child_count); + +// 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 = 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 = (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(); + +// return i; +// } + +// fn writeCommon(self: *const Writer, 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; + +// if (parser.nodeParentNode(n)) |p| { +// const parent_node = try self.registry.register(p); +// 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.write(@intFromEnum(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((parser.nodeValue(n)) orelse ""); + +// if (include_child_count) { +// try w.objectField("childNodeCount"); +// const child_nodes = try parser.nodeGetChildNodes(n); +// try w.write(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" { +// parser.init(); +// defer parser.deinit(); + +// var registry = Registry.init(testing.allocator); +// defer registry.deinit(); + +// try testing.expectEqual(0, registry.lookup_by_id.count()); +// try testing.expectEqual(0, registry.lookup_by_node.count()); + +// var doc = try testing.Document.init("link1

other

"); +// defer doc.deinit(); + +// { +// const n = (try doc.querySelector("#a1")).?; +// const node = try registry.register(n); +// const n1b = registry.lookup_by_id.get(1).?; +// const n1c = registry.lookup_by_node.get(node._node).?; +// try testing.expectEqual(node, n1b); +// try testing.expectEqual(node, n1c); + +// try testing.expectEqual(1, node.id); +// try testing.expectEqual(n, node._node); +// } + +// { +// const n = (try doc.querySelector("p")).?; +// const node = try registry.register(n); +// const n1b = registry.lookup_by_id.get(2).?; +// const n1c = registry.lookup_by_node.get(node._node).?; +// try testing.expectEqual(node, n1b); +// try testing.expectEqual(node, n1c); + +// try testing.expectEqual(2, node.id); +// try testing.expectEqual(n, node._node); +// } +// } + +// test "cdp Node: search list" { +// parser.init(); +// defer parser.deinit(); + +// var registry = Registry.init(testing.allocator); +// defer registry.deinit(); + +// var search_list = Search.List.init(testing.allocator, ®istry); +// defer search_list.deinit(); + +// { +// // empty search list, noops +// search_list.remove("0"); +// try testing.expectEqual(null, search_list.get("0")); +// } + +// { +// // empty nodes +// const s1 = try search_list.create(&.{}); +// try testing.expectEqual("0", s1.name); +// try testing.expectEqual(0, s1.node_ids.len); + +// const s2 = search_list.get("0").?; +// try testing.expectEqual("0", s2.name); +// try testing.expectEqual(0, s2.node_ids.len); + +// search_list.remove("0"); +// try testing.expectEqual(null, search_list.get("0")); +// } + +// { +// var doc = try testing.Document.init(""); +// defer doc.deinit(); + +// const s1 = try search_list.create(try doc.querySelectorAll("a")); +// try testing.expectEqual("1", s1.name); +// try testing.expectEqualSlices(u32, &.{ 1, 2 }, s1.node_ids); + +// try testing.expectEqual(2, registry.lookup_by_id.count()); +// try testing.expectEqual(2, registry.lookup_by_node.count()); + +// const s2 = try search_list.create(try doc.querySelectorAll("#a1")); +// try testing.expectEqual("2", s2.name); +// try testing.expectEqualSlices(u32, &.{1}, s2.node_ids); + +// const s3 = try search_list.create(try doc.querySelectorAll("#a2")); +// try testing.expectEqual("3", s3.name); +// try testing.expectEqualSlices(u32, &.{2}, s3.node_ids); + +// try testing.expectEqual(2, registry.lookup_by_id.count()); +// try testing.expectEqual(2, registry.lookup_by_node.count()); +// } +// } + +// test "cdp Node: Writer" { +// parser.init(); +// defer parser.deinit(); + +// 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.Stringify.valueAlloc(testing.allocator, Writer{ +// .root = node, +// .depth = 0, +// .exclude_root = false, +// .registry = ®istry, +// }, .{}); +// defer testing.allocator.free(json); + +// try testing.expectJson(.{ +// .nodeId = 1, +// .backendNodeId = 1, +// .nodeType = 9, +// .nodeName = "#document", +// .localName = "", +// .nodeValue = "", +// .documentURL = null, +// .baseURL = null, +// .xmlVersion = "", +// .isScrollable = false, +// .compatibilityMode = "NoQuirksMode", +// .childNodeCount = 1, +// .children = &.{.{ +// .nodeId = 2, +// .backendNodeId = 2, +// .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(2).?; +// const json = try std.json.Stringify.valueAlloc(testing.allocator, Writer{ +// .root = node, +// .depth = 1, +// .exclude_root = false, +// .registry = ®istry, +// }, .{}); +// defer testing.allocator.free(json); + +// try testing.expectJson(.{ +// .nodeId = 2, +// .backendNodeId = 2, +// .nodeType = 1, +// .nodeName = "HTML", +// .localName = "html", +// .nodeValue = "", +// .childNodeCount = 2, +// .documentURL = null, +// .baseURL = null, +// .xmlVersion = "", +// .compatibilityMode = "NoQuirksMode", +// .isScrollable = false, +// .children = &.{ .{ +// .nodeId = 3, +// .backendNodeId = 3, +// .nodeType = 1, +// .nodeName = "HEAD", +// .localName = "head", +// .nodeValue = "", +// .childNodeCount = 0, +// .documentURL = null, +// .baseURL = null, +// .xmlVersion = "", +// .compatibilityMode = "NoQuirksMode", +// .isScrollable = false, +// .parentId = 2, +// }, .{ +// .nodeId = 4, +// .backendNodeId = 4, +// .nodeType = 1, +// .nodeName = "BODY", +// .localName = "body", +// .nodeValue = "", +// .childNodeCount = 2, +// .documentURL = null, +// .baseURL = null, +// .xmlVersion = "", +// .compatibilityMode = "NoQuirksMode", +// .isScrollable = false, +// .parentId = 2, +// } }, +// }, json); +// } + +// { +// const node = registry.lookup_by_id.get(2).?; +// const json = try std.json.Stringify.valueAlloc(testing.allocator, Writer{ +// .root = node, +// .depth = -1, +// .exclude_root = true, +// .registry = ®istry, +// }, .{}); +// defer testing.allocator.free(json); + +// try testing.expectJson(&.{ .{ +// .nodeId = 3, +// .backendNodeId = 3, +// .nodeType = 1, +// .nodeName = "HEAD", +// .localName = "head", +// .nodeValue = "", +// .childNodeCount = 0, +// .documentURL = null, +// .baseURL = null, +// .xmlVersion = "", +// .compatibilityMode = "NoQuirksMode", +// .isScrollable = false, +// .parentId = 2, +// }, .{ +// .nodeId = 4, +// .backendNodeId = 4, +// .nodeType = 1, +// .nodeName = "BODY", +// .localName = "body", +// .nodeValue = "", +// .childNodeCount = 2, +// .documentURL = null, +// .baseURL = null, +// .xmlVersion = "", +// .compatibilityMode = "NoQuirksMode", +// .isScrollable = false, +// .children = &.{ .{ +// .nodeId = 5, +// .localName = "a", +// .childNodeCount = 0, +// .parentId = 4, +// }, .{ +// .nodeId = 6, +// .localName = "div", +// .childNodeCount = 1, +// .parentId = 4, +// .children = &.{.{ +// .nodeId = 7, +// .localName = "a", +// .childNodeCount = 0, +// .parentId = 6, +// }}, +// } }, +// } }, json); +// } +// } diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 7b6590e8..73c5e514 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -24,12 +24,12 @@ const log = @import("../log.zig"); const js = @import("../browser/js/js.zig"); const polyfill = @import("../browser/polyfill/polyfill.zig"); -const App = @import("../app.zig").App; -const Browser = @import("../browser/browser.zig").Browser; -const Session = @import("../browser/session.zig").Session; -const Page = @import("../browser/page.zig").Page; +const App = @import("../App.zig"); +const Browser = @import("../browser/Browser.zig"); +const Session = @import("../browser/Session.zig"); +const Page = @import("../browser/Page.zig"); const Incrementing = @import("../id.zig").Incrementing; -const Notification = @import("../notification.zig").Notification; +const Notification = @import("../Notification.zig"); const LogInterceptor = @import("domains/log.zig").LogInterceptor; const InterceptState = @import("domains/fetch.zig").InterceptState; @@ -37,7 +37,7 @@ pub const URL_BASE = "chrome://newtab/"; pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C"; pub const CDP = CDPT(struct { - const Client = *@import("../server.zig").Client; + const Client = *@import("../Server.zig").Client; }); const SessionIdGen = Incrementing(u32, "SID"); @@ -117,7 +117,7 @@ pub fn CDPT(comptime TypeProvider: type) type { // timeouts (or http events) which are ready to be processed. pub fn hasPage() bool {} - pub fn pageWait(self: *Self, ms: i32) Session.WaitResult { + pub fn pageWait(self: *Self, ms: u32) Session.WaitResult { const session = &(self.browser.session orelse return .no_page); return session.wait(ms); } @@ -203,7 +203,8 @@ pub fn CDPT(comptime TypeProvider: type) type { }, 5 => switch (@as(u40, @bitCast(domain[0..5].*))) { asUint(u40, "Fetch") => return @import("domains/fetch.zig").processMessage(command), - asUint(u40, "Input") => return @import("domains/input.zig").processMessage(command), + // @ZIGDOM + // asUint(u40, "Input") => return @import("domains/input.zig").processMessage(command), else => {}, }, 6 => switch (@as(u48, @bitCast(domain[0..6].*))) { @@ -286,7 +287,8 @@ pub fn CDPT(comptime TypeProvider: type) type { } pub fn BrowserContext(comptime CDP_T: type) type { - const Node = @import("Node.zig"); + // @ZIGMOD + // const Node = @import("Node.zig"); return struct { id: []const u8, @@ -326,8 +328,9 @@ pub fn BrowserContext(comptime CDP_T: type) type { security_origin: []const u8, page_life_cycle_events: bool, secure_context_type: []const u8, - node_registry: Node.Registry, - node_search_list: Node.Search.List, + // @ZIGDOM + // node_registry: Node.Registry, + // node_search_list: Node.Search.List, inspector: js.Inspector, isolated_worlds: std.ArrayListUnmanaged(IsolatedWorld), @@ -360,8 +363,9 @@ pub fn BrowserContext(comptime CDP_T: type) type { const inspector = try cdp.browser.env.newInspector(arena, self); - var registry = Node.Registry.init(allocator); - errdefer registry.deinit(); + // @ZIGDOM + // var registry = Node.Registry.init(allocator); + // errdefer registry.deinit(); self.* = .{ .id = id, @@ -374,8 +378,9 @@ pub fn BrowserContext(comptime CDP_T: type) type { .secure_context_type = "Secure", // TODO = enum .loader_id = LOADER_ID, .page_life_cycle_events = false, // TODO; Target based value - .node_registry = registry, - .node_search_list = undefined, + // @ZIGDOM + // .node_registry = registry, + // .node_search_list = undefined, .isolated_worlds = .empty, .inspector = inspector, .notification_arena = cdp.notification_arena.allocator(), @@ -383,7 +388,8 @@ pub fn BrowserContext(comptime CDP_T: type) type { .captured_responses = .empty, .log_interceptor = LogInterceptor(Self).init(allocator, self), }; - self.node_search_list = Node.Search.List.init(allocator, &self.node_registry); + // ZIGDOM + // self.node_search_list = Node.Search.List.init(allocator, &self.node_registry); errdefer self.deinit(); try cdp.browser.notification.register(.page_remove, self, onPageRemove); @@ -418,8 +424,9 @@ pub fn BrowserContext(comptime CDP_T: type) type { world.deinit(); } self.isolated_worlds.clearRetainingCapacity(); - self.node_registry.deinit(); - self.node_search_list.deinit(); + // @ZIGDOM + // self.node_registry.deinit(); + // self.node_search_list.deinit(); self.cdp.browser.notification.unregisterAll(self); if (self.http_proxy_changed) { @@ -433,8 +440,10 @@ pub fn BrowserContext(comptime CDP_T: type) type { } pub fn reset(self: *Self) void { - self.node_registry.reset(); - self.node_search_list.reset(); + // @ZIGDOM + _ = self; + // self.node_registry.reset(); + // self.node_search_list.reset(); } pub fn createIsolatedWorld(self: *Self, world_name: []const u8, grant_universal_access: bool) !*IsolatedWorld { @@ -453,19 +462,20 @@ pub fn BrowserContext(comptime CDP_T: type) type { return world; } - pub fn nodeWriter(self: *Self, root: *const Node, opts: Node.Writer.Opts) Node.Writer { - return .{ - .root = root, - .depth = opts.depth, - .exclude_root = opts.exclude_root, - .registry = &self.node_registry, - }; - } + // @ZIGDOM + // pub fn nodeWriter(self: *Self, root: *const Node, opts: Node.Writer.Opts) Node.Writer { + // return .{ + // .root = root, + // .depth = opts.depth, + // .exclude_root = opts.exclude_root, + // .registry = &self.node_registry, + // }; + // } - pub fn getURL(self: *const Self) ?[]const u8 { + pub fn getURL(self: *const Self) ?[:0]const u8 { const page = self.session.currentPage() orelse return null; - const raw_url = page.url.raw; - return if (raw_url.len == 0) null else raw_url; + const url = page.url; + return if (url.len == 0) null else url; } pub fn networkEnable(self: *Self) !void { diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 0f0ff8f8..e99fd6b6 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -19,655 +19,656 @@ const std = @import("std"); const log = @import("../../log.zig"); const Allocator = std.mem.Allocator; -const Node = @import("../Node.zig"); -const css = @import("../../browser/dom/css.zig"); -const parser = @import("../../browser/netsurf.zig"); -const dom_node = @import("../../browser/dom/node.zig"); -const Element = @import("../../browser/dom/element.zig").Element; +// const css = @import("../../browser/dom/css.zig"); +// const parser = @import("../../browser/netsurf.zig"); +// const dom_node = @import("../../browser/dom/node.zig"); pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { enable, - getDocument, - performSearch, - getSearchResults, - discardSearchResults, - querySelector, - querySelectorAll, - resolveNode, - describeNode, - scrollIntoViewIfNeeded, - getContentQuads, - getBoxModel, - requestChildNodes, - getFrameOwner, + // ZIGDOM + // getDocument, + // performSearch, + // getSearchResults, + // discardSearchResults, + // querySelector, + // querySelectorAll, + // resolveNode, + // describeNode, + // scrollIntoViewIfNeeded, + // getContentQuads, + // getBoxModel, + // requestChildNodes, + // getFrameOwner, }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { .enable => return cmd.sendResult(null, .{}), - .getDocument => return getDocument(cmd), - .performSearch => return performSearch(cmd), - .getSearchResults => return getSearchResults(cmd), - .discardSearchResults => return discardSearchResults(cmd), - .querySelector => return querySelector(cmd), - .querySelectorAll => return querySelectorAll(cmd), - .resolveNode => return resolveNode(cmd), - .describeNode => return describeNode(cmd), - .scrollIntoViewIfNeeded => return scrollIntoViewIfNeeded(cmd), - .getContentQuads => return getContentQuads(cmd), - .getBoxModel => return getBoxModel(cmd), - .requestChildNodes => return requestChildNodes(cmd), - .getFrameOwner => return getFrameOwner(cmd), + // @ZIGDOM + // .getDocument => return getDocument(cmd), + // .performSearch => return performSearch(cmd), + // .getSearchResults => return getSearchResults(cmd), + // .discardSearchResults => return discardSearchResults(cmd), + // .querySelector => return querySelector(cmd), + // .querySelectorAll => return querySelectorAll(cmd), + // .resolveNode => return resolveNode(cmd), + // .describeNode => return describeNode(cmd), + // .scrollIntoViewIfNeeded => return scrollIntoViewIfNeeded(cmd), + // .getContentQuads => return getContentQuads(cmd), + // .getBoxModel => return getBoxModel(cmd), + // .requestChildNodes => return requestChildNodes(cmd), + // .getFrameOwner => return getFrameOwner(cmd), } } -// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument -fn getDocument(cmd: anytype) !void { - const Params = struct { - // CDP documentation implies that 0 isn't valid, but it _does_ work in Chrome - depth: i32 = 3, - pierce: bool = false, - }; - const params = try cmd.params(Params) orelse Params{}; - - if (params.pierce) { - log.warn(.cdp, "not implemented", .{ .feature = "DOM.getDocument: Not implemented pierce parameter" }); - } - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const page = bc.session.currentPage() orelse return error.PageNotLoaded; - const doc = parser.documentHTMLToDocument(page.window.document); - - const node = try bc.node_registry.register(parser.documentToNode(doc)); - return cmd.sendResult(.{ .root = bc.nodeWriter(node, .{ .depth = params.depth }) }, .{}); -} - -// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch -fn performSearch(cmd: anytype) !void { - const params = (try cmd.params(struct { - query: []const u8, - includeUserAgentShadowDOM: ?bool = null, - })) orelse return error.InvalidParams; - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const page = bc.session.currentPage() orelse return error.PageNotLoaded; - const doc = parser.documentHTMLToDocument(page.window.document); - - const allocator = cmd.cdp.allocator; - var list = try css.querySelectorAll(allocator, parser.documentToNode(doc), params.query); - defer list.deinit(allocator); - - 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(.{ - .searchId = search.name, - .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 = 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 = 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 -fn discardSearchResults(cmd: anytype) !void { - const params = (try cmd.params(struct { - searchId: []const u8, - })) orelse return error.InvalidParams; - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - - bc.node_search_list.remove(params.searchId); - return cmd.sendResult(null, .{}); -} - -// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getSearchResults -fn getSearchResults(cmd: anytype) !void { - const params = (try cmd.params(struct { - searchId: []const u8, - fromIndex: u32, - toIndex: u32, - })) orelse return error.InvalidParams; - - if (params.fromIndex >= params.toIndex) { - return error.BadIndices; - } - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - - const search = bc.node_search_list.get(params.searchId) orelse { - return error.SearchResultNotFound; - }; - - const node_ids = search.node_ids; - - if (params.fromIndex >= node_ids.len) return error.BadFromIndex; - if (params.toIndex > node_ids.len) return error.BadToIndex; - - return cmd.sendResult(.{ .nodeIds = node_ids[params.fromIndex..params.toIndex] }, .{}); -} - -fn querySelector(cmd: anytype) !void { - const params = (try cmd.params(struct { - nodeId: Node.Id, - selector: []const u8, - })) orelse return error.InvalidParams; - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - - const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse { - return cmd.sendError(-32000, "Could not find node with given id", .{}); - }; - - const selected_node = try css.querySelector( - cmd.arena, - node._node, - params.selector, - ) orelse return error.NodeNotFoundForGivenId; - - const registered_node = try bc.node_registry.register(selected_node); - - // Dispatch setChildNodesEvents to inform the client of the subpart of node tree covering the results. - var array = [1]*parser.Node{selected_node}; - try dispatchSetChildNodes(cmd, array[0..]); - - return cmd.sendResult(.{ - .nodeId = registered_node.id, - }, .{}); -} - -fn querySelectorAll(cmd: anytype) !void { - const params = (try cmd.params(struct { - nodeId: Node.Id, - selector: []const u8, - })) orelse return error.InvalidParams; - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - - const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse { - return cmd.sendError(-32000, "Could not find node with given id", .{}); - }; - - const arena = cmd.arena; - const selected_nodes = try css.querySelectorAll(arena, node._node, params.selector); - const nodes = selected_nodes.nodes.items; - - const node_ids = try arena.alloc(Node.Id, nodes.len); - for (nodes, node_ids) |selected_node, *node_id| { - node_id.* = (try bc.node_registry.register(selected_node)).id; - } - - // Dispatch setChildNodesEvents to inform the client of the subpart of node tree covering the results. - try dispatchSetChildNodes(cmd, nodes); - - return cmd.sendResult(.{ - .nodeIds = node_ids, - }, .{}); -} - -fn resolveNode(cmd: anytype) !void { - const params = (try cmd.params(struct { - nodeId: ?Node.Id = null, - backendNodeId: ?u32 = null, - objectGroup: ?[]const u8 = null, - executionContextId: ?u32 = null, - })) orelse return error.InvalidParams; - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const page = bc.session.currentPage() orelse return error.PageNotLoaded; - - var js_context = page.js; - if (params.executionContextId) |context_id| { - if (js_context.v8_context.debugContextId() != context_id) { - for (bc.isolated_worlds.items) |*isolated_world| { - js_context = &(isolated_world.executor.context orelse return error.ContextNotFound); - if (js_context.v8_context.debugContextId() == context_id) { - break; - } - } else return error.ContextNotFound; - } - } - - const input_node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam; - const node = bc.node_registry.lookup_by_id.get(input_node_id) orelse return error.UnknownNode; - - // node._node is a *parser.Node we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement - // So we use the Node.Union when retrieve the value from the environment - const remote_object = try bc.inspector.getRemoteObject( - js_context, - params.objectGroup orelse "", - try dom_node.Node.toInterface(node._node), - ); - defer remote_object.deinit(); - - const arena = cmd.arena; - return cmd.sendResult(.{ .object = .{ - .type = try remote_object.getType(arena), - .subtype = try remote_object.getSubtype(arena), - .className = try remote_object.getClassName(arena), - .description = try remote_object.getDescription(arena), - .objectId = try remote_object.getObjectId(arena), - } }, .{}); -} - -fn describeNode(cmd: anytype) !void { - const params = (try cmd.params(struct { - nodeId: ?Node.Id = null, - backendNodeId: ?Node.Id = null, - objectId: ?[]const u8 = null, - depth: i32 = 1, - pierce: bool = false, - })) orelse return error.InvalidParams; - - if (params.pierce) { - log.warn(.cdp, "not implemented", .{ .feature = "DOM.describeNode: Not implemented pierce parameter" }); - } - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - - const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); - - return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{ .depth = params.depth }) }, .{}); -} - -// An array of quad vertices, x immediately followed by y for each point, points clock-wise. -// Note Y points downward -// We are assuming the start/endpoint is not repeated. -const Quad = [8]f64; - -const BoxModel = struct { - content: Quad, - padding: Quad, - border: Quad, - margin: Quad, - width: i32, - height: i32, - // shapeOutside: ?ShapeOutsideInfo, -}; - -fn rectToQuad(rect: Element.DOMRect) Quad { - return Quad{ - rect.x, - rect.y, - rect.x + rect.width, - rect.y, - rect.x + rect.width, - rect.y + rect.height, - rect.x, - rect.y + rect.height, - }; -} - -fn scrollIntoViewIfNeeded(cmd: anytype) !void { - const params = (try cmd.params(struct { - nodeId: ?Node.Id = null, - backendNodeId: ?u32 = null, - objectId: ?[]const u8 = null, - rect: ?Element.DOMRect = null, - })) orelse return error.InvalidParams; - // Only 1 of nodeId, backendNodeId, objectId may be set, but chrome just takes the first non-null - - // We retrieve the node to at least check if it exists and is valid. - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); - - const node_type = parser.nodeType(node._node); - switch (node_type) { - .element => {}, - .document => {}, - .text => {}, - else => return error.NodeDoesNotHaveGeometry, - } - - return cmd.sendResult(null, .{}); -} - -fn getNode(arena: Allocator, browser_context: anytype, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node { - const input_node_id = node_id orelse backend_node_id; - if (input_node_id) |input_node_id_| { - return browser_context.node_registry.lookup_by_id.get(input_node_id_) orelse return error.NodeNotFound; - } - if (object_id) |object_id_| { - // Retrieve the object from which ever context it is in. - const parser_node = try browser_context.inspector.getNodePtr(arena, object_id_); - return try browser_context.node_registry.register(@ptrCast(@alignCast(parser_node))); - } - return error.MissingParams; -} - -// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getContentQuads -// Related to: https://drafts.csswg.org/cssom-view/#the-geometryutils-interface -fn getContentQuads(cmd: anytype) !void { - const params = (try cmd.params(struct { - nodeId: ?Node.Id = null, - backendNodeId: ?Node.Id = null, - objectId: ?[]const u8 = null, - })) orelse return error.InvalidParams; - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const page = bc.session.currentPage() orelse return error.PageNotLoaded; - - const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); - - // TODO likely if the following CSS properties are set the quads should be empty - // visibility: hidden - // display: none - - if (parser.nodeType(node._node) != .element) return error.NodeIsNotAnElement; - // TODO implement for document or text - // Most likely document would require some hierachgy in the renderer. It is left unimplemented till we have a good example. - // Text may be tricky, multiple quads in case of multiple lines? empty quads of text = ""? - // Elements like SVGElement may have multiple quads. - - const element = parser.nodeToElement(node._node); - const rect = try Element._getBoundingClientRect(element, page); - const quad = rectToQuad(rect); - - return cmd.sendResult(.{ .quads = &.{quad} }, .{}); -} - -fn getBoxModel(cmd: anytype) !void { - const params = (try cmd.params(struct { - nodeId: ?Node.Id = null, - backendNodeId: ?u32 = null, - objectId: ?[]const u8 = null, - })) orelse return error.InvalidParams; - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const page = bc.session.currentPage() orelse return error.PageNotLoaded; - - const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); - - // TODO implement for document or text - if (parser.nodeType(node._node) != .element) return error.NodeIsNotAnElement; - const element = parser.nodeToElement(node._node); - - const rect = try Element._getBoundingClientRect(element, page); - const quad = rectToQuad(rect); - - return cmd.sendResult(.{ .model = BoxModel{ - .content = quad, - .padding = quad, - .border = quad, - .margin = quad, - .width = @intFromFloat(rect.width), - .height = @intFromFloat(rect.height), - } }, .{}); -} - -fn requestChildNodes(cmd: anytype) !void { - const params = (try cmd.params(struct { - nodeId: Node.Id, - depth: i32 = 1, - pierce: bool = false, - })) orelse return error.InvalidParams; - - if (params.depth == 0) 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, .{}); -} - -fn getFrameOwner(cmd: anytype) !void { - const params = (try cmd.params(struct { - frameId: []const u8, - })) orelse return error.InvalidParams; - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const target_id = bc.target_id orelse return error.TargetNotLoaded; - if (std.mem.eql(u8, target_id, params.frameId) == false) { - return cmd.sendError(-32000, "Frame with the given id does not belong to the target.", .{}); - } - - const page = bc.session.currentPage() orelse return error.PageNotLoaded; - const doc = parser.documentHTMLToDocument(page.window.document); - - const node = try bc.node_registry.register(parser.documentToNode(doc)); - return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{}); -} - -const testing = @import("../testing.zig"); - -test "cdp.dom: getSearchResults unknown search id" { - var ctx = testing.context(); - defer ctx.deinit(); - - try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ - .id = 8, - .method = "DOM.getSearchResults", - .params = .{ .searchId = "Nope", .fromIndex = 0, .toIndex = 10 }, - })); -} - -test "cdp.dom: search flow" { - var ctx = testing.context(); - defer ctx.deinit(); - - _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

1

2

" }); - - try ctx.processMessage(.{ - .id = 12, - .method = "DOM.performSearch", - .params = .{ .query = "p" }, - }); - try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 2 }, .{ .id = 12 }); - - { - // getSearchResults - try ctx.processMessage(.{ - .id = 13, - .method = "DOM.getSearchResults", - .params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 2 }, - }); - try ctx.expectSentResult(.{ .nodeIds = &.{ 1, 2 } }, .{ .id = 13 }); - - // different fromIndex - try ctx.processMessage(.{ - .id = 14, - .method = "DOM.getSearchResults", - .params = .{ .searchId = "0", .fromIndex = 1, .toIndex = 2 }, - }); - try ctx.expectSentResult(.{ .nodeIds = &.{2} }, .{ .id = 14 }); - - // different toIndex - try ctx.processMessage(.{ - .id = 15, - .method = "DOM.getSearchResults", - .params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 }, - }); - try ctx.expectSentResult(.{ .nodeIds = &.{1} }, .{ .id = 15 }); - } - - try ctx.processMessage(.{ - .id = 16, - .method = "DOM.discardSearchResults", - .params = .{ .searchId = "0" }, - }); - try ctx.expectSentResult(null, .{ .id = 16 }); - - // make sure the delete actually did something - try testing.expectError(error.SearchResultNotFound, ctx.processMessage(.{ - .id = 17, - .method = "DOM.getSearchResults", - .params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 }, - })); -} - -test "cdp.dom: querySelector unknown search id" { - var ctx = testing.context(); - defer ctx.deinit(); - - _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

1

2

" }); - - try ctx.processMessage(.{ - .id = 9, - .method = "DOM.querySelector", - .params = .{ .nodeId = 99, .selector = "" }, - }); - try ctx.expectSentError(-32000, "Could not find node with given id", .{}); - - try ctx.processMessage(.{ - .id = 9, - .method = "DOM.querySelectorAll", - .params = .{ .nodeId = 99, .selector = "" }, - }); - try ctx.expectSentError(-32000, "Could not find node with given id", .{}); -} - -test "cdp.dom: querySelector Node not found" { - var ctx = testing.context(); - defer ctx.deinit(); - - _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

1

2

" }); - - try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry - .id = 3, - .method = "DOM.performSearch", - .params = .{ .query = "p" }, - }); - try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 2 }, .{ .id = 3 }); - - try testing.expectError(error.NodeNotFoundForGivenId, ctx.processMessage(.{ - .id = 4, - .method = "DOM.querySelector", - .params = .{ .nodeId = 1, .selector = "a" }, - })); - - try ctx.processMessage(.{ - .id = 5, - .method = "DOM.querySelectorAll", - .params = .{ .nodeId = 1, .selector = "a" }, - }); - try ctx.expectSentResult(.{ .nodeIds = &[_]u32{} }, .{ .id = 5 }); -} - -test "cdp.dom: querySelector Nodes found" { - var ctx = testing.context(); - defer ctx.deinit(); - - _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

2

" }); - - try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry - .id = 3, - .method = "DOM.performSearch", - .params = .{ .query = "div" }, - }); - try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 1 }, .{ .id = 3 }); - - try ctx.processMessage(.{ - .id = 4, - .method = "DOM.querySelector", - .params = .{ .nodeId = 1, .selector = "p" }, - }); - try ctx.expectSentEvent("DOM.setChildNodes", null, .{}); - try ctx.expectSentResult(.{ .nodeId = 6 }, .{ .id = 4 }); - - try ctx.processMessage(.{ - .id = 5, - .method = "DOM.querySelectorAll", - .params = .{ .nodeId = 1, .selector = "p" }, - }); - try ctx.expectSentEvent("DOM.setChildNodes", null, .{}); - try ctx.expectSentResult(.{ .nodeIds = &.{6} }, .{ .id = 5 }); -} - -test "cdp.dom: getBoxModel" { - var ctx = testing.context(); - defer ctx.deinit(); - - _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

2

" }); - - try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry - .id = 3, - .method = "DOM.getDocument", - }); - - try ctx.processMessage(.{ - .id = 4, - .method = "DOM.querySelector", - .params = .{ .nodeId = 1, .selector = "p" }, - }); - try ctx.expectSentResult(.{ .nodeId = 3 }, .{ .id = 4 }); - - try ctx.processMessage(.{ - .id = 5, - .method = "DOM.getBoxModel", - .params = .{ .nodeId = 6 }, - }); - try ctx.expectSentResult(.{ .model = BoxModel{ - .content = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, - .padding = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, - .border = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, - .margin = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, - .width = 1, - .height = 1, - } }, .{ .id = 5 }); -} +// ZIGDOM +// // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument +// fn getDocument(cmd: anytype) !void { +// const Params = struct { +// // CDP documentation implies that 0 isn't valid, but it _does_ work in Chrome +// depth: i32 = 3, +// pierce: bool = false, +// }; +// const params = try cmd.params(Params) orelse Params{}; + +// if (params.pierce) { +// log.warn(.cdp, "not implemented", .{ .feature = "DOM.getDocument: Not implemented pierce parameter" }); +// } + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; +// const page = bc.session.currentPage() orelse return error.PageNotLoaded; +// const doc = parser.documentHTMLToDocument(page.window.document); + +// const node = try bc.node_registry.register(parser.documentToNode(doc)); +// return cmd.sendResult(.{ .root = bc.nodeWriter(node, .{ .depth = params.depth }) }, .{}); +// } + +// // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch +// fn performSearch(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// query: []const u8, +// includeUserAgentShadowDOM: ?bool = null, +// })) orelse return error.InvalidParams; + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; +// const page = bc.session.currentPage() orelse return error.PageNotLoaded; +// const doc = parser.documentHTMLToDocument(page.window.document); + +// const allocator = cmd.cdp.allocator; +// var list = try css.querySelectorAll(allocator, parser.documentToNode(doc), params.query); +// defer list.deinit(allocator); + +// 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(.{ +// .searchId = search.name, +// .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 = 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 = 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 +// fn discardSearchResults(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// searchId: []const u8, +// })) orelse return error.InvalidParams; + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + +// bc.node_search_list.remove(params.searchId); +// return cmd.sendResult(null, .{}); +// } + +// // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getSearchResults +// fn getSearchResults(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// searchId: []const u8, +// fromIndex: u32, +// toIndex: u32, +// })) orelse return error.InvalidParams; + +// if (params.fromIndex >= params.toIndex) { +// return error.BadIndices; +// } + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + +// const search = bc.node_search_list.get(params.searchId) orelse { +// return error.SearchResultNotFound; +// }; + +// const node_ids = search.node_ids; + +// if (params.fromIndex >= node_ids.len) return error.BadFromIndex; +// if (params.toIndex > node_ids.len) return error.BadToIndex; + +// return cmd.sendResult(.{ .nodeIds = node_ids[params.fromIndex..params.toIndex] }, .{}); +// } + +// fn querySelector(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// nodeId: Node.Id, +// selector: []const u8, +// })) orelse return error.InvalidParams; + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + +// const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse { +// return cmd.sendError(-32000, "Could not find node with given id", .{}); +// }; + +// const selected_node = try css.querySelector( +// cmd.arena, +// node._node, +// params.selector, +// ) orelse return error.NodeNotFoundForGivenId; + +// const registered_node = try bc.node_registry.register(selected_node); + +// // Dispatch setChildNodesEvents to inform the client of the subpart of node tree covering the results. +// var array = [1]*parser.Node{selected_node}; +// try dispatchSetChildNodes(cmd, array[0..]); + +// return cmd.sendResult(.{ +// .nodeId = registered_node.id, +// }, .{}); +// } + +// fn querySelectorAll(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// nodeId: Node.Id, +// selector: []const u8, +// })) orelse return error.InvalidParams; + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + +// const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse { +// return cmd.sendError(-32000, "Could not find node with given id", .{}); +// }; + +// const arena = cmd.arena; +// const selected_nodes = try css.querySelectorAll(arena, node._node, params.selector); +// const nodes = selected_nodes.nodes.items; + +// const node_ids = try arena.alloc(Node.Id, nodes.len); +// for (nodes, node_ids) |selected_node, *node_id| { +// node_id.* = (try bc.node_registry.register(selected_node)).id; +// } + +// // Dispatch setChildNodesEvents to inform the client of the subpart of node tree covering the results. +// try dispatchSetChildNodes(cmd, nodes); + +// return cmd.sendResult(.{ +// .nodeIds = node_ids, +// }, .{}); +// } + +// fn resolveNode(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// nodeId: ?Node.Id = null, +// backendNodeId: ?u32 = null, +// objectGroup: ?[]const u8 = null, +// executionContextId: ?u32 = null, +// })) orelse return error.InvalidParams; + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; +// const page = bc.session.currentPage() orelse return error.PageNotLoaded; + +// var js_context = page.js; +// if (params.executionContextId) |context_id| { +// if (js_context.v8_context.debugContextId() != context_id) { +// for (bc.isolated_worlds.items) |*isolated_world| { +// js_context = &(isolated_world.executor.context orelse return error.ContextNotFound); +// if (js_context.v8_context.debugContextId() == context_id) { +// break; +// } +// } else return error.ContextNotFound; +// } +// } + +// const input_node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam; +// const node = bc.node_registry.lookup_by_id.get(input_node_id) orelse return error.UnknownNode; + +// // node._node is a *parser.Node we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement +// // So we use the Node.Union when retrieve the value from the environment +// const remote_object = try bc.inspector.getRemoteObject( +// js_context, +// params.objectGroup orelse "", +// try dom_node.Node.toInterface(node._node), +// ); +// defer remote_object.deinit(); + +// const arena = cmd.arena; +// return cmd.sendResult(.{ .object = .{ +// .type = try remote_object.getType(arena), +// .subtype = try remote_object.getSubtype(arena), +// .className = try remote_object.getClassName(arena), +// .description = try remote_object.getDescription(arena), +// .objectId = try remote_object.getObjectId(arena), +// } }, .{}); +// } + +// fn describeNode(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// nodeId: ?Node.Id = null, +// backendNodeId: ?Node.Id = null, +// objectId: ?[]const u8 = null, +// depth: i32 = 1, +// pierce: bool = false, +// })) orelse return error.InvalidParams; + +// if (params.pierce) { +// log.warn(.cdp, "not implemented", .{ .feature = "DOM.describeNode: Not implemented pierce parameter" }); +// } +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + +// const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); + +// return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{ .depth = params.depth }) }, .{}); +// } + +// // An array of quad vertices, x immediately followed by y for each point, points clock-wise. +// // Note Y points downward +// // We are assuming the start/endpoint is not repeated. +// const Quad = [8]f64; + +// const BoxModel = struct { +// content: Quad, +// padding: Quad, +// border: Quad, +// margin: Quad, +// width: i32, +// height: i32, +// // shapeOutside: ?ShapeOutsideInfo, +// }; + +// fn rectToQuad(rect: Element.DOMRect) Quad { +// return Quad{ +// rect.x, +// rect.y, +// rect.x + rect.width, +// rect.y, +// rect.x + rect.width, +// rect.y + rect.height, +// rect.x, +// rect.y + rect.height, +// }; +// } + +// fn scrollIntoViewIfNeeded(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// nodeId: ?Node.Id = null, +// backendNodeId: ?u32 = null, +// objectId: ?[]const u8 = null, +// rect: ?Element.DOMRect = null, +// })) orelse return error.InvalidParams; +// // Only 1 of nodeId, backendNodeId, objectId may be set, but chrome just takes the first non-null + +// // We retrieve the node to at least check if it exists and is valid. +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; +// const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); + +// const node_type = parser.nodeType(node._node); +// switch (node_type) { +// .element => {}, +// .document => {}, +// .text => {}, +// else => return error.NodeDoesNotHaveGeometry, +// } + +// return cmd.sendResult(null, .{}); +// } + +// fn getNode(arena: Allocator, browser_context: anytype, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node { +// const input_node_id = node_id orelse backend_node_id; +// if (input_node_id) |input_node_id_| { +// return browser_context.node_registry.lookup_by_id.get(input_node_id_) orelse return error.NodeNotFound; +// } +// if (object_id) |object_id_| { +// // Retrieve the object from which ever context it is in. +// const parser_node = try browser_context.inspector.getNodePtr(arena, object_id_); +// return try browser_context.node_registry.register(@ptrCast(@alignCast(parser_node))); +// } +// return error.MissingParams; +// } + +// // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getContentQuads +// // Related to: https://drafts.csswg.org/cssom-view/#the-geometryutils-interface +// fn getContentQuads(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// nodeId: ?Node.Id = null, +// backendNodeId: ?Node.Id = null, +// objectId: ?[]const u8 = null, +// })) orelse return error.InvalidParams; + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; +// const page = bc.session.currentPage() orelse return error.PageNotLoaded; + +// const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); + +// // TODO likely if the following CSS properties are set the quads should be empty +// // visibility: hidden +// // display: none + +// if (parser.nodeType(node._node) != .element) return error.NodeIsNotAnElement; +// // TODO implement for document or text +// // Most likely document would require some hierachgy in the renderer. It is left unimplemented till we have a good example. +// // Text may be tricky, multiple quads in case of multiple lines? empty quads of text = ""? +// // Elements like SVGElement may have multiple quads. + +// const element = parser.nodeToElement(node._node); +// const rect = try Element._getBoundingClientRect(element, page); +// const quad = rectToQuad(rect); + +// return cmd.sendResult(.{ .quads = &.{quad} }, .{}); +// } + +// fn getBoxModel(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// nodeId: ?Node.Id = null, +// backendNodeId: ?u32 = null, +// objectId: ?[]const u8 = null, +// })) orelse return error.InvalidParams; + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; +// const page = bc.session.currentPage() orelse return error.PageNotLoaded; + +// const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); + +// // TODO implement for document or text +// if (parser.nodeType(node._node) != .element) return error.NodeIsNotAnElement; +// const element = parser.nodeToElement(node._node); + +// const rect = try Element._getBoundingClientRect(element, page); +// const quad = rectToQuad(rect); + +// return cmd.sendResult(.{ .model = BoxModel{ +// .content = quad, +// .padding = quad, +// .border = quad, +// .margin = quad, +// .width = @intFromFloat(rect.width), +// .height = @intFromFloat(rect.height), +// } }, .{}); +// } + +// fn requestChildNodes(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// nodeId: Node.Id, +// depth: i32 = 1, +// pierce: bool = false, +// })) orelse return error.InvalidParams; + +// if (params.depth == 0) 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, .{}); +// } + +// fn getFrameOwner(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// frameId: []const u8, +// })) orelse return error.InvalidParams; + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; +// const target_id = bc.target_id orelse return error.TargetNotLoaded; +// if (std.mem.eql(u8, target_id, params.frameId) == false) { +// return cmd.sendError(-32000, "Frame with the given id does not belong to the target.", .{}); +// } + +// const page = bc.session.currentPage() orelse return error.PageNotLoaded; +// const doc = parser.documentHTMLToDocument(page.window.document); + +// const node = try bc.node_registry.register(parser.documentToNode(doc)); +// return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{}); +// } + +// const testing = @import("../testing.zig"); + +// test "cdp.dom: getSearchResults unknown search id" { +// var ctx = testing.context(); +// defer ctx.deinit(); + +// try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ +// .id = 8, +// .method = "DOM.getSearchResults", +// .params = .{ .searchId = "Nope", .fromIndex = 0, .toIndex = 10 }, +// })); +// } + +// test "cdp.dom: search flow" { +// var ctx = testing.context(); +// defer ctx.deinit(); + +// _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

1

2

" }); + +// try ctx.processMessage(.{ +// .id = 12, +// .method = "DOM.performSearch", +// .params = .{ .query = "p" }, +// }); +// try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 2 }, .{ .id = 12 }); + +// { +// // getSearchResults +// try ctx.processMessage(.{ +// .id = 13, +// .method = "DOM.getSearchResults", +// .params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 2 }, +// }); +// try ctx.expectSentResult(.{ .nodeIds = &.{ 1, 2 } }, .{ .id = 13 }); + +// // different fromIndex +// try ctx.processMessage(.{ +// .id = 14, +// .method = "DOM.getSearchResults", +// .params = .{ .searchId = "0", .fromIndex = 1, .toIndex = 2 }, +// }); +// try ctx.expectSentResult(.{ .nodeIds = &.{2} }, .{ .id = 14 }); + +// // different toIndex +// try ctx.processMessage(.{ +// .id = 15, +// .method = "DOM.getSearchResults", +// .params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 }, +// }); +// try ctx.expectSentResult(.{ .nodeIds = &.{1} }, .{ .id = 15 }); +// } + +// try ctx.processMessage(.{ +// .id = 16, +// .method = "DOM.discardSearchResults", +// .params = .{ .searchId = "0" }, +// }); +// try ctx.expectSentResult(null, .{ .id = 16 }); + +// // make sure the delete actually did something +// try testing.expectError(error.SearchResultNotFound, ctx.processMessage(.{ +// .id = 17, +// .method = "DOM.getSearchResults", +// .params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 }, +// })); +// } + +// test "cdp.dom: querySelector unknown search id" { +// var ctx = testing.context(); +// defer ctx.deinit(); + +// _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

1

2

" }); + +// try ctx.processMessage(.{ +// .id = 9, +// .method = "DOM.querySelector", +// .params = .{ .nodeId = 99, .selector = "" }, +// }); +// try ctx.expectSentError(-32000, "Could not find node with given id", .{}); + +// try ctx.processMessage(.{ +// .id = 9, +// .method = "DOM.querySelectorAll", +// .params = .{ .nodeId = 99, .selector = "" }, +// }); +// try ctx.expectSentError(-32000, "Could not find node with given id", .{}); +// } + +// test "cdp.dom: querySelector Node not found" { +// var ctx = testing.context(); +// defer ctx.deinit(); + +// _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

1

2

" }); + +// try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry +// .id = 3, +// .method = "DOM.performSearch", +// .params = .{ .query = "p" }, +// }); +// try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 2 }, .{ .id = 3 }); + +// try testing.expectError(error.NodeNotFoundForGivenId, ctx.processMessage(.{ +// .id = 4, +// .method = "DOM.querySelector", +// .params = .{ .nodeId = 1, .selector = "a" }, +// })); + +// try ctx.processMessage(.{ +// .id = 5, +// .method = "DOM.querySelectorAll", +// .params = .{ .nodeId = 1, .selector = "a" }, +// }); +// try ctx.expectSentResult(.{ .nodeIds = &[_]u32{} }, .{ .id = 5 }); +// } + +// test "cdp.dom: querySelector Nodes found" { +// var ctx = testing.context(); +// defer ctx.deinit(); + +// _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

2

" }); + +// try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry +// .id = 3, +// .method = "DOM.performSearch", +// .params = .{ .query = "div" }, +// }); +// try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 1 }, .{ .id = 3 }); + +// try ctx.processMessage(.{ +// .id = 4, +// .method = "DOM.querySelector", +// .params = .{ .nodeId = 1, .selector = "p" }, +// }); +// try ctx.expectSentEvent("DOM.setChildNodes", null, .{}); +// try ctx.expectSentResult(.{ .nodeId = 6 }, .{ .id = 4 }); + +// try ctx.processMessage(.{ +// .id = 5, +// .method = "DOM.querySelectorAll", +// .params = .{ .nodeId = 1, .selector = "p" }, +// }); +// try ctx.expectSentEvent("DOM.setChildNodes", null, .{}); +// try ctx.expectSentResult(.{ .nodeIds = &.{6} }, .{ .id = 5 }); +// } + +// test "cdp.dom: getBoxModel" { +// var ctx = testing.context(); +// defer ctx.deinit(); + +// _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

2

" }); + +// try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry +// .id = 3, +// .method = "DOM.getDocument", +// }); + +// try ctx.processMessage(.{ +// .id = 4, +// .method = "DOM.querySelector", +// .params = .{ .nodeId = 1, .selector = "p" }, +// }); +// try ctx.expectSentResult(.{ .nodeId = 3 }, .{ .id = 4 }); + +// try ctx.processMessage(.{ +// .id = 5, +// .method = "DOM.getBoxModel", +// .params = .{ .nodeId = 6 }, +// }); +// try ctx.expectSentResult(.{ .model = BoxModel{ +// .content = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, +// .padding = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, +// .border = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, +// .margin = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, +// .width = 1, +// .height = 1, +// } }, .{ .id = 5 }); +// } diff --git a/src/cdp/domains/fetch.zig b/src/cdp/domains/fetch.zig index f6fb302b..ef11e15d 100644 --- a/src/cdp/domains/fetch.zig +++ b/src/cdp/domains/fetch.zig @@ -23,7 +23,7 @@ const log = @import("../../log.zig"); const network = @import("network.zig"); const Http = @import("../../http/Http.zig"); -const Notification = @import("../../notification.zig").Notification; +const Notification = @import("../../Notification.zig"); pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { diff --git a/src/cdp/domains/input.zig b/src/cdp/domains/input.zig index d81fb1c8..b4f2990a 100644 --- a/src/cdp/domains/input.zig +++ b/src/cdp/domains/input.zig @@ -17,7 +17,7 @@ // along with this program. If not, see . const std = @import("std"); -const Page = @import("../../browser/page.zig").Page; +const Page = @import("../../browser/Page.zig"); pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { diff --git a/src/cdp/domains/log.zig b/src/cdp/domains/log.zig index 368a7954..07d3c6d6 100644 --- a/src/cdp/domains/log.zig +++ b/src/cdp/domains/log.zig @@ -101,7 +101,7 @@ pub fn LogInterceptor(comptime BC: type) type { .fatal => "error", }, .text = self.allocating.written(), - .timestamp = @import("../../datetime.zig").milliTimestamp(), + .timestamp = @import("../../datetime.zig").milliTimestamp(.monotonic), }, }, .{ .session_id = self.bc.session_id, diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index 0d7014d0..c41d1988 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -21,7 +21,7 @@ const Allocator = std.mem.Allocator; const CdpStorage = @import("storage.zig"); const Transfer = @import("../../http/Client.zig").Transfer; -const Notification = @import("../../notification.zig").Notification; +const Notification = @import("../../Notification.zig"); pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { @@ -87,7 +87,7 @@ fn setExtraHTTPHeaders(cmd: anytype) !void { return cmd.sendResult(null, .{}); } -const Cookie = @import("../../browser/storage/storage.zig").Cookie; +const Cookie = @import("../../browser/webapi/storage/storage.zig").Cookie; // Only matches the cookie on provided parameters fn cookieMatches(cookie: *const Cookie, name: []const u8, domain: ?[]const u8, path: ?[]const u8) bool { @@ -173,7 +173,7 @@ fn getCookies(cmd: anytype) !void { const params = (try cmd.params(GetCookiesParam)) orelse GetCookiesParam{}; // If not specified, use the URLs of the page and all of its subframes. TODO subframes - const page_url = if (bc.session.page) |*page| page.url.raw else null; // @speed: avoid repasing the URL + const page_url = if (bc.session.page) |page| page.url else null; const param_urls = params.urls orelse &[_][]const u8{page_url orelse return error.InvalidParams}; var urls = try std.ArrayListUnmanaged(CdpStorage.PreparedUri).initCapacity(cmd.arena, param_urls.len); @@ -247,7 +247,7 @@ pub fn httpRequestStart(arena: Allocator, bc: anytype, msg: *const Notification. .requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}), .frameId = target_id, .loaderId = bc.loader_id, - .documentUrl = DocumentUrlWriter.init(&page.url.uri), + .documentUrl = page.url, .request = TransferAsRequestWriter.init(transfer), .initiator = .{ .type = "other" }, }, .{ .session_id = session_id }); @@ -416,34 +416,35 @@ const TransferAsResponseWriter = struct { } }; -const DocumentUrlWriter = struct { - uri: *std.Uri, +// @ZIGDOM - do we still need this? just send the full URL? +// const DocumentUrlWriter = struct { +// uri: *std.Uri, - fn init(uri: *std.Uri) DocumentUrlWriter { - return .{ - .uri = uri, - }; - } +// fn init(uri: *std.Uri) DocumentUrlWriter { +// return .{ +// .uri = uri, +// }; +// } - pub fn jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void { - self._jsonStringify(jws) catch return error.WriteFailed; - } - fn _jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void { - const writer = jws.writer; +// pub fn jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void { +// self._jsonStringify(jws) catch return error.WriteFailed; +// } +// fn _jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void { +// const writer = jws.writer; - try jws.beginWriteRaw(); - try writer.writeByte('\"'); - try self.uri.writeToStream(writer, .{ - .scheme = true, - .authentication = true, - .authority = true, - .path = true, - .query = true, - }); - try writer.writeByte('\"'); - jws.endWriteRaw(); - } -}; +// try jws.beginWriteRaw(); +// try writer.writeByte('\"'); +// try self.uri.writeToStream(writer, .{ +// .scheme = true, +// .authentication = true, +// .authority = true, +// .path = true, +// .query = true, +// }); +// try writer.writeByte('\"'); +// jws.endWriteRaw(); +// } +// }; fn idFromRequestId(request_id: []const u8) !u64 { if (!std.mem.startsWith(u8, request_id, "REQ-")) { diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 1f6b720a..7107d686 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -17,8 +17,8 @@ // along with this program. If not, see . const std = @import("std"); -const Page = @import("../../browser/page.zig").Page; -const Notification = @import("../../notification.zig").Notification; +const Page = @import("../../browser/Page.zig"); +const Notification = @import("../../Notification.zig"); const Allocator = std.mem.Allocator; @@ -134,7 +134,7 @@ fn createIsolatedWorld(cmd: anytype) !void { fn navigate(cmd: anytype) !void { const params = (try cmd.params(struct { - url: []const u8, + url: [:0]const u8, // referrer: ?[]const u8 = null, // transitionType: ?[]const u8 = null, // TODO: enum // frameId: ?[]const u8 = null, @@ -253,7 +253,8 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa bc.inspector.contextCreated( page.js, "", - try page.origin(arena), + "", // @ZIGDOM + // try page.origin(arena), aux_data, true, ); @@ -360,7 +361,7 @@ pub fn pageNetworkAlmostIdle(bc: anytype, event: *const Notification.PageNetwork return sendPageLifecycle(bc, "networkAlmostIdle", event.timestamp); } -fn sendPageLifecycle(bc: anytype, name: []const u8, timestamp: u32) !void { +fn sendPageLifecycle(bc: anytype, name: []const u8, timestamp: u64) !void { // detachTarget could be called, in which case, we still have a page doing // things, but no session. const session_id = bc.session_id orelse return; @@ -379,7 +380,7 @@ const LifecycleEvent = struct { frameId: []const u8, loaderId: ?[]const u8, name: []const u8, - timestamp: u32, + timestamp: u64, }; const testing = @import("../testing.zig"); diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig index 662d079f..83547502 100644 --- a/src/cdp/domains/storage.zig +++ b/src/cdp/domains/storage.zig @@ -19,9 +19,9 @@ const std = @import("std"); const log = @import("../../log.zig"); -const Cookie = @import("../../browser/storage/storage.zig").Cookie; -const CookieJar = @import("../../browser/storage/storage.zig").CookieJar; -pub const PreparedUri = @import("../../browser/storage/cookie.zig").PreparedUri; +const Cookie = @import("../../browser/webapi/storage/storage.zig").Cookie; +const CookieJar = @import("../../browser/webapi/storage/storage.zig").Jar; +pub const PreparedUri = @import("../../browser/webapi/storage/cookie.zig").PreparedUri; pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { diff --git a/src/cdp/domains/target.zig b/src/cdp/domains/target.zig index 26f4cfbe..3ea78b71 100644 --- a/src/cdp/domains/target.zig +++ b/src/cdp/domains/target.zig @@ -143,13 +143,14 @@ fn createTarget(cmd: anytype) !void { bc.target_id = target_id; - var page = try bc.session.createPage(); + const page = try bc.session.createPage(); { const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id}); bc.inspector.contextCreated( page.js, "", - try page.origin(cmd.arena), + "", // @ZIGDOM + // try page.origin(arena), aux_data, true, ); diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index 0c052d12..7c086f6f 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -24,7 +24,6 @@ const ArenaAllocator = std.heap.ArenaAllocator; const Testing = @This(); const main = @import("cdp.zig"); -const parser = @import("../browser/netsurf.zig"); const base = @import("../testing.zig"); pub const allocator = base.allocator; diff --git a/src/http/Client.zig b/src/http/Client.zig index fe0a5a1f..65f31066 100644 --- a/src/http/Client.zig +++ b/src/http/Client.zig @@ -176,7 +176,7 @@ pub fn abort(self: *Client) void { } } -pub fn tick(self: *Client, timeout_ms: i32) !PerformStatus { +pub fn tick(self: *Client, timeout_ms: u32) !PerformStatus { while (true) { if (self.handles.hasAvailable() == false) { break; @@ -188,7 +188,7 @@ pub fn tick(self: *Client, timeout_ms: i32) !PerformStatus { const handle = self.handles.getFreeHandle().?; try self.makeRequest(handle, transfer); } - return self.perform(timeout_ms); + return self.perform(@intCast(timeout_ms)); } pub fn request(self: *Client, req: Request) !void { diff --git a/src/http/Http.zig b/src/http/Http.zig index 17b481d0..e5be87ee 100644 --- a/src/http/Http.zig +++ b/src/http/Http.zig @@ -83,7 +83,7 @@ pub fn deinit(self: *Http) void { self.arena.deinit(); } -pub fn poll(self: *Http, timeout_ms: i32) Client.PerformStatus { +pub fn poll(self: *Http, timeout_ms: u32) Client.PerformStatus { return self.client.tick(timeout_ms) catch |err| { log.err(.app, "http poll", .{ .err = err }); return .normal; diff --git a/src/lightpanda.zig b/src/lightpanda.zig index 54e42573..f037ce3e 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -1,5 +1,7 @@ const std = @import("std"); pub const App = @import("App.zig"); +pub const Server = @import("Server.zig"); + pub const log = @import("log.zig"); pub const dump = @import("browser/dump.zig"); pub const build_config = @import("build_config"); diff --git a/src/main.zig b/src/main.zig index 6c90196d..1da7af4b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -99,27 +99,24 @@ fn run(allocator: Allocator, main_arena: Allocator) !void { app.telemetry.record(.{ .run = {} }); switch (args.mode) { - .serve => { - return; - // @ZIGDOM-CDP - // .serve => |opts| { - // log.debug(.app, "startup", .{ .mode = "serve" }); - // const address = std.net.Address.parseIp4(opts.host, opts.port) catch |err| { - // log.fatal(.app, "invalid server address", .{ .err = err, .host = opts.host, .port = opts.port }); - // return args.printUsageAndExit(false); - // }; + .serve => |opts| { + log.debug(.app, "startup", .{ .mode = "serve" }); + const address = std.net.Address.parseIp4(opts.host, opts.port) catch |err| { + log.fatal(.app, "invalid server address", .{ .err = err, .host = opts.host, .port = opts.port }); + return args.printUsageAndExit(false); + }; - // // _server is global to handle graceful shutdown. - // _server = try lp.Server.init(app, address); - // const server = &_server.?; - // defer server.deinit(); + // _server is global to handle graceful shutdown. + _server = try lp.Server.init(app, address); + const server = &_server.?; + defer server.deinit(); - // // max timeout of 1 week. - // const timeout = if (opts.timeout > 604_800) 604_800_000 else @as(i32, opts.timeout) * 1000; - // server.run(address, timeout) catch |err| { - // log.fatal(.app, "server run error", .{ .err = err }); - // return err; - // }; + // max timeout of 1 week. + const timeout = if (opts.timeout > 604_800) 604_800_000 else @as(u32, opts.timeout) * 1000; + server.run(address, timeout) catch |err| { + log.fatal(.app, "server run error", .{ .err = err }); + return err; + }; }, .fetch => |opts| { const url = opts.url; diff --git a/src/server.zig b/src/server.zig index afb55e43..4d42f001 100644 --- a/src/server.zig +++ b/src/server.zig @@ -26,7 +26,7 @@ const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const log = @import("log.zig"); -const App = @import("app.zig").App; +const App = @import("App.zig"); const CDP = @import("cdp/cdp.zig").CDP; const MAX_HTTP_REQUEST_SIZE = 4096; @@ -69,7 +69,7 @@ pub fn deinit(self: *Server) void { self.allocator.free(self.json_version_response); } -pub fn run(self: *Server, address: net.Address, timeout_ms: i32) !void { +pub fn run(self: *Server, address: net.Address, timeout_ms: u32) !void { const flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC; const listener = try posix.socket(address.any.family, flags, posix.IPPROTO.TCP); self.listener = listener; @@ -112,7 +112,7 @@ pub fn run(self: *Server, address: net.Address, timeout_ms: i32) !void { } } -fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: i32) !void { +fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: u32) !void { // This shouldn't be necessary, but the Client is HUGE (> 512KB) because // it has a large read buffer. I don't know why, but v8 crashes if this // is on the stack (and I assume it's related to its size). @@ -143,7 +143,7 @@ fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: i32) !void { } var cdp = &client.mode.cdp; - var last_message = timestamp(); + var last_message = timestamp(.monotonic); var ms_remaining = timeout_ms; while (true) { switch (cdp.pageWait(ms_remaining)) { @@ -151,7 +151,7 @@ fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: i32) !void { if (try client.readSocket() == false) { return; } - last_message = timestamp(); + last_message = timestamp(.monotonic); ms_remaining = timeout_ms; }, .no_page => { @@ -162,16 +162,16 @@ fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: i32) !void { if (try client.readSocket() == false) { return; } - last_message = timestamp(); + last_message = timestamp(.monotonic); ms_remaining = timeout_ms; }, .done => { - const elapsed = timestamp() - last_message; + const elapsed = timestamp(.monotonic) - last_message; if (elapsed > ms_remaining) { log.info(.app, "CDP timeout", .{}); return; } - ms_remaining -= @as(i32, @intCast(elapsed)); + ms_remaining -= @intCast(elapsed); }, } } @@ -928,9 +928,7 @@ fn buildJSONVersionResponse( return try std.fmt.allocPrint(allocator, response_format, .{ body_len, address }); } -fn timestamp() u32 { - return @import("datetime.zig").timestamp(); -} +pub const timestamp = @import("datetime.zig").timestamp; // In-place string lowercase fn toLower(str: []u8) []u8 { diff --git a/src/telemetry/lightpanda.zig b/src/telemetry/lightpanda.zig index 621f4742..cd87bf8e 100644 --- a/src/telemetry/lightpanda.zig +++ b/src/telemetry/lightpanda.zig @@ -6,7 +6,7 @@ const Thread = std.Thread; const Allocator = std.mem.Allocator; const log = @import("../log.zig"); -const App = @import("../app.zig").App; +const App = @import("../App.zig"); const Http = @import("../http/Http.zig"); const telemetry = @import("telemetry.zig");