mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-31 09:29:42 +00:00
Compare commits
11 Commits
http-cache
...
xhr_expand
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
492fd86bad | ||
|
|
55666ab7cd | ||
|
|
008235222b | ||
|
|
b03dbc77ab | ||
|
|
fc057a3bb3 | ||
|
|
16f17ead9a | ||
|
|
367d20d39f | ||
|
|
a6a8752787 | ||
|
|
568fa25add | ||
|
|
752184b12b | ||
|
|
9c8fe9b20f |
@@ -74,6 +74,8 @@ const EventListeners = struct {
|
||||
page_network_idle: List = .{},
|
||||
page_network_almost_idle: List = .{},
|
||||
page_frame_created: List = .{},
|
||||
page_dom_content_loaded: List = .{},
|
||||
page_loaded: List = .{},
|
||||
http_request_fail: List = .{},
|
||||
http_request_start: List = .{},
|
||||
http_request_intercept: List = .{},
|
||||
@@ -91,6 +93,8 @@ const Events = union(enum) {
|
||||
page_network_idle: *const PageNetworkIdle,
|
||||
page_network_almost_idle: *const PageNetworkAlmostIdle,
|
||||
page_frame_created: *const PageFrameCreated,
|
||||
page_dom_content_loaded: *const PageDOMContentLoaded,
|
||||
page_loaded: *const PageLoaded,
|
||||
http_request_fail: *const RequestFail,
|
||||
http_request_start: *const RequestStart,
|
||||
http_request_intercept: *const RequestIntercept,
|
||||
@@ -137,6 +141,18 @@ pub const PageFrameCreated = struct {
|
||||
timestamp: u64,
|
||||
};
|
||||
|
||||
pub const PageDOMContentLoaded = struct {
|
||||
req_id: u32,
|
||||
frame_id: u32,
|
||||
timestamp: u64,
|
||||
};
|
||||
|
||||
pub const PageLoaded = struct {
|
||||
req_id: u32,
|
||||
frame_id: u32,
|
||||
timestamp: u64,
|
||||
};
|
||||
|
||||
pub const RequestStart = struct {
|
||||
transfer: *Transfer,
|
||||
};
|
||||
|
||||
@@ -95,9 +95,11 @@ const NodeData = struct {
|
||||
name: ?[]const u8,
|
||||
value: ?[]const u8,
|
||||
options: ?[]OptionData = null,
|
||||
checked: ?bool = null,
|
||||
xpath: []const u8,
|
||||
is_interactive: bool,
|
||||
node_name: []const u8,
|
||||
interactive: bool,
|
||||
disabled: bool,
|
||||
tag_name: []const u8,
|
||||
};
|
||||
|
||||
const WalkContext = struct {
|
||||
@@ -148,16 +150,21 @@ fn walk(
|
||||
const role = try axn.getRole();
|
||||
|
||||
var is_interactive = false;
|
||||
var is_disabled = false;
|
||||
var value: ?[]const u8 = null;
|
||||
var options: ?[]OptionData = null;
|
||||
var node_name: []const u8 = "text";
|
||||
var checked: ?bool = null;
|
||||
var tag_name: []const u8 = "text";
|
||||
|
||||
if (node.is(Element)) |el| {
|
||||
node_name = el.getTagNameLower();
|
||||
tag_name = el.getTagNameLower();
|
||||
|
||||
if (el.is(Element.Html.Input)) |input| {
|
||||
value = input.getValue();
|
||||
if (el.getAttributeSafe(comptime lp.String.wrap("list"))) |list_id| {
|
||||
if (input._input_type == .checkbox or input._input_type == .radio) {
|
||||
checked = input.getChecked();
|
||||
}
|
||||
if (el.getAttributeSafe(comptime .wrap("list"))) |list_id| {
|
||||
options = try extractDataListOptions(list_id, self.page, self.arena);
|
||||
}
|
||||
} else if (el.is(Element.Html.TextArea)) |textarea| {
|
||||
@@ -172,8 +179,10 @@ fn walk(
|
||||
is_interactive = true;
|
||||
}
|
||||
}
|
||||
|
||||
is_disabled = el.isDisabled();
|
||||
} else if (node._type == .document or node._type == .document_fragment) {
|
||||
node_name = "root";
|
||||
tag_name = "root";
|
||||
}
|
||||
|
||||
const initial_xpath_len = ctx.xpath_buffer.items.len;
|
||||
@@ -234,9 +243,11 @@ fn walk(
|
||||
.name = name,
|
||||
.value = value,
|
||||
.options = options,
|
||||
.checked = checked,
|
||||
.xpath = xpath,
|
||||
.is_interactive = is_interactive,
|
||||
.node_name = node_name,
|
||||
.interactive = is_interactive,
|
||||
.disabled = is_disabled,
|
||||
.tag_name = tag_name,
|
||||
};
|
||||
|
||||
if (should_visit) {
|
||||
@@ -335,7 +346,7 @@ const JsonVisitor = struct {
|
||||
try self.jw.write(data.id);
|
||||
|
||||
try self.jw.objectField("nodeName");
|
||||
try self.jw.write(data.node_name);
|
||||
try self.jw.write(data.tag_name);
|
||||
|
||||
try self.jw.objectField("xpath");
|
||||
try self.jw.write(data.xpath);
|
||||
@@ -345,7 +356,12 @@ const JsonVisitor = struct {
|
||||
try self.jw.write(1);
|
||||
|
||||
try self.jw.objectField("isInteractive");
|
||||
try self.jw.write(data.is_interactive);
|
||||
try self.jw.write(data.interactive);
|
||||
|
||||
if (data.disabled) {
|
||||
try self.jw.objectField("isDisabled");
|
||||
try self.jw.write(true);
|
||||
}
|
||||
|
||||
try self.jw.objectField("role");
|
||||
try self.jw.write(data.role);
|
||||
@@ -373,6 +389,11 @@ const JsonVisitor = struct {
|
||||
try self.jw.endObject();
|
||||
}
|
||||
|
||||
if (data.checked) |checked| {
|
||||
try self.jw.objectField("checked");
|
||||
try self.jw.write(checked);
|
||||
}
|
||||
|
||||
if (data.options) |options| {
|
||||
try self.jw.objectField("options");
|
||||
try self.jw.beginArray();
|
||||
@@ -459,6 +480,9 @@ const TextVisitor = struct {
|
||||
const is_text_only = std.mem.eql(u8, data.role, "StaticText") or std.mem.eql(u8, data.role, "none") or std.mem.eql(u8, data.role, "generic");
|
||||
|
||||
try self.writer.print("{d}", .{data.id});
|
||||
if (data.interactive) {
|
||||
try self.writer.writeAll(if (data.disabled) " [i:disabled]" else " [i]");
|
||||
}
|
||||
if (!is_text_only) {
|
||||
try self.writer.print(" {s}", .{data.role});
|
||||
}
|
||||
@@ -472,6 +496,14 @@ const TextVisitor = struct {
|
||||
}
|
||||
}
|
||||
|
||||
if (data.checked) |c| {
|
||||
if (c) {
|
||||
try self.writer.writeAll(" [checked]");
|
||||
} else {
|
||||
try self.writer.writeAll(" [unchecked]");
|
||||
}
|
||||
}
|
||||
|
||||
if (data.options) |options| {
|
||||
try self.writer.writeAll(" options=[");
|
||||
for (options, 0..) |opt, i| {
|
||||
@@ -509,6 +541,182 @@ const TextVisitor = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const NodeDetails = struct {
|
||||
backendNodeId: CDPNode.Id,
|
||||
tag_name: []const u8,
|
||||
role: []const u8,
|
||||
name: ?[]const u8,
|
||||
interactive: bool,
|
||||
disabled: bool,
|
||||
value: ?[]const u8 = null,
|
||||
input_type: ?[]const u8 = null,
|
||||
placeholder: ?[]const u8 = null,
|
||||
href: ?[]const u8 = null,
|
||||
id: ?[]const u8 = null,
|
||||
class: ?[]const u8 = null,
|
||||
checked: ?bool = null,
|
||||
options: ?[]OptionData = null,
|
||||
|
||||
pub fn jsonStringify(self: *const NodeDetails, jw: anytype) !void {
|
||||
try jw.beginObject();
|
||||
|
||||
try jw.objectField("backendNodeId");
|
||||
try jw.write(self.backendNodeId);
|
||||
|
||||
try jw.objectField("tagName");
|
||||
try jw.write(self.tag_name);
|
||||
|
||||
try jw.objectField("role");
|
||||
try jw.write(self.role);
|
||||
|
||||
if (self.name) |n| {
|
||||
try jw.objectField("name");
|
||||
try jw.write(n);
|
||||
}
|
||||
|
||||
try jw.objectField("isInteractive");
|
||||
try jw.write(self.interactive);
|
||||
|
||||
if (self.disabled) {
|
||||
try jw.objectField("isDisabled");
|
||||
try jw.write(true);
|
||||
}
|
||||
|
||||
if (self.value) |v| {
|
||||
try jw.objectField("value");
|
||||
try jw.write(v);
|
||||
}
|
||||
|
||||
if (self.input_type) |v| {
|
||||
try jw.objectField("inputType");
|
||||
try jw.write(v);
|
||||
}
|
||||
|
||||
if (self.placeholder) |v| {
|
||||
try jw.objectField("placeholder");
|
||||
try jw.write(v);
|
||||
}
|
||||
|
||||
if (self.href) |v| {
|
||||
try jw.objectField("href");
|
||||
try jw.write(v);
|
||||
}
|
||||
|
||||
if (self.id) |v| {
|
||||
try jw.objectField("id");
|
||||
try jw.write(v);
|
||||
}
|
||||
|
||||
if (self.class) |v| {
|
||||
try jw.objectField("class");
|
||||
try jw.write(v);
|
||||
}
|
||||
|
||||
if (self.checked) |c| {
|
||||
try jw.objectField("checked");
|
||||
try jw.write(c);
|
||||
}
|
||||
|
||||
if (self.options) |opts| {
|
||||
try jw.objectField("options");
|
||||
try jw.beginArray();
|
||||
for (opts) |opt| {
|
||||
try jw.beginObject();
|
||||
try jw.objectField("value");
|
||||
try jw.write(opt.value);
|
||||
try jw.objectField("text");
|
||||
try jw.write(opt.text);
|
||||
if (opt.selected) {
|
||||
try jw.objectField("selected");
|
||||
try jw.write(true);
|
||||
}
|
||||
try jw.endObject();
|
||||
}
|
||||
try jw.endArray();
|
||||
}
|
||||
|
||||
try jw.endObject();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn getNodeDetails(
|
||||
arena: std.mem.Allocator,
|
||||
node: *Node,
|
||||
registry: *CDPNode.Registry,
|
||||
page: *Page,
|
||||
) !NodeDetails {
|
||||
const cdp_node = try registry.register(node);
|
||||
const axn = AXNode.fromNode(node);
|
||||
const role = try axn.getRole();
|
||||
const name = try axn.getName(page, arena);
|
||||
|
||||
var is_interactive = false;
|
||||
var is_disabled = false;
|
||||
var tag_name: []const u8 = "text";
|
||||
var value: ?[]const u8 = null;
|
||||
var input_type: ?[]const u8 = null;
|
||||
var placeholder: ?[]const u8 = null;
|
||||
var href: ?[]const u8 = null;
|
||||
var id_attr: ?[]const u8 = null;
|
||||
var class_attr: ?[]const u8 = null;
|
||||
var checked: ?bool = null;
|
||||
var options: ?[]OptionData = null;
|
||||
|
||||
if (node.is(Element)) |el| {
|
||||
tag_name = el.getTagNameLower();
|
||||
is_disabled = el.isDisabled();
|
||||
id_attr = el.getAttributeSafe(comptime .wrap("id"));
|
||||
class_attr = el.getAttributeSafe(comptime .wrap("class"));
|
||||
placeholder = el.getAttributeSafe(comptime .wrap("placeholder"));
|
||||
|
||||
if (el.getAttributeSafe(comptime .wrap("href"))) |h| {
|
||||
const URL = lp.URL;
|
||||
href = URL.resolve(arena, page.base(), h, .{ .encode = true }) catch h;
|
||||
}
|
||||
|
||||
if (el.is(Element.Html.Input)) |input| {
|
||||
value = input.getValue();
|
||||
input_type = input._input_type.toString();
|
||||
if (input._input_type == .checkbox or input._input_type == .radio) {
|
||||
checked = input.getChecked();
|
||||
}
|
||||
if (el.getAttributeSafe(comptime .wrap("list"))) |list_id| {
|
||||
options = try extractDataListOptions(list_id, page, arena);
|
||||
}
|
||||
} else if (el.is(Element.Html.TextArea)) |textarea| {
|
||||
value = textarea.getValue();
|
||||
} else if (el.is(Element.Html.Select)) |select| {
|
||||
value = select.getValue(page);
|
||||
options = try extractSelectOptions(el.asNode(), page, arena);
|
||||
}
|
||||
|
||||
if (el.is(Element.Html)) |html_el| {
|
||||
const listener_targets = try interactive.buildListenerTargetMap(page, arena);
|
||||
var pointer_events_cache: Element.PointerEventsCache = .empty;
|
||||
if (interactive.classifyInteractivity(page, el, html_el, listener_targets, &pointer_events_cache) != null) {
|
||||
is_interactive = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.backendNodeId = cdp_node.id,
|
||||
.tag_name = tag_name,
|
||||
.role = role,
|
||||
.name = name,
|
||||
.interactive = is_interactive,
|
||||
.disabled = is_disabled,
|
||||
.value = value,
|
||||
.input_type = input_type,
|
||||
.placeholder = placeholder,
|
||||
.href = href,
|
||||
.id = id_attr,
|
||||
.class = class_attr,
|
||||
.checked = checked,
|
||||
.options = options,
|
||||
};
|
||||
}
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
test "SemanticTree backendDOMNodeId" {
|
||||
|
||||
@@ -1348,15 +1348,15 @@ pub const Transfer = struct {
|
||||
}
|
||||
}
|
||||
|
||||
transfer.req.notification.dispatch(.http_response_header_done, &.{
|
||||
.transfer = transfer,
|
||||
});
|
||||
|
||||
const proceed = transfer.req.header_callback(transfer) catch |err| {
|
||||
log.err(.http, "header_callback", .{ .err = err, .req = transfer });
|
||||
return err;
|
||||
};
|
||||
|
||||
transfer.req.notification.dispatch(.http_response_header_done, &.{
|
||||
.transfer = transfer,
|
||||
});
|
||||
|
||||
return proceed and transfer.aborted == false;
|
||||
}
|
||||
|
||||
|
||||
@@ -487,7 +487,6 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
||||
return error.InjectBlankFailed;
|
||||
};
|
||||
}
|
||||
self.documentIsComplete();
|
||||
|
||||
session.notification.dispatch(.page_navigate, &.{
|
||||
.frame_id = self._frame_id,
|
||||
@@ -519,6 +518,8 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
||||
|
||||
// force next request id manually b/c we won't create a real req.
|
||||
_ = session.browser.http_client.incrReqId();
|
||||
|
||||
self.documentIsComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -738,6 +739,12 @@ pub fn _documentIsLoaded(self: *Page) !void {
|
||||
self.document.asEventTarget(),
|
||||
event,
|
||||
);
|
||||
|
||||
self._session.notification.dispatch(.page_dom_content_loaded, &.{
|
||||
.frame_id = self._frame_id,
|
||||
.req_id = self._req_id,
|
||||
.timestamp = timestamp(.monotonic),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn scriptsCompletedLoading(self: *Page) void {
|
||||
@@ -796,19 +803,6 @@ pub fn documentIsComplete(self: *Page) void {
|
||||
self._documentIsComplete() catch |err| {
|
||||
log.err(.page, "document is complete", .{ .err = err, .type = self._type, .url = self.url });
|
||||
};
|
||||
|
||||
if (self._navigated_options) |no| {
|
||||
// _navigated_options will be null in special short-circuit cases, like
|
||||
// "navigating" to about:blank, in which case this notification has
|
||||
// already been sent
|
||||
self._session.notification.dispatch(.page_navigated, &.{
|
||||
.frame_id = self._frame_id,
|
||||
.req_id = self._req_id,
|
||||
.opts = no,
|
||||
.url = self.url,
|
||||
.timestamp = timestamp(.monotonic),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn _documentIsComplete(self: *Page) !void {
|
||||
@@ -827,6 +821,12 @@ fn _documentIsComplete(self: *Page) !void {
|
||||
try self._event_manager.dispatchDirect(window_target, event, self.window._on_load, .{ .inject_target = false, .context = "page load" });
|
||||
}
|
||||
|
||||
self._session.notification.dispatch(.page_loaded, &.{
|
||||
.frame_id = self._frame_id,
|
||||
.req_id = self._req_id,
|
||||
.timestamp = timestamp(.monotonic),
|
||||
});
|
||||
|
||||
if (self._event_manager.hasDirectListeners(window_target, "pageshow", self.window._on_pageshow)) {
|
||||
const pageshow_event = (try PageTransitionEvent.initTrusted(comptime .wrap("pageshow"), .{}, self)).asEvent();
|
||||
try self._event_manager.dispatchDirect(window_target, pageshow_event, self.window._on_pageshow, .{ .context = "page show" });
|
||||
@@ -879,6 +879,19 @@ fn pageHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {
|
||||
});
|
||||
}
|
||||
|
||||
if (self._navigated_options) |no| {
|
||||
// _navigated_options will be null in special short-circuit cases, like
|
||||
// "navigating" to about:blank, in which case this notification has
|
||||
// already been sent
|
||||
self._session.notification.dispatch(.page_navigated, &.{
|
||||
.frame_id = self._frame_id,
|
||||
.req_id = self._req_id,
|
||||
.opts = no,
|
||||
.url = self.url,
|
||||
.timestamp = timestamp(.monotonic),
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ _page: *Page,
|
||||
_proto: *XMLHttpRequestEventTarget,
|
||||
_arena: Allocator,
|
||||
_transfer: ?*HttpClient.Transfer = null,
|
||||
_has_ref: bool = false,
|
||||
|
||||
_url: [:0]const u8 = "",
|
||||
_method: net_http.Method = .GET,
|
||||
@@ -136,6 +137,14 @@ pub fn deinit(self: *XMLHttpRequest, session: *Session) void {
|
||||
session.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
fn releaseSelfRef(self: *XMLHttpRequest) void {
|
||||
if (self._has_ref == false) {
|
||||
return;
|
||||
}
|
||||
self.releaseRef(self._page._session);
|
||||
self._has_ref = false;
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *XMLHttpRequest, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
}
|
||||
@@ -252,6 +261,8 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
|
||||
.error_callback = httpErrorCallback,
|
||||
.shutdown_callback = httpShutdownCallback,
|
||||
});
|
||||
self.acquireRef();
|
||||
self._has_ref = true;
|
||||
}
|
||||
|
||||
fn handleBlobUrl(self: *XMLHttpRequest, page: *Page) !void {
|
||||
@@ -393,7 +404,6 @@ fn httpStartCallback(transfer: *HttpClient.Transfer) !void {
|
||||
log.debug(.http, "request start", .{ .method = self._method, .url = self._url, .source = "xhr" });
|
||||
}
|
||||
self._transfer = transfer;
|
||||
self.acquireRef();
|
||||
}
|
||||
|
||||
fn httpHeaderCallback(transfer: *HttpClient.Transfer, header: net_http.Header) !void {
|
||||
@@ -501,8 +511,8 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
self.handleError(err);
|
||||
if (self._transfer != null) {
|
||||
self._transfer = null;
|
||||
self.releaseRef(self._page._session);
|
||||
}
|
||||
self.releaseSelfRef();
|
||||
}
|
||||
|
||||
fn httpShutdownCallback(ctx: *anyopaque) void {
|
||||
@@ -515,8 +525,8 @@ pub fn abort(self: *XMLHttpRequest) void {
|
||||
if (self._transfer) |transfer| {
|
||||
self._transfer = null;
|
||||
transfer.abort(error.Abort);
|
||||
self.releaseRef(self._page._session);
|
||||
}
|
||||
self.releaseSelfRef();
|
||||
}
|
||||
|
||||
fn handleError(self: *XMLHttpRequest, err: anyerror) void {
|
||||
|
||||
@@ -432,6 +432,8 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
try notification.register(.page_navigate, self, onPageNavigate);
|
||||
try notification.register(.page_navigated, self, onPageNavigated);
|
||||
try notification.register(.page_frame_created, self, onPageFrameCreated);
|
||||
try notification.register(.page_dom_content_loaded, self, onPageDOMContentLoaded);
|
||||
try notification.register(.page_loaded, self, onPageLoaded);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
@@ -607,6 +609,16 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
return @import("domains/page.zig").pageFrameCreated(self, msg);
|
||||
}
|
||||
|
||||
pub fn onPageDOMContentLoaded(ctx: *anyopaque, msg: *const Notification.PageDOMContentLoaded) !void {
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
return @import("domains/page.zig").pageDOMContentLoaded(self, msg);
|
||||
}
|
||||
|
||||
pub fn onPageLoaded(ctx: *anyopaque, msg: *const Notification.PageLoaded) !void {
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
return @import("domains/page.zig").pageLoaded(self, msg);
|
||||
}
|
||||
|
||||
pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void {
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
return @import("domains/page.zig").pageNetworkIdle(self, msg);
|
||||
|
||||
@@ -30,6 +30,7 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
getMarkdown,
|
||||
getSemanticTree,
|
||||
getInteractiveElements,
|
||||
getNodeDetails,
|
||||
getStructuredData,
|
||||
detectForms,
|
||||
clickNode,
|
||||
@@ -42,6 +43,7 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
.getMarkdown => return getMarkdown(cmd),
|
||||
.getSemanticTree => return getSemanticTree(cmd),
|
||||
.getInteractiveElements => return getInteractiveElements(cmd),
|
||||
.getNodeDetails => return getNodeDetails(cmd),
|
||||
.getStructuredData => return getStructuredData(cmd),
|
||||
.detectForms => return detectForms(cmd),
|
||||
.clickNode => return clickNode(cmd),
|
||||
@@ -141,6 +143,24 @@ fn getInteractiveElements(cmd: anytype) !void {
|
||||
}, .{});
|
||||
}
|
||||
|
||||
fn getNodeDetails(cmd: anytype) !void {
|
||||
const Params = struct {
|
||||
backendNodeId: Node.Id,
|
||||
};
|
||||
const params = (try cmd.params(Params)) orelse return error.InvalidParam;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.NoBrowserContext;
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
|
||||
const node = (bc.node_registry.lookup_by_id.get(params.backendNodeId) orelse return error.InvalidNodeId).dom;
|
||||
|
||||
const details = SemanticTree.getNodeDetails(cmd.arena, node, &bc.node_registry, page) catch return error.InternalError;
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.nodeDetails = details,
|
||||
}, .{});
|
||||
}
|
||||
|
||||
fn getStructuredData(cmd: anytype) !void {
|
||||
const bc = cmd.browser_context orelse return error.NoBrowserContext;
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
|
||||
@@ -420,7 +420,6 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
|
||||
// things, but no session.
|
||||
const session_id = bc.session_id orelse return;
|
||||
|
||||
const timestamp = event.timestamp;
|
||||
const frame_id = &id.toFrameId(event.frame_id);
|
||||
const loader_id = &id.toLoaderId(event.req_id);
|
||||
|
||||
@@ -472,9 +471,9 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
|
||||
// When we actually recreated the context we should have the inspector send
|
||||
// this event, see: resetContextGroup Sending this event will tell the
|
||||
// client that the context ids they had are invalid and the context shouls
|
||||
// be dropped The client will expect us to send new contextCreated events,
|
||||
// this event, see: resetContextGroup. Sending this event will tell the
|
||||
// client that the context ids they had are invalid and the context should
|
||||
// be dropped. The client will expect us to send new contextCreated events,
|
||||
// such that the client has new id's for the active contexts.
|
||||
// Only send executionContextsCleared for main frame navigations. For child
|
||||
// frames (iframes), clearing all contexts would destroy the main frame's
|
||||
@@ -484,6 +483,18 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
|
||||
try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
// frameNavigated event
|
||||
try cdp.sendEvent("Page.frameNavigated", .{
|
||||
.type = "Navigation",
|
||||
.frame = Frame{
|
||||
.id = frame_id,
|
||||
.url = event.url,
|
||||
.loaderId = loader_id,
|
||||
.securityOrigin = bc.security_origin,
|
||||
.secureContextType = bc.secure_context_type,
|
||||
},
|
||||
}, .{ .session_id = session_id });
|
||||
|
||||
{
|
||||
const aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\",\"loaderId\":\"{s}\"}}", .{ frame_id, loader_id });
|
||||
|
||||
@@ -554,18 +565,22 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
|
||||
// chromedp client expects to receive the events is this order.
|
||||
// see https://github.com/chromedp/chromedp/issues/1558
|
||||
try cdp.sendEvent("DOM.documentUpdated", null, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
pub fn pageDOMContentLoaded(bc: anytype, event: *const Notification.PageDOMContentLoaded) !void {
|
||||
const session_id = bc.session_id orelse return;
|
||||
const timestamp = event.timestamp;
|
||||
var cdp = bc.cdp;
|
||||
|
||||
// domContentEventFired event
|
||||
// TODO: partially hard coded
|
||||
try cdp.sendEvent(
|
||||
"Page.domContentEventFired",
|
||||
.{ .timestamp = timestamp },
|
||||
.{ .session_id = session_id },
|
||||
);
|
||||
|
||||
// lifecycle DOMContentLoaded event
|
||||
// TODO: partially hard coded
|
||||
if (bc.page_life_cycle_events) {
|
||||
const frame_id = &id.toFrameId(event.frame_id);
|
||||
const loader_id = &id.toLoaderId(event.req_id);
|
||||
try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
|
||||
.timestamp = timestamp,
|
||||
.name = "DOMContentLoaded",
|
||||
@@ -573,16 +588,23 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
|
||||
.loaderId = loader_id,
|
||||
}, .{ .session_id = session_id });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pageLoaded(bc: anytype, event: *const Notification.PageLoaded) !void {
|
||||
const session_id = bc.session_id orelse return;
|
||||
const timestamp = event.timestamp;
|
||||
var cdp = bc.cdp;
|
||||
|
||||
const frame_id = &id.toFrameId(event.frame_id);
|
||||
|
||||
// loadEventFired event
|
||||
try cdp.sendEvent(
|
||||
"Page.loadEventFired",
|
||||
.{ .timestamp = timestamp },
|
||||
.{ .session_id = session_id },
|
||||
);
|
||||
|
||||
// lifecycle DOMContentLoaded event
|
||||
if (bc.page_life_cycle_events) {
|
||||
const loader_id = &id.toLoaderId(event.req_id);
|
||||
try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
|
||||
.timestamp = timestamp,
|
||||
.name = "load",
|
||||
@@ -591,7 +613,6 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
|
||||
}, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
// frameStoppedLoading
|
||||
return cdp.sendEvent("Page.frameStoppedLoading", .{
|
||||
.frameId = frame_id,
|
||||
}, .{ .session_id = session_id });
|
||||
|
||||
@@ -237,6 +237,10 @@ pub fn RC(comptime T: type) type {
|
||||
session.releaseArena(kv.value.arena);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(self: @This(), writer: *std.Io.Writer) !void {
|
||||
return writer.print("{d}", .{self._refs});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,19 @@ pub const tool_list = [_]protocol.Tool{
|
||||
\\}
|
||||
),
|
||||
},
|
||||
.{
|
||||
.name = "nodeDetails",
|
||||
.description = "Get detailed information about a specific node by its backend node ID. Returns tag, role, name, interactivity, disabled state, value, input type, placeholder, href, checked state, and select options.",
|
||||
.inputSchema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "backendNodeId": { "type": "integer", "description": "The backend node ID of the element to inspect." }
|
||||
\\ },
|
||||
\\ "required": ["backendNodeId"]
|
||||
\\}
|
||||
),
|
||||
},
|
||||
.{
|
||||
.name = "interactiveElements",
|
||||
.description = "Extract interactive elements from the opened page. If a url is provided, it navigates to that url first.",
|
||||
@@ -256,6 +269,7 @@ const ToolAction = enum {
|
||||
navigate,
|
||||
markdown,
|
||||
links,
|
||||
nodeDetails,
|
||||
interactiveElements,
|
||||
structuredData,
|
||||
detectForms,
|
||||
@@ -272,6 +286,7 @@ const tool_map = std.StaticStringMap(ToolAction).initComptime(.{
|
||||
.{ "navigate", .navigate },
|
||||
.{ "markdown", .markdown },
|
||||
.{ "links", .links },
|
||||
.{ "nodeDetails", .nodeDetails },
|
||||
.{ "interactiveElements", .interactiveElements },
|
||||
.{ "structuredData", .structuredData },
|
||||
.{ "detectForms", .detectForms },
|
||||
@@ -305,6 +320,7 @@ pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Reque
|
||||
.goto, .navigate => try handleGoto(server, arena, req.id.?, call_params.arguments),
|
||||
.markdown => try handleMarkdown(server, arena, req.id.?, call_params.arguments),
|
||||
.links => try handleLinks(server, arena, req.id.?, call_params.arguments),
|
||||
.nodeDetails => try handleNodeDetails(server, arena, req.id.?, call_params.arguments),
|
||||
.interactiveElements => try handleInteractiveElements(server, arena, req.id.?, call_params.arguments),
|
||||
.structuredData => try handleStructuredData(server, arena, req.id.?, call_params.arguments),
|
||||
.detectForms => try handleDetectForms(server, arena, req.id.?, call_params.arguments),
|
||||
@@ -373,6 +389,32 @@ fn handleSemanticTree(server: *Server, arena: std.mem.Allocator, id: std.json.Va
|
||||
};
|
||||
}
|
||||
|
||||
fn handleNodeDetails(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {
|
||||
const Params = struct {
|
||||
backendNodeId: CDPNode.Id,
|
||||
};
|
||||
const args = try parseArgs(Params, arena, arguments, server, id, "nodeDetails");
|
||||
|
||||
_ = server.session.currentPage() orelse {
|
||||
return server.sendError(id, .PageNotLoaded, "Page not loaded");
|
||||
};
|
||||
|
||||
const node = server.node_registry.lookup_by_id.get(args.backendNodeId) orelse {
|
||||
return server.sendError(id, .InvalidParams, "Node not found");
|
||||
};
|
||||
|
||||
const page = server.session.currentPage().?;
|
||||
const details = lp.SemanticTree.getNodeDetails(arena, node.dom, &server.node_registry, page) catch {
|
||||
return server.sendError(id, .InternalError, "Failed to get node details");
|
||||
};
|
||||
|
||||
var aw: std.Io.Writer.Allocating = .init(arena);
|
||||
try std.json.Stringify.value(&details, .{}, &aw.writer);
|
||||
|
||||
const content = [_]protocol.TextContent([]const u8){.{ .text = aw.written() }};
|
||||
try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });
|
||||
}
|
||||
|
||||
fn handleInteractiveElements(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {
|
||||
const args = try parseArgsOrDefault(UrlParams, arena, arguments, server, id);
|
||||
const page = try ensurePage(server, id, args.url);
|
||||
|
||||
Reference in New Issue
Block a user