mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
Merge branch 'main' into semantic-tree
This commit is contained in:
@@ -82,7 +82,7 @@
|
|||||||
testing.expectEqual('ceil', atob('Y2VpbA')); // 6 chars, len%4==2, needs '=='
|
testing.expectEqual('ceil', atob('Y2VpbA')); // 6 chars, len%4==2, needs '=='
|
||||||
|
|
||||||
// length % 4 == 1 must still throw
|
// length % 4 == 1 must still throw
|
||||||
testing.expectError('Error: InvalidCharacterError', () => {
|
testing.expectError('InvalidCharacterError: Invalid Character', () => {
|
||||||
atob('Y');
|
atob('Y');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ pub const JsApi = struct {
|
|||||||
|
|
||||||
pub const now = bridge.function(Performance.now, .{});
|
pub const now = bridge.function(Performance.now, .{});
|
||||||
pub const mark = bridge.function(Performance.mark, .{});
|
pub const mark = bridge.function(Performance.mark, .{});
|
||||||
pub const measure = bridge.function(Performance.measure, .{});
|
pub const measure = bridge.function(Performance.measure, .{ .dom_exception = true });
|
||||||
pub const clearMarks = bridge.function(Performance.clearMarks, .{});
|
pub const clearMarks = bridge.function(Performance.clearMarks, .{});
|
||||||
pub const clearMeasures = bridge.function(Performance.clearMeasures, .{});
|
pub const clearMeasures = bridge.function(Performance.clearMeasures, .{});
|
||||||
pub const getEntries = bridge.function(Performance.getEntries, .{});
|
pub const getEntries = bridge.function(Performance.getEntries, .{});
|
||||||
|
|||||||
@@ -798,7 +798,7 @@ pub const JsApi = struct {
|
|||||||
pub const matchMedia = bridge.function(Window.matchMedia, .{});
|
pub const matchMedia = bridge.function(Window.matchMedia, .{});
|
||||||
pub const postMessage = bridge.function(Window.postMessage, .{});
|
pub const postMessage = bridge.function(Window.postMessage, .{});
|
||||||
pub const btoa = bridge.function(Window.btoa, .{});
|
pub const btoa = bridge.function(Window.btoa, .{});
|
||||||
pub const atob = bridge.function(Window.atob, .{});
|
pub const atob = bridge.function(Window.atob, .{ .dom_exception = true });
|
||||||
pub const reportError = bridge.function(Window.reportError, .{});
|
pub const reportError = bridge.function(Window.reportError, .{});
|
||||||
pub const getComputedStyle = bridge.function(Window.getComputedStyle, .{});
|
pub const getComputedStyle = bridge.function(Window.getComputedStyle, .{});
|
||||||
pub const getSelection = bridge.function(Window.getSelection, .{});
|
pub const getSelection = bridge.function(Window.getSelection, .{});
|
||||||
|
|||||||
@@ -474,12 +474,12 @@ pub const JsApi = struct {
|
|||||||
pub const canGoForward = bridge.accessor(Navigation.getCanGoForward, null, .{});
|
pub const canGoForward = bridge.accessor(Navigation.getCanGoForward, null, .{});
|
||||||
pub const currentEntry = bridge.accessor(Navigation.getCurrentEntry, null, .{});
|
pub const currentEntry = bridge.accessor(Navigation.getCurrentEntry, null, .{});
|
||||||
pub const transition = bridge.accessor(Navigation.getTransition, null, .{});
|
pub const transition = bridge.accessor(Navigation.getTransition, null, .{});
|
||||||
pub const back = bridge.function(Navigation.back, .{});
|
pub const back = bridge.function(Navigation.back, .{ .dom_exception = true });
|
||||||
pub const entries = bridge.function(Navigation.entries, .{});
|
pub const entries = bridge.function(Navigation.entries, .{});
|
||||||
pub const forward = bridge.function(Navigation.forward, .{});
|
pub const forward = bridge.function(Navigation.forward, .{ .dom_exception = true });
|
||||||
pub const navigate = bridge.function(Navigation.navigate, .{});
|
pub const navigate = bridge.function(Navigation.navigate, .{ .dom_exception = true });
|
||||||
pub const traverseTo = bridge.function(Navigation.traverseTo, .{});
|
pub const traverseTo = bridge.function(Navigation.traverseTo, .{ .dom_exception = true });
|
||||||
pub const updateCurrentEntry = bridge.function(Navigation.updateCurrentEntry, .{});
|
pub const updateCurrentEntry = bridge.function(Navigation.updateCurrentEntry, .{ .dom_exception = true });
|
||||||
|
|
||||||
pub const oncurrententrychange = bridge.accessor(
|
pub const oncurrententrychange = bridge.accessor(
|
||||||
Navigation.getOnCurrentEntryChange,
|
Navigation.getOnCurrentEntryChange,
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ http_client: *HttpClient,
|
|||||||
notification: *lp.Notification,
|
notification: *lp.Notification,
|
||||||
browser: lp.Browser,
|
browser: lp.Browser,
|
||||||
session: *lp.Session,
|
session: *lp.Session,
|
||||||
page: *lp.Page,
|
|
||||||
node_registry: CDPNode.Registry,
|
node_registry: CDPNode.Registry,
|
||||||
|
|
||||||
writer: *std.io.Writer,
|
writer: *std.io.Writer,
|
||||||
@@ -47,13 +46,10 @@ pub fn init(allocator: std.mem.Allocator, app: *App, writer: *std.io.Writer) !*S
|
|||||||
.http_client = http_client,
|
.http_client = http_client,
|
||||||
.notification = notification,
|
.notification = notification,
|
||||||
.session = undefined,
|
.session = undefined,
|
||||||
.page = undefined,
|
|
||||||
.node_registry = CDPNode.Registry.init(allocator),
|
.node_registry = CDPNode.Registry.init(allocator),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.session = try self.browser.newSession(self.notification);
|
self.session = try self.browser.newSession(self.notification);
|
||||||
self.page = try self.session.createPage();
|
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +87,7 @@ pub fn sendResult(self: *Self, id: std.json.Value, result: anytype) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn sendError(self: *Self, id: std.json.Value, code: protocol.ErrorCode, message: []const u8) !void {
|
pub fn sendError(self: *Self, id: std.json.Value, code: protocol.ErrorCode, message: []const u8) !void {
|
||||||
try self.sendResponse(protocol.Response{
|
try self.sendResponse(.{
|
||||||
.id = id,
|
.id = id,
|
||||||
.@"error" = protocol.Error{
|
.@"error" = protocol.Error{
|
||||||
.code = @intFromEnum(code),
|
.code = @intFromEnum(code),
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ pub const ErrorCode = enum(i64) {
|
|||||||
MethodNotFound = -32601,
|
MethodNotFound = -32601,
|
||||||
InvalidParams = -32602,
|
InvalidParams = -32602,
|
||||||
InternalError = -32603,
|
InternalError = -32603,
|
||||||
|
PageNotLoaded = -32604,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Notification = struct {
|
pub const Notification = struct {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const ResourceStreamingResult = struct {
|
|||||||
},
|
},
|
||||||
|
|
||||||
const StreamingText = struct {
|
const StreamingText = struct {
|
||||||
server: *Server,
|
page: *lp.Page,
|
||||||
format: enum { html, markdown },
|
format: enum { html, markdown },
|
||||||
|
|
||||||
pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void {
|
pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void {
|
||||||
@@ -45,10 +45,10 @@ const ResourceStreamingResult = struct {
|
|||||||
try jw.writer.writeByte('"');
|
try jw.writer.writeByte('"');
|
||||||
var escaped = protocol.JsonEscapingWriter.init(jw.writer);
|
var escaped = protocol.JsonEscapingWriter.init(jw.writer);
|
||||||
switch (self.format) {
|
switch (self.format) {
|
||||||
.html => lp.dump.root(self.server.page.document, .{}, &escaped.writer, self.server.page) catch |err| {
|
.html => lp.dump.root(self.page.document, .{}, &escaped.writer, self.page) catch |err| {
|
||||||
log.err(.mcp, "html dump failed", .{ .err = err });
|
log.err(.mcp, "html dump failed", .{ .err = err });
|
||||||
},
|
},
|
||||||
.markdown => lp.markdown.dump(self.server.page.document.asNode(), .{}, &escaped.writer, self.server.page) catch |err| {
|
.markdown => lp.markdown.dump(self.page.document.asNode(), .{}, &escaped.writer, self.page) catch |err| {
|
||||||
log.err(.mcp, "markdown dump failed", .{ .err = err });
|
log.err(.mcp, "markdown dump failed", .{ .err = err });
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -69,16 +69,21 @@ const resource_map = std.StaticStringMap(ResourceUri).initComptime(.{
|
|||||||
});
|
});
|
||||||
|
|
||||||
pub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
|
pub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
|
||||||
if (req.params == null) {
|
if (req.params == null or req.id == null) {
|
||||||
return server.sendError(req.id.?, .InvalidParams, "Missing params");
|
return server.sendError(req.id orelse .{ .integer = -1 }, .InvalidParams, "Missing params");
|
||||||
}
|
}
|
||||||
|
const req_id = req.id.?;
|
||||||
|
|
||||||
const params = std.json.parseFromValueLeaky(ReadParams, arena, req.params.?, .{ .ignore_unknown_fields = true }) catch {
|
const params = std.json.parseFromValueLeaky(ReadParams, arena, req.params.?, .{ .ignore_unknown_fields = true }) catch {
|
||||||
return server.sendError(req.id.?, .InvalidParams, "Invalid params");
|
return server.sendError(req_id, .InvalidParams, "Invalid params");
|
||||||
};
|
};
|
||||||
|
|
||||||
const uri = resource_map.get(params.uri) orelse {
|
const uri = resource_map.get(params.uri) orelse {
|
||||||
return server.sendError(req.id.?, .InvalidRequest, "Resource not found");
|
return server.sendError(req_id, .InvalidRequest, "Resource not found");
|
||||||
|
};
|
||||||
|
|
||||||
|
const page = server.session.currentPage() orelse {
|
||||||
|
return server.sendError(req_id, .PageNotLoaded, "Page not loaded");
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (uri) {
|
switch (uri) {
|
||||||
@@ -87,20 +92,20 @@ pub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Reque
|
|||||||
.contents = &.{.{
|
.contents = &.{.{
|
||||||
.uri = params.uri,
|
.uri = params.uri,
|
||||||
.mimeType = "text/html",
|
.mimeType = "text/html",
|
||||||
.text = .{ .server = server, .format = .html },
|
.text = .{ .page = page, .format = .html },
|
||||||
}},
|
}},
|
||||||
};
|
};
|
||||||
try server.sendResult(req.id.?, result);
|
try server.sendResult(req_id, result);
|
||||||
},
|
},
|
||||||
.@"mcp://page/markdown" => {
|
.@"mcp://page/markdown" => {
|
||||||
const result: ResourceStreamingResult = .{
|
const result: ResourceStreamingResult = .{
|
||||||
.contents = &.{.{
|
.contents = &.{.{
|
||||||
.uri = params.uri,
|
.uri = params.uri,
|
||||||
.mimeType = "text/markdown",
|
.mimeType = "text/markdown",
|
||||||
.text = .{ .server = server, .format = .markdown },
|
.text = .{ .page = page, .format = .markdown },
|
||||||
}},
|
}},
|
||||||
};
|
};
|
||||||
try server.sendResult(req.id.?, result);
|
try server.sendResult(req_id, result);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const Element = @import("../browser/webapi/Element.zig");
|
|||||||
const Selector = @import("../browser/webapi/selector/Selector.zig");
|
const Selector = @import("../browser/webapi/selector/Selector.zig");
|
||||||
const protocol = @import("protocol.zig");
|
const protocol = @import("protocol.zig");
|
||||||
const Server = @import("Server.zig");
|
const Server = @import("Server.zig");
|
||||||
|
const CDPNode = @import("../cdp/Node.zig");
|
||||||
|
|
||||||
pub const tool_list = [_]protocol.Tool{
|
pub const tool_list = [_]protocol.Tool{
|
||||||
.{
|
.{
|
||||||
@@ -90,17 +91,18 @@ const EvaluateParams = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ToolStreamingText = struct {
|
const ToolStreamingText = struct {
|
||||||
server: *Server,
|
page: *lp.Page,
|
||||||
action: enum { markdown, links, semantic_tree },
|
action: enum { markdown, links, semantic_tree },
|
||||||
arena: std.mem.Allocator,
|
registry: ?*CDPNode.Registry = null,
|
||||||
|
arena: ?std.mem.Allocator = null,
|
||||||
|
|
||||||
pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void {
|
pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void {
|
||||||
switch (self.action) {
|
switch (self.action) {
|
||||||
.markdown => {
|
.markdown => {
|
||||||
try jw.beginWriteRaw();
|
try jw.beginWriteRaw();
|
||||||
try jw.writer.writeByte('"');
|
try jw.writer.writeByte('"');
|
||||||
var escaped = protocol.JsonEscapingWriter.init(jw.writer);
|
var escaped: protocol.JsonEscapingWriter = .init(jw.writer);
|
||||||
lp.markdown.dump(self.server.page.document.asNode(), .{}, &escaped.writer, self.server.page) catch |err| {
|
lp.markdown.dump(self.page.document.asNode(), .{}, &escaped.writer, self.page) catch |err| {
|
||||||
log.err(.mcp, "markdown dump failed", .{ .err = err });
|
log.err(.mcp, "markdown dump failed", .{ .err = err });
|
||||||
};
|
};
|
||||||
try jw.writer.writeByte('"');
|
try jw.writer.writeByte('"');
|
||||||
@@ -109,14 +111,14 @@ const ToolStreamingText = struct {
|
|||||||
.links => {
|
.links => {
|
||||||
try jw.beginWriteRaw();
|
try jw.beginWriteRaw();
|
||||||
try jw.writer.writeByte('"');
|
try jw.writer.writeByte('"');
|
||||||
var escaped = protocol.JsonEscapingWriter.init(jw.writer);
|
var escaped: protocol.JsonEscapingWriter = .init(jw.writer);
|
||||||
const w = &escaped.writer;
|
const w = &escaped.writer;
|
||||||
if (Selector.querySelectorAll(self.server.page.document.asNode(), "a[href]", self.server.page)) |list| {
|
if (Selector.querySelectorAll(self.page.document.asNode(), "a[href]", self.page)) |list| {
|
||||||
defer list.deinit(self.server.page);
|
defer list.deinit(self.page);
|
||||||
var first = true;
|
var first = true;
|
||||||
for (list._nodes) |node| {
|
for (list._nodes) |node| {
|
||||||
if (node.is(Element.Html.Anchor)) |anchor| {
|
if (node.is(Element.Html.Anchor)) |anchor| {
|
||||||
const href = anchor.getHref(self.server.page) catch |err| {
|
const href = anchor.getHref(self.page) catch |err| {
|
||||||
log.err(.mcp, "resolve href failed", .{ .err = err });
|
log.err(.mcp, "resolve href failed", .{ .err = err });
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -138,13 +140,13 @@ const ToolStreamingText = struct {
|
|||||||
// Return the highly compressed Stagehand-style text format for maximum token efficiency
|
// Return the highly compressed Stagehand-style text format for maximum token efficiency
|
||||||
try jw.beginWriteRaw();
|
try jw.beginWriteRaw();
|
||||||
try jw.writer.writeByte('"');
|
try jw.writer.writeByte('"');
|
||||||
var escaped = protocol.JsonEscapingWriter.init(jw.writer);
|
var escaped: protocol.JsonEscapingWriter = .init(jw.writer);
|
||||||
|
|
||||||
const st = lp.SemanticTree{
|
const st = lp.SemanticTree{
|
||||||
.dom_node = self.server.page.document.asNode(),
|
.dom_node = self.page.document.asNode(),
|
||||||
.registry = &self.server.node_registry,
|
.registry = self.registry.?,
|
||||||
.page = self.server.page,
|
.page = self.page,
|
||||||
.arena = self.arena,
|
.arena = self.arena.?,
|
||||||
};
|
};
|
||||||
|
|
||||||
st.textStringify(&escaped.writer) catch |err| {
|
st.textStringify(&escaped.writer) catch |err| {
|
||||||
@@ -177,8 +179,8 @@ const tool_map = std.StaticStringMap(ToolAction).initComptime(.{
|
|||||||
});
|
});
|
||||||
|
|
||||||
pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
|
pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
|
||||||
if (req.params == null) {
|
if (req.params == null or req.id == null) {
|
||||||
return server.sendError(req.id.?, .InvalidParams, "Missing params");
|
return server.sendError(req.id orelse .{ .integer = -1 }, .InvalidParams, "Missing params");
|
||||||
}
|
}
|
||||||
|
|
||||||
const CallParams = struct {
|
const CallParams = struct {
|
||||||
@@ -222,9 +224,12 @@ fn handleMarkdown(server: *Server, arena: std.mem.Allocator, id: std.json.Value,
|
|||||||
}
|
}
|
||||||
} else |_| {}
|
} else |_| {}
|
||||||
}
|
}
|
||||||
|
const page = server.session.currentPage() orelse {
|
||||||
|
return server.sendError(id, .PageNotLoaded, "Page not loaded");
|
||||||
|
};
|
||||||
|
|
||||||
const content = [_]protocol.TextContent(ToolStreamingText){.{
|
const content = [_]protocol.TextContent(ToolStreamingText){.{
|
||||||
.text = .{ .server = server, .action = .markdown, .arena = arena },
|
.text = .{ .page = page, .action = .markdown },
|
||||||
}};
|
}};
|
||||||
try server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content });
|
try server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content });
|
||||||
}
|
}
|
||||||
@@ -240,9 +245,12 @@ fn handleLinks(server: *Server, arena: std.mem.Allocator, id: std.json.Value, ar
|
|||||||
}
|
}
|
||||||
} else |_| {}
|
} else |_| {}
|
||||||
}
|
}
|
||||||
|
const page = server.session.currentPage() orelse {
|
||||||
|
return server.sendError(id, .PageNotLoaded, "Page not loaded");
|
||||||
|
};
|
||||||
|
|
||||||
const content = [_]protocol.TextContent(ToolStreamingText){.{
|
const content = [_]protocol.TextContent(ToolStreamingText){.{
|
||||||
.text = .{ .server = server, .action = .links, .arena = arena },
|
.text = .{ .page = page, .action = .links },
|
||||||
}};
|
}};
|
||||||
try server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content });
|
try server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content });
|
||||||
}
|
}
|
||||||
@@ -258,9 +266,12 @@ fn handleSemanticTree(server: *Server, arena: std.mem.Allocator, id: std.json.Va
|
|||||||
}
|
}
|
||||||
} else |_| {}
|
} else |_| {}
|
||||||
}
|
}
|
||||||
|
const page = server.session.currentPage() orelse {
|
||||||
|
return server.sendError(id, .PageNotLoaded, "Page not loaded");
|
||||||
|
};
|
||||||
|
|
||||||
const content = [_]protocol.TextContent(ToolStreamingText){.{
|
const content = [_]protocol.TextContent(ToolStreamingText){.{
|
||||||
.text = .{ .server = server, .action = .semantic_tree, .arena = arena },
|
.text = .{ .page = page, .action = .semantic_tree, .registry = &server.node_registry, .arena = arena },
|
||||||
}};
|
}};
|
||||||
try server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content });
|
try server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content });
|
||||||
}
|
}
|
||||||
@@ -271,9 +282,12 @@ fn handleEvaluate(server: *Server, arena: std.mem.Allocator, id: std.json.Value,
|
|||||||
if (args.url) |url| {
|
if (args.url) |url| {
|
||||||
try performGoto(server, url, id);
|
try performGoto(server, url, id);
|
||||||
}
|
}
|
||||||
|
const page = server.session.currentPage() orelse {
|
||||||
|
return server.sendError(id, .PageNotLoaded, "Page not loaded");
|
||||||
|
};
|
||||||
|
|
||||||
var ls: js.Local.Scope = undefined;
|
var ls: js.Local.Scope = undefined;
|
||||||
server.page.js.localScope(&ls);
|
page.js.localScope(&ls);
|
||||||
defer ls.deinit();
|
defer ls.deinit();
|
||||||
|
|
||||||
var try_catch: js.TryCatch = undefined;
|
var try_catch: js.TryCatch = undefined;
|
||||||
@@ -308,7 +322,12 @@ fn parseArguments(comptime T: type, arena: std.mem.Allocator, arguments: ?std.js
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn performGoto(server: *Server, url: [:0]const u8, id: std.json.Value) !void {
|
fn performGoto(server: *Server, url: [:0]const u8, id: std.json.Value) !void {
|
||||||
_ = server.page.navigate(url, .{
|
const session = server.session;
|
||||||
|
if (session.page != null) {
|
||||||
|
session.removePage();
|
||||||
|
}
|
||||||
|
const page = try session.createPage();
|
||||||
|
page.navigate(url, .{
|
||||||
.reason = .address_bar,
|
.reason = .address_bar,
|
||||||
.kind = .{ .push = null },
|
.kind = .{ .push = null },
|
||||||
}) catch {
|
}) catch {
|
||||||
@@ -332,6 +351,7 @@ test "MCP - evaluate error reporting" {
|
|||||||
|
|
||||||
var server = try Server.init(allocator, app, &out_alloc.writer);
|
var server = try Server.init(allocator, app, &out_alloc.writer);
|
||||||
defer server.deinit();
|
defer server.deinit();
|
||||||
|
_ = try server.session.createPage();
|
||||||
|
|
||||||
const aa = testing.arena_allocator;
|
const aa = testing.arena_allocator;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user