mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 22:53:28 +00:00
Move Session, Page and Renderer into their own respective files
This commit is contained in:
@@ -17,33 +17,16 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const Dump = @import("dump.zig");
|
||||
const Mime = @import("mime.zig").Mime;
|
||||
const DataURI = @import("datauri.zig").DataURI;
|
||||
const parser = @import("netsurf.zig");
|
||||
|
||||
const Window = @import("html/window.zig").Window;
|
||||
const Walker = @import("dom/walker.zig").WalkerDepthFirst;
|
||||
|
||||
const Env = @import("env.zig").Env;
|
||||
const App = @import("../app.zig").App;
|
||||
const Loop = @import("../runtime/loop.zig").Loop;
|
||||
|
||||
const URL = @import("../url.zig").URL;
|
||||
|
||||
const http = @import("../http/client.zig");
|
||||
const storage = @import("storage/storage.zig");
|
||||
const SessionState = @import("env.zig").SessionState;
|
||||
const Session = @import("session.zig").Session;
|
||||
const Notification = @import("../notification.zig").Notification;
|
||||
|
||||
const polyfill = @import("polyfill/polyfill.zig");
|
||||
|
||||
const log = std.log.scoped(.browser);
|
||||
const http = @import("../http/client.zig");
|
||||
|
||||
// Browser is an instance of the browser.
|
||||
// You can create multiple browser instances.
|
||||
@@ -107,816 +90,6 @@ pub const Browser = struct {
|
||||
}
|
||||
};
|
||||
|
||||
// Session is like a browser's tab.
|
||||
// It owns the js env and the loader for all the pages of the session.
|
||||
// You can create successively multiple pages for a session, but you must
|
||||
// deinit a page before running another one.
|
||||
pub const Session = struct {
|
||||
browser: *Browser,
|
||||
|
||||
// Used to create our Inspector and in the BrowserContext.
|
||||
arena: ArenaAllocator,
|
||||
|
||||
executor: Env.Executor,
|
||||
storage_shed: storage.Shed,
|
||||
cookie_jar: storage.CookieJar,
|
||||
|
||||
page: ?Page = null,
|
||||
|
||||
fn init(self: *Session, browser: *Browser) !void {
|
||||
var executor = try browser.env.newExecutor();
|
||||
errdefer executor.deinit();
|
||||
|
||||
const allocator = browser.app.allocator;
|
||||
self.* = .{
|
||||
.browser = browser,
|
||||
.executor = executor,
|
||||
.arena = ArenaAllocator.init(allocator),
|
||||
.storage_shed = storage.Shed.init(allocator),
|
||||
.cookie_jar = storage.CookieJar.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(self: *Session) void {
|
||||
if (self.page != null) {
|
||||
self.removePage();
|
||||
}
|
||||
self.arena.deinit();
|
||||
self.cookie_jar.deinit();
|
||||
self.storage_shed.deinit();
|
||||
self.executor.deinit();
|
||||
}
|
||||
|
||||
// NOTE: the caller is not the owner of the returned value,
|
||||
// the pointer on Page is just returned as a convenience
|
||||
pub fn createPage(self: *Session) !*Page {
|
||||
std.debug.assert(self.page == null);
|
||||
|
||||
// Start netsurf memory arena.
|
||||
// We need to init this early as JS event handlers may be registered through Runtime.evaluate before the first html doc is loaded
|
||||
try parser.init();
|
||||
|
||||
const page_arena = &self.browser.page_arena;
|
||||
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
|
||||
self.page = @as(Page, undefined);
|
||||
const page = &self.page.?;
|
||||
try Page.init(page, page_arena.allocator(), self);
|
||||
|
||||
// start JS env
|
||||
log.debug("start new js scope", .{});
|
||||
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
||||
self.browser.notification.dispatch(.page_created, page);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
pub fn removePage(self: *Session) void {
|
||||
// Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one
|
||||
self.browser.notification.dispatch(.page_remove, .{});
|
||||
|
||||
std.debug.assert(self.page != null);
|
||||
// Reset all existing callbacks.
|
||||
self.browser.app.loop.reset();
|
||||
self.executor.endScope();
|
||||
self.page = null;
|
||||
|
||||
// clear netsurf memory arena.
|
||||
parser.deinit();
|
||||
}
|
||||
|
||||
pub fn currentPage(self: *Session) ?*Page {
|
||||
return &(self.page orelse return null);
|
||||
}
|
||||
|
||||
fn pageNavigate(self: *Session, url_string: []const u8) !void {
|
||||
// currently, this is only called from the page, so let's hope
|
||||
// it isn't null!
|
||||
std.debug.assert(self.page != null);
|
||||
|
||||
// can't use the page arena, because we're about to reset it
|
||||
// and don't want to use the session's arena, because that'll start to
|
||||
// look like a leak if we navigate from page to page a lot.
|
||||
var buf: [2048]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buf);
|
||||
const url = try self.page.?.url.resolve(fba.allocator(), url_string);
|
||||
|
||||
self.removePage();
|
||||
var page = try self.createPage();
|
||||
return page.navigate(url, .{
|
||||
.reason = .anchor,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Page navigates to an url.
|
||||
// You can navigates multiple urls with the same page, but you have to call
|
||||
// end() to stop the previous navigation before starting a new one.
|
||||
// The page handle all its memory in an arena allocator. The arena is reseted
|
||||
// when end() is called.
|
||||
pub const Page = struct {
|
||||
session: *Session,
|
||||
|
||||
// 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
|
||||
url: URL,
|
||||
|
||||
raw_data: ?[]const u8,
|
||||
|
||||
renderer: FlatRenderer,
|
||||
|
||||
microtask_node: Loop.CallbackNode,
|
||||
|
||||
window_clicked_event_node: parser.EventNode,
|
||||
|
||||
scope: *Env.Scope,
|
||||
|
||||
// List of modules currently fetched/loaded.
|
||||
module_map: std.StringHashMapUnmanaged([]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,
|
||||
|
||||
fn init(self: *Page, arena: Allocator, session: *Session) !void {
|
||||
const browser = session.browser;
|
||||
self.* = .{
|
||||
.window = try Window.create(null, null),
|
||||
.arena = arena,
|
||||
.doc = null,
|
||||
.raw_data = null,
|
||||
.url = URL.empty,
|
||||
.session = session,
|
||||
.renderer = FlatRenderer.init(arena),
|
||||
.microtask_node = .{ .func = microtaskCallback },
|
||||
.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),
|
||||
.module_map = .empty,
|
||||
};
|
||||
|
||||
// load polyfills
|
||||
try polyfill.load(self.arena, self.scope);
|
||||
|
||||
// _ = try session.browser.app.loop.timeout(1 * std.time.ns_per_ms, &self.microtask_node);
|
||||
}
|
||||
|
||||
fn microtaskCallback(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
||||
const self: *Page = @fieldParentPtr("microtask_node", node);
|
||||
self.session.browser.runMicrotasks();
|
||||
repeat_delay.* = 1 * std.time.ns_per_ms;
|
||||
}
|
||||
|
||||
// dump writes the page content into the given file.
|
||||
pub fn dump(self: *const Page, out: std.fs.File) !void {
|
||||
// if no HTML document pointer available, dump the data content only.
|
||||
if (self.doc == null) {
|
||||
// no data loaded, nothing to do.
|
||||
if (self.raw_data == null) return;
|
||||
return try out.writeAll(self.raw_data.?);
|
||||
}
|
||||
|
||||
// if the page has a pointer to a document, dumps the HTML.
|
||||
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});
|
||||
|
||||
const base = if (self.current_script) |s| s.src else null;
|
||||
|
||||
const file_src = blk: {
|
||||
if (base) |_base| {
|
||||
break :blk try URL.stitch(self.arena, specifier, _base);
|
||||
} else break :blk specifier;
|
||||
};
|
||||
|
||||
if (self.module_map.get(file_src)) |module| return module;
|
||||
|
||||
const module = try self.fetchData(specifier, base);
|
||||
if (module) |_module| try self.module_map.putNoClobber(self.arena, file_src, _module);
|
||||
return module;
|
||||
}
|
||||
|
||||
pub fn wait(self: *Page) !void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(self.scope);
|
||||
defer try_catch.deinit();
|
||||
|
||||
try self.session.browser.app.loop.run();
|
||||
|
||||
if (try_catch.hasCaught() == false) {
|
||||
log.debug("wait: OK", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = (try try_catch.err(self.arena)) orelse "unknown";
|
||||
log.info("wait error: {s}", .{msg});
|
||||
}
|
||||
|
||||
pub fn origin(self: *const Page, arena: Allocator) ![]const u8 {
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
try self.url.origin(arr.writer(arena));
|
||||
return arr.items;
|
||||
}
|
||||
|
||||
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
|
||||
pub fn navigate(self: *Page, request_url: URL, opts: NavigateOpts) !void {
|
||||
const arena = self.arena;
|
||||
const session = self.session;
|
||||
|
||||
log.debug("starting GET {s}", .{request_url});
|
||||
|
||||
// if the url is about:blank, nothing to do.
|
||||
if (std.mem.eql(u8, "about:blank", request_url.raw)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we don't clone url, because we're going to replace self.url
|
||||
// later in this function, with the final request url (since we might
|
||||
// redirect)
|
||||
self.url = request_url;
|
||||
|
||||
// load the data
|
||||
var request = try self.newHTTPRequest(.GET, &self.url, .{ .navigation = true });
|
||||
defer request.deinit();
|
||||
|
||||
session.browser.notification.dispatch(.page_navigate, &.{
|
||||
.url = &self.url,
|
||||
.reason = opts.reason,
|
||||
.timestamp = timestamp(),
|
||||
});
|
||||
|
||||
var response = try request.sendSync(.{});
|
||||
|
||||
// would be different than self.url in the case of a redirect
|
||||
self.url = try URL.fromURI(arena, request.request_uri);
|
||||
|
||||
const header = response.header;
|
||||
try session.cookie_jar.populateFromResponse(&self.url.uri, &header);
|
||||
|
||||
// TODO handle fragment in url.
|
||||
try self.window.replaceLocation(.{ .url = try self.url.toWebApi(arena) });
|
||||
|
||||
log.info("GET {any} {d}", .{ self.url, header.status });
|
||||
|
||||
const content_type = header.get("content-type");
|
||||
|
||||
const mime: Mime = blk: {
|
||||
if (content_type) |ct| {
|
||||
break :blk try Mime.parse(arena, ct);
|
||||
}
|
||||
break :blk Mime.sniff(try response.peek());
|
||||
} orelse .unknown;
|
||||
|
||||
if (mime.isHTML()) {
|
||||
try self.loadHTMLDoc(&response, mime.charset orelse "utf-8");
|
||||
} else {
|
||||
log.info("non-HTML document: {s}", .{content_type orelse "null"});
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
while (try response.next()) |data| {
|
||||
try arr.appendSlice(arena, try arena.dupe(u8, data));
|
||||
}
|
||||
// save the body into the page.
|
||||
self.raw_data = arr.items;
|
||||
}
|
||||
|
||||
session.browser.notification.dispatch(.page_navigated, &.{
|
||||
.url = &self.url,
|
||||
.timestamp = timestamp(),
|
||||
});
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/#read-html
|
||||
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void {
|
||||
const arena = self.arena;
|
||||
|
||||
log.debug("parse html with charset {s}", .{charset});
|
||||
|
||||
const ccharset = try arena.dupeZ(u8, charset);
|
||||
|
||||
const html_doc = try parser.documentHTMLParse(reader, ccharset);
|
||||
const doc = parser.documentHTMLToDocument(html_doc);
|
||||
|
||||
// save a document's pointer in the page.
|
||||
self.doc = doc;
|
||||
|
||||
const document_element = (try parser.documentGetDocumentElement(doc)) orelse return error.DocumentElementError;
|
||||
try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Element, document_element),
|
||||
"click",
|
||||
&self.window_clicked_event_node,
|
||||
false,
|
||||
);
|
||||
|
||||
// TODO set document.readyState to interactive
|
||||
// https://html.spec.whatwg.org/#reporting-document-loading-status
|
||||
|
||||
// inject the URL to the document including the fragment.
|
||||
try parser.documentSetDocumentURI(doc, self.url.raw);
|
||||
|
||||
// TODO set the referrer to the document.
|
||||
try self.window.replaceDocument(html_doc);
|
||||
self.window.setStorageShelf(
|
||||
try self.session.storage_shed.getOrPut(try self.origin(self.arena)),
|
||||
);
|
||||
|
||||
// https://html.spec.whatwg.org/#read-html
|
||||
|
||||
// update the sessions state
|
||||
self.state.document = html_doc;
|
||||
|
||||
// browse the DOM tree to retrieve scripts
|
||||
// TODO execute the synchronous scripts during the HTL parsing.
|
||||
// TODO fetch the script resources concurrently but execute them in the
|
||||
// declaration order for synchronous ones.
|
||||
|
||||
// async_scripts stores scripts which can be run asynchronously.
|
||||
// for now they are just run after the non-async one in order to
|
||||
// dispatch DOMContentLoaded the sooner as possible.
|
||||
var async_scripts: std.ArrayListUnmanaged(Script) = .{};
|
||||
|
||||
// defer_scripts stores scripts which are meant to be deferred. For now
|
||||
// this doesn't have a huge impact, since normal scripts are parsed
|
||||
// after the document is loaded. But (a) we should fix that and (b)
|
||||
// this results in JavaScript being loaded in the same order as browsers
|
||||
// which can help debug issues (and might actually fix issues if websites
|
||||
// are expecting this execution order)
|
||||
var defer_scripts: std.ArrayListUnmanaged(Script) = .{};
|
||||
|
||||
const root = parser.documentToNode(doc);
|
||||
const walker = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
while (true) {
|
||||
next = try walker.get_next(root, next) orelse break;
|
||||
|
||||
// ignore non-elements nodes.
|
||||
if (try parser.nodeType(next.?) != .element) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const e = parser.nodeToElement(next.?);
|
||||
|
||||
// ignore non-js script.
|
||||
const script = try Script.init(e) orelse continue;
|
||||
|
||||
// TODO use fetchpriority
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#fetchpriority
|
||||
|
||||
// > async
|
||||
// > For classic scripts, if the async attribute is present,
|
||||
// > then the classic script will be fetched in parallel to
|
||||
// > parsing and evaluated as soon as it is available.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async
|
||||
if (script.is_async) {
|
||||
try async_scripts.append(arena, script);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (script.is_defer) {
|
||||
try defer_scripts.append(arena, script);
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO handle for attribute
|
||||
// TODO handle event attribute
|
||||
|
||||
// > Scripts without async, defer or type="module"
|
||||
// > attributes, as well as inline scripts without the
|
||||
// > type="module" attribute, are fetched and executed
|
||||
// > immediately before the browser continues to parse the
|
||||
// > page.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
|
||||
self.evalScript(&script) catch |err| log.warn("evaljs: {any}", .{err});
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, null);
|
||||
}
|
||||
|
||||
for (defer_scripts.items) |s| {
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(s.element));
|
||||
self.evalScript(&s) catch |err| log.warn("evaljs: {any}", .{err});
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, null);
|
||||
}
|
||||
|
||||
// dispatch DOMContentLoaded before the transition to "complete",
|
||||
// at the point where all subresources apart from async script elements
|
||||
// have loaded.
|
||||
// https://html.spec.whatwg.org/#reporting-document-loading-status
|
||||
const evt = try parser.eventCreate();
|
||||
defer parser.eventDestroy(evt);
|
||||
|
||||
try parser.eventInit(evt, "DOMContentLoaded", .{ .bubbles = true, .cancelable = true });
|
||||
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt);
|
||||
|
||||
// eval async scripts.
|
||||
for (async_scripts.items) |s| {
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(s.element));
|
||||
self.evalScript(&s) catch |err| log.warn("evaljs: {any}", .{err});
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, null);
|
||||
}
|
||||
|
||||
// TODO wait for async scripts
|
||||
|
||||
// TODO set document.readyState to complete
|
||||
|
||||
// dispatch window.load event
|
||||
const loadevt = try parser.eventCreate();
|
||||
defer parser.eventDestroy(loadevt);
|
||||
|
||||
try parser.eventInit(loadevt, "load", .{});
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(Window, &self.window),
|
||||
loadevt,
|
||||
);
|
||||
}
|
||||
|
||||
// evalScript evaluates the src in priority.
|
||||
// if no src is present, we evaluate the text source.
|
||||
// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model
|
||||
fn evalScript(self: *Page, script: *const Script) !void {
|
||||
const src = script.src orelse {
|
||||
// source is inline
|
||||
// TODO handle charset attribute
|
||||
if (try parser.nodeTextContent(parser.elementToNode(script.element))) |text| {
|
||||
try script.eval(self, text);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
self.current_script = script;
|
||||
defer self.current_script = null;
|
||||
|
||||
log.debug("starting GET {s}", .{src});
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
|
||||
const body = (try self.fetchData(src, null)) orelse {
|
||||
// TODO If el's result is null, then fire an event named error at
|
||||
// el, and return
|
||||
return;
|
||||
};
|
||||
|
||||
script.eval(self, body) catch |err| switch (err) {
|
||||
error.JsErr => {}, // nothing to do here.
|
||||
else => return err,
|
||||
};
|
||||
|
||||
// TODO If el's from an external file is true, then fire an event
|
||||
// named load at el.
|
||||
}
|
||||
|
||||
// fetchData returns the data corresponding to the src target.
|
||||
// It resolves src using the page's uri.
|
||||
// If a base path is given, src is resolved according to the base first.
|
||||
// the caller owns the returned string
|
||||
fn fetchData(self: *const Page, src: []const u8, base: ?[]const u8) !?[]const u8 {
|
||||
log.debug("starting fetch {s}", .{src});
|
||||
|
||||
const arena = self.arena;
|
||||
|
||||
// Handle data URIs.
|
||||
if (try DataURI.parse(arena, src)) |data_uri| {
|
||||
return data_uri.data;
|
||||
}
|
||||
|
||||
var res_src = src;
|
||||
|
||||
// if a base path is given, we resolve src using base.
|
||||
if (base) |_base| {
|
||||
res_src = try URL.stitch(arena, src, _base);
|
||||
}
|
||||
|
||||
var origin_url = &self.url;
|
||||
const url = try origin_url.resolve(arena, res_src);
|
||||
|
||||
var request = try self.newHTTPRequest(.GET, &url, .{
|
||||
.origin_uri = &origin_url.uri,
|
||||
.navigation = false,
|
||||
});
|
||||
defer request.deinit();
|
||||
|
||||
var response = try request.sendSync(.{});
|
||||
var header = response.header;
|
||||
try self.session.cookie_jar.populateFromResponse(&url.uri, &header);
|
||||
|
||||
log.info("fetch {any}: {d}", .{ url, header.status });
|
||||
|
||||
if (header.status != 200) {
|
||||
return error.BadStatusCode;
|
||||
}
|
||||
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
while (try response.next()) |data| {
|
||||
try arr.appendSlice(arena, try arena.dupe(u8, data));
|
||||
}
|
||||
|
||||
// TODO check content-type
|
||||
|
||||
// check no body
|
||||
if (arr.items.len == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return arr.items;
|
||||
}
|
||||
|
||||
fn newHTTPRequest(self: *const Page, method: http.Request.Method, url: *const URL, opts: storage.cookie.LookupOpts) !http.Request {
|
||||
var request = try self.state.http_client.request(method, &url.uri);
|
||||
errdefer request.deinit();
|
||||
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
try self.state.cookie_jar.forRequest(&url.uri, arr.writer(self.arena), opts);
|
||||
|
||||
if (arr.items.len > 0) {
|
||||
try request.addHeader("Cookie", arr.items, .{});
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
pub const MouseEvent = struct {
|
||||
x: i32,
|
||||
y: i32,
|
||||
type: Type,
|
||||
|
||||
const Type = enum {
|
||||
pressed,
|
||||
released,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn mouseEvent(self: *Page, me: MouseEvent) !void {
|
||||
if (me.type != .pressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = self.renderer.getElementAtPosition(me.x, me.y) orelse return;
|
||||
|
||||
const event = try parser.mouseEventCreate();
|
||||
defer parser.mouseEventDestroy(event);
|
||||
try parser.mouseEventInit(event, "click", .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.x = me.x,
|
||||
.y = me.y,
|
||||
});
|
||||
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
|
||||
}
|
||||
|
||||
fn windowClicked(node: *parser.EventNode, event: *parser.Event) void {
|
||||
const self: *Page = @fieldParentPtr("window_clicked_event_node", node);
|
||||
self._windowClicked(event) catch |err| {
|
||||
log.err("window click handler: {}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn _windowClicked(self: *Page, event: *parser.Event) !void {
|
||||
const target = (try parser.eventTarget(event)) orelse return;
|
||||
|
||||
const node = parser.eventTargetToNode(target);
|
||||
if (try parser.nodeType(node) != .element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const html_element: *parser.ElementHTML = @ptrCast(node);
|
||||
switch (try parser.elementHTMLGetTagType(html_element)) {
|
||||
.a => {
|
||||
const element: *parser.Element = @ptrCast(node);
|
||||
const href = (try parser.elementGetAttribute(element, "href")) orelse return;
|
||||
|
||||
// We cannot navigate immediately as navigating will delete the DOM tree, which holds this event's node.
|
||||
// As such we schedule the function to be called as soon as possible.
|
||||
// NOTE Using the page.arena assumes that the scheduling loop does use this object after invoking the callback
|
||||
// If that changes we may want to consider storing DelayedNavigation in the session instead.
|
||||
const arena = self.arena;
|
||||
const navi = try arena.create(DelayedNavigation);
|
||||
navi.* = .{
|
||||
.session = self.session,
|
||||
.href = try arena.dupe(u8, href),
|
||||
};
|
||||
_ = try self.state.loop.timeout(0, &navi.navigate_node);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
const DelayedNavigation = struct {
|
||||
navigate_node: Loop.CallbackNode = .{ .func = DelayedNavigation.delay_navigate },
|
||||
session: *Session,
|
||||
href: []const u8,
|
||||
|
||||
fn delay_navigate(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
||||
_ = repeat_delay;
|
||||
const self: *DelayedNavigation = @fieldParentPtr("navigate_node", node);
|
||||
self.session.pageNavigate(self.href) catch |err| {
|
||||
log.err("Delayed navigation error {}", .{err}); // TODO: should we trigger a specific event here?
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const Script = struct {
|
||||
kind: Kind,
|
||||
is_async: bool,
|
||||
is_defer: bool,
|
||||
src: ?[]const u8,
|
||||
element: *parser.Element,
|
||||
// The javascript to load after we successfully load the script
|
||||
onload: ?[]const u8,
|
||||
|
||||
// The javascript to load if we have an error executing the script
|
||||
// For now, we ignore this, since we still have a lot of errors that we
|
||||
// shouldn't
|
||||
//onerror: ?[]const u8,
|
||||
|
||||
const Kind = enum {
|
||||
module,
|
||||
javascript,
|
||||
};
|
||||
|
||||
fn init(e: *parser.Element) !?Script {
|
||||
// ignore non-script tags
|
||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
|
||||
if (tag != .script) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (try parser.elementGetAttribute(e, "nomodule") != null) {
|
||||
// these scripts should only be loaded if we don't support modules
|
||||
// but since we do support modules, we can just skip them.
|
||||
return null;
|
||||
}
|
||||
|
||||
const kind = parseKind(try parser.elementGetAttribute(e, "type")) orelse {
|
||||
return null;
|
||||
};
|
||||
|
||||
return .{
|
||||
.kind = kind,
|
||||
.element = e,
|
||||
.src = try parser.elementGetAttribute(e, "src"),
|
||||
.onload = try parser.elementGetAttribute(e, "onload"),
|
||||
.is_async = try parser.elementGetAttribute(e, "async") != null,
|
||||
.is_defer = try parser.elementGetAttribute(e, "defer") != null,
|
||||
};
|
||||
}
|
||||
|
||||
// > type
|
||||
// > Attribute is not set (default), an empty string, or a JavaScript MIME
|
||||
// > type indicates that the script is a "classic script", containing
|
||||
// > JavaScript code.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
|
||||
fn parseKind(script_type_: ?[]const u8) ?Kind {
|
||||
const script_type = script_type_ orelse return .javascript;
|
||||
if (script_type.len == 0) {
|
||||
return .javascript;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, script_type, "application/javascript")) return .javascript;
|
||||
if (std.mem.eql(u8, script_type, "text/javascript")) return .javascript;
|
||||
if (std.mem.eql(u8, script_type, "module")) return .module;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn eval(self: *const Script, page: *Page, body: []const u8) !void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(page.scope);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const src = self.src orelse "inline";
|
||||
const res = switch (self.kind) {
|
||||
.javascript => page.scope.exec(body, src),
|
||||
.module => page.scope.module(body, src),
|
||||
} catch {
|
||||
if (try try_catch.err(page.arena)) |msg| {
|
||||
log.info("eval script {s}: {s}", .{ src, msg });
|
||||
}
|
||||
return error.JsErr;
|
||||
};
|
||||
|
||||
if (builtin.mode == .Debug) {
|
||||
const msg = try res.toString(page.arena);
|
||||
log.debug("eval script {s}: {s}", .{ src, msg });
|
||||
}
|
||||
|
||||
if (self.onload) |onload| {
|
||||
_ = page.scope.exec(onload, "script_on_load") catch {
|
||||
if (try try_catch.err(page.arena)) |msg| {
|
||||
log.info("eval script onload {s}: {s}", .{ src, msg });
|
||||
}
|
||||
return error.JsErr;
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
pub const NavigateReason = enum {
|
||||
anchor,
|
||||
address_bar,
|
||||
};
|
||||
|
||||
const NavigateOpts = struct {
|
||||
reason: NavigateReason = .address_bar,
|
||||
};
|
||||
|
||||
// provide very poor abstration to the rest of the code. In theory, we can change
|
||||
// the FlatRenderer to a different implementation, and it'll all just work.
|
||||
pub const Renderer = FlatRenderer;
|
||||
|
||||
// This "renderer" positions elements in a single row in an unspecified order.
|
||||
// The important thing is that elements have a consistent position/index within
|
||||
// that row, which can be turned into a rectangle.
|
||||
const FlatRenderer = struct {
|
||||
allocator: Allocator,
|
||||
|
||||
// key is a @ptrFromInt of the element
|
||||
// value is the index position
|
||||
positions: std.AutoHashMapUnmanaged(u64, u32),
|
||||
|
||||
// given an index, get the element
|
||||
elements: std.ArrayListUnmanaged(u64),
|
||||
|
||||
const Element = @import("dom/element.zig").Element;
|
||||
|
||||
// we expect allocator to be an arena
|
||||
pub fn init(allocator: Allocator) FlatRenderer {
|
||||
return .{
|
||||
.elements = .{},
|
||||
.positions = .{},
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getRect(self: *FlatRenderer, e: *parser.Element) !Element.DOMRect {
|
||||
var elements = &self.elements;
|
||||
const gop = try self.positions.getOrPut(self.allocator, @intFromPtr(e));
|
||||
var x: u32 = gop.value_ptr.*;
|
||||
if (gop.found_existing == false) {
|
||||
x = @intCast(elements.items.len);
|
||||
try elements.append(self.allocator, @intFromPtr(e));
|
||||
gop.value_ptr.* = x;
|
||||
}
|
||||
|
||||
return .{
|
||||
.x = @floatFromInt(x),
|
||||
.y = 0.0,
|
||||
.width = 1.0,
|
||||
.height = 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn boundingRect(self: *const FlatRenderer) Element.DOMRect {
|
||||
return .{
|
||||
.x = 0.0,
|
||||
.y = 0.0,
|
||||
.width = @floatFromInt(self.width()),
|
||||
.height = @floatFromInt(self.height()),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn width(self: *const FlatRenderer) u32 {
|
||||
return @max(@as(u32, @intCast(self.elements.items.len)), 1); // At least 1 pixel even if empty
|
||||
}
|
||||
|
||||
pub fn height(_: *const FlatRenderer) u32 {
|
||||
return 1;
|
||||
}
|
||||
|
||||
pub fn getElementAtPosition(self: *const FlatRenderer, x: i32, y: i32) ?*parser.Element {
|
||||
if (y != 0 or x < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const elements = self.elements.items;
|
||||
return if (x < elements.len) @ptrFromInt(elements[@intCast(x)]) else null;
|
||||
}
|
||||
};
|
||||
|
||||
fn timestamp() u32 {
|
||||
const ts = std.posix.clock_gettime(std.posix.CLOCK.MONOTONIC) catch unreachable;
|
||||
return @intCast(ts.sec);
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "Browser" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
// 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 parser = @import("../netsurf.zig");
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
|
||||
@@ -17,14 +17,12 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Element = @import("element.zig").Element;
|
||||
const Document = @import("document.zig").Document;
|
||||
|
||||
pub const Interfaces = .{
|
||||
IntersectionObserver,
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
// 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 parser = @import("../netsurf.zig");
|
||||
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
@@ -5,9 +5,9 @@ const URL = @import("../url.zig").URL;
|
||||
const js = @import("../runtime/js.zig");
|
||||
const storage = @import("storage/storage.zig");
|
||||
const generate = @import("../runtime/generate.zig");
|
||||
const Renderer = @import("renderer.zig").Renderer;
|
||||
const Loop = @import("../runtime/loop.zig").Loop;
|
||||
const HttpClient = @import("../http/client.zig").Client;
|
||||
const Renderer = @import("browser.zig").Renderer;
|
||||
|
||||
const WebApis = struct {
|
||||
// Wrapped like this for debug ergonomics.
|
||||
|
||||
678
src/browser/page.zig
Normal file
678
src/browser/page.zig
Normal file
@@ -0,0 +1,678 @@
|
||||
// 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 builtin = @import("builtin");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Dump = @import("dump.zig");
|
||||
const Mime = @import("mime.zig").Mime;
|
||||
const DataURI = @import("datauri.zig").DataURI;
|
||||
const Session = @import("session.zig").Session;
|
||||
const Renderer = @import("renderer.zig").Renderer;
|
||||
const SessionState = @import("env.zig").SessionState;
|
||||
const Window = @import("html/window.zig").Window;
|
||||
const Walker = @import("dom/walker.zig").WalkerDepthFirst;
|
||||
const Env = @import("env.zig").Env;
|
||||
const Loop = @import("../runtime/loop.zig").Loop;
|
||||
|
||||
const URL = @import("../url.zig").URL;
|
||||
|
||||
const parser = @import("netsurf.zig");
|
||||
const http = @import("../http/client.zig");
|
||||
const storage = @import("storage/storage.zig");
|
||||
|
||||
const polyfill = @import("polyfill/polyfill.zig");
|
||||
|
||||
const log = std.log.scoped(.page);
|
||||
|
||||
// Page navigates to an url.
|
||||
// You can navigates multiple urls with the same page, but you have to call
|
||||
// end() to stop the previous navigation before starting a new one.
|
||||
// The page handle all its memory in an arena allocator. The arena is reseted
|
||||
// when end() is called.
|
||||
pub const Page = struct {
|
||||
session: *Session,
|
||||
|
||||
// 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
|
||||
url: URL,
|
||||
|
||||
raw_data: ?[]const u8,
|
||||
|
||||
renderer: Renderer,
|
||||
|
||||
microtask_node: Loop.CallbackNode,
|
||||
|
||||
window_clicked_event_node: parser.EventNode,
|
||||
|
||||
scope: *Env.Scope,
|
||||
|
||||
// List of modules currently fetched/loaded.
|
||||
module_map: std.StringHashMapUnmanaged([]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,
|
||||
|
||||
pub fn init(self: *Page, arena: Allocator, session: *Session) !void {
|
||||
const browser = session.browser;
|
||||
self.* = .{
|
||||
.window = try Window.create(null, null),
|
||||
.arena = arena,
|
||||
.doc = null,
|
||||
.raw_data = null,
|
||||
.url = URL.empty,
|
||||
.session = session,
|
||||
.renderer = Renderer.init(arena),
|
||||
.microtask_node = .{ .func = microtaskCallback },
|
||||
.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),
|
||||
.module_map = .empty,
|
||||
};
|
||||
|
||||
// load polyfills
|
||||
try polyfill.load(self.arena, self.scope);
|
||||
|
||||
// _ = try session.browser.app.loop.timeout(1 * std.time.ns_per_ms, &self.microtask_node);
|
||||
}
|
||||
|
||||
fn microtaskCallback(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
||||
const self: *Page = @fieldParentPtr("microtask_node", node);
|
||||
self.session.browser.runMicrotasks();
|
||||
repeat_delay.* = 1 * std.time.ns_per_ms;
|
||||
}
|
||||
|
||||
// dump writes the page content into the given file.
|
||||
pub fn dump(self: *const Page, out: std.fs.File) !void {
|
||||
// if no HTML document pointer available, dump the data content only.
|
||||
if (self.doc == null) {
|
||||
// no data loaded, nothing to do.
|
||||
if (self.raw_data == null) return;
|
||||
return try out.writeAll(self.raw_data.?);
|
||||
}
|
||||
|
||||
// if the page has a pointer to a document, dumps the HTML.
|
||||
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});
|
||||
|
||||
const base = if (self.current_script) |s| s.src else null;
|
||||
|
||||
const file_src = blk: {
|
||||
if (base) |_base| {
|
||||
break :blk try URL.stitch(self.arena, specifier, _base);
|
||||
} else break :blk specifier;
|
||||
};
|
||||
|
||||
if (self.module_map.get(file_src)) |module| return module;
|
||||
|
||||
const module = try self.fetchData(specifier, base);
|
||||
if (module) |_module| try self.module_map.putNoClobber(self.arena, file_src, _module);
|
||||
return module;
|
||||
}
|
||||
|
||||
pub fn wait(self: *Page) !void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(self.scope);
|
||||
defer try_catch.deinit();
|
||||
|
||||
try self.session.browser.app.loop.run();
|
||||
|
||||
if (try_catch.hasCaught() == false) {
|
||||
log.debug("wait: OK", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = (try try_catch.err(self.arena)) orelse "unknown";
|
||||
log.info("wait error: {s}", .{msg});
|
||||
}
|
||||
|
||||
pub fn origin(self: *const Page, arena: Allocator) ![]const u8 {
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
try self.url.origin(arr.writer(arena));
|
||||
return arr.items;
|
||||
}
|
||||
|
||||
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
|
||||
pub fn navigate(self: *Page, request_url: URL, opts: NavigateOpts) !void {
|
||||
const arena = self.arena;
|
||||
const session = self.session;
|
||||
|
||||
log.debug("starting GET {s}", .{request_url});
|
||||
|
||||
// if the url is about:blank, nothing to do.
|
||||
if (std.mem.eql(u8, "about:blank", request_url.raw)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we don't clone url, because we're going to replace self.url
|
||||
// later in this function, with the final request url (since we might
|
||||
// redirect)
|
||||
self.url = request_url;
|
||||
|
||||
// load the data
|
||||
var request = try self.newHTTPRequest(.GET, &self.url, .{ .navigation = true });
|
||||
defer request.deinit();
|
||||
|
||||
session.browser.notification.dispatch(.page_navigate, &.{
|
||||
.url = &self.url,
|
||||
.reason = opts.reason,
|
||||
.timestamp = timestamp(),
|
||||
});
|
||||
|
||||
var response = try request.sendSync(.{});
|
||||
|
||||
// would be different than self.url in the case of a redirect
|
||||
self.url = try URL.fromURI(arena, request.request_uri);
|
||||
|
||||
const header = response.header;
|
||||
try session.cookie_jar.populateFromResponse(&self.url.uri, &header);
|
||||
|
||||
// TODO handle fragment in url.
|
||||
try self.window.replaceLocation(.{ .url = try self.url.toWebApi(arena) });
|
||||
|
||||
log.info("GET {any} {d}", .{ self.url, header.status });
|
||||
|
||||
const content_type = header.get("content-type");
|
||||
|
||||
const mime: Mime = blk: {
|
||||
if (content_type) |ct| {
|
||||
break :blk try Mime.parse(arena, ct);
|
||||
}
|
||||
break :blk Mime.sniff(try response.peek());
|
||||
} orelse .unknown;
|
||||
|
||||
if (mime.isHTML()) {
|
||||
try self.loadHTMLDoc(&response, mime.charset orelse "utf-8");
|
||||
} else {
|
||||
log.info("non-HTML document: {s}", .{content_type orelse "null"});
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
while (try response.next()) |data| {
|
||||
try arr.appendSlice(arena, try arena.dupe(u8, data));
|
||||
}
|
||||
// save the body into the page.
|
||||
self.raw_data = arr.items;
|
||||
}
|
||||
|
||||
session.browser.notification.dispatch(.page_navigated, &.{
|
||||
.url = &self.url,
|
||||
.timestamp = timestamp(),
|
||||
});
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/#read-html
|
||||
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void {
|
||||
const arena = self.arena;
|
||||
|
||||
log.debug("parse html with charset {s}", .{charset});
|
||||
|
||||
const ccharset = try arena.dupeZ(u8, charset);
|
||||
|
||||
const html_doc = try parser.documentHTMLParse(reader, ccharset);
|
||||
const doc = parser.documentHTMLToDocument(html_doc);
|
||||
|
||||
// save a document's pointer in the page.
|
||||
self.doc = doc;
|
||||
|
||||
const document_element = (try parser.documentGetDocumentElement(doc)) orelse return error.DocumentElementError;
|
||||
try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Element, document_element),
|
||||
"click",
|
||||
&self.window_clicked_event_node,
|
||||
false,
|
||||
);
|
||||
|
||||
// TODO set document.readyState to interactive
|
||||
// https://html.spec.whatwg.org/#reporting-document-loading-status
|
||||
|
||||
// inject the URL to the document including the fragment.
|
||||
try parser.documentSetDocumentURI(doc, self.url.raw);
|
||||
|
||||
// TODO set the referrer to the document.
|
||||
try self.window.replaceDocument(html_doc);
|
||||
self.window.setStorageShelf(
|
||||
try self.session.storage_shed.getOrPut(try self.origin(self.arena)),
|
||||
);
|
||||
|
||||
// https://html.spec.whatwg.org/#read-html
|
||||
|
||||
// update the sessions state
|
||||
self.state.document = html_doc;
|
||||
|
||||
// browse the DOM tree to retrieve scripts
|
||||
// TODO execute the synchronous scripts during the HTL parsing.
|
||||
// TODO fetch the script resources concurrently but execute them in the
|
||||
// declaration order for synchronous ones.
|
||||
|
||||
// async_scripts stores scripts which can be run asynchronously.
|
||||
// for now they are just run after the non-async one in order to
|
||||
// dispatch DOMContentLoaded the sooner as possible.
|
||||
var async_scripts: std.ArrayListUnmanaged(Script) = .{};
|
||||
|
||||
// defer_scripts stores scripts which are meant to be deferred. For now
|
||||
// this doesn't have a huge impact, since normal scripts are parsed
|
||||
// after the document is loaded. But (a) we should fix that and (b)
|
||||
// this results in JavaScript being loaded in the same order as browsers
|
||||
// which can help debug issues (and might actually fix issues if websites
|
||||
// are expecting this execution order)
|
||||
var defer_scripts: std.ArrayListUnmanaged(Script) = .{};
|
||||
|
||||
const root = parser.documentToNode(doc);
|
||||
const walker = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
while (true) {
|
||||
next = try walker.get_next(root, next) orelse break;
|
||||
|
||||
// ignore non-elements nodes.
|
||||
if (try parser.nodeType(next.?) != .element) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const e = parser.nodeToElement(next.?);
|
||||
|
||||
// ignore non-js script.
|
||||
const script = try Script.init(e) orelse continue;
|
||||
|
||||
// TODO use fetchpriority
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#fetchpriority
|
||||
|
||||
// > async
|
||||
// > For classic scripts, if the async attribute is present,
|
||||
// > then the classic script will be fetched in parallel to
|
||||
// > parsing and evaluated as soon as it is available.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async
|
||||
if (script.is_async) {
|
||||
try async_scripts.append(arena, script);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (script.is_defer) {
|
||||
try defer_scripts.append(arena, script);
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO handle for attribute
|
||||
// TODO handle event attribute
|
||||
|
||||
// > Scripts without async, defer or type="module"
|
||||
// > attributes, as well as inline scripts without the
|
||||
// > type="module" attribute, are fetched and executed
|
||||
// > immediately before the browser continues to parse the
|
||||
// > page.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
|
||||
self.evalScript(&script) catch |err| log.warn("evaljs: {any}", .{err});
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, null);
|
||||
}
|
||||
|
||||
for (defer_scripts.items) |s| {
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(s.element));
|
||||
self.evalScript(&s) catch |err| log.warn("evaljs: {any}", .{err});
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, null);
|
||||
}
|
||||
|
||||
// dispatch DOMContentLoaded before the transition to "complete",
|
||||
// at the point where all subresources apart from async script elements
|
||||
// have loaded.
|
||||
// https://html.spec.whatwg.org/#reporting-document-loading-status
|
||||
const evt = try parser.eventCreate();
|
||||
defer parser.eventDestroy(evt);
|
||||
|
||||
try parser.eventInit(evt, "DOMContentLoaded", .{ .bubbles = true, .cancelable = true });
|
||||
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt);
|
||||
|
||||
// eval async scripts.
|
||||
for (async_scripts.items) |s| {
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(s.element));
|
||||
self.evalScript(&s) catch |err| log.warn("evaljs: {any}", .{err});
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, null);
|
||||
}
|
||||
|
||||
// TODO wait for async scripts
|
||||
|
||||
// TODO set document.readyState to complete
|
||||
|
||||
// dispatch window.load event
|
||||
const loadevt = try parser.eventCreate();
|
||||
defer parser.eventDestroy(loadevt);
|
||||
|
||||
try parser.eventInit(loadevt, "load", .{});
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(Window, &self.window),
|
||||
loadevt,
|
||||
);
|
||||
}
|
||||
|
||||
// evalScript evaluates the src in priority.
|
||||
// if no src is present, we evaluate the text source.
|
||||
// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model
|
||||
fn evalScript(self: *Page, script: *const Script) !void {
|
||||
const src = script.src orelse {
|
||||
// source is inline
|
||||
// TODO handle charset attribute
|
||||
if (try parser.nodeTextContent(parser.elementToNode(script.element))) |text| {
|
||||
try script.eval(self, text);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
self.current_script = script;
|
||||
defer self.current_script = null;
|
||||
|
||||
log.debug("starting GET {s}", .{src});
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
|
||||
const body = (try self.fetchData(src, null)) orelse {
|
||||
// TODO If el's result is null, then fire an event named error at
|
||||
// el, and return
|
||||
return;
|
||||
};
|
||||
|
||||
script.eval(self, body) catch |err| switch (err) {
|
||||
error.JsErr => {}, // nothing to do here.
|
||||
else => return err,
|
||||
};
|
||||
|
||||
// TODO If el's from an external file is true, then fire an event
|
||||
// named load at el.
|
||||
}
|
||||
|
||||
// fetchData returns the data corresponding to the src target.
|
||||
// It resolves src using the page's uri.
|
||||
// If a base path is given, src is resolved according to the base first.
|
||||
// the caller owns the returned string
|
||||
fn fetchData(self: *const Page, src: []const u8, base: ?[]const u8) !?[]const u8 {
|
||||
log.debug("starting fetch {s}", .{src});
|
||||
|
||||
const arena = self.arena;
|
||||
|
||||
// Handle data URIs.
|
||||
if (try DataURI.parse(arena, src)) |data_uri| {
|
||||
return data_uri.data;
|
||||
}
|
||||
|
||||
var res_src = src;
|
||||
|
||||
// if a base path is given, we resolve src using base.
|
||||
if (base) |_base| {
|
||||
res_src = try URL.stitch(arena, src, _base);
|
||||
}
|
||||
|
||||
var origin_url = &self.url;
|
||||
const url = try origin_url.resolve(arena, res_src);
|
||||
|
||||
var request = try self.newHTTPRequest(.GET, &url, .{
|
||||
.origin_uri = &origin_url.uri,
|
||||
.navigation = false,
|
||||
});
|
||||
defer request.deinit();
|
||||
|
||||
var response = try request.sendSync(.{});
|
||||
var header = response.header;
|
||||
try self.session.cookie_jar.populateFromResponse(&url.uri, &header);
|
||||
|
||||
log.info("fetch {any}: {d}", .{ url, header.status });
|
||||
|
||||
if (header.status != 200) {
|
||||
return error.BadStatusCode;
|
||||
}
|
||||
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
while (try response.next()) |data| {
|
||||
try arr.appendSlice(arena, try arena.dupe(u8, data));
|
||||
}
|
||||
|
||||
// TODO check content-type
|
||||
|
||||
// check no body
|
||||
if (arr.items.len == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return arr.items;
|
||||
}
|
||||
|
||||
fn newHTTPRequest(self: *const Page, method: http.Request.Method, url: *const URL, opts: storage.cookie.LookupOpts) !http.Request {
|
||||
var request = try self.state.http_client.request(method, &url.uri);
|
||||
errdefer request.deinit();
|
||||
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
try self.state.cookie_jar.forRequest(&url.uri, arr.writer(self.arena), opts);
|
||||
|
||||
if (arr.items.len > 0) {
|
||||
try request.addHeader("Cookie", arr.items, .{});
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
pub const MouseEvent = struct {
|
||||
x: i32,
|
||||
y: i32,
|
||||
type: Type,
|
||||
|
||||
const Type = enum {
|
||||
pressed,
|
||||
released,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn mouseEvent(self: *Page, me: MouseEvent) !void {
|
||||
if (me.type != .pressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = self.renderer.getElementAtPosition(me.x, me.y) orelse return;
|
||||
|
||||
const event = try parser.mouseEventCreate();
|
||||
defer parser.mouseEventDestroy(event);
|
||||
try parser.mouseEventInit(event, "click", .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.x = me.x,
|
||||
.y = me.y,
|
||||
});
|
||||
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
|
||||
}
|
||||
|
||||
fn windowClicked(node: *parser.EventNode, event: *parser.Event) void {
|
||||
const self: *Page = @fieldParentPtr("window_clicked_event_node", node);
|
||||
self._windowClicked(event) catch |err| {
|
||||
log.err("window click handler: {}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn _windowClicked(self: *Page, event: *parser.Event) !void {
|
||||
const target = (try parser.eventTarget(event)) orelse return;
|
||||
|
||||
const node = parser.eventTargetToNode(target);
|
||||
if (try parser.nodeType(node) != .element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const html_element: *parser.ElementHTML = @ptrCast(node);
|
||||
switch (try parser.elementHTMLGetTagType(html_element)) {
|
||||
.a => {
|
||||
const element: *parser.Element = @ptrCast(node);
|
||||
const href = (try parser.elementGetAttribute(element, "href")) orelse return;
|
||||
|
||||
// We cannot navigate immediately as navigating will delete the DOM tree, which holds this event's node.
|
||||
// As such we schedule the function to be called as soon as possible.
|
||||
// NOTE Using the page.arena assumes that the scheduling loop does use this object after invoking the callback
|
||||
// If that changes we may want to consider storing DelayedNavigation in the session instead.
|
||||
const arena = self.arena;
|
||||
const navi = try arena.create(DelayedNavigation);
|
||||
navi.* = .{
|
||||
.session = self.session,
|
||||
.href = try arena.dupe(u8, href),
|
||||
};
|
||||
_ = try self.state.loop.timeout(0, &navi.navigate_node);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
const DelayedNavigation = struct {
|
||||
navigate_node: Loop.CallbackNode = .{ .func = DelayedNavigation.delay_navigate },
|
||||
session: *Session,
|
||||
href: []const u8,
|
||||
|
||||
fn delay_navigate(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
||||
_ = repeat_delay;
|
||||
const self: *DelayedNavigation = @fieldParentPtr("navigate_node", node);
|
||||
self.session.pageNavigate(self.href) catch |err| {
|
||||
log.err("Delayed navigation error {}", .{err}); // TODO: should we trigger a specific event here?
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const Script = struct {
|
||||
kind: Kind,
|
||||
is_async: bool,
|
||||
is_defer: bool,
|
||||
src: ?[]const u8,
|
||||
element: *parser.Element,
|
||||
// The javascript to load after we successfully load the script
|
||||
onload: ?[]const u8,
|
||||
|
||||
// The javascript to load if we have an error executing the script
|
||||
// For now, we ignore this, since we still have a lot of errors that we
|
||||
// shouldn't
|
||||
//onerror: ?[]const u8,
|
||||
|
||||
const Kind = enum {
|
||||
module,
|
||||
javascript,
|
||||
};
|
||||
|
||||
fn init(e: *parser.Element) !?Script {
|
||||
// ignore non-script tags
|
||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
|
||||
if (tag != .script) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (try parser.elementGetAttribute(e, "nomodule") != null) {
|
||||
// these scripts should only be loaded if we don't support modules
|
||||
// but since we do support modules, we can just skip them.
|
||||
return null;
|
||||
}
|
||||
|
||||
const kind = parseKind(try parser.elementGetAttribute(e, "type")) orelse {
|
||||
return null;
|
||||
};
|
||||
|
||||
return .{
|
||||
.kind = kind,
|
||||
.element = e,
|
||||
.src = try parser.elementGetAttribute(e, "src"),
|
||||
.onload = try parser.elementGetAttribute(e, "onload"),
|
||||
.is_async = try parser.elementGetAttribute(e, "async") != null,
|
||||
.is_defer = try parser.elementGetAttribute(e, "defer") != null,
|
||||
};
|
||||
}
|
||||
|
||||
// > type
|
||||
// > Attribute is not set (default), an empty string, or a JavaScript MIME
|
||||
// > type indicates that the script is a "classic script", containing
|
||||
// > JavaScript code.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
|
||||
fn parseKind(script_type_: ?[]const u8) ?Kind {
|
||||
const script_type = script_type_ orelse return .javascript;
|
||||
if (script_type.len == 0) {
|
||||
return .javascript;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, script_type, "application/javascript")) return .javascript;
|
||||
if (std.mem.eql(u8, script_type, "text/javascript")) return .javascript;
|
||||
if (std.mem.eql(u8, script_type, "module")) return .module;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn eval(self: *const Script, page: *Page, body: []const u8) !void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(page.scope);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const src = self.src orelse "inline";
|
||||
const res = switch (self.kind) {
|
||||
.javascript => page.scope.exec(body, src),
|
||||
.module => page.scope.module(body, src),
|
||||
} catch {
|
||||
if (try try_catch.err(page.arena)) |msg| {
|
||||
log.info("eval script {s}: {s}", .{ src, msg });
|
||||
}
|
||||
return error.JsErr;
|
||||
};
|
||||
|
||||
if (builtin.mode == .Debug) {
|
||||
const msg = try res.toString(page.arena);
|
||||
log.debug("eval script {s}: {s}", .{ src, msg });
|
||||
}
|
||||
|
||||
if (self.onload) |onload| {
|
||||
_ = page.scope.exec(onload, "script_on_load") catch {
|
||||
if (try try_catch.err(page.arena)) |msg| {
|
||||
log.info("eval script onload {s}: {s}", .{ src, msg });
|
||||
}
|
||||
return error.JsErr;
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
pub const NavigateReason = enum {
|
||||
anchor,
|
||||
address_bar,
|
||||
};
|
||||
|
||||
const NavigateOpts = struct {
|
||||
reason: NavigateReason = .address_bar,
|
||||
};
|
||||
|
||||
fn timestamp() u32 {
|
||||
const ts = std.posix.clock_gettime(std.posix.CLOCK.MONOTONIC) catch unreachable;
|
||||
return @intCast(ts.sec);
|
||||
}
|
||||
96
src/browser/renderer.zig
Normal file
96
src/browser/renderer.zig
Normal file
@@ -0,0 +1,96 @@
|
||||
// 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 parser = @import("netsurf.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// provide very poor abstration to the rest of the code. In theory, we can change
|
||||
// the FlatRenderer to a different implementation, and it'll all just work.
|
||||
pub const Renderer = FlatRenderer;
|
||||
|
||||
// This "renderer" positions elements in a single row in an unspecified order.
|
||||
// The important thing is that elements have a consistent position/index within
|
||||
// that row, which can be turned into a rectangle.
|
||||
const FlatRenderer = struct {
|
||||
allocator: Allocator,
|
||||
|
||||
// key is a @ptrFromInt of the element
|
||||
// value is the index position
|
||||
positions: std.AutoHashMapUnmanaged(u64, u32),
|
||||
|
||||
// given an index, get the element
|
||||
elements: std.ArrayListUnmanaged(u64),
|
||||
|
||||
const Element = @import("dom/element.zig").Element;
|
||||
|
||||
// we expect allocator to be an arena
|
||||
pub fn init(allocator: Allocator) FlatRenderer {
|
||||
return .{
|
||||
.elements = .{},
|
||||
.positions = .{},
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getRect(self: *FlatRenderer, e: *parser.Element) !Element.DOMRect {
|
||||
var elements = &self.elements;
|
||||
const gop = try self.positions.getOrPut(self.allocator, @intFromPtr(e));
|
||||
var x: u32 = gop.value_ptr.*;
|
||||
if (gop.found_existing == false) {
|
||||
x = @intCast(elements.items.len);
|
||||
try elements.append(self.allocator, @intFromPtr(e));
|
||||
gop.value_ptr.* = x;
|
||||
}
|
||||
|
||||
return .{
|
||||
.x = @floatFromInt(x),
|
||||
.y = 0.0,
|
||||
.width = 1.0,
|
||||
.height = 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn boundingRect(self: *const FlatRenderer) Element.DOMRect {
|
||||
return .{
|
||||
.x = 0.0,
|
||||
.y = 0.0,
|
||||
.width = @floatFromInt(self.width()),
|
||||
.height = @floatFromInt(self.height()),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn width(self: *const FlatRenderer) u32 {
|
||||
return @max(@as(u32, @intCast(self.elements.items.len)), 1); // At least 1 pixel even if empty
|
||||
}
|
||||
|
||||
pub fn height(_: *const FlatRenderer) u32 {
|
||||
return 1;
|
||||
}
|
||||
|
||||
pub fn getElementAtPosition(self: *const FlatRenderer, x: i32, y: i32) ?*parser.Element {
|
||||
if (y != 0 or x < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const elements = self.elements.items;
|
||||
return if (x < elements.len) @ptrFromInt(elements[@intCast(x)]) else null;
|
||||
}
|
||||
};
|
||||
132
src/browser/session.zig
Normal file
132
src/browser/session.zig
Normal file
@@ -0,0 +1,132 @@
|
||||
// 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 ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const Env = @import("env.zig").Env;
|
||||
const Page = @import("page.zig").Page;
|
||||
const Browser = @import("browser.zig").Browser;
|
||||
|
||||
const parser = @import("netsurf.zig");
|
||||
const storage = @import("storage/storage.zig");
|
||||
|
||||
const log = std.log.scoped(.session);
|
||||
|
||||
// Session is like a browser's tab.
|
||||
// It owns the js env and the loader for all the pages of the session.
|
||||
// You can create successively multiple pages for a session, but you must
|
||||
// deinit a page before running another one.
|
||||
pub const Session = struct {
|
||||
browser: *Browser,
|
||||
|
||||
// Used to create our Inspector and in the BrowserContext.
|
||||
arena: ArenaAllocator,
|
||||
|
||||
executor: Env.Executor,
|
||||
storage_shed: storage.Shed,
|
||||
cookie_jar: storage.CookieJar,
|
||||
|
||||
page: ?Page = null,
|
||||
|
||||
pub fn init(self: *Session, browser: *Browser) !void {
|
||||
var executor = try browser.env.newExecutor();
|
||||
errdefer executor.deinit();
|
||||
|
||||
const allocator = browser.app.allocator;
|
||||
self.* = .{
|
||||
.browser = browser,
|
||||
.executor = executor,
|
||||
.arena = ArenaAllocator.init(allocator),
|
||||
.storage_shed = storage.Shed.init(allocator),
|
||||
.cookie_jar = storage.CookieJar.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Session) void {
|
||||
if (self.page != null) {
|
||||
self.removePage();
|
||||
}
|
||||
self.arena.deinit();
|
||||
self.cookie_jar.deinit();
|
||||
self.storage_shed.deinit();
|
||||
self.executor.deinit();
|
||||
}
|
||||
|
||||
// NOTE: the caller is not the owner of the returned value,
|
||||
// the pointer on Page is just returned as a convenience
|
||||
pub fn createPage(self: *Session) !*Page {
|
||||
std.debug.assert(self.page == null);
|
||||
|
||||
// Start netsurf memory arena.
|
||||
// We need to init this early as JS event handlers may be registered through Runtime.evaluate before the first html doc is loaded
|
||||
try parser.init();
|
||||
|
||||
const page_arena = &self.browser.page_arena;
|
||||
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
|
||||
self.page = @as(Page, undefined);
|
||||
const page = &self.page.?;
|
||||
try Page.init(page, page_arena.allocator(), self);
|
||||
|
||||
// start JS env
|
||||
log.debug("start new js scope", .{});
|
||||
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
||||
self.browser.notification.dispatch(.page_created, page);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
pub fn removePage(self: *Session) void {
|
||||
// Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one
|
||||
self.browser.notification.dispatch(.page_remove, .{});
|
||||
|
||||
std.debug.assert(self.page != null);
|
||||
// Reset all existing callbacks.
|
||||
self.browser.app.loop.reset();
|
||||
self.executor.endScope();
|
||||
self.page = null;
|
||||
|
||||
// clear netsurf memory arena.
|
||||
parser.deinit();
|
||||
}
|
||||
|
||||
pub fn currentPage(self: *Session) ?*Page {
|
||||
return &(self.page orelse return null);
|
||||
}
|
||||
|
||||
pub fn pageNavigate(self: *Session, url_string: []const u8) !void {
|
||||
// currently, this is only called from the page, so let's hope
|
||||
// it isn't null!
|
||||
std.debug.assert(self.page != null);
|
||||
|
||||
// can't use the page arena, because we're about to reset it
|
||||
// and don't want to use the session's arena, because that'll start to
|
||||
// look like a leak if we navigate from page to page a lot.
|
||||
var buf: [2048]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buf);
|
||||
const url = try self.page.?.url.resolve(fba.allocator(), url_string);
|
||||
|
||||
self.removePage();
|
||||
var page = try self.createPage();
|
||||
return page.navigate(url, .{
|
||||
.reason = .anchor,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -20,7 +20,6 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const DOMError = @import("../netsurf.zig").DOMError;
|
||||
const DOMException = @import("../dom/exceptions.zig").DOMException;
|
||||
|
||||
const ProgressEvent = @import("progress_event.zig").ProgressEvent;
|
||||
const XMLHttpRequestEventTarget = @import("event_target.zig").XMLHttpRequestEventTarget;
|
||||
|
||||
@@ -24,8 +24,8 @@ const App = @import("../app.zig").App;
|
||||
const Env = @import("../browser/env.zig").Env;
|
||||
const asUint = @import("../str/parser.zig").asUint;
|
||||
const Browser = @import("../browser/browser.zig").Browser;
|
||||
const Session = @import("../browser/browser.zig").Session;
|
||||
const Page = @import("../browser/browser.zig").Page;
|
||||
const Session = @import("../browser/session.zig").Session;
|
||||
const Page = @import("../browser/page.zig").Page;
|
||||
const Inspector = @import("../browser/env.zig").Env.Inspector;
|
||||
const Incrementing = @import("../id.zig").Incrementing;
|
||||
const Notification = @import("../notification.zig").Notification;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Page = @import("../../browser/browser.zig").Page;
|
||||
const Page = @import("../../browser/page.zig").Page;
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
|
||||
const std = @import("std");
|
||||
const URL = @import("../../url.zig").URL;
|
||||
const Page = @import("../../browser/page.zig").Page;
|
||||
const Notification = @import("../../notification.zig").Notification;
|
||||
const Page = @import("../../browser/browser.zig").Page;
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const std = @import("std");
|
||||
|
||||
const URL = @import("url.zig").URL;
|
||||
const browser = @import("browser/browser.zig");
|
||||
const page = @import("browser/page.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
@@ -64,7 +64,7 @@ pub const Notification = struct {
|
||||
|
||||
const Events = union(enum) {
|
||||
page_remove: PageRemove,
|
||||
page_created: *browser.Page,
|
||||
page_created: *page.Page,
|
||||
page_navigate: *const PageNavigate,
|
||||
page_navigated: *const PageNavigated,
|
||||
notification_created: *Notification,
|
||||
@@ -76,7 +76,7 @@ pub const Notification = struct {
|
||||
pub const PageNavigate = struct {
|
||||
timestamp: u32,
|
||||
url: *const URL,
|
||||
reason: browser.NavigateReason,
|
||||
reason: page.NavigateReason,
|
||||
};
|
||||
|
||||
pub const PageNavigated = struct {
|
||||
|
||||
@@ -22,8 +22,6 @@ const MemoryPool = std.heap.MemoryPool;
|
||||
|
||||
pub const IO = @import("tigerbeetle-io").IO;
|
||||
|
||||
const JSCallback = @import("../browser/env.zig").Env.Callback;
|
||||
|
||||
const log = std.log.scoped(.loop);
|
||||
|
||||
// SingleThreaded I/O Loop based on Tigerbeetle io_uring loop.
|
||||
|
||||
@@ -371,7 +371,7 @@ pub const JsRunner = struct {
|
||||
const HttpClient = @import("http/client.zig").Client;
|
||||
const storage = @import("browser/storage/storage.zig");
|
||||
const Window = @import("browser/html/window.zig").Window;
|
||||
const Renderer = @import("browser/browser.zig").Renderer;
|
||||
const Renderer = @import("browser/renderer.zig").Renderer;
|
||||
const SessionState = @import("browser/env.zig").SessionState;
|
||||
|
||||
url: URL,
|
||||
|
||||
Reference in New Issue
Block a user