mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 07:03:29 +00:00
Merge pull request #345 from lightpanda-io/modules
browser: support for modules
This commit is contained in:
2
.github/actions/install/action.yml
vendored
2
.github/actions/install/action.yml
vendored
@@ -17,7 +17,7 @@ inputs:
|
|||||||
zig-v8:
|
zig-v8:
|
||||||
description: 'zig v8 version to install'
|
description: 'zig v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
default: 'v0.1.9'
|
default: 'v0.1.11'
|
||||||
v8:
|
v8:
|
||||||
description: 'v8 version to install'
|
description: 'v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const Mime = @import("mime.zig");
|
|||||||
const jsruntime = @import("jsruntime");
|
const jsruntime = @import("jsruntime");
|
||||||
const Loop = jsruntime.Loop;
|
const Loop = jsruntime.Loop;
|
||||||
const Env = jsruntime.Env;
|
const Env = jsruntime.Env;
|
||||||
|
const Module = jsruntime.Module;
|
||||||
|
|
||||||
const apiweb = @import("../apiweb.zig");
|
const apiweb = @import("../apiweb.zig");
|
||||||
|
|
||||||
@@ -125,6 +126,21 @@ pub const Session = struct {
|
|||||||
try self.env.load(&self.jstypes);
|
try self.env.load(&self.jstypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fetchModule(ctx: *anyopaque, referrer: ?jsruntime.Module, specifier: []const u8) !jsruntime.Module {
|
||||||
|
_ = referrer;
|
||||||
|
|
||||||
|
const self: *Session = @ptrCast(@alignCast(ctx));
|
||||||
|
|
||||||
|
if (self.page == null) return error.NoPage;
|
||||||
|
|
||||||
|
log.debug("fetch module: specifier: {s}", .{specifier});
|
||||||
|
const alloc = self.arena.allocator();
|
||||||
|
const body = try self.page.?.fetchData(alloc, specifier);
|
||||||
|
defer alloc.free(body);
|
||||||
|
|
||||||
|
return self.env.compileModule(body, specifier);
|
||||||
|
}
|
||||||
|
|
||||||
fn deinit(self: *Session) void {
|
fn deinit(self: *Session) void {
|
||||||
if (self.page) |*p| p.end();
|
if (self.page) |*p| p.end();
|
||||||
|
|
||||||
@@ -362,6 +378,9 @@ pub const Page = struct {
|
|||||||
log.debug("start js env", .{});
|
log.debug("start js env", .{});
|
||||||
try self.session.env.start();
|
try self.session.env.start();
|
||||||
|
|
||||||
|
// register the module loader
|
||||||
|
try self.session.env.setModuleLoadFn(self.session, Session.fetchModule);
|
||||||
|
|
||||||
// load polyfills
|
// load polyfills
|
||||||
try polyfill.load(alloc, self.session.env);
|
try polyfill.load(alloc, self.session.env);
|
||||||
|
|
||||||
@@ -388,7 +407,7 @@ pub const Page = struct {
|
|||||||
// sasync stores scripts which can be run asynchronously.
|
// sasync stores scripts which can be run asynchronously.
|
||||||
// for now they are just run after the non-async one in order to
|
// for now they are just run after the non-async one in order to
|
||||||
// dispatch DOMContentLoaded the sooner as possible.
|
// dispatch DOMContentLoaded the sooner as possible.
|
||||||
var sasync = std.ArrayList(*parser.Element).init(alloc);
|
var sasync = std.ArrayList(Script).init(alloc);
|
||||||
defer sasync.deinit();
|
defer sasync.deinit();
|
||||||
|
|
||||||
const root = parser.documentToNode(doc);
|
const root = parser.documentToNode(doc);
|
||||||
@@ -403,21 +422,10 @@ pub const Page = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const e = parser.nodeToElement(next.?);
|
const e = parser.nodeToElement(next.?);
|
||||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
|
|
||||||
|
|
||||||
// ignore non-script tags
|
|
||||||
if (tag != .script) continue;
|
|
||||||
|
|
||||||
// ignore non-js script.
|
// ignore non-js script.
|
||||||
// > type
|
const script = try Script.init(e) orelse continue;
|
||||||
// > Attribute is not set (default), an empty string, or a JavaScript MIME
|
if (script.kind == .unknown) continue;
|
||||||
// > 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
|
|
||||||
const stype = try parser.elementGetAttribute(e, "type");
|
|
||||||
if (!isJS(stype)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore the defer attribute b/c we analyze all script
|
// Ignore the defer attribute b/c we analyze all script
|
||||||
// after the document has been parsed.
|
// after the document has been parsed.
|
||||||
@@ -431,8 +439,8 @@ pub const Page = struct {
|
|||||||
// > then the classic script will be fetched in parallel to
|
// > then the classic script will be fetched in parallel to
|
||||||
// > parsing and evaluated as soon as it is available.
|
// > parsing and evaluated as soon as it is available.
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async
|
||||||
if (try parser.elementGetAttribute(e, "async") != null) {
|
if (script.isasync) {
|
||||||
try sasync.append(e);
|
try sasync.append(script);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,7 +463,7 @@ pub const Page = struct {
|
|||||||
// > page.
|
// > page.
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes
|
||||||
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
|
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
|
||||||
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
|
self.evalScript(script) catch |err| log.warn("evaljs: {any}", .{err});
|
||||||
try parser.documentHTMLSetCurrentScript(html_doc, null);
|
try parser.documentHTMLSetCurrentScript(html_doc, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,9 +480,9 @@ pub const Page = struct {
|
|||||||
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt);
|
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt);
|
||||||
|
|
||||||
// eval async scripts.
|
// eval async scripts.
|
||||||
for (sasync.items) |e| {
|
for (sasync.items) |s| {
|
||||||
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
|
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(s.element));
|
||||||
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
|
self.evalScript(s) catch |err| log.warn("evaljs: {any}", .{err});
|
||||||
try parser.documentHTMLSetCurrentScript(html_doc, null);
|
try parser.documentHTMLSetCurrentScript(html_doc, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,15 +504,15 @@ pub const Page = struct {
|
|||||||
// evalScript evaluates the src in priority.
|
// evalScript evaluates the src in priority.
|
||||||
// if no src is present, we evaluate the text source.
|
// if no src is present, we evaluate the text source.
|
||||||
// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model
|
// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model
|
||||||
fn evalScript(self: *Page, e: *parser.Element) !void {
|
fn evalScript(self: *Page, s: Script) !void {
|
||||||
const alloc = self.arena.allocator();
|
const alloc = self.arena.allocator();
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
|
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
|
||||||
const opt_src = try parser.elementGetAttribute(e, "src");
|
const opt_src = try parser.elementGetAttribute(s.element, "src");
|
||||||
if (opt_src) |src| {
|
if (opt_src) |src| {
|
||||||
log.debug("starting GET {s}", .{src});
|
log.debug("starting GET {s}", .{src});
|
||||||
|
|
||||||
self.fetchScript(src) catch |err| {
|
self.fetchScript(s) catch |err| {
|
||||||
switch (err) {
|
switch (err) {
|
||||||
FetchError.BadStatusCode => return err,
|
FetchError.BadStatusCode => return err,
|
||||||
|
|
||||||
@@ -523,26 +531,10 @@ pub const Page = struct {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var try_catch: jsruntime.TryCatch = undefined;
|
// TODO handle charset attribute
|
||||||
try_catch.init(self.session.env);
|
const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element));
|
||||||
defer try_catch.deinit();
|
|
||||||
|
|
||||||
const opt_text = try parser.nodeTextContent(parser.elementToNode(e));
|
|
||||||
if (opt_text) |text| {
|
if (opt_text) |text| {
|
||||||
// TODO handle charset attribute
|
try s.eval(alloc, self.session.env, text);
|
||||||
const res = self.session.env.exec(text, "") catch {
|
|
||||||
if (try try_catch.err(alloc, self.session.env)) |msg| {
|
|
||||||
defer alloc.free(msg);
|
|
||||||
log.info("eval inline {s}: {s}", .{ text, msg });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (builtin.mode == .Debug) {
|
|
||||||
const msg = try res.toString(alloc, self.session.env);
|
|
||||||
defer alloc.free(msg);
|
|
||||||
log.debug("eval inline {s}", .{msg});
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,12 +549,9 @@ pub const Page = struct {
|
|||||||
JsErr,
|
JsErr,
|
||||||
};
|
};
|
||||||
|
|
||||||
// fetchScript senf a GET request to the src and execute the script
|
// the caller owns the returned string
|
||||||
// received.
|
fn fetchData(self: *Page, alloc: std.mem.Allocator, src: []const u8) ![]const u8 {
|
||||||
fn fetchScript(self: *Page, src: []const u8) !void {
|
log.debug("starting fetch {s}", .{src});
|
||||||
const alloc = self.arena.allocator();
|
|
||||||
|
|
||||||
log.debug("starting fetch script {s}", .{src});
|
|
||||||
|
|
||||||
var buffer: [1024]u8 = undefined;
|
var buffer: [1024]u8 = undefined;
|
||||||
var b: []u8 = buffer[0..];
|
var b: []u8 = buffer[0..];
|
||||||
@@ -573,46 +562,91 @@ pub const Page = struct {
|
|||||||
|
|
||||||
const resp = fetchres.req.response;
|
const resp = fetchres.req.response;
|
||||||
|
|
||||||
log.info("fetch script {any}: {d}", .{ u, resp.status });
|
log.info("fetch {any}: {d}", .{ u, resp.status });
|
||||||
|
|
||||||
if (resp.status != .ok) return FetchError.BadStatusCode;
|
if (resp.status != .ok) return FetchError.BadStatusCode;
|
||||||
|
|
||||||
// TODO check content-type
|
// TODO check content-type
|
||||||
const body = try fetchres.req.reader().readAllAlloc(alloc, 16 * 1024 * 1024);
|
const body = try fetchres.req.reader().readAllAlloc(alloc, 16 * 1024 * 1024);
|
||||||
defer alloc.free(body);
|
|
||||||
|
|
||||||
// check no body
|
// check no body
|
||||||
if (body.len == 0) return FetchError.NoBody;
|
if (body.len == 0) return FetchError.NoBody;
|
||||||
|
|
||||||
var try_catch: jsruntime.TryCatch = undefined;
|
return body;
|
||||||
try_catch.init(self.session.env);
|
}
|
||||||
defer try_catch.deinit();
|
|
||||||
|
|
||||||
const res = self.session.env.exec(body, src) catch {
|
// fetchScript senf a GET request to the src and execute the script
|
||||||
if (try try_catch.err(alloc, self.session.env)) |msg| {
|
// received.
|
||||||
defer alloc.free(msg);
|
fn fetchScript(self: *Page, s: Script) !void {
|
||||||
log.info("eval remote {s}: {s}", .{ src, msg });
|
const alloc = self.arena.allocator();
|
||||||
}
|
const body = try self.fetchData(alloc, s.src);
|
||||||
return FetchError.JsErr;
|
defer alloc.free(body);
|
||||||
|
|
||||||
|
try s.eval(alloc, self.session.env, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Script = struct {
|
||||||
|
element: *parser.Element,
|
||||||
|
kind: Kind,
|
||||||
|
isasync: bool,
|
||||||
|
|
||||||
|
src: []const u8,
|
||||||
|
|
||||||
|
const Kind = enum {
|
||||||
|
unknown,
|
||||||
|
javascript,
|
||||||
|
module,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (builtin.mode == .Debug) {
|
fn init(e: *parser.Element) !?Script {
|
||||||
const msg = try res.toString(alloc, self.session.env);
|
// ignore non-script tags
|
||||||
defer alloc.free(msg);
|
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
|
||||||
log.debug("eval remote {s}: {s}", .{ src, msg });
|
if (tag != .script) return null;
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.element = e,
|
||||||
|
.kind = kind(try parser.elementGetAttribute(e, "type")),
|
||||||
|
.isasync = try parser.elementGetAttribute(e, "async") != null,
|
||||||
|
|
||||||
|
.src = try parser.elementGetAttribute(e, "src") orelse "inline",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// > type
|
// > type
|
||||||
// > Attribute is not set (default), an empty string, or a JavaScript MIME
|
// > Attribute is not set (default), an empty string, or a JavaScript MIME
|
||||||
// > type indicates that the script is a "classic script", containing
|
// > type indicates that the script is a "classic script", containing
|
||||||
// > JavaScript code.
|
// > 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
|
// 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 isJS(stype: ?[]const u8) bool {
|
fn kind(stype: ?[]const u8) Kind {
|
||||||
if (stype == null or stype.?.len == 0) return true;
|
if (stype == null or stype.?.len == 0) return .javascript;
|
||||||
if (std.mem.eql(u8, stype.?, "application/javascript")) return true;
|
if (std.mem.eql(u8, stype.?, "application/javascript")) return .javascript;
|
||||||
if (!std.mem.eql(u8, stype.?, "module")) return true;
|
if (std.mem.eql(u8, stype.?, "module")) return .module;
|
||||||
|
|
||||||
return false;
|
return .unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn eval(self: Script, alloc: std.mem.Allocator, env: Env, body: []const u8) !void {
|
||||||
|
var try_catch: jsruntime.TryCatch = undefined;
|
||||||
|
try_catch.init(env);
|
||||||
|
defer try_catch.deinit();
|
||||||
|
|
||||||
|
const res = switch (self.kind) {
|
||||||
|
.unknown => return error.UnknownScript,
|
||||||
|
.javascript => env.exec(body, self.src),
|
||||||
|
.module => env.module(body, self.src),
|
||||||
|
} catch {
|
||||||
|
if (try try_catch.err(alloc, env)) |msg| {
|
||||||
|
defer alloc.free(msg);
|
||||||
|
log.info("eval script {s}: {s}", .{ self.src, msg });
|
||||||
|
}
|
||||||
|
return FetchError.JsErr;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (builtin.mode == .Debug) {
|
||||||
|
const msg = try res.toString(alloc, env);
|
||||||
|
defer alloc.free(msg);
|
||||||
|
log.debug("eval script {s}: {s}", .{ self.src, msg });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
104
src/html/history.zig
Normal file
104
src/html/history.zig
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// 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 jsruntime = @import("jsruntime");
|
||||||
|
|
||||||
|
const Case = jsruntime.test_utils.Case;
|
||||||
|
const checkCases = jsruntime.test_utils.checkCases;
|
||||||
|
|
||||||
|
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
|
||||||
|
pub const History = struct {
|
||||||
|
pub const mem_guarantied = true;
|
||||||
|
|
||||||
|
const ScrollRestaurationMode = enum {
|
||||||
|
auto,
|
||||||
|
manual,
|
||||||
|
};
|
||||||
|
|
||||||
|
scrollRestauration: ScrollRestaurationMode = .audio,
|
||||||
|
|
||||||
|
pub fn get_length(_: *History) u64 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_appCodeName(_: *Navigator) []const u8 {
|
||||||
|
return "Mozilla";
|
||||||
|
}
|
||||||
|
pub fn get_appName(_: *Navigator) []const u8 {
|
||||||
|
return "Netscape";
|
||||||
|
}
|
||||||
|
pub fn get_appVersion(self: *Navigator) []const u8 {
|
||||||
|
return self.version;
|
||||||
|
}
|
||||||
|
pub fn get_platform(self: *Navigator) []const u8 {
|
||||||
|
return self.platform;
|
||||||
|
}
|
||||||
|
pub fn get_product(_: *Navigator) []const u8 {
|
||||||
|
return "Gecko";
|
||||||
|
}
|
||||||
|
pub fn get_productSub(_: *Navigator) []const u8 {
|
||||||
|
return "20030107";
|
||||||
|
}
|
||||||
|
pub fn get_vendor(self: *Navigator) []const u8 {
|
||||||
|
return self.vendor;
|
||||||
|
}
|
||||||
|
pub fn get_vendorSub(_: *Navigator) []const u8 {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
pub fn get_language(self: *Navigator) []const u8 {
|
||||||
|
return self.language;
|
||||||
|
}
|
||||||
|
// TODO wait for arrays.
|
||||||
|
//pub fn get_languages(self: *Navigator) [][]const u8 {
|
||||||
|
// return .{self.language};
|
||||||
|
//}
|
||||||
|
pub fn get_online(_: *Navigator) bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
pub fn _registerProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void {
|
||||||
|
_ = scheme;
|
||||||
|
_ = url;
|
||||||
|
}
|
||||||
|
pub fn _unregisterProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void {
|
||||||
|
_ = scheme;
|
||||||
|
_ = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_cookieEnabled(_: *Navigator) bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
// -----
|
||||||
|
|
||||||
|
pub fn testExecFn(
|
||||||
|
_: std.mem.Allocator,
|
||||||
|
js_env: *jsruntime.Env,
|
||||||
|
) anyerror!void {
|
||||||
|
var navigator = [_]Case{
|
||||||
|
.{ .src = "navigator.userAgent", .ex = "Lightpanda/1.0" },
|
||||||
|
.{ .src = "navigator.appVersion", .ex = "1.0" },
|
||||||
|
.{ .src = "navigator.language", .ex = "en-US" },
|
||||||
|
};
|
||||||
|
try checkCases(js_env, &navigator);
|
||||||
|
}
|
||||||
|
|
||||||
2
vendor/zig-js-runtime
vendored
2
vendor/zig-js-runtime
vendored
Submodule vendor/zig-js-runtime updated: 599c173b34...1b1b3431ff
Reference in New Issue
Block a user