diff --git a/src/browser/browser.zig b/src/browser/browser.zig index e7e2445c..a7b684b1 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -82,6 +82,12 @@ pub const Browser = struct { self.session.deinit(); try Session.init(&self.session, alloc, loop, uri); } + + pub fn currentPage(self: *Browser) ?*Page { + if (self.session.page == null) return null; + + return &self.session.page.?; + } }; // Session is like a browser's tab. @@ -147,7 +153,7 @@ pub const Session = struct { } fn deinit(self: *Session) void { - if (self.page) |*p| p.end(); + if (self.page) |*p| p.deinit(); if (self.inspector) |inspector| { inspector.deinit(self.alloc); @@ -259,6 +265,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(); @@ -267,6 +274,7 @@ pub const Page = struct { } pub fn deinit(self: *Page) void { + self.end(); self.arena.deinit(); self.session.page = null; } diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 65e2cc85..0ba40531 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -34,6 +34,7 @@ const IncomingMessage = @import("msg.zig").IncomingMessage; const Input = @import("msg.zig").Input; const inspector = @import("inspector.zig").inspector; const dom = @import("dom.zig").dom; +const cdpdom = @import("dom.zig"); const css = @import("css.zig").css; const security = @import("security.zig").security; @@ -129,6 +130,33 @@ pub const State = struct { loaderID: []const u8 = LoaderID, page_life_cycle_events: bool = false, // TODO; Target based value + + // DOM + nodelist: cdpdom.NodeList, + nodesearchlist: cdpdom.NodeSearchList, + + pub fn init(alloc: std.mem.Allocator) State { + return .{ + .nodelist = cdpdom.NodeList.init(alloc), + .nodesearchlist = cdpdom.NodeSearchList.init(alloc), + }; + } + + pub fn deinit(self: *State) void { + self.nodelist.deinit(); + + // deinit all node searches. + for (self.nodesearchlist.items) |*s| s.deinit(); + self.nodesearchlist.deinit(); + } + + pub fn reset(self: *State) void { + self.nodelist.reset(); + + // deinit all node searches. + for (self.nodesearchlist.items) |*s| s.deinit(); + self.nodesearchlist.clearAndFree(); + } }; // Utils diff --git a/src/cdp/dom.zig b/src/cdp/dom.zig index 0e8c1d66..d5c661f6 100644 --- a/src/cdp/dom.zig +++ b/src/cdp/dom.zig @@ -24,11 +24,18 @@ const cdp = @import("cdp.zig"); const result = cdp.result; const IncomingMessage = @import("msg.zig").IncomingMessage; const Input = @import("msg.zig").Input; +const css = @import("../dom/css.zig"); + +const parser = @import("netsurf"); const log = std.log.scoped(.cdp); const Methods = enum { enable, + getDocument, + performSearch, + getSearchResults, + discardSearchResults, }; pub fn dom( @@ -42,6 +49,10 @@ pub fn dom( return switch (method) { .enable => enable(alloc, msg, ctx), + .getDocument => getDocument(alloc, msg, ctx), + .performSearch => performSearch(alloc, msg, ctx), + .getSearchResults => getSearchResults(alloc, msg, ctx), + .discardSearchResults => discardSearchResults(alloc, msg, ctx), }; } @@ -57,3 +68,275 @@ fn enable( return result(alloc, input.id, null, null, input.sessionId); } + +// 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 { + for (self.coll.items, 0..) |n, i| { + if (n == node) return @intCast(i); + } + + try self.coll.append(node); + return @intCast(self.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); + + var i: u32 = 0; + while (i < ln) { + defer i += 1; + const child = try parser.nodeListItem(children, i) orelse continue; + try list.append(try Node.init(child, nlist)); + } + + self.children = list.items; + + return list; + } +}; + +// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument +fn getDocument( + alloc: std.mem.Allocator, + msg: *IncomingMessage, + ctx: *Ctx, +) ![]const u8 { + // input + const Params = struct { + depth: ?u32 = null, + pierce: ?bool = null, + }; + const input = try Input(Params).get(alloc, msg); + defer input.deinit(); + std.debug.assert(input.sessionId != null); + log.debug("Req > id {d}, method {s}", .{ input.id, "DOM.getDocument" }); + + // retrieve the root node + const page = ctx.browser.currentPage() orelse return error.NoPage; + + if (page.doc == null) return error.NoDocument; + + const node = parser.documentToNode(page.doc.?); + var n = try Node.init(node, &ctx.state.nodelist); + var list = try n.initChildren(alloc, node, &ctx.state.nodelist); + defer list.deinit(); + + // output + const Resp = struct { + root: Node, + }; + const resp: Resp = .{ + .root = n, + }; + + const res = try result(alloc, input.id, Resp, resp, input.sessionId); + try ctx.send(res); + + return ""; +} + +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( + alloc: std.mem.Allocator, + msg: *IncomingMessage, + ctx: *Ctx, +) ![]const u8 { + // input + const Params = struct { + query: []const u8, + includeUserAgentShadowDOM: ?bool = null, + }; + const input = try Input(Params).get(alloc, msg); + defer input.deinit(); + std.debug.assert(input.sessionId != null); + log.debug("Req > id {d}, method {s}", .{ input.id, "DOM.performSearch" }); + + // retrieve the root node + const page = ctx.browser.currentPage() orelse return error.NoPage; + + if (page.doc == null) return error.NoDocument; + + const list = try css.querySelectorAll(alloc, parser.documentToNode(page.doc.?), input.params.query); + const ln = list.nodes.items.len; + var ns = try NodeSearch.initCapacity(alloc, ln); + + for (list.nodes.items) |n| { + const id = try ctx.state.nodelist.set(n); + try ns.append(id); + } + + try ctx.state.nodesearchlist.append(ns); + + // output + const Resp = struct { + searchId: []const u8, + resultCount: u32, + }; + const resp: Resp = .{ + .searchId = ns.name, + .resultCount = @intCast(ln), + }; + + return result(alloc, input.id, Resp, resp, input.sessionId); +} + +// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults +fn discardSearchResults( + alloc: std.mem.Allocator, + msg: *IncomingMessage, + ctx: *Ctx, +) ![]const u8 { + // input + const Params = struct { + searchId: []const u8, + }; + const input = try Input(Params).get(alloc, msg); + defer input.deinit(); + std.debug.assert(input.sessionId != null); + log.debug("Req > id {d}, method {s}", .{ input.id, "DOM.discardSearchResults" }); + + // retrieve the search from context + for (ctx.state.nodesearchlist.items, 0..) |*s, i| { + if (!std.mem.eql(u8, s.name, input.params.searchId)) continue; + + s.deinit(); + _ = ctx.state.nodesearchlist.swapRemove(i); + break; + } + + return result(alloc, input.id, null, null, input.sessionId); +} + +// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getSearchResults +fn getSearchResults( + alloc: std.mem.Allocator, + msg: *IncomingMessage, + ctx: *Ctx, +) ![]const u8 { + // input + const Params = struct { + searchId: []const u8, + fromIndex: u32, + toIndex: u32, + }; + const input = try Input(Params).get(alloc, msg); + defer input.deinit(); + std.debug.assert(input.sessionId != null); + log.debug("Req > id {d}, method {s}", .{ input.id, "DOM.getSearchResults" }); + + if (input.params.fromIndex >= input.params.toIndex) return error.BadIndices; + + // retrieve the search from context + var ns: ?*const NodeSearch = undefined; + for (ctx.state.nodesearchlist.items) |s| { + if (!std.mem.eql(u8, s.name, input.params.searchId)) continue; + + ns = &s; + break; + } + + if (ns == null) return error.searchResultNotFound; + const items = ns.?.coll.items; + + if (input.params.fromIndex >= items.len) return error.BadFromIndex; + if (input.params.toIndex > items.len) return error.BadToIndex; + + // output + const Resp = struct { + nodeIds: []NodeId, + }; + const resp: Resp = .{ + .nodeIds = ns.?.coll.items[input.params.fromIndex..input.params.toIndex], + }; + + return result(alloc, input.id, Resp, resp, input.sessionId); +} diff --git a/src/cdp/page.zig b/src/cdp/page.zig index 2079f106..8b1470b9 100644 --- a/src/cdp/page.zig +++ b/src/cdp/page.zig @@ -259,6 +259,7 @@ fn navigate( log.debug("Req > id {d}, method {s}", .{ input.id, "page.navigate" }); // change state + ctx.state.reset(); ctx.state.url = input.params.url; // TODO: hard coded ID ctx.state.loaderID = "AF8667A203C5392DBE9AC290044AA4C2"; @@ -333,7 +334,7 @@ fn navigate( // Launch navigate, the page must have been created by a // target.createTarget. - var p = ctx.browser.session.page orelse return error.NoPage; + var p = ctx.browser.currentPage() orelse return error.NoPage; ctx.state.executionContextId += 1; const auxData = try std.fmt.allocPrint( alloc, @@ -361,6 +362,16 @@ fn navigate( ); } + // DOM.documentUpdated + try sendEvent( + alloc, + ctx, + "DOM.documentUpdated", + struct {}, + .{}, + input.sessionId, + ); + // frameNavigated event const FrameNavigated = struct { frame: Frame, diff --git a/src/cdp/target.zig b/src/cdp/target.zig index 90384b64..ca9ae90f 100644 --- a/src/cdp/target.zig +++ b/src/cdp/target.zig @@ -353,7 +353,7 @@ fn createTarget( } // TODO stop the previous page instead? - if (ctx.browser.session.page != null) return error.pageAlreadyExists; + if (ctx.browser.currentPage() != null) return error.pageAlreadyExists; // create the page const p = try ctx.browser.session.createPage(); @@ -464,7 +464,7 @@ fn closeTarget( null, ); - if (ctx.browser.session.page != null) ctx.browser.session.page.?.end(); + if (ctx.browser.currentPage()) |page| page.end(); return ""; } 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.?); } diff --git a/src/server.zig b/src/server.zig index cbe16e12..bc58c64b 100644 --- a/src/server.zig +++ b/src/server.zig @@ -69,13 +69,17 @@ pub const Ctx = struct { last_active: ?std.time.Instant = null, // CDP - state: cdp.State = .{}, + state: cdp.State = undefined, // JS fields browser: *Browser, // TODO: is pointer mandatory here? sessionNew: bool, // try_catch: jsruntime.TryCatch, // TODO + pub fn deinit(self: *Ctx) void { + self.state.deinit(); + } + // callbacks // --------- @@ -458,7 +462,10 @@ pub fn handle( .accept_completion = &accept_completion, .conn_completion = &conn_completion, .timeout_completion = &timeout_completion, + .state = cdp.State.init(browser.session.alloc), }; + defer ctx.deinit(); + try browser.session.initInspector( &ctx, Ctx.onInspectorResp,