diff --git a/src/browser/browser.zig b/src/browser/browser.zig index a9bc2425..afb9d72c 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -98,6 +98,12 @@ pub const Browser = struct { self.session = null; } } + + pub fn currentPage(self: *Browser) ?*Page { + if (self.session.page == null) return null; + + return &self.session.page.?; + } }; // Session is like a browser's tab. @@ -239,6 +245,7 @@ pub const Page = struct { } pub fn deinit(self: *Page) void { + self.end(); self.arena.deinit(); self.session.page = null; } @@ -276,6 +283,7 @@ pub const Page = struct { self.session.window.replaceLocation(&self.location) catch |e| { log.err("reset window location: {any}", .{e}); }; + self.doc = null; // clear netsurf memory arena. parser.deinit(); diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 10b837f9..61f245e3 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -20,6 +20,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const json = std.json; +const dom = @import("dom.zig"); const Loop = @import("jsruntime").Loop; const Client = @import("../server.zig").Client; const asUint = @import("../str/parser.zig").asUint; @@ -64,6 +65,8 @@ pub const CDP = struct { security_origin: []const u8, page_life_cycle_events: bool, secure_context_type: []const u8, + node_list: dom.NodeList, + node_search_list: dom.NodeSearchList, pub fn init(allocator: Allocator, client: *Client, loop: *Loop) CDP { return .{ @@ -81,14 +84,32 @@ pub const CDP = struct { .loader_id = LOADER_ID, .message_arena = std.heap.ArenaAllocator.init(allocator), .page_life_cycle_events = false, // TODO; Target based value + .node_list = dom.NodeList.init(allocator), + .node_search_list = dom.NodeSearchList.init(allocator), }; } pub fn deinit(self: *CDP) void { + self.node_list.deinit(); + for (self.node_search_list.items) |*s| { + s.deinit(); + } + self.node_search_list.deinit(); + self.browser.deinit(); self.message_arena.deinit(); } + pub fn reset(self: *CDP) void { + self.node_list.reset(); + + // deinit all node searches. + for (self.node_search_list.items) |*s| { + s.deinit(); + } + self.node_search_list.clearAndFree(); + } + pub fn newSession(self: *CDP) !void { self.session = try self.browser.newSession(self); } diff --git a/src/cdp/dom.zig b/src/cdp/dom.zig index 21834d83..349d0225 100644 --- a/src/cdp/dom.zig +++ b/src/cdp/dom.zig @@ -18,13 +18,245 @@ const std = @import("std"); const cdp = @import("cdp.zig"); +const css = @import("../dom/css.zig"); + +const parser = @import("netsurf"); pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { enable, + getDocument, + performSearch, + getSearchResults, + discardSearchResults, }, cmd.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), + } } + +// NodeList references tree nodes with an array id. +pub const NodeList = struct { + coll: List, + + const List = std.ArrayList(*parser.Node); + + pub fn init(alloc: std.mem.Allocator) NodeList { + return .{ + .coll = List.init(alloc), + }; + } + + pub fn deinit(self: *NodeList) void { + self.coll.deinit(); + } + + pub fn reset(self: *NodeList) void { + self.coll.clearAndFree(); + } + + pub fn set(self: *NodeList, node: *parser.Node) !NodeId { + const coll = &self.coll; + for (coll.items, 0..) |n, i| { + if (n == node) { + return @intCast(i); + } + } + + try coll.append(node); + return @intCast(coll.items.len); + } +}; + +const NodeId = u32; + +const Node = struct { + nodeId: NodeId, + parentId: ?NodeId = null, + backendNodeId: NodeId, + nodeType: u32, + nodeName: []const u8 = "", + localName: []const u8 = "", + nodeValue: []const u8 = "", + childNodeCount: ?u32 = null, + children: ?[]const Node = null, + documentURL: ?[]const u8 = null, + baseURL: ?[]const u8 = null, + xmlVersion: []const u8 = "", + compatibilityMode: []const u8 = "NoQuirksMode", + isScrollable: bool = false, + + fn init(n: *parser.Node, nlist: *NodeList) !Node { + const id = try nlist.set(n); + return .{ + .nodeId = id, + .backendNodeId = id, + .nodeType = @intFromEnum(try parser.nodeType(n)), + .nodeName = try parser.nodeName(n), + .localName = try parser.nodeLocalName(n), + .nodeValue = try parser.nodeValue(n) orelse "", + }; + } + + fn initChildren( + self: *Node, + alloc: std.mem.Allocator, + n: *parser.Node, + nlist: *NodeList, + ) !std.ArrayList(Node) { + const children = try parser.nodeGetChildNodes(n); + const ln = try parser.nodeListLength(children); + self.childNodeCount = ln; + + var list = try std.ArrayList(Node).initCapacity(alloc, ln); + + for (0..ln) |i| { + const child = try parser.nodeListItem(children, @intCast(i)) orelse continue; + list.appendAssumeCapacity(try Node.init(child, nlist)); + } + + self.children = list.items; + + return list; + } +}; + +// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument +fn getDocument(cmd: anytype) !void { + // const params = (try cmd.params(struct { + // depth: ?u32 = null, + // pierce: ?bool = null, + // })) orelse return error.InvalidParams; + + // retrieve the root node + const page = cmd.session.page orelse return error.NoPage; + const doc = page.doc orelse return error.NoDocument; + + const state = cmd.cdp; + const node = parser.documentToNode(doc); + var n = try Node.init(node, &state.node_list); + _ = try n.initChildren(cmd.arena, node, &state.node_list); + + return cmd.sendResult(.{ + .root = n, + }, .{}); +} + +pub const NodeSearch = struct { + coll: List, + name: []u8, + alloc: std.mem.Allocator, + + var count: u8 = 0; + + const List = std.ArrayListUnmanaged(NodeId); + + pub fn initCapacity(alloc: std.mem.Allocator, ln: usize) !NodeSearch { + count += 1; + + return .{ + .alloc = alloc, + .coll = try List.initCapacity(alloc, ln), + .name = try std.fmt.allocPrint(alloc, "{d}", .{count}), + }; + } + + pub fn deinit(self: *NodeSearch) void { + self.coll.deinit(self.alloc); + self.alloc.free(self.name); + } + + pub fn append(self: *NodeSearch, id: NodeId) !void { + try self.coll.append(self.alloc, id); + } +}; +pub const NodeSearchList = std.ArrayList(NodeSearch); + +// 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; + + // retrieve the root node + const page = cmd.session.page orelse return error.NoPage; + const doc = page.doc orelse return error.NoDocument; + + const list = try css.querySelectorAll(cmd.cdp.allocator, parser.documentToNode(doc), params.query); + const ln = list.nodes.items.len; + var ns = try NodeSearch.initCapacity(cmd.cdp.allocator, ln); + + var state = cmd.cdp; + for (list.nodes.items) |n| { + const id = try state.node_list.set(n); + try ns.append(id); + } + + try state.node_search_list.append(ns); + + return cmd.sendResult(.{ + .searchId = ns.name, + .resultCount = @as(u32, @intCast(ln)), + }, .{}); +} + +// 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; + + var state = cmd.cdp; + // retrieve the search from context + for (state.node_search_list.items, 0..) |*s, i| { + if (!std.mem.eql(u8, s.name, params.searchId)) continue; + + s.deinit(); + _ = state.node_search_list.swapRemove(i); + break; + } + + 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 state = cmd.cdp; + // retrieve the search from context + var ns: ?*const NodeSearch = undefined; + for (state.node_search_list.items) |s| { + if (!std.mem.eql(u8, s.name, params.searchId)) continue; + ns = &s; + break; + } + + if (ns == null) { + return error.searchResultNotFound; + } + + const items = ns.?.coll.items; + + if (params.fromIndex >= items.len) return error.BadFromIndex; + if (params.toIndex > items.len) return error.BadToIndex; + + return cmd.sendResult(.{ + .nodeIds = ns.?.coll.items[params.fromIndex..params.toIndex] + }, .{}); +} diff --git a/src/cdp/page.zig b/src/cdp/page.zig index ac5ef674..dbc3218e 100644 --- a/src/cdp/page.zig +++ b/src/cdp/page.zig @@ -178,6 +178,7 @@ fn navigate(cmd: anytype) !void { // change state var state = cmd.cdp; + state.reset(); state.url = params.url; // TODO: hard coded ID @@ -264,6 +265,9 @@ fn navigate(cmd: anytype) !void { try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id }); } + + try cmd.sendEvent("DOM.documentUpdated", null, .{.session_id = session_id}); + // frameNavigated event try cmd.sendEvent("Page.frameNavigated", .{ .type = "Navigation", diff --git a/src/netsurf/netsurf.zig b/src/netsurf/netsurf.zig index 97716683..171e61be 100644 --- a/src/netsurf/netsurf.zig +++ b/src/netsurf/netsurf.zig @@ -1008,6 +1008,7 @@ pub fn nodeLocalName(node: *Node) ![]const u8 { var s: ?*String = undefined; const err = nodeVtable(node).dom_node_get_local_name.?(node, &s); try DOMErr(err); + if (s == null) return ""; var s_lower: ?*String = undefined; const errStr = c.dom_string_tolower(s, true, &s_lower); try DOMErr(errStr); @@ -1098,6 +1099,7 @@ pub fn nodeName(node: *Node) ![]const u8 { var s: ?*String = undefined; const err = nodeVtable(node).dom_node_get_node_name.?(node, &s); try DOMErr(err); + if (s == null) return ""; return strToData(s.?); }