mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-17 00:38:59 +00:00
Merge branch 'main' into unified_intrusive_events
This commit is contained in:
@@ -13,8 +13,8 @@
|
|||||||
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
|
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
|
||||||
},
|
},
|
||||||
.v8 = .{
|
.v8 = .{
|
||||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/5d46f159ca44535cfb4fccd9d46f719eb7eac5fc.tar.gz",
|
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/363e2899e6d782ad999edbfae048228871230467.tar.gz",
|
||||||
.hash = "v8-0.0.0-xddH66zuIADu8FcQx2kkczC0yhqBY7LoA08-GRWF_zMA",
|
.hash = "v8-0.0.0-xddH6wHzIAARDy1uFvPqqBpTXzhlnEGDTuX9IAUQz3oU",
|
||||||
},
|
},
|
||||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||||
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },
|
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const std = @import("std");
|
|||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
const Dump = @import("dump.zig");
|
const Dump = @import("dump.zig");
|
||||||
const Mime = @import("mime.zig").Mime;
|
const Mime = @import("mime.zig").Mime;
|
||||||
@@ -53,13 +54,10 @@ pub const user_agent = "Lightpanda/1.0";
|
|||||||
pub const Browser = struct {
|
pub const Browser = struct {
|
||||||
env: *Env,
|
env: *Env,
|
||||||
app: *App,
|
app: *App,
|
||||||
session: ?*Session,
|
session: ?Session,
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
http_client: *http.Client,
|
http_client: *http.Client,
|
||||||
session_pool: SessionPool,
|
page_arena: ArenaAllocator,
|
||||||
page_arena: std.heap.ArenaAllocator,
|
|
||||||
|
|
||||||
const SessionPool = std.heap.MemoryPool(Session);
|
|
||||||
|
|
||||||
pub fn init(app: *App) !Browser {
|
pub fn init(app: *App) !Browser {
|
||||||
const allocator = app.allocator;
|
const allocator = app.allocator;
|
||||||
@@ -75,31 +73,27 @@ pub const Browser = struct {
|
|||||||
.session = null,
|
.session = null,
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.http_client = &app.http_client,
|
.http_client = &app.http_client,
|
||||||
.session_pool = SessionPool.init(allocator),
|
.page_arena = ArenaAllocator.init(allocator),
|
||||||
.page_arena = std.heap.ArenaAllocator.init(allocator),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Browser) void {
|
pub fn deinit(self: *Browser) void {
|
||||||
self.closeSession();
|
self.closeSession();
|
||||||
self.env.deinit();
|
self.env.deinit();
|
||||||
self.session_pool.deinit();
|
|
||||||
self.page_arena.deinit();
|
self.page_arena.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn newSession(self: *Browser, ctx: anytype) !*Session {
|
pub fn newSession(self: *Browser, ctx: anytype) !*Session {
|
||||||
self.closeSession();
|
self.closeSession();
|
||||||
|
self.session = @as(Session, undefined);
|
||||||
const session = try self.session_pool.create();
|
const session = &self.session.?;
|
||||||
try Session.init(session, self, ctx);
|
try Session.init(session, self, ctx);
|
||||||
self.session = session;
|
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn closeSession(self: *Browser) void {
|
pub fn closeSession(self: *Browser) void {
|
||||||
if (self.session) |session| {
|
if (self.session) |*session| {
|
||||||
session.deinit();
|
session.deinit();
|
||||||
self.session_pool.destroy(session);
|
|
||||||
self.session = null;
|
self.session = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,33 +108,16 @@ pub const Browser = struct {
|
|||||||
// You can create successively multiple pages for a session, but you must
|
// You can create successively multiple pages for a session, but you must
|
||||||
// deinit a page before running another one.
|
// deinit a page before running another one.
|
||||||
pub const Session = struct {
|
pub const Session = struct {
|
||||||
state: SessionState,
|
|
||||||
executor: *Env.Executor,
|
|
||||||
inspector: Env.Inspector,
|
|
||||||
|
|
||||||
app: *App,
|
|
||||||
browser: *Browser,
|
browser: *Browser,
|
||||||
|
|
||||||
// The arena is used only to bound the js env init b/c it leaks memory.
|
// Used to create our Inspector and in the BrowserContext.
|
||||||
// see https://github.com/lightpanda-io/jsruntime-lib/issues/181
|
arena: ArenaAllocator,
|
||||||
//
|
|
||||||
// The arena is initialised with self.alloc allocator.
|
|
||||||
// all others Session deps use directly self.alloc and not the arena.
|
|
||||||
// The arena is also used in the BrowserContext
|
|
||||||
arena: std.heap.ArenaAllocator,
|
|
||||||
|
|
||||||
window: Window,
|
executor: Env.Executor,
|
||||||
|
|
||||||
// TODO move the shed/jar to the browser?
|
|
||||||
storage_shed: storage.Shed,
|
storage_shed: storage.Shed,
|
||||||
cookie_jar: storage.CookieJar,
|
cookie_jar: storage.CookieJar,
|
||||||
|
|
||||||
// arbitrary that we pass to the inspector, which the inspector will include
|
|
||||||
// in any response/event that it emits.
|
|
||||||
aux_data: ?[]const u8 = null,
|
|
||||||
|
|
||||||
page: ?Page = null,
|
page: ?Page = null,
|
||||||
http_client: *http.Client,
|
|
||||||
|
|
||||||
// recipient of notification, passed as the first parameter to notify
|
// recipient of notification, passed as the first parameter to notify
|
||||||
notify_ctx: *anyopaque,
|
notify_ctx: *anyopaque,
|
||||||
@@ -159,109 +136,45 @@ pub const Session = struct {
|
|||||||
// need to play a little game.
|
// need to play a little game.
|
||||||
const any_ctx: *anyopaque = if (@TypeOf(ctx) == void) @constCast(@ptrCast(&{})) else ctx;
|
const any_ctx: *anyopaque = if (@TypeOf(ctx) == void) @constCast(@ptrCast(&{})) else ctx;
|
||||||
|
|
||||||
const app = browser.app;
|
var executor = try browser.env.newExecutor();
|
||||||
const allocator = app.allocator;
|
errdefer executor.deinit();
|
||||||
|
|
||||||
|
const allocator = browser.app.allocator;
|
||||||
self.* = .{
|
self.* = .{
|
||||||
.app = app,
|
|
||||||
.aux_data = null,
|
|
||||||
.browser = browser,
|
.browser = browser,
|
||||||
|
.executor = executor,
|
||||||
.notify_ctx = any_ctx,
|
.notify_ctx = any_ctx,
|
||||||
.inspector = undefined,
|
|
||||||
.notify_func = ContextStruct.notify,
|
.notify_func = ContextStruct.notify,
|
||||||
.http_client = browser.http_client,
|
.arena = ArenaAllocator.init(allocator),
|
||||||
.executor = undefined,
|
|
||||||
.storage_shed = storage.Shed.init(allocator),
|
.storage_shed = storage.Shed.init(allocator),
|
||||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
|
||||||
.cookie_jar = storage.CookieJar.init(allocator),
|
.cookie_jar = storage.CookieJar.init(allocator),
|
||||||
.window = Window.create(null, .{ .agent = user_agent }),
|
|
||||||
.state = .{
|
|
||||||
.loop = app.loop,
|
|
||||||
.document = null,
|
|
||||||
.http_client = browser.http_client,
|
|
||||||
|
|
||||||
// we'll set this immediately after
|
|
||||||
.cookie_jar = undefined,
|
|
||||||
|
|
||||||
// nothing should be used on the state until we have a page
|
|
||||||
// at which point we'll set these fields
|
|
||||||
.renderer = undefined,
|
|
||||||
.url = undefined,
|
|
||||||
.arena = undefined,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
self.state.cookie_jar = &self.cookie_jar;
|
|
||||||
errdefer self.arena.deinit();
|
|
||||||
|
|
||||||
self.executor = try browser.env.startExecutor(Window, &self.state, self, .main);
|
|
||||||
errdefer browser.env.stopExecutor(self.executor);
|
|
||||||
self.inspector = try Env.Inspector.init(self.arena.allocator(), self.executor, ctx);
|
|
||||||
|
|
||||||
self.microtaskLoop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deinit(self: *Session) void {
|
fn deinit(self: *Session) void {
|
||||||
self.app.loop.resetZig();
|
|
||||||
if (self.page != null) {
|
if (self.page != null) {
|
||||||
self.removePage();
|
self.removePage();
|
||||||
}
|
}
|
||||||
self.inspector.deinit();
|
|
||||||
self.arena.deinit();
|
self.arena.deinit();
|
||||||
self.cookie_jar.deinit();
|
self.cookie_jar.deinit();
|
||||||
self.storage_shed.deinit();
|
self.storage_shed.deinit();
|
||||||
self.browser.env.stopExecutor(self.executor);
|
self.executor.deinit();
|
||||||
}
|
|
||||||
|
|
||||||
fn microtaskLoop(self: *Session) void {
|
|
||||||
self.browser.runMicrotasks();
|
|
||||||
self.app.loop.zigTimeout(1 * std.time.ns_per_ms, *Session, self, microtaskLoop);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 {
|
|
||||||
const self: *Session = @ptrCast(@alignCast(ctx));
|
|
||||||
const page = &(self.page orelse return error.NoPage);
|
|
||||||
|
|
||||||
log.debug("fetch module: specifier: {s}", .{specifier});
|
|
||||||
// fetchModule is called within the context of processing a page.
|
|
||||||
// Use the page_arena for this, which has a more appropriate lifetime
|
|
||||||
// and which has more retained memory between sessions and pages.
|
|
||||||
const arena = self.browser.page_arena.allocator();
|
|
||||||
return try page.fetchData(
|
|
||||||
arena,
|
|
||||||
specifier,
|
|
||||||
if (page.current_script) |s| s.src else null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn callInspector(self: *const Session, msg: []const u8) void {
|
|
||||||
self.inspector.send(msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: the caller is not the owner of the returned value,
|
// NOTE: the caller is not the owner of the returned value,
|
||||||
// the pointer on Page is just returned as a convenience
|
// the pointer on Page is just returned as a convenience
|
||||||
pub fn createPage(self: *Session, aux_data: ?[]const u8) !*Page {
|
pub fn createPage(self: *Session) !*Page {
|
||||||
std.debug.assert(self.page == null);
|
std.debug.assert(self.page == null);
|
||||||
|
|
||||||
_ = self.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
const page_arena = &self.browser.page_arena;
|
||||||
|
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||||
|
|
||||||
self.page = Page.init(self);
|
self.page = @as(Page, undefined);
|
||||||
const page = &self.page.?;
|
const page = &self.page.?;
|
||||||
|
try Page.init(page, page_arena.allocator(), self);
|
||||||
|
|
||||||
// start JS env
|
// start JS env
|
||||||
log.debug("start new js scope", .{});
|
log.debug("start new js scope", .{});
|
||||||
self.state.arena = self.browser.page_arena.allocator();
|
|
||||||
errdefer self.state.arena = undefined;
|
|
||||||
|
|
||||||
try self.executor.startScope(&self.window);
|
|
||||||
|
|
||||||
// load polyfills
|
|
||||||
try polyfill.load(self.arena.allocator(), self.executor);
|
|
||||||
|
|
||||||
if (aux_data) |ad| {
|
|
||||||
self.aux_data = try self.arena.allocator().dupe(u8, ad);
|
|
||||||
}
|
|
||||||
|
|
||||||
// inspector
|
|
||||||
self.contextCreated(page);
|
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
@@ -269,20 +182,13 @@ pub const Session = struct {
|
|||||||
pub fn removePage(self: *Session) void {
|
pub fn removePage(self: *Session) void {
|
||||||
std.debug.assert(self.page != null);
|
std.debug.assert(self.page != null);
|
||||||
// Reset all existing callbacks.
|
// Reset all existing callbacks.
|
||||||
self.app.loop.resetJS();
|
self.browser.app.loop.resetJS();
|
||||||
|
self.browser.app.loop.resetZig();
|
||||||
self.executor.endScope();
|
self.executor.endScope();
|
||||||
|
self.page = null;
|
||||||
// TODO unload document: https://html.spec.whatwg.org/#unloading-documents
|
|
||||||
|
|
||||||
self.window.replaceLocation(.{ .url = null }) catch |e| {
|
|
||||||
log.err("reset window location: {any}", .{e});
|
|
||||||
};
|
|
||||||
|
|
||||||
// clear netsurf memory arena.
|
// clear netsurf memory arena.
|
||||||
parser.deinit();
|
parser.deinit();
|
||||||
self.state.arena = undefined;
|
|
||||||
|
|
||||||
self.page = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn currentPage(self: *Session) ?*Page {
|
pub fn currentPage(self: *Session) ?*Page {
|
||||||
@@ -299,20 +205,15 @@ pub const Session = struct {
|
|||||||
// look like a leak if we navigate from page to page a lot.
|
// look like a leak if we navigate from page to page a lot.
|
||||||
var buf: [1024]u8 = undefined;
|
var buf: [1024]u8 = undefined;
|
||||||
var fba = std.heap.FixedBufferAllocator.init(&buf);
|
var fba = std.heap.FixedBufferAllocator.init(&buf);
|
||||||
const url = try self.page.?.url.?.resolve(fba.allocator(), url_string);
|
const url = try self.page.?.url.resolve(fba.allocator(), url_string);
|
||||||
|
|
||||||
self.removePage();
|
self.removePage();
|
||||||
var page = try self.createPage(null);
|
var page = try self.createPage();
|
||||||
return page.navigate(url, .{
|
return page.navigate(url, .{
|
||||||
.reason = .anchor,
|
.reason = .anchor,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn contextCreated(self: *Session, page: *Page) void {
|
|
||||||
log.debug("inspector context created", .{});
|
|
||||||
self.inspector.contextCreated(self.executor, "", (page.origin() catch "://") orelse "://", self.aux_data, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn notify(self: *const Session, notification: *const Notification) void {
|
fn notify(self: *const Session, notification: *const Notification) void {
|
||||||
self.notify_func(self.notify_ctx, notification) catch |err| {
|
self.notify_func(self.notify_ctx, notification) catch |err| {
|
||||||
log.err("notify {}: {}", .{ std.meta.activeTag(notification.*), err });
|
log.err("notify {}: {}", .{ std.meta.activeTag(notification.*), err });
|
||||||
@@ -326,31 +227,67 @@ pub const Session = struct {
|
|||||||
// The page handle all its memory in an arena allocator. The arena is reseted
|
// The page handle all its memory in an arena allocator. The arena is reseted
|
||||||
// when end() is called.
|
// when end() is called.
|
||||||
pub const Page = struct {
|
pub const Page = struct {
|
||||||
arena: Allocator,
|
|
||||||
session: *Session,
|
session: *Session,
|
||||||
doc: ?*parser.Document = null,
|
|
||||||
|
// an arena with a lifetime for the entire duration of the page
|
||||||
|
arena: Allocator,
|
||||||
|
|
||||||
|
// Gets injected into any WebAPI method that needs it
|
||||||
|
state: SessionState,
|
||||||
|
|
||||||
|
// Serves are the root object of our JavaScript environment
|
||||||
|
window: Window,
|
||||||
|
|
||||||
|
doc: ?*parser.Document,
|
||||||
|
|
||||||
// The URL of the page
|
// The URL of the page
|
||||||
url: ?URL = null,
|
url: URL,
|
||||||
|
|
||||||
raw_data: ?[]const u8 = null,
|
raw_data: ?[]const u8,
|
||||||
|
|
||||||
// current_script is the script currently evaluated by the page.
|
|
||||||
// current_script could by fetch module to resolve module's url to fetch.
|
|
||||||
current_script: ?*const Script = null,
|
|
||||||
|
|
||||||
renderer: FlatRenderer,
|
renderer: FlatRenderer,
|
||||||
|
|
||||||
window_clicked_event_node: parser.EventNode,
|
window_clicked_event_node: parser.EventNode,
|
||||||
|
|
||||||
fn init(session: *Session) Page {
|
scope: *Env.Scope,
|
||||||
const arena = session.browser.page_arena.allocator();
|
|
||||||
return .{
|
// current_script is the script currently evaluated by the page.
|
||||||
|
// current_script could by fetch module to resolve module's url to fetch.
|
||||||
|
current_script: ?*const Script = null,
|
||||||
|
|
||||||
|
fn init(self: *Page, arena: Allocator, session: *Session) !void {
|
||||||
|
const browser = session.browser;
|
||||||
|
self.* = .{
|
||||||
|
.window = .{},
|
||||||
.arena = arena,
|
.arena = arena,
|
||||||
|
.doc = null,
|
||||||
|
.raw_data = null,
|
||||||
|
.url = URL.empty,
|
||||||
.session = session,
|
.session = session,
|
||||||
.renderer = FlatRenderer.init(arena),
|
.renderer = FlatRenderer.init(arena),
|
||||||
.window_clicked_event_node = .{ .func = windowClicked },
|
.window_clicked_event_node = .{ .func = windowClicked },
|
||||||
|
.state = .{
|
||||||
|
.arena = arena,
|
||||||
|
.document = null,
|
||||||
|
.url = &self.url,
|
||||||
|
.renderer = &self.renderer,
|
||||||
|
.loop = browser.app.loop,
|
||||||
|
.cookie_jar = &session.cookie_jar,
|
||||||
|
.http_client = browser.http_client,
|
||||||
|
},
|
||||||
|
.scope = try session.executor.startScope(&self.window, &self.state, self, true),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// load polyfills
|
||||||
|
try polyfill.load(self.arena, self.scope);
|
||||||
|
|
||||||
|
self.microtaskLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn microtaskLoop(self: *Page) void {
|
||||||
|
const browser = self.session.browser;
|
||||||
|
browser.runMicrotasks();
|
||||||
|
browser.app.loop.zigTimeout(1 * std.time.ns_per_ms, *Page, self, microtaskLoop);
|
||||||
}
|
}
|
||||||
|
|
||||||
// dump writes the page content into the given file.
|
// dump writes the page content into the given file.
|
||||||
@@ -366,13 +303,23 @@ pub const Page = struct {
|
|||||||
try Dump.writeHTML(self.doc.?, out);
|
try Dump.writeHTML(self.doc.?, out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 {
|
||||||
|
const self: *Page = @ptrCast(@alignCast(ctx));
|
||||||
|
|
||||||
|
log.debug("fetch module: specifier: {s}", .{specifier});
|
||||||
|
return try self.fetchData(
|
||||||
|
specifier,
|
||||||
|
if (self.current_script) |s| s.src else null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn wait(self: *Page) !void {
|
pub fn wait(self: *Page) !void {
|
||||||
// try catch
|
// try catch
|
||||||
var try_catch: Env.TryCatch = undefined;
|
var try_catch: Env.TryCatch = undefined;
|
||||||
try_catch.init(self.session.executor);
|
try_catch.init(self.scope);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
self.session.app.loop.run() catch |err| {
|
self.session.browser.app.loop.run() catch |err| {
|
||||||
if (try try_catch.err(self.arena)) |msg| {
|
if (try try_catch.err(self.arena)) |msg| {
|
||||||
log.info("wait error: {s}", .{msg});
|
log.info("wait error: {s}", .{msg});
|
||||||
return;
|
return;
|
||||||
@@ -383,16 +330,13 @@ pub const Page = struct {
|
|||||||
log.debug("wait: OK", .{});
|
log.debug("wait: OK", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn origin(self: *const Page) !?[]const u8 {
|
pub fn origin(self: *const Page, arena: Allocator) ![]const u8 {
|
||||||
const url = &(self.url orelse return null);
|
|
||||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||||
try url.origin(arr.writer(self.arena));
|
try self.url.origin(arr.writer(arena));
|
||||||
return arr.items;
|
return arr.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
|
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
|
||||||
// - aux_data: extra data forwarded to the Inspector
|
|
||||||
// see Inspector.contextCreated
|
|
||||||
pub fn navigate(self: *Page, request_url: URL, opts: NavigateOpts) !void {
|
pub fn navigate(self: *Page, request_url: URL, opts: NavigateOpts) !void {
|
||||||
const arena = self.arena;
|
const arena = self.arena;
|
||||||
const session = self.session;
|
const session = self.session;
|
||||||
@@ -408,19 +352,18 @@ pub const Page = struct {
|
|||||||
// later in this function, with the final request url (since we might
|
// later in this function, with the final request url (since we might
|
||||||
// redirect)
|
// redirect)
|
||||||
self.url = request_url;
|
self.url = request_url;
|
||||||
var url = &self.url.?;
|
|
||||||
|
|
||||||
session.app.telemetry.record(.{ .navigate = .{
|
session.browser.app.telemetry.record(.{ .navigate = .{
|
||||||
.proxy = false,
|
.proxy = false,
|
||||||
.tls = std.ascii.eqlIgnoreCase(url.scheme(), "https"),
|
.tls = std.ascii.eqlIgnoreCase(request_url.scheme(), "https"),
|
||||||
} });
|
} });
|
||||||
|
|
||||||
// load the data
|
// load the data
|
||||||
var request = try self.newHTTPRequest(.GET, url, .{ .navigation = true });
|
var request = try self.newHTTPRequest(.GET, &self.url, .{ .navigation = true });
|
||||||
defer request.deinit();
|
defer request.deinit();
|
||||||
|
|
||||||
session.notify(&.{ .page_navigate = .{
|
session.notify(&.{ .page_navigate = .{
|
||||||
.url = url,
|
.url = &self.url,
|
||||||
.reason = opts.reason,
|
.reason = opts.reason,
|
||||||
.timestamp = timestamp(),
|
.timestamp = timestamp(),
|
||||||
} });
|
} });
|
||||||
@@ -429,15 +372,14 @@ pub const Page = struct {
|
|||||||
|
|
||||||
// would be different than self.url in the case of a redirect
|
// would be different than self.url in the case of a redirect
|
||||||
self.url = try URL.fromURI(arena, request.uri);
|
self.url = try URL.fromURI(arena, request.uri);
|
||||||
url = &self.url.?;
|
|
||||||
|
|
||||||
const header = response.header;
|
const header = response.header;
|
||||||
try session.cookie_jar.populateFromResponse(&url.uri, &header);
|
try session.cookie_jar.populateFromResponse(&self.url.uri, &header);
|
||||||
|
|
||||||
// TODO handle fragment in url.
|
// TODO handle fragment in url.
|
||||||
try session.window.replaceLocation(.{ .url = try url.toWebApi(arena) });
|
try self.window.replaceLocation(.{ .url = try self.url.toWebApi(arena) });
|
||||||
|
|
||||||
log.info("GET {any} {d}", .{ url, header.status });
|
log.info("GET {any} {d}", .{ self.url, header.status });
|
||||||
|
|
||||||
const content_type = header.get("content-type");
|
const content_type = header.get("content-type");
|
||||||
|
|
||||||
@@ -461,7 +403,7 @@ pub const Page = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
session.notify(&.{ .page_navigated = .{
|
session.notify(&.{ .page_navigated = .{
|
||||||
.url = url,
|
.url = &self.url,
|
||||||
.timestamp = timestamp(),
|
.timestamp = timestamp(),
|
||||||
} });
|
} });
|
||||||
}
|
}
|
||||||
@@ -495,27 +437,18 @@ pub const Page = struct {
|
|||||||
// https://html.spec.whatwg.org/#reporting-document-loading-status
|
// https://html.spec.whatwg.org/#reporting-document-loading-status
|
||||||
|
|
||||||
// inject the URL to the document including the fragment.
|
// inject the URL to the document including the fragment.
|
||||||
try parser.documentSetDocumentURI(doc, self.url.?.raw);
|
try parser.documentSetDocumentURI(doc, self.url.raw);
|
||||||
|
|
||||||
const session = self.session;
|
|
||||||
// TODO set the referrer to the document.
|
// TODO set the referrer to the document.
|
||||||
try session.window.replaceDocument(html_doc);
|
try self.window.replaceDocument(html_doc);
|
||||||
session.window.setStorageShelf(
|
self.window.setStorageShelf(
|
||||||
try session.storage_shed.getOrPut((try self.origin()) orelse "null"),
|
try self.session.storage_shed.getOrPut(try self.origin(self.arena)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/#read-html
|
// https://html.spec.whatwg.org/#read-html
|
||||||
|
|
||||||
// inspector
|
// update the sessions state
|
||||||
session.contextCreated(self);
|
self.state.document = html_doc;
|
||||||
|
|
||||||
{
|
|
||||||
// update the sessions state
|
|
||||||
const state = &session.state;
|
|
||||||
state.url = &self.url.?;
|
|
||||||
state.document = html_doc;
|
|
||||||
state.renderer = &self.renderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// browse the DOM tree to retrieve scripts
|
// browse the DOM tree to retrieve scripts
|
||||||
// TODO execute the synchronous scripts during the HTL parsing.
|
// TODO execute the synchronous scripts during the HTL parsing.
|
||||||
@@ -613,7 +546,7 @@ pub const Page = struct {
|
|||||||
|
|
||||||
try parser.eventInit(loadevt, "load", .{});
|
try parser.eventInit(loadevt, "load", .{});
|
||||||
_ = try parser.eventTargetDispatchEvent(
|
_ = try parser.eventTargetDispatchEvent(
|
||||||
parser.toEventTarget(Window, &self.session.window),
|
parser.toEventTarget(Window, &self.window),
|
||||||
loadevt,
|
loadevt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -652,7 +585,7 @@ pub const Page = struct {
|
|||||||
// TODO handle charset attribute
|
// TODO handle charset attribute
|
||||||
const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element));
|
const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element));
|
||||||
if (opt_text) |text| {
|
if (opt_text) |text| {
|
||||||
try s.eval(self.arena, self.session, text);
|
try s.eval(self, text);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,9 +604,10 @@ pub const Page = struct {
|
|||||||
// It resolves src using the page's uri.
|
// It resolves src using the page's uri.
|
||||||
// If a base path is given, src is resolved according to the base first.
|
// If a base path is given, src is resolved according to the base first.
|
||||||
// the caller owns the returned string
|
// the caller owns the returned string
|
||||||
fn fetchData(self: *const Page, arena: Allocator, src: []const u8, base: ?[]const u8) ![]const u8 {
|
fn fetchData(self: *const Page, src: []const u8, base: ?[]const u8) ![]const u8 {
|
||||||
log.debug("starting fetch {s}", .{src});
|
log.debug("starting fetch {s}", .{src});
|
||||||
|
|
||||||
|
const arena = self.arena;
|
||||||
var res_src = src;
|
var res_src = src;
|
||||||
|
|
||||||
// if a base path is given, we resolve src using base.
|
// if a base path is given, we resolve src using base.
|
||||||
@@ -683,7 +617,7 @@ pub const Page = struct {
|
|||||||
res_src = try std.fs.path.resolve(arena, &.{ _dir, src });
|
res_src = try std.fs.path.resolve(arena, &.{ _dir, src });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var origin_url = &self.url.?;
|
var origin_url = &self.url;
|
||||||
const url = try origin_url.resolve(arena, res_src);
|
const url = try origin_url.resolve(arena, res_src);
|
||||||
|
|
||||||
var request = try self.newHTTPRequest(.GET, &url, .{
|
var request = try self.newHTTPRequest(.GET, &url, .{
|
||||||
@@ -717,19 +651,17 @@ pub const Page = struct {
|
|||||||
return arr.items;
|
return arr.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetchScript(self: *const Page, s: *const Script) !void {
|
fn fetchScript(self: *Page, s: *const Script) !void {
|
||||||
const arena = self.arena;
|
const body = try self.fetchData(s.src, null);
|
||||||
const body = try self.fetchData(arena, s.src, null);
|
try s.eval(self, body);
|
||||||
try s.eval(arena, self.session, body);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn newHTTPRequest(self: *const Page, method: http.Request.Method, url: *const URL, opts: storage.cookie.LookupOpts) !http.Request {
|
fn newHTTPRequest(self: *const Page, method: http.Request.Method, url: *const URL, opts: storage.cookie.LookupOpts) !http.Request {
|
||||||
const session = self.session;
|
var request = try self.state.http_client.request(method, &url.uri);
|
||||||
var request = try session.http_client.request(method, &url.uri);
|
|
||||||
errdefer request.deinit();
|
errdefer request.deinit();
|
||||||
|
|
||||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||||
try session.cookie_jar.forRequest(&url.uri, arr.writer(self.arena), opts);
|
try self.state.cookie_jar.forRequest(&url.uri, arr.writer(self.arena), opts);
|
||||||
|
|
||||||
if (arr.items.len > 0) {
|
if (arr.items.len > 0) {
|
||||||
try request.addHeader("Cookie", arr.items, .{});
|
try request.addHeader("Cookie", arr.items, .{});
|
||||||
@@ -833,24 +765,24 @@ pub const Page = struct {
|
|||||||
return .unknown;
|
return .unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eval(self: Script, arena: Allocator, session: *Session, body: []const u8) !void {
|
fn eval(self: Script, page: *Page, body: []const u8) !void {
|
||||||
var try_catch: Env.TryCatch = undefined;
|
var try_catch: Env.TryCatch = undefined;
|
||||||
try_catch.init(session.executor);
|
try_catch.init(page.scope);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
const res = switch (self.kind) {
|
const res = switch (self.kind) {
|
||||||
.unknown => return error.UnknownScript,
|
.unknown => return error.UnknownScript,
|
||||||
.javascript => session.executor.exec(body, self.src),
|
.javascript => page.scope.exec(body, self.src),
|
||||||
.module => session.executor.module(body, self.src),
|
.module => page.scope.module(body, self.src),
|
||||||
} catch {
|
} catch {
|
||||||
if (try try_catch.err(arena)) |msg| {
|
if (try try_catch.err(page.arena)) |msg| {
|
||||||
log.info("eval script {s}: {s}", .{ self.src, msg });
|
log.info("eval script {s}: {s}", .{ self.src, msg });
|
||||||
}
|
}
|
||||||
return FetchError.JsErr;
|
return FetchError.JsErr;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (builtin.mode == .Debug) {
|
if (builtin.mode == .Debug) {
|
||||||
const msg = try res.toString(arena);
|
const msg = try res.toString(page.arena);
|
||||||
log.debug("eval script {s}: {s}", .{ self.src, msg });
|
log.debug("eval script {s}: {s}", .{ self.src, msg });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ pub const Element = struct {
|
|||||||
|
|
||||||
pub fn toInterface(e: *parser.Element) !Union {
|
pub fn toInterface(e: *parser.Element) !Union {
|
||||||
return try HTMLElem.toInterface(Union, e);
|
return try HTMLElem.toInterface(Union, e);
|
||||||
|
// SVGElement and MathML are not supported yet.
|
||||||
}
|
}
|
||||||
|
|
||||||
// JS funcs
|
// JS funcs
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ pub const MutationObserver = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn jsScopeEnd(self: *MutationObserver, _: anytype) void {
|
pub fn jsCallScopeEnd(self: *MutationObserver, _: anytype) void {
|
||||||
const record = self.observed.items;
|
const record = self.observed.items;
|
||||||
if (record.len == 0) {
|
if (record.len == 0) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ pub const JsThis = Env.JsThis;
|
|||||||
pub const JsObject = Env.JsObject;
|
pub const JsObject = Env.JsObject;
|
||||||
pub const Callback = Env.Callback;
|
pub const Callback = Env.Callback;
|
||||||
pub const Env = js.Env(*SessionState, Interfaces{});
|
pub const Env = js.Env(*SessionState, Interfaces{});
|
||||||
|
pub const Global = @import("html/window.zig").Window;
|
||||||
|
|
||||||
pub const SessionState = struct {
|
pub const SessionState = struct {
|
||||||
loop: *Loop,
|
loop: *Loop,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
const HTMLDocument = @import("document.zig").HTMLDocument;
|
const HTMLDocument = @import("document.zig").HTMLDocument;
|
||||||
const HTMLElem = @import("elements.zig");
|
const HTMLElem = @import("elements.zig");
|
||||||
|
const SVGElem = @import("svg_elements.zig");
|
||||||
const Window = @import("window.zig").Window;
|
const Window = @import("window.zig").Window;
|
||||||
const Navigator = @import("navigator.zig").Navigator;
|
const Navigator = @import("navigator.zig").Navigator;
|
||||||
const History = @import("history.zig").History;
|
const History = @import("history.zig").History;
|
||||||
@@ -28,6 +29,7 @@ pub const Interfaces = .{
|
|||||||
HTMLElem.HTMLElement,
|
HTMLElem.HTMLElement,
|
||||||
HTMLElem.HTMLMediaElement,
|
HTMLElem.HTMLMediaElement,
|
||||||
HTMLElem.Interfaces,
|
HTMLElem.Interfaces,
|
||||||
|
SVGElem.SVGElement,
|
||||||
Window,
|
Window,
|
||||||
Navigator,
|
Navigator,
|
||||||
History,
|
History,
|
||||||
|
|||||||
41
src/browser/html/svg_elements.zig
Normal file
41
src/browser/html/svg_elements.zig
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const Element = @import("../dom/element.zig").Element;
|
||||||
|
|
||||||
|
// Support for SVGElements is very limited, this is a dummy implementation.
|
||||||
|
// This is here no to be able to support `element instanceof SVGElement;` in JavaScript.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/SVGElement
|
||||||
|
pub const SVGElement = struct {
|
||||||
|
// Currently the prototype chain is not implemented (will not be returned by toInterface())
|
||||||
|
// For that we need parser.SvgElement and the derived types with tags in the v-table.
|
||||||
|
pub const prototype = *Element;
|
||||||
|
// While this is a Node, could consider not exposing the subtype untill we have
|
||||||
|
// a Self type to cast to.
|
||||||
|
pub const subtype = .node;
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "Browser.HTML.SVGElement" {
|
||||||
|
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||||
|
defer runner.deinit();
|
||||||
|
|
||||||
|
try runner.testCases(&.{
|
||||||
|
.{ "'AString' instanceof SVGElement", "false" },
|
||||||
|
}, .{});
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ test "Browser.fetch" {
|
|||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||||
defer runner.deinit();
|
defer runner.deinit();
|
||||||
|
|
||||||
try @import("polyfill.zig").load(testing.allocator, runner.executor);
|
try @import("polyfill.zig").load(testing.allocator, runner.scope);
|
||||||
|
|
||||||
try runner.testCases(&.{
|
try runner.testCases(&.{
|
||||||
.{
|
.{
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ const modules = [_]struct {
|
|||||||
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
|
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn load(allocator: Allocator, executor: *Env.Executor) !void {
|
pub fn load(allocator: Allocator, scope: *Env.Scope) !void {
|
||||||
var try_catch: Env.TryCatch = undefined;
|
var try_catch: Env.TryCatch = undefined;
|
||||||
try_catch.init(executor);
|
try_catch.init(scope);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
for (modules) |m| {
|
for (modules) |m| {
|
||||||
const res = executor.exec(m.source, m.name) catch |err| {
|
const res = scope.exec(m.source, m.name) catch |err| {
|
||||||
if (try try_catch.err(allocator)) |msg| {
|
if (try try_catch.err(allocator)) |msg| {
|
||||||
defer allocator.free(msg);
|
defer allocator.free(msg);
|
||||||
log.err("load {s}: {s}", .{ m.name, msg });
|
log.err("load {s}: {s}", .{ m.name, msg });
|
||||||
|
|||||||
114
src/cdp/cdp.zig
114
src/cdp/cdp.zig
@@ -25,6 +25,8 @@ const Env = @import("../browser/env.zig").Env;
|
|||||||
const asUint = @import("../str/parser.zig").asUint;
|
const asUint = @import("../str/parser.zig").asUint;
|
||||||
const Browser = @import("../browser/browser.zig").Browser;
|
const Browser = @import("../browser/browser.zig").Browser;
|
||||||
const Session = @import("../browser/browser.zig").Session;
|
const Session = @import("../browser/browser.zig").Session;
|
||||||
|
const Page = @import("../browser/browser.zig").Page;
|
||||||
|
const Inspector = @import("../browser/env.zig").Env.Inspector;
|
||||||
const Incrementing = @import("../id.zig").Incrementing;
|
const Incrementing = @import("../id.zig").Incrementing;
|
||||||
const Notification = @import("../notification.zig").Notification;
|
const Notification = @import("../notification.zig").Notification;
|
||||||
|
|
||||||
@@ -61,9 +63,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
|||||||
session_id_gen: SessionIdGen = .{},
|
session_id_gen: SessionIdGen = .{},
|
||||||
browser_context_id_gen: BrowserContextIdGen = .{},
|
browser_context_id_gen: BrowserContextIdGen = .{},
|
||||||
|
|
||||||
browser_context: ?*BrowserContext(Self),
|
browser_context: ?BrowserContext(Self),
|
||||||
|
|
||||||
browser_context_pool: std.heap.MemoryPool(BrowserContext(Self)),
|
|
||||||
|
|
||||||
// Re-used arena for processing a message. We're assuming that we're getting
|
// Re-used arena for processing a message. We're assuming that we're getting
|
||||||
// 1 message at a time.
|
// 1 message at a time.
|
||||||
@@ -82,17 +82,15 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
|||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.browser_context = null,
|
.browser_context = null,
|
||||||
.message_arena = std.heap.ArenaAllocator.init(allocator),
|
.message_arena = std.heap.ArenaAllocator.init(allocator),
|
||||||
.browser_context_pool = std.heap.MemoryPool(BrowserContext(Self)).init(allocator),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: *Self) void {
|
||||||
if (self.browser_context) |bc| {
|
if (self.browser_context) |*bc| {
|
||||||
bc.deinit();
|
bc.deinit();
|
||||||
}
|
}
|
||||||
self.browser.deinit();
|
self.browser.deinit();
|
||||||
self.message_arena.deinit();
|
self.message_arena.deinit();
|
||||||
self.browser_context_pool.deinit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handleMessage(self: *Self, msg: []const u8) bool {
|
pub fn handleMessage(self: *Self, msg: []const u8) bool {
|
||||||
@@ -126,7 +124,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
|||||||
.cdp = self,
|
.cdp = self,
|
||||||
.arena = arena,
|
.arena = arena,
|
||||||
.sender = sender,
|
.sender = sender,
|
||||||
.browser_context = if (self.browser_context) |bc| bc else null,
|
.browser_context = if (self.browser_context) |*bc| bc else null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// See dispatchStartupCommand for more info on this.
|
// See dispatchStartupCommand for more info on this.
|
||||||
@@ -221,7 +219,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn isValidSessionId(self: *const Self, input_session_id: []const u8) bool {
|
fn isValidSessionId(self: *const Self, input_session_id: []const u8) bool {
|
||||||
const browser_context = self.browser_context orelse return false;
|
const browser_context = &(self.browser_context orelse return false);
|
||||||
const session_id = browser_context.session_id orelse return false;
|
const session_id = browser_context.session_id orelse return false;
|
||||||
return std.mem.eql(u8, session_id, input_session_id);
|
return std.mem.eql(u8, session_id, input_session_id);
|
||||||
}
|
}
|
||||||
@@ -230,24 +228,22 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
|||||||
if (self.browser_context != null) {
|
if (self.browser_context != null) {
|
||||||
return error.AlreadyExists;
|
return error.AlreadyExists;
|
||||||
}
|
}
|
||||||
const browser_context_id = self.browser_context_id_gen.next();
|
const id = self.browser_context_id_gen.next();
|
||||||
|
|
||||||
const browser_context = try self.browser_context_pool.create();
|
self.browser_context = @as(BrowserContext(Self), undefined);
|
||||||
errdefer self.browser_context_pool.destroy(browser_context);
|
const browser_context = &self.browser_context.?;
|
||||||
|
|
||||||
try BrowserContext(Self).init(browser_context, browser_context_id, self);
|
try BrowserContext(Self).init(browser_context, id, self);
|
||||||
self.browser_context = browser_context;
|
return id;
|
||||||
return browser_context_id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disposeBrowserContext(self: *Self, browser_context_id: []const u8) bool {
|
pub fn disposeBrowserContext(self: *Self, browser_context_id: []const u8) bool {
|
||||||
const bc = self.browser_context orelse return false;
|
const bc = &(self.browser_context orelse return false);
|
||||||
if (std.mem.eql(u8, bc.id, browser_context_id) == false) {
|
if (std.mem.eql(u8, bc.id, browser_context_id) == false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
bc.deinit();
|
bc.deinit();
|
||||||
self.browser.closeSession();
|
self.browser.closeSession();
|
||||||
self.browser_context_pool.destroy(bc);
|
|
||||||
self.browser_context = null;
|
self.browser_context = null;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -309,40 +305,51 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
|||||||
node_registry: Node.Registry,
|
node_registry: Node.Registry,
|
||||||
node_search_list: Node.Search.List,
|
node_search_list: Node.Search.List,
|
||||||
|
|
||||||
isolated_world: ?IsolatedWorld(Env),
|
inspector: Inspector,
|
||||||
|
isolated_world: ?IsolatedWorld,
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void {
|
fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void {
|
||||||
const allocator = cdp.allocator;
|
const allocator = cdp.allocator;
|
||||||
|
|
||||||
|
const session = try cdp.browser.newSession(self);
|
||||||
|
const arena = session.arena.allocator();
|
||||||
|
|
||||||
|
const inspector = try cdp.browser.env.newInspector(arena, self);
|
||||||
|
|
||||||
var registry = Node.Registry.init(allocator);
|
var registry = Node.Registry.init(allocator);
|
||||||
errdefer registry.deinit();
|
errdefer registry.deinit();
|
||||||
|
|
||||||
const session = try cdp.browser.newSession(self);
|
|
||||||
self.* = .{
|
self.* = .{
|
||||||
.id = id,
|
.id = id,
|
||||||
.cdp = cdp,
|
.cdp = cdp,
|
||||||
|
.arena = arena,
|
||||||
.target_id = null,
|
.target_id = null,
|
||||||
.session_id = null,
|
.session_id = null,
|
||||||
|
.session = session,
|
||||||
.security_origin = URL_BASE,
|
.security_origin = URL_BASE,
|
||||||
.secure_context_type = "Secure", // TODO = enum
|
.secure_context_type = "Secure", // TODO = enum
|
||||||
.loader_id = LOADER_ID,
|
.loader_id = LOADER_ID,
|
||||||
.session = session,
|
|
||||||
.arena = session.arena.allocator(),
|
|
||||||
.page_life_cycle_events = false, // TODO; Target based value
|
.page_life_cycle_events = false, // TODO; Target based value
|
||||||
.node_registry = registry,
|
.node_registry = registry,
|
||||||
.node_search_list = undefined,
|
.node_search_list = undefined,
|
||||||
.isolated_world = null,
|
.isolated_world = null,
|
||||||
|
.inspector = inspector,
|
||||||
};
|
};
|
||||||
self.node_search_list = Node.Search.List.init(allocator, &self.node_registry);
|
self.node_search_list = Node.Search.List.init(allocator, &self.node_registry);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: *Self) void {
|
||||||
if (self.isolated_world) |isolated_world| {
|
self.inspector.deinit();
|
||||||
isolated_world.executor.endScope();
|
|
||||||
self.cdp.browser.env.stopExecutor(isolated_world.executor);
|
// If the session has a page, we need to clear it first. The page
|
||||||
self.isolated_world = null;
|
// context is always nested inside of the isolated world context,
|
||||||
|
// so we need to shutdown the page one first.
|
||||||
|
self.cdp.browser.closeSession();
|
||||||
|
|
||||||
|
if (self.isolated_world) |*world| {
|
||||||
|
world.deinit();
|
||||||
}
|
}
|
||||||
self.node_registry.deinit();
|
self.node_registry.deinit();
|
||||||
self.node_search_list.deinit();
|
self.node_search_list.deinit();
|
||||||
@@ -353,25 +360,28 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
|||||||
self.node_search_list.reset();
|
self.node_search_list.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn createIsolatedWorld(
|
pub fn createIsolatedWorld(self: *Self, page: *Page) !void {
|
||||||
self: *Self,
|
if (self.isolated_world != null) {
|
||||||
world_name: []const u8,
|
return error.CurrentlyOnly1IsolatedWorldSupported;
|
||||||
grant_universal_access: bool,
|
}
|
||||||
) !void {
|
|
||||||
if (self.isolated_world != null) return error.CurrentlyOnly1IsolatedWorldSupported;
|
|
||||||
|
|
||||||
const executor = try self.cdp.browser.env.startExecutor(@import("../browser/html/window.zig").Window, &self.session.state, self.session, .isolated);
|
var executor = try self.cdp.browser.env.newExecutor();
|
||||||
errdefer self.cdp.browser.env.stopExecutor(executor);
|
errdefer executor.deinit();
|
||||||
|
|
||||||
// TBD should we endScope on removePage and re-startScope on createPage?
|
|
||||||
// Window will be refactored into the executor so we leave it ugly here for now as a reminder.
|
|
||||||
try executor.startScope(@import("../browser/html/window.zig").Window{});
|
|
||||||
|
|
||||||
self.isolated_world = .{
|
self.isolated_world = .{
|
||||||
.name = try self.arena.dupe(u8, world_name),
|
.name = "",
|
||||||
.grant_universal_access = grant_universal_access,
|
.scope = undefined,
|
||||||
.executor = executor,
|
.executor = executor,
|
||||||
|
.grant_universal_access = true,
|
||||||
};
|
};
|
||||||
|
var world = &self.isolated_world.?;
|
||||||
|
|
||||||
|
// The isolate world must share at least some of the state with the related page, specifically the DocumentHTML
|
||||||
|
// (assuming grantUniveralAccess will be set to True!).
|
||||||
|
// We just created the world and the page. The page's state lives in the session, but is update on navigation.
|
||||||
|
// This also means this pointer becomes invalid after removePage untill a new page is created.
|
||||||
|
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
|
||||||
|
world.scope = try world.executor.startScope(&page.window, &page.state, {}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nodeWriter(self: *Self, node: *const Node, opts: Node.Writer.Opts) Node.Writer {
|
pub fn nodeWriter(self: *Self, node: *const Node, opts: Node.Writer.Opts) Node.Writer {
|
||||||
@@ -384,7 +394,8 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
|||||||
|
|
||||||
pub fn getURL(self: *const Self) ?[]const u8 {
|
pub fn getURL(self: *const Self) ?[]const u8 {
|
||||||
const page = self.session.currentPage() orelse return null;
|
const page = self.session.currentPage() orelse return null;
|
||||||
return if (page.url) |*url| url.raw else null;
|
const raw_url = page.url.raw;
|
||||||
|
return if (raw_url.len == 0) null else raw_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn notify(ctx: *anyopaque, notification: *const Notification) !void {
|
pub fn notify(ctx: *anyopaque, notification: *const Notification) !void {
|
||||||
@@ -396,6 +407,12 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn callInspector(self: *const Self, msg: []const u8) void {
|
||||||
|
self.inspector.send(msg);
|
||||||
|
// force running micro tasks after send input to the inspector.
|
||||||
|
self.cdp.browser.runMicrotasks();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
|
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
|
||||||
if (std.log.defaultLogEnabled(.debug)) {
|
if (std.log.defaultLogEnabled(.debug)) {
|
||||||
// msg should be {"id":<id>,...
|
// msg should be {"id":<id>,...
|
||||||
@@ -481,13 +498,16 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
|||||||
/// An isolated world has it's own instance of globals like Window.
|
/// An isolated world has it's own instance of globals like Window.
|
||||||
/// Generally the client needs to resolve a node into the isolated world to be able to work with it.
|
/// Generally the client needs to resolve a node into the isolated world to be able to work with it.
|
||||||
/// An object id is unique across all contexts, different object ids can refer to the same Node in different contexts.
|
/// An object id is unique across all contexts, different object ids can refer to the same Node in different contexts.
|
||||||
pub fn IsolatedWorld(comptime E: type) type {
|
const IsolatedWorld = struct {
|
||||||
return struct {
|
name: []const u8,
|
||||||
name: []const u8,
|
scope: *Env.Scope,
|
||||||
grant_universal_access: bool,
|
executor: Env.Executor,
|
||||||
executor: *E.Executor,
|
grant_universal_access: bool,
|
||||||
};
|
|
||||||
}
|
pub fn deinit(self: *IsolatedWorld) void {
|
||||||
|
self.executor.deinit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// This is a generic because when we send a result we have two different
|
// This is a generic because when we send a result we have two different
|
||||||
// behaviors. Normally, we're sending the result to the client. But in some cases
|
// behaviors. Normally, we're sending the result to the client. But in some cases
|
||||||
@@ -530,7 +550,7 @@ pub fn Command(comptime CDP_T: type, comptime Sender: type) type {
|
|||||||
|
|
||||||
pub fn createBrowserContext(self: *Self) !*BrowserContext(CDP_T) {
|
pub fn createBrowserContext(self: *Self) !*BrowserContext(CDP_T) {
|
||||||
_ = try self.cdp.createBrowserContext();
|
_ = try self.cdp.createBrowserContext();
|
||||||
self.browser_context = self.cdp.browser_context.?;
|
self.browser_context = &(self.cdp.browser_context.?);
|
||||||
return self.browser_context.?;
|
return self.browser_context.?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,24 +127,27 @@ fn resolveNode(cmd: anytype) !void {
|
|||||||
objectGroup: ?[]const u8 = null,
|
objectGroup: ?[]const u8 = null,
|
||||||
executionContextId: ?u32 = null,
|
executionContextId: ?u32 = null,
|
||||||
})) orelse return error.InvalidParams;
|
})) orelse return error.InvalidParams;
|
||||||
|
|
||||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||||
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||||
|
|
||||||
var executor = bc.session.executor;
|
var scope = page.scope;
|
||||||
if (params.executionContextId) |context_id| {
|
if (params.executionContextId) |context_id| {
|
||||||
if (executor.context.debugContextId() != context_id) {
|
if (scope.context.debugContextId() != context_id) {
|
||||||
const isolated_world = bc.isolated_world orelse return error.ContextNotFound;
|
const isolated_world = bc.isolated_world orelse return error.ContextNotFound;
|
||||||
executor = isolated_world.executor;
|
scope = isolated_world.scope;
|
||||||
|
|
||||||
if (executor.context.debugContextId() != context_id) return error.ContextNotFound;
|
if (scope.context.debugContextId() != context_id) return error.ContextNotFound;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const input_node_id = if (params.nodeId) |node_id| node_id else params.backendNodeId orelse return error.InvalidParams;
|
|
||||||
|
const input_node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam;
|
||||||
const node = bc.node_registry.lookup_by_id.get(input_node_id) orelse return error.UnknownNode;
|
const node = bc.node_registry.lookup_by_id.get(input_node_id) orelse return error.UnknownNode;
|
||||||
|
|
||||||
// node._node is a *parser.Node we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement
|
// node._node is a *parser.Node we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement
|
||||||
// So we use the Node.Union when retrieve the value from the environment
|
// So we use the Node.Union when retrieve the value from the environment
|
||||||
const remote_object = try bc.session.inspector.getRemoteObject(
|
const remote_object = try bc.inspector.getRemoteObject(
|
||||||
executor,
|
scope,
|
||||||
params.objectGroup orelse "",
|
params.objectGroup orelse "",
|
||||||
try dom_node.Node.toInterface(node._node),
|
try dom_node.Node.toInterface(node._node),
|
||||||
);
|
);
|
||||||
@@ -180,7 +183,7 @@ fn describeNode(cmd: anytype) !void {
|
|||||||
}
|
}
|
||||||
if (params.objectId != null) {
|
if (params.objectId != null) {
|
||||||
// Retrieve the object from which ever context it is in.
|
// Retrieve the object from which ever context it is in.
|
||||||
const parser_node = try bc.session.inspector.getNodePtr(cmd.arena, params.objectId.?);
|
const parser_node = try bc.inspector.getNodePtr(cmd.arena, params.objectId.?);
|
||||||
const node = try bc.node_registry.register(@ptrCast(parser_node));
|
const node = try bc.node_registry.register(@ptrCast(parser_node));
|
||||||
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
|
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,15 +112,15 @@ fn createIsolatedWorld(cmd: anytype) !void {
|
|||||||
}
|
}
|
||||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||||
|
|
||||||
try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);
|
|
||||||
const world = &bc.isolated_world.?;
|
const world = &bc.isolated_world.?;
|
||||||
|
world.name = try bc.arena.dupe(u8, params.worldName);
|
||||||
|
world.grant_universal_access = params.grantUniveralAccess;
|
||||||
// Create the auxdata json for the contextCreated event
|
// Create the auxdata json for the contextCreated event
|
||||||
// Calling contextCreated will assign a Id to the context and send the contextCreated event
|
// Calling contextCreated will assign a Id to the context and send the contextCreated event
|
||||||
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{params.frameId});
|
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{params.frameId});
|
||||||
bc.session.inspector.contextCreated(world.executor, world.name, "", aux_data, false);
|
bc.inspector.contextCreated(world.scope, world.name, "", aux_data, false);
|
||||||
|
|
||||||
return cmd.sendResult(.{ .executionContextId = world.executor.context.debugContextId() }, .{});
|
return cmd.sendResult(.{ .executionContextId = world.scope.context.debugContextId() }, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn navigate(cmd: anytype) !void {
|
fn navigate(cmd: anytype) !void {
|
||||||
@@ -217,13 +217,25 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void
|
|||||||
// The client will expect us to send new contextCreated events, such that the client has new id's for the active contexts.
|
// The client will expect us to send new contextCreated events, such that the client has new id's for the active contexts.
|
||||||
try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
|
try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
|
||||||
|
|
||||||
if (bc.isolated_world) |*isolated_world| {
|
var buffer: [512]u8 = undefined;
|
||||||
var buffer: [256]u8 = undefined;
|
{
|
||||||
const aux_json = try std.fmt.bufPrint(&buffer, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{bc.target_id.?});
|
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||||
|
const page = bc.session.currentPage().?;
|
||||||
|
const aux_data = try std.fmt.allocPrint(fba.allocator(), "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
|
||||||
|
bc.inspector.contextCreated(
|
||||||
|
page.scope,
|
||||||
|
"",
|
||||||
|
try page.origin(fba.allocator()),
|
||||||
|
aux_data,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bc.isolated_world) |*isolated_world| {
|
||||||
|
const aux_json = try std.fmt.bufPrint(&buffer, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{target_id});
|
||||||
// Calling contextCreated will assign a new Id to the context and send the contextCreated event
|
// Calling contextCreated will assign a new Id to the context and send the contextCreated event
|
||||||
bc.session.inspector.contextCreated(
|
bc.inspector.contextCreated(
|
||||||
isolated_world.executor,
|
isolated_world.scope,
|
||||||
isolated_world.name,
|
isolated_world.name,
|
||||||
"://",
|
"://",
|
||||||
aux_json,
|
aux_json,
|
||||||
|
|||||||
@@ -44,10 +44,7 @@ fn sendInspector(cmd: anytype, action: anytype) !void {
|
|||||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||||
|
|
||||||
// the result to return is handled directly by the inspector.
|
// the result to return is handled directly by the inspector.
|
||||||
bc.session.callInspector(cmd.input.json);
|
bc.callInspector(cmd.input.json);
|
||||||
|
|
||||||
// force running micro tasks after send input to the inspector.
|
|
||||||
cmd.cdp.browser.runMicrotasks();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn logInspector(cmd: anytype, action: anytype) !void {
|
fn logInspector(cmd: anytype, action: anytype) !void {
|
||||||
|
|||||||
@@ -122,14 +122,21 @@ fn createTarget(cmd: anytype) !void {
|
|||||||
|
|
||||||
const target_id = cmd.cdp.target_id_gen.next();
|
const target_id = cmd.cdp.target_id_gen.next();
|
||||||
|
|
||||||
// start the js env
|
bc.target_id = target_id;
|
||||||
const aux_data = try std.fmt.allocPrint(
|
|
||||||
cmd.arena,
|
var page = try bc.session.createPage();
|
||||||
// NOTE: we assume this is the default web page
|
try bc.createIsolatedWorld(page);
|
||||||
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
|
|
||||||
.{target_id},
|
{
|
||||||
);
|
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
|
||||||
_ = try bc.session.createPage(aux_data);
|
bc.inspector.contextCreated(
|
||||||
|
page.scope,
|
||||||
|
"",
|
||||||
|
try page.origin(cmd.arena),
|
||||||
|
aux_data,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// change CDP state
|
// change CDP state
|
||||||
bc.security_origin = "://";
|
bc.security_origin = "://";
|
||||||
@@ -154,8 +161,6 @@ fn createTarget(cmd: anytype) !void {
|
|||||||
try doAttachtoTarget(cmd, target_id);
|
try doAttachtoTarget(cmd, target_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
bc.target_id = target_id;
|
|
||||||
|
|
||||||
try cmd.sendResult(.{
|
try cmd.sendResult(.{
|
||||||
.targetId = target_id,
|
.targetId = target_id,
|
||||||
}, .{});
|
}, .{});
|
||||||
@@ -219,6 +224,10 @@ fn closeTarget(cmd: anytype) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bc.session.removePage();
|
bc.session.removePage();
|
||||||
|
if (bc.isolated_world) |*world| {
|
||||||
|
world.deinit();
|
||||||
|
bc.isolated_world = null;
|
||||||
|
}
|
||||||
bc.target_id = null;
|
bc.target_id = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,10 +529,6 @@ test "cdp.target: createTarget" {
|
|||||||
{
|
{
|
||||||
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
|
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
|
||||||
try testing.expectEqual(true, bc.target_id != null);
|
try testing.expectEqual(true, bc.target_id != null);
|
||||||
try testing.expectEqual(
|
|
||||||
\\{"isDefault":true,"type":"default","frameId":"TID-1"}
|
|
||||||
, bc.session.aux_data);
|
|
||||||
|
|
||||||
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
|
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
|
||||||
try ctx.expectSentEvent("Target.targetCreated", .{ .targetInfo = .{ .url = "about:blank", .title = "about:blank", .attached = false, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{});
|
try ctx.expectSentEvent("Target.targetCreated", .{ .targetInfo = .{ .url = "about:blank", .title = "about:blank", .attached = false, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{});
|
||||||
}
|
}
|
||||||
@@ -545,7 +550,7 @@ test "cdp.target: closeTarget" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// pretend we createdTarget first
|
// pretend we createdTarget first
|
||||||
_ = try bc.session.createPage(null);
|
_ = try bc.session.createPage();
|
||||||
bc.target_id = "TID-A";
|
bc.target_id = "TID-A";
|
||||||
{
|
{
|
||||||
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } }));
|
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } }));
|
||||||
@@ -576,7 +581,7 @@ test "cdp.target: attachToTarget" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// pretend we createdTarget first
|
// pretend we createdTarget first
|
||||||
_ = try bc.session.createPage(null);
|
_ = try bc.session.createPage();
|
||||||
bc.target_id = "TID-B";
|
bc.target_id = "TID-B";
|
||||||
{
|
{
|
||||||
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } }));
|
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } }));
|
||||||
@@ -620,7 +625,7 @@ test "cdp.target: getTargetInfo" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// pretend we createdTarget first
|
// pretend we createdTarget first
|
||||||
_ = try bc.session.createPage(null);
|
_ = try bc.session.createPage();
|
||||||
bc.target_id = "TID-A";
|
bc.target_id = "TID-A";
|
||||||
{
|
{
|
||||||
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } }));
|
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } }));
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ const TestContext = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = try c.createBrowserContext();
|
_ = try c.createBrowserContext();
|
||||||
var bc = c.browser_context.?;
|
var bc = &c.browser_context.?;
|
||||||
|
|
||||||
if (opts.id) |id| {
|
if (opts.id) |id| {
|
||||||
bc.id = id;
|
bc.id = id;
|
||||||
@@ -122,7 +122,7 @@ const TestContext = struct {
|
|||||||
if (opts.html) |html| {
|
if (opts.html) |html| {
|
||||||
parser.deinit();
|
parser.deinit();
|
||||||
try parser.init();
|
try parser.init();
|
||||||
const page = try bc.session.createPage(null);
|
const page = try bc.session.createPage();
|
||||||
page.doc = (try Document.init(html)).doc;
|
page.doc = (try Document.init(html)).doc;
|
||||||
}
|
}
|
||||||
return bc;
|
return bc;
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
pub const html: []const u8 =
|
|
||||||
\\<main id='content'>
|
|
||||||
\\<a href='foo'>OK</a>
|
|
||||||
\\<p>blah-blah-blah</p>
|
|
||||||
\\</main>
|
|
||||||
;
|
|
||||||
@@ -100,7 +100,7 @@ pub fn main() !void {
|
|||||||
var session = try browser.newSession({});
|
var session = try browser.newSession({});
|
||||||
|
|
||||||
// page
|
// page
|
||||||
const page = try session.createPage(null);
|
const page = try session.createPage();
|
||||||
|
|
||||||
_ = page.navigate(url, .{}) catch |err| switch (err) {
|
_ = page.navigate(url, .{}) catch |err| switch (err) {
|
||||||
error.UnsupportedUriScheme, error.UriMissingHost => {
|
error.UnsupportedUriScheme, error.UriMissingHost => {
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const jsruntime = @import("jsruntime");
|
|
||||||
|
|
||||||
const parser = @import("netsurf");
|
|
||||||
const apiweb = @import("apiweb.zig");
|
|
||||||
const Window = @import("html/window.zig").Window;
|
|
||||||
const storage = @import("storage/storage.zig");
|
|
||||||
const Client = @import("asyncio").Client;
|
|
||||||
|
|
||||||
const html_test = @import("html_test.zig").html;
|
|
||||||
|
|
||||||
pub const Types = jsruntime.reflect(apiweb.Interfaces);
|
|
||||||
pub const UserContext = apiweb.UserContext;
|
|
||||||
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
|
|
||||||
|
|
||||||
var doc: *parser.DocumentHTML = undefined;
|
|
||||||
|
|
||||||
fn execJS(
|
|
||||||
alloc: std.mem.Allocator,
|
|
||||||
js_env: *jsruntime.Env,
|
|
||||||
) anyerror!void {
|
|
||||||
// start JS env
|
|
||||||
try js_env.start();
|
|
||||||
defer js_env.stop();
|
|
||||||
|
|
||||||
var cli = Client{ .allocator = alloc };
|
|
||||||
defer cli.deinit();
|
|
||||||
|
|
||||||
try js_env.setUserContext(UserContext{
|
|
||||||
.document = doc,
|
|
||||||
.httpClient = &cli,
|
|
||||||
});
|
|
||||||
|
|
||||||
var storageShelf = storage.Shelf.init(alloc);
|
|
||||||
defer storageShelf.deinit();
|
|
||||||
|
|
||||||
// alias global as self and window
|
|
||||||
var window = Window.create(null, null);
|
|
||||||
try window.replaceDocument(doc);
|
|
||||||
window.setStorageShelf(&storageShelf);
|
|
||||||
try js_env.bindGlobal(window);
|
|
||||||
|
|
||||||
// launch shellExec
|
|
||||||
try jsruntime.shellExec(alloc, js_env);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn main() !void {
|
|
||||||
|
|
||||||
// allocator
|
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
||||||
defer _ = gpa.deinit();
|
|
||||||
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
|
|
||||||
defer arena.deinit();
|
|
||||||
|
|
||||||
try parser.init();
|
|
||||||
defer parser.deinit();
|
|
||||||
|
|
||||||
// document
|
|
||||||
const file = try std.fs.cwd().openFile("test.html", .{});
|
|
||||||
defer file.close();
|
|
||||||
|
|
||||||
doc = try parser.documentHTMLParse(file.reader(), "UTF-8");
|
|
||||||
defer parser.documentHTMLClose(doc) catch |err| {
|
|
||||||
std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)});
|
|
||||||
};
|
|
||||||
|
|
||||||
// create JS vm
|
|
||||||
const vm = jsruntime.VM.init();
|
|
||||||
defer vm.deinit();
|
|
||||||
|
|
||||||
// launch shell
|
|
||||||
try jsruntime.shell(&arena, execJS, .{ .app_name = "lightpanda-shell" });
|
|
||||||
}
|
|
||||||
1839
src/runtime/js.zig
1839
src/runtime/js.zig
File diff suppressed because it is too large
Load Diff
@@ -84,7 +84,6 @@ const Primitives = struct {
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
pub fn _checkNonOptional(_: *const Primitives, v: u8) u8 {
|
pub fn _checkNonOptional(_: *const Primitives, v: u8) u8 {
|
||||||
std.debug.print("x: {d}\n", .{v});
|
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
pub fn _checkOptionalReturn(_: *const Primitives) ?bool {
|
pub fn _checkOptionalReturn(_: *const Primitives) ?bool {
|
||||||
|
|||||||
@@ -30,29 +30,32 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty
|
|||||||
|
|
||||||
return struct {
|
return struct {
|
||||||
env: *Env,
|
env: *Env,
|
||||||
executor: *Env.Executor,
|
scope: *Env.Scope,
|
||||||
|
executor: Env.Executor,
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
pub fn init(state: State, global: Global) !*Self {
|
pub fn init(state: State, global: Global) !*Self {
|
||||||
const runner = try allocator.create(Self);
|
const self = try allocator.create(Self);
|
||||||
errdefer allocator.destroy(runner);
|
errdefer allocator.destroy(self);
|
||||||
|
|
||||||
runner.env = try Env.init(allocator, .{});
|
self.env = try Env.init(allocator, .{});
|
||||||
errdefer runner.env.deinit();
|
errdefer self.env.deinit();
|
||||||
|
|
||||||
const G = if (Global == void) DefaultGlobal else Global;
|
self.executor = try self.env.newExecutor();
|
||||||
|
errdefer self.executor.deinit();
|
||||||
|
|
||||||
runner.executor = try runner.env.startExecutor(G, state, runner, .main);
|
self.scope = try self.executor.startScope(
|
||||||
errdefer runner.env.stopExecutor(runner.executor);
|
if (Global == void) &default_global else global,
|
||||||
|
state,
|
||||||
try runner.executor.startScope(if (Global == void) &default_global else global);
|
{},
|
||||||
return runner;
|
true,
|
||||||
|
);
|
||||||
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: *Self) void {
|
||||||
self.executor.endScope();
|
self.executor.deinit();
|
||||||
self.env.stopExecutor(self.executor);
|
|
||||||
self.env.deinit();
|
self.env.deinit();
|
||||||
allocator.destroy(self);
|
allocator.destroy(self);
|
||||||
}
|
}
|
||||||
@@ -62,10 +65,10 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty
|
|||||||
pub fn testCases(self: *Self, cases: []const Case, _: RunOpts) !void {
|
pub fn testCases(self: *Self, cases: []const Case, _: RunOpts) !void {
|
||||||
for (cases, 0..) |case, i| {
|
for (cases, 0..) |case, i| {
|
||||||
var try_catch: Env.TryCatch = undefined;
|
var try_catch: Env.TryCatch = undefined;
|
||||||
try_catch.init(self.executor);
|
try_catch.init(self.scope);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
const value = self.executor.exec(case.@"0", null) catch |err| {
|
const value = self.scope.exec(case.@"0", null) catch |err| {
|
||||||
if (try try_catch.err(allocator)) |msg| {
|
if (try try_catch.err(allocator)) |msg| {
|
||||||
defer allocator.free(msg);
|
defer allocator.free(msg);
|
||||||
if (isExpectedTypeError(case.@"1", msg)) {
|
if (isExpectedTypeError(case.@"1", msg)) {
|
||||||
@@ -84,12 +87,6 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 {
|
|
||||||
_ = ctx;
|
|
||||||
_ = specifier;
|
|
||||||
return error.DummyModuleLoader;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -381,7 +381,8 @@ pub const JsRunner = struct {
|
|||||||
arena: Allocator,
|
arena: Allocator,
|
||||||
renderer: Renderer,
|
renderer: Renderer,
|
||||||
http_client: HttpClient,
|
http_client: HttpClient,
|
||||||
executor: *Env.Executor,
|
scope: *Env.Scope,
|
||||||
|
executor: Env.Executor,
|
||||||
storage_shelf: storage.Shelf,
|
storage_shelf: storage.Shelf,
|
||||||
cookie_jar: storage.CookieJar,
|
cookie_jar: storage.CookieJar,
|
||||||
|
|
||||||
@@ -394,55 +395,55 @@ pub const JsRunner = struct {
|
|||||||
errdefer aa.deinit();
|
errdefer aa.deinit();
|
||||||
|
|
||||||
const arena = aa.allocator();
|
const arena = aa.allocator();
|
||||||
const runner = try arena.create(JsRunner);
|
const self = try arena.create(JsRunner);
|
||||||
runner.arena = arena;
|
self.arena = arena;
|
||||||
|
|
||||||
runner.env = try Env.init(arena, .{});
|
self.env = try Env.init(arena, .{});
|
||||||
errdefer runner.env.deinit();
|
errdefer self.env.deinit();
|
||||||
|
|
||||||
runner.url = try URL.parse("https://lightpanda.io/opensource-browser/", null);
|
self.url = try URL.parse("https://lightpanda.io/opensource-browser/", null);
|
||||||
|
|
||||||
runner.renderer = Renderer.init(arena);
|
self.renderer = Renderer.init(arena);
|
||||||
runner.cookie_jar = storage.CookieJar.init(arena);
|
self.cookie_jar = storage.CookieJar.init(arena);
|
||||||
runner.loop = try Loop.init(arena);
|
self.loop = try Loop.init(arena);
|
||||||
errdefer runner.loop.deinit();
|
errdefer self.loop.deinit();
|
||||||
|
|
||||||
var html = std.io.fixedBufferStream(opts.html);
|
var html = std.io.fixedBufferStream(opts.html);
|
||||||
const document = try parser.documentHTMLParse(html.reader(), "UTF-8");
|
const document = try parser.documentHTMLParse(html.reader(), "UTF-8");
|
||||||
|
|
||||||
runner.state = .{
|
self.state = .{
|
||||||
.arena = arena,
|
.arena = arena,
|
||||||
.loop = &runner.loop,
|
.loop = &self.loop,
|
||||||
.document = document,
|
.document = document,
|
||||||
.url = &runner.url,
|
.url = &self.url,
|
||||||
.renderer = &runner.renderer,
|
.renderer = &self.renderer,
|
||||||
.cookie_jar = &runner.cookie_jar,
|
.cookie_jar = &self.cookie_jar,
|
||||||
.http_client = &runner.http_client,
|
.http_client = &self.http_client,
|
||||||
};
|
};
|
||||||
|
|
||||||
runner.window = .{};
|
self.window = .{};
|
||||||
try runner.window.replaceDocument(document);
|
try self.window.replaceDocument(document);
|
||||||
try runner.window.replaceLocation(.{
|
try self.window.replaceLocation(.{
|
||||||
.url = try runner.url.toWebApi(arena),
|
.url = try self.url.toWebApi(arena),
|
||||||
});
|
});
|
||||||
|
|
||||||
runner.storage_shelf = storage.Shelf.init(arena);
|
self.storage_shelf = storage.Shelf.init(arena);
|
||||||
runner.window.setStorageShelf(&runner.storage_shelf);
|
self.window.setStorageShelf(&self.storage_shelf);
|
||||||
|
|
||||||
runner.http_client = try HttpClient.init(arena, 1, .{
|
self.http_client = try HttpClient.init(arena, 1, .{
|
||||||
.tls_verify_host = false,
|
.tls_verify_host = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
runner.executor = try runner.env.startExecutor(Window, &runner.state, runner, .main);
|
self.executor = try self.env.newExecutor();
|
||||||
errdefer runner.env.stopExecutor(runner.executor);
|
errdefer self.executor.deinit();
|
||||||
|
|
||||||
try runner.executor.startScope(&runner.window);
|
self.scope = try self.executor.startScope(&self.window, &self.state, {}, true);
|
||||||
return runner;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *JsRunner) void {
|
pub fn deinit(self: *JsRunner) void {
|
||||||
self.loop.deinit();
|
self.loop.deinit();
|
||||||
self.executor.endScope();
|
self.executor.deinit();
|
||||||
self.env.deinit();
|
self.env.deinit();
|
||||||
self.http_client.deinit();
|
self.http_client.deinit();
|
||||||
self.storage_shelf.deinit();
|
self.storage_shelf.deinit();
|
||||||
@@ -459,10 +460,10 @@ pub const JsRunner = struct {
|
|||||||
|
|
||||||
for (cases, 0..) |case, i| {
|
for (cases, 0..) |case, i| {
|
||||||
var try_catch: Env.TryCatch = undefined;
|
var try_catch: Env.TryCatch = undefined;
|
||||||
try_catch.init(self.executor);
|
try_catch.init(self.scope);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
const value = self.executor.exec(case.@"0", null) catch |err| {
|
const value = self.scope.exec(case.@"0", null) catch |err| {
|
||||||
if (try try_catch.err(self.arena)) |msg| {
|
if (try try_catch.err(self.arena)) |msg| {
|
||||||
std.debug.print("{s}\n\nCase: {d}\n{s}\n", .{ msg, i + 1, case.@"0" });
|
std.debug.print("{s}\n\nCase: {d}\n{s}\n", .{ msg, i + 1, case.@"0" });
|
||||||
}
|
}
|
||||||
@@ -485,10 +486,10 @@ pub const JsRunner = struct {
|
|||||||
|
|
||||||
pub fn eval(self: *JsRunner, src: []const u8, name: ?[]const u8, err_msg: *?[]const u8) !Env.Value {
|
pub fn eval(self: *JsRunner, src: []const u8, name: ?[]const u8, err_msg: *?[]const u8) !Env.Value {
|
||||||
var try_catch: Env.TryCatch = undefined;
|
var try_catch: Env.TryCatch = undefined;
|
||||||
try_catch.init(self.executor);
|
try_catch.init(self.scope);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
return self.executor.exec(src, name) catch |err| {
|
return self.scope.exec(src, name) catch |err| {
|
||||||
if (try try_catch.err(self.arena)) |msg| {
|
if (try try_catch.err(self.arena)) |msg| {
|
||||||
err_msg.* = msg;
|
err_msg.* = msg;
|
||||||
std.debug.print("Error running script: {s}\n", .{msg});
|
std.debug.print("Error running script: {s}\n", .{msg});
|
||||||
@@ -496,12 +497,6 @@ pub const JsRunner = struct {
|
|||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 {
|
|
||||||
_ = ctx;
|
|
||||||
_ = specifier;
|
|
||||||
return error.DummyModuleLoader;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const RunnerOpts = struct {
|
const RunnerOpts = struct {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ pub const URL = struct {
|
|||||||
uri: Uri,
|
uri: Uri,
|
||||||
raw: []const u8,
|
raw: []const u8,
|
||||||
|
|
||||||
|
pub const empty = URL{ .uri = .{ .scheme = "" }, .raw = "" };
|
||||||
|
|
||||||
// We assume str will last as long as the URL
|
// We assume str will last as long as the URL
|
||||||
// In some cases, this is safe to do, because we know the URL is short lived.
|
// In some cases, this is safe to do, because we know the URL is short lived.
|
||||||
// In most cases though, we assume the caller will just dupe the string URL
|
// In most cases though, we assume the caller will just dupe the string URL
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ pub fn run(arena: Allocator, comptime dir: []const u8, f: []const u8, loader: *F
|
|||||||
.html = html,
|
.html = html,
|
||||||
});
|
});
|
||||||
defer runner.deinit();
|
defer runner.deinit();
|
||||||
try polyfill.load(arena, runner.executor);
|
try polyfill.load(arena, runner.scope);
|
||||||
|
|
||||||
// display console logs
|
// display console logs
|
||||||
defer {
|
defer {
|
||||||
@@ -106,7 +106,7 @@ pub fn run(arena: Allocator, comptime dir: []const u8, f: []const u8, loader: *F
|
|||||||
// wait for all async executions
|
// wait for all async executions
|
||||||
{
|
{
|
||||||
var try_catch: Env.TryCatch = undefined;
|
var try_catch: Env.TryCatch = undefined;
|
||||||
try_catch.init(runner.executor);
|
try_catch.init(runner.scope);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
runner.loop.run() catch |err| {
|
runner.loop.run() catch |err| {
|
||||||
if (try try_catch.err(arena)) |msg| {
|
if (try try_catch.err(arena)) |msg| {
|
||||||
|
|||||||
Reference in New Issue
Block a user