Skip to content

Commit 6ba3e57

Browse files
Merge pull request #404 from lightpanda-io/cdp-documentUpdated
cdp: dispatch a DOM.documentUpdated event
2 parents 50b53b0 + 055530c commit 6ba3e57

File tree

7 files changed

+344
-5
lines changed

7 files changed

+344
-5
lines changed

src/browser/browser.zig

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ pub const Browser = struct {
8282
self.session.deinit();
8383
try Session.init(&self.session, alloc, loop, uri);
8484
}
85+
86+
pub fn currentPage(self: *Browser) ?*Page {
87+
if (self.session.page == null) return null;
88+
89+
return &self.session.page.?;
90+
}
8591
};
8692

8793
// Session is like a browser's tab.
@@ -147,7 +153,7 @@ pub const Session = struct {
147153
}
148154

149155
fn deinit(self: *Session) void {
150-
if (self.page) |*p| p.end();
156+
if (self.page) |*p| p.deinit();
151157

152158
if (self.inspector) |inspector| {
153159
inspector.deinit(self.alloc);
@@ -259,6 +265,7 @@ pub const Page = struct {
259265
self.session.window.replaceLocation(&self.location) catch |e| {
260266
log.err("reset window location: {any}", .{e});
261267
};
268+
self.doc = null;
262269

263270
// clear netsurf memory arena.
264271
parser.deinit();
@@ -267,6 +274,7 @@ pub const Page = struct {
267274
}
268275

269276
pub fn deinit(self: *Page) void {
277+
self.end();
270278
self.arena.deinit();
271279
self.session.page = null;
272280
}

src/cdp/cdp.zig

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const IncomingMessage = @import("msg.zig").IncomingMessage;
3434
const Input = @import("msg.zig").Input;
3535
const inspector = @import("inspector.zig").inspector;
3636
const dom = @import("dom.zig").dom;
37+
const cdpdom = @import("dom.zig");
3738
const css = @import("css.zig").css;
3839
const security = @import("security.zig").security;
3940

@@ -129,6 +130,33 @@ pub const State = struct {
129130
loaderID: []const u8 = LoaderID,
130131

131132
page_life_cycle_events: bool = false, // TODO; Target based value
133+
134+
// DOM
135+
nodelist: cdpdom.NodeList,
136+
nodesearchlist: cdpdom.NodeSearchList,
137+
138+
pub fn init(alloc: std.mem.Allocator) State {
139+
return .{
140+
.nodelist = cdpdom.NodeList.init(alloc),
141+
.nodesearchlist = cdpdom.NodeSearchList.init(alloc),
142+
};
143+
}
144+
145+
pub fn deinit(self: *State) void {
146+
self.nodelist.deinit();
147+
148+
// deinit all node searches.
149+
for (self.nodesearchlist.items) |*s| s.deinit();
150+
self.nodesearchlist.deinit();
151+
}
152+
153+
pub fn reset(self: *State) void {
154+
self.nodelist.reset();
155+
156+
// deinit all node searches.
157+
for (self.nodesearchlist.items) |*s| s.deinit();
158+
self.nodesearchlist.clearAndFree();
159+
}
132160
};
133161

134162
// Utils

src/cdp/dom.zig

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,18 @@ const cdp = @import("cdp.zig");
2424
const result = cdp.result;
2525
const IncomingMessage = @import("msg.zig").IncomingMessage;
2626
const Input = @import("msg.zig").Input;
27+
const css = @import("../dom/css.zig");
28+
29+
const parser = @import("netsurf");
2730

2831
const log = std.log.scoped(.cdp);
2932

3033
const Methods = enum {
3134
enable,
35+
getDocument,
36+
performSearch,
37+
getSearchResults,
38+
discardSearchResults,
3239
};
3340

3441
pub fn dom(
@@ -42,6 +49,10 @@ pub fn dom(
4249

4350
return switch (method) {
4451
.enable => enable(alloc, msg, ctx),
52+
.getDocument => getDocument(alloc, msg, ctx),
53+
.performSearch => performSearch(alloc, msg, ctx),
54+
.getSearchResults => getSearchResults(alloc, msg, ctx),
55+
.discardSearchResults => discardSearchResults(alloc, msg, ctx),
4556
};
4657
}
4758

@@ -57,3 +68,275 @@ fn enable(
5768

5869
return result(alloc, input.id, null, null, input.sessionId);
5970
}
71+
72+
// NodeList references tree nodes with an array id.
73+
pub const NodeList = struct {
74+
coll: List,
75+
76+
const List = std.ArrayList(*parser.Node);
77+
78+
pub fn init(alloc: std.mem.Allocator) NodeList {
79+
return .{
80+
.coll = List.init(alloc),
81+
};
82+
}
83+
84+
pub fn deinit(self: *NodeList) void {
85+
self.coll.deinit();
86+
}
87+
88+
pub fn reset(self: *NodeList) void {
89+
self.coll.clearAndFree();
90+
}
91+
92+
pub fn set(self: *NodeList, node: *parser.Node) !NodeId {
93+
for (self.coll.items, 0..) |n, i| {
94+
if (n == node) return @intCast(i);
95+
}
96+
97+
try self.coll.append(node);
98+
return @intCast(self.coll.items.len);
99+
}
100+
};
101+
102+
const NodeId = u32;
103+
104+
const Node = struct {
105+
nodeId: NodeId,
106+
parentId: ?NodeId = null,
107+
backendNodeId: NodeId,
108+
nodeType: u32,
109+
nodeName: []const u8 = "",
110+
localName: []const u8 = "",
111+
nodeValue: []const u8 = "",
112+
childNodeCount: ?u32 = null,
113+
children: ?[]const Node = null,
114+
documentURL: ?[]const u8 = null,
115+
baseURL: ?[]const u8 = null,
116+
xmlVersion: []const u8 = "",
117+
compatibilityMode: []const u8 = "NoQuirksMode",
118+
isScrollable: bool = false,
119+
120+
fn init(n: *parser.Node, nlist: *NodeList) !Node {
121+
const id = try nlist.set(n);
122+
return .{
123+
.nodeId = id,
124+
.backendNodeId = id,
125+
.nodeType = @intFromEnum(try parser.nodeType(n)),
126+
.nodeName = try parser.nodeName(n),
127+
.localName = try parser.nodeLocalName(n),
128+
.nodeValue = try parser.nodeValue(n) orelse "",
129+
};
130+
}
131+
132+
fn initChildren(
133+
self: *Node,
134+
alloc: std.mem.Allocator,
135+
n: *parser.Node,
136+
nlist: *NodeList,
137+
) !std.ArrayList(Node) {
138+
const children = try parser.nodeGetChildNodes(n);
139+
const ln = try parser.nodeListLength(children);
140+
self.childNodeCount = ln;
141+
142+
var list = try std.ArrayList(Node).initCapacity(alloc, ln);
143+
144+
var i: u32 = 0;
145+
while (i < ln) {
146+
defer i += 1;
147+
const child = try parser.nodeListItem(children, i) orelse continue;
148+
try list.append(try Node.init(child, nlist));
149+
}
150+
151+
self.children = list.items;
152+
153+
return list;
154+
}
155+
};
156+
157+
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument
158+
fn getDocument(
159+
alloc: std.mem.Allocator,
160+
msg: *IncomingMessage,
161+
ctx: *Ctx,
162+
) ![]const u8 {
163+
// input
164+
const Params = struct {
165+
depth: ?u32 = null,
166+
pierce: ?bool = null,
167+
};
168+
const input = try Input(Params).get(alloc, msg);
169+
defer input.deinit();
170+
std.debug.assert(input.sessionId != null);
171+
log.debug("Req > id {d}, method {s}", .{ input.id, "DOM.getDocument" });
172+
173+
// retrieve the root node
174+
const page = ctx.browser.currentPage() orelse return error.NoPage;
175+
176+
if (page.doc == null) return error.NoDocument;
177+
178+
const node = parser.documentToNode(page.doc.?);
179+
var n = try Node.init(node, &ctx.state.nodelist);
180+
var list = try n.initChildren(alloc, node, &ctx.state.nodelist);
181+
defer list.deinit();
182+
183+
// output
184+
const Resp = struct {
185+
root: Node,
186+
};
187+
const resp: Resp = .{
188+
.root = n,
189+
};
190+
191+
const res = try result(alloc, input.id, Resp, resp, input.sessionId);
192+
try ctx.send(res);
193+
194+
return "";
195+
}
196+
197+
pub const NodeSearch = struct {
198+
coll: List,
199+
name: []u8,
200+
alloc: std.mem.Allocator,
201+
202+
var count: u8 = 0;
203+
204+
const List = std.ArrayListUnmanaged(NodeId);
205+
206+
pub fn initCapacity(alloc: std.mem.Allocator, ln: usize) !NodeSearch {
207+
count += 1;
208+
209+
return .{
210+
.alloc = alloc,
211+
.coll = try List.initCapacity(alloc, ln),
212+
.name = try std.fmt.allocPrint(alloc, "{d}", .{count}),
213+
};
214+
}
215+
216+
pub fn deinit(self: *NodeSearch) void {
217+
self.coll.deinit(self.alloc);
218+
self.alloc.free(self.name);
219+
}
220+
221+
pub fn append(self: *NodeSearch, id: NodeId) !void {
222+
try self.coll.append(self.alloc, id);
223+
}
224+
};
225+
pub const NodeSearchList = std.ArrayList(NodeSearch);
226+
227+
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch
228+
fn performSearch(
229+
alloc: std.mem.Allocator,
230+
msg: *IncomingMessage,
231+
ctx: *Ctx,
232+
) ![]const u8 {
233+
// input
234+
const Params = struct {
235+
query: []const u8,
236+
includeUserAgentShadowDOM: ?bool = null,
237+
};
238+
const input = try Input(Params).get(alloc, msg);
239+
defer input.deinit();
240+
std.debug.assert(input.sessionId != null);
241+
log.debug("Req > id {d}, method {s}", .{ input.id, "DOM.performSearch" });
242+
243+
// retrieve the root node
244+
const page = ctx.browser.currentPage() orelse return error.NoPage;
245+
246+
if (page.doc == null) return error.NoDocument;
247+
248+
const list = try css.querySelectorAll(alloc, parser.documentToNode(page.doc.?), input.params.query);
249+
const ln = list.nodes.items.len;
250+
var ns = try NodeSearch.initCapacity(alloc, ln);
251+
252+
for (list.nodes.items) |n| {
253+
const id = try ctx.state.nodelist.set(n);
254+
try ns.append(id);
255+
}
256+
257+
try ctx.state.nodesearchlist.append(ns);
258+
259+
// output
260+
const Resp = struct {
261+
searchId: []const u8,
262+
resultCount: u32,
263+
};
264+
const resp: Resp = .{
265+
.searchId = ns.name,
266+
.resultCount = @intCast(ln),
267+
};
268+
269+
return result(alloc, input.id, Resp, resp, input.sessionId);
270+
}
271+
272+
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults
273+
fn discardSearchResults(
274+
alloc: std.mem.Allocator,
275+
msg: *IncomingMessage,
276+
ctx: *Ctx,
277+
) ![]const u8 {
278+
// input
279+
const Params = struct {
280+
searchId: []const u8,
281+
};
282+
const input = try Input(Params).get(alloc, msg);
283+
defer input.deinit();
284+
std.debug.assert(input.sessionId != null);
285+
log.debug("Req > id {d}, method {s}", .{ input.id, "DOM.discardSearchResults" });
286+
287+
// retrieve the search from context
288+
for (ctx.state.nodesearchlist.items, 0..) |*s, i| {
289+
if (!std.mem.eql(u8, s.name, input.params.searchId)) continue;
290+
291+
s.deinit();
292+
_ = ctx.state.nodesearchlist.swapRemove(i);
293+
break;
294+
}
295+
296+
return result(alloc, input.id, null, null, input.sessionId);
297+
}
298+
299+
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getSearchResults
300+
fn getSearchResults(
301+
alloc: std.mem.Allocator,
302+
msg: *IncomingMessage,
303+
ctx: *Ctx,
304+
) ![]const u8 {
305+
// input
306+
const Params = struct {
307+
searchId: []const u8,
308+
fromIndex: u32,
309+
toIndex: u32,
310+
};
311+
const input = try Input(Params).get(alloc, msg);
312+
defer input.deinit();
313+
std.debug.assert(input.sessionId != null);
314+
log.debug("Req > id {d}, method {s}", .{ input.id, "DOM.getSearchResults" });
315+
316+
if (input.params.fromIndex >= input.params.toIndex) return error.BadIndices;
317+
318+
// retrieve the search from context
319+
var ns: ?*const NodeSearch = undefined;
320+
for (ctx.state.nodesearchlist.items) |s| {
321+
if (!std.mem.eql(u8, s.name, input.params.searchId)) continue;
322+
323+
ns = &s;
324+
break;
325+
}
326+
327+
if (ns == null) return error.searchResultNotFound;
328+
const items = ns.?.coll.items;
329+
330+
if (input.params.fromIndex >= items.len) return error.BadFromIndex;
331+
if (input.params.toIndex > items.len) return error.BadToIndex;
332+
333+
// output
334+
const Resp = struct {
335+
nodeIds: []NodeId,
336+
};
337+
const resp: Resp = .{
338+
.nodeIds = ns.?.coll.items[input.params.fromIndex..input.params.toIndex],
339+
};
340+
341+
return result(alloc, input.id, Resp, resp, input.sessionId);
342+
}

0 commit comments

Comments
 (0)