From 7ac945bf88f5adc86d9e85901612cfee90d5535d Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 31 Dec 2024 12:37:05 +0100 Subject: [PATCH 1/6] browser: refacto script --- src/browser/browser.zig | 152 ++++++++++++++++++++++------------------ 1 file changed, 82 insertions(+), 70 deletions(-) diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 3ca5dbaf..1f062846 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -388,7 +388,7 @@ pub const Page = struct { // sasync 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 sasync = std.ArrayList(*parser.Element).init(alloc); + var sasync = std.ArrayList(Script).init(alloc); defer sasync.deinit(); const root = parser.documentToNode(doc); @@ -403,21 +403,11 @@ pub const Page = struct { } 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. - // > 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 - const stype = try parser.elementGetAttribute(e, "type"); - if (!isJS(stype)) { - continue; - } + const script = try Script.init(e) orelse continue; + if (script.kind == .unknown) continue; + if (script.kind == .module) continue; // Ignore the defer attribute b/c we analyze all script // after the document has been parsed. @@ -431,8 +421,8 @@ pub const Page = struct { // > 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 (try parser.elementGetAttribute(e, "async") != null) { - try sasync.append(e); + if (script.isasync) { + try sasync.append(script); continue; } @@ -455,7 +445,7 @@ pub const Page = struct { // > page. // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes 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); } @@ -472,9 +462,9 @@ pub const Page = struct { _ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt); // eval async scripts. - for (sasync.items) |e| { - try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e)); - self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err}); + for (sasync.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); } @@ -496,15 +486,15 @@ pub const Page = struct { // 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, e: *parser.Element) !void { + fn evalScript(self: *Page, s: Script) !void { const alloc = self.arena.allocator(); // 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| { log.debug("starting GET {s}", .{src}); - self.fetchScript(src) catch |err| { + self.fetchScript(s) catch |err| { switch (err) { FetchError.BadStatusCode => return err, @@ -523,26 +513,10 @@ pub const Page = struct { return; } - var try_catch: jsruntime.TryCatch = undefined; - try_catch.init(self.session.env); - defer try_catch.deinit(); - - const opt_text = try parser.nodeTextContent(parser.elementToNode(e)); + // TODO handle charset attribute + const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element)); if (opt_text) |text| { - // TODO handle charset attribute - 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}); - } + try s.eval(alloc, self.session.env, text); return; } @@ -559,14 +533,14 @@ pub const Page = struct { // fetchScript senf a GET request to the src and execute the script // received. - fn fetchScript(self: *Page, src: []const u8) !void { + fn fetchScript(self: *Page, s: Script) !void { const alloc = self.arena.allocator(); - log.debug("starting fetch script {s}", .{src}); + log.debug("starting fetch script {s}", .{s.src}); var buffer: [1024]u8 = undefined; var b: []u8 = buffer[0..]; - const u = try std.Uri.resolve_inplace(self.uri, src, &b); + const u = try std.Uri.resolve_inplace(self.uri, s.src, &b); var fetchres = try self.session.loader.get(alloc, u); defer fetchres.deinit(); @@ -584,35 +558,73 @@ pub const Page = struct { // check no body if (body.len == 0) return FetchError.NoBody; - var try_catch: jsruntime.TryCatch = undefined; - try_catch.init(self.session.env); - defer try_catch.deinit(); + try s.eval(alloc, self.session.env, body); + } - const res = self.session.env.exec(body, src) catch { - if (try try_catch.err(alloc, self.session.env)) |msg| { - defer alloc.free(msg); - log.info("eval remote {s}: {s}", .{ src, msg }); - } - return FetchError.JsErr; + const Script = struct { + element: *parser.Element, + kind: Kind, + isasync: bool, + + src: []const u8, + + const Kind = enum { + unknown, + javascript, + module, }; - if (builtin.mode == .Debug) { - const msg = try res.toString(alloc, self.session.env); - defer alloc.free(msg); - log.debug("eval remote {s}: {s}", .{ src, msg }); + 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; + + 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 - // > 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 isJS(stype: ?[]const u8) bool { - if (stype == null or stype.?.len == 0) return true; - if (std.mem.eql(u8, stype.?, "application/javascript")) return true; - if (!std.mem.eql(u8, stype.?, "module")) return true; + // > 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 kind(stype: ?[]const u8) Kind { + if (stype == null or stype.?.len == 0) return .javascript; + if (std.mem.eql(u8, stype.?, "application/javascript")) return .javascript; + 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 { + switch (self.kind) { + .unknown => return error.UnknownScript, + .javascript => {}, + .module => {}, + } + + var try_catch: jsruntime.TryCatch = undefined; + try_catch.init(env); + defer try_catch.deinit(); + + const res = env.exec(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 }); + } + } + }; }; From 680d634725df929c67b57c5571878e74a8e4a51d Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 2 Jan 2025 17:08:10 +0100 Subject: [PATCH 2/6] update zig-js-runtime --- vendor/zig-js-runtime | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/zig-js-runtime b/vendor/zig-js-runtime index 599c173b..3be71ed6 160000 --- a/vendor/zig-js-runtime +++ b/vendor/zig-js-runtime @@ -1 +1 @@ -Subproject commit 599c173b346461f3e8deb5aca2e2e004cb51733e +Subproject commit 3be71ed62b8d1790a69958a143ec5f7fc5989068 From 766f9798f6df07fadc97d0d7c08f5122672a348f Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 2 Jan 2025 17:07:49 +0100 Subject: [PATCH 3/6] browser: load module --- src/browser/browser.zig | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 1f062846..79dba350 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -407,7 +407,6 @@ pub const Page = struct { // ignore non-js script. const script = try Script.init(e) orelse continue; if (script.kind == .unknown) continue; - if (script.kind == .module) continue; // Ignore the defer attribute b/c we analyze all script // after the document has been parsed. @@ -596,23 +595,21 @@ pub const Page = struct { fn kind(stype: ?[]const u8) Kind { if (stype == null or stype.?.len == 0) return .javascript; if (std.mem.eql(u8, stype.?, "application/javascript")) return .javascript; - if (!std.mem.eql(u8, stype.?, "module")) return .module; + if (std.mem.eql(u8, stype.?, "module")) return .module; return .unknown; } fn eval(self: Script, alloc: std.mem.Allocator, env: Env, body: []const u8) !void { - switch (self.kind) { - .unknown => return error.UnknownScript, - .javascript => {}, - .module => {}, - } - var try_catch: jsruntime.TryCatch = undefined; try_catch.init(env); defer try_catch.deinit(); - const res = env.exec(body, self.src) catch { + 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 }); From 48e7c8ad0f1e716f76d6da22559974e3c37da0b6 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 10 Jan 2025 16:00:44 +0100 Subject: [PATCH 4/6] browser: implement fetch module --- src/browser/browser.zig | 43 ++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 79dba350..635390c4 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -29,6 +29,7 @@ const Mime = @import("mime.zig"); const jsruntime = @import("jsruntime"); const Loop = jsruntime.Loop; const Env = jsruntime.Env; +const Module = jsruntime.Module; const apiweb = @import("../apiweb.zig"); @@ -125,6 +126,21 @@ pub const Session = struct { 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 { if (self.page) |*p| p.end(); @@ -362,6 +378,9 @@ pub const Page = struct { log.debug("start js env", .{}); try self.session.env.start(); + // register the module loader + try self.session.env.setModuleLoadFn(self.session, Session.fetchModule); + // load polyfills try polyfill.load(alloc, self.session.env); @@ -530,33 +549,39 @@ pub const Page = struct { JsErr, }; - // fetchScript senf a GET request to the src and execute the script - // received. - fn fetchScript(self: *Page, s: Script) !void { - const alloc = self.arena.allocator(); - - log.debug("starting fetch script {s}", .{s.src}); + // the caller owns the returned string + fn fetchData(self: *Page, alloc: std.mem.Allocator, src: []const u8) ![]const u8 { + log.debug("starting fetch {s}", .{src}); var buffer: [1024]u8 = undefined; var b: []u8 = buffer[0..]; - const u = try std.Uri.resolve_inplace(self.uri, s.src, &b); + const u = try std.Uri.resolve_inplace(self.uri, src, &b); var fetchres = try self.session.loader.get(alloc, u); defer fetchres.deinit(); 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; // TODO check content-type const body = try fetchres.req.reader().readAllAlloc(alloc, 16 * 1024 * 1024); - defer alloc.free(body); // check no body if (body.len == 0) return FetchError.NoBody; + return body; + } + + // fetchScript senf a GET request to the src and execute the script + // received. + fn fetchScript(self: *Page, s: Script) !void { + const alloc = self.arena.allocator(); + const body = try self.fetchData(alloc, s.src); + defer alloc.free(body); + try s.eval(alloc, self.session.env, body); } From 43678f8dc00d298c95ebaf6361a22367837fd2ac Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 10 Jan 2025 16:43:13 +0100 Subject: [PATCH 5/6] upgrade zig-js-runtime --- src/html/history.zig | 104 ++++++++++++++++++++++++++++++++++++++++++ vendor/zig-js-runtime | 2 +- 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 src/html/history.zig diff --git a/src/html/history.zig b/src/html/history.zig new file mode 100644 index 00000000..b507c3ab --- /dev/null +++ b/src/html/history.zig @@ -0,0 +1,104 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +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); +} + diff --git a/vendor/zig-js-runtime b/vendor/zig-js-runtime index 3be71ed6..1b1b3431 160000 --- a/vendor/zig-js-runtime +++ b/vendor/zig-js-runtime @@ -1 +1 @@ -Subproject commit 3be71ed62b8d1790a69958a143ec5f7fc5989068 +Subproject commit 1b1b3431ff8ca06c2344654fe2ca5e71d3be8845 From d777d77b06336f37ff7b2e199ffb3907d25acab8 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 13 Jan 2025 10:36:52 +0100 Subject: [PATCH 6/6] ci: update sig-v8 version --- .github/actions/install/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 6afea5fb..97210bbf 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -17,7 +17,7 @@ inputs: zig-v8: description: 'zig v8 version to install' required: false - default: 'v0.1.9' + default: 'v0.1.11' v8: description: 'v8 version to install' required: false