From 7fa7f4ed8abe695260980140bf241b565c2df46d Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 7 May 2025 18:14:06 +0800 Subject: [PATCH] Work on DDG support (but still not working) - Add dummy MediaQueryList and window.matchMedia - Execute deferred scripts after non-deferred I realize this doesn't change much, given how we currently load all scripts after the document is parsed, but scripts _could_ depend on execution order. - Add support for executing the `onload` attribute of I also cleaned up some of the Script code, i.e. removimg `unknown` kind and simply returning a null script, and removing the EmptyBody error and returning a null body string. Finally, I re-enabled the microtask loop which I must have previously disabled. --- src/browser/browser.zig | 182 ++++++++++++++------------ src/browser/html/html.zig | 2 + src/browser/html/media_query_list.zig | 45 +++++++ src/browser/html/window.zig | 10 +- src/runtime/js.zig | 6 +- 5 files changed, 155 insertions(+), 90 deletions(-) create mode 100644 src/browser/html/media_query_list.zig diff --git a/src/browser/browser.zig b/src/browser/browser.zig index eed8dd60..31687e9f 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -289,7 +289,7 @@ pub const Page = struct { try Dump.writeHTML(self.doc.?, out); } - pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 { + pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) !?[]const u8 { const self: *Page = @ptrCast(@alignCast(ctx)); log.debug("fetch module: specifier: {s}", .{specifier}); @@ -435,10 +435,18 @@ pub const Page = struct { // TODO fetch the script resources concurrently but execute them in the // declaration order for synchronous ones. - // sasync stores scripts which can be run asynchronously. + // 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 sasync: std.ArrayListUnmanaged(Script) = .{}; + 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{}; @@ -455,11 +463,6 @@ pub const Page = struct { // ignore non-js script. const script = try Script.init(e) orelse continue; - if (script.kind == .unknown) continue; - - // Ignore the defer attribute b/c we analyze all script - // after the document has been parsed. - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer // TODO use fetchpriority // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#fetchpriority @@ -470,22 +473,18 @@ pub const Page = struct { // > 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 sasync.append(arena, script); + 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 - // TODO defer - // > This Boolean attribute is set to indicate to a browser - // > that the script is meant to be executed after the - // > document has been parsed, but before firing - // > DOMContentLoaded. - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer - // defer allow us to load a script w/o blocking the rest of - // evaluations. - // > Scripts without async, defer or type="module" // > attributes, as well as inline scripts without the // > type="module" attribute, are fetched and executed @@ -497,7 +496,11 @@ pub const Page = struct { try parser.documentHTMLSetCurrentScript(html_doc, null); } - // TODO wait for deferred scripts + 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 @@ -510,7 +513,7 @@ pub const Page = struct { _ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt); // eval async scripts. - for (sasync.items) |s| { + 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); @@ -534,57 +537,42 @@ 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, s: *const Script) !void { - self.current_script = s; + 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 opt_src = try parser.elementGetAttribute(s.element, "src"); - if (opt_src) |src| { - log.debug("starting GET {s}", .{src}); - - self.fetchScript(s) catch |err| { - switch (err) { - FetchError.BadStatusCode => return err, - - // TODO If el's result is null, then fire an event named error at - // el, and return. - FetchError.NoBody => return, - - FetchError.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. - + 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; - } + }; - // TODO handle charset attribute - const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element)); - if (opt_text) |text| { - try s.eval(self, text); - return; - } + script.eval(self, body) catch |err| switch (err) { + error.JsErr => {}, // nothing to do here. + else => return err, + }; - // nothing has been loaded. - // TODO If el's result is null, then fire an event named error at - // el, and return. + // TODO If el's from an external file is true, then fire an event + // named load at el. } - const FetchError = error{ - BadStatusCode, - NoBody, - JsErr, - }; - // 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 { + fn fetchData(self: *const Page, src: []const u8, base: ?[]const u8) !?[]const u8 { log.debug("starting fetch {s}", .{src}); const arena = self.arena; @@ -619,7 +607,7 @@ pub const Page = struct { log.info("fetch {any}: {d}", .{ url, header.status }); if (header.status != 200) { - return FetchError.BadStatusCode; + return error.BadStatusCode; } var arr: std.ArrayListUnmanaged(u8) = .{}; @@ -631,17 +619,12 @@ pub const Page = struct { // check no body if (arr.items.len == 0) { - return FetchError.NoBody; + return null; } return arr.items; } - fn fetchScript(self: *Page, s: *const Script) !void { - const body = try self.fetchData(s.src, null); - try s.eval(self, body); - } - 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(); @@ -712,28 +695,42 @@ pub const Page = struct { } const Script = struct { - element: *parser.Element, 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, - src: []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 { - unknown, - javascript, 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 (tag != .script) { + return null; + } + + const kind = parseKind(try parser.elementGetAttribute(e, "type")) orelse { + return null; + }; return .{ + .kind = kind, .element = e, - .kind = parseKind(try parser.elementGetAttribute(e, "type")), + .src = try parser.elementGetAttribute(e, "src"), + .onload = try parser.elementGetAttribute(e, "onload"), .is_async = try parser.elementGetAttribute(e, "async") != null, - .src = try parser.elementGetAttribute(e, "src") orelse "inline", + .is_defer = try parser.elementGetAttribute(e, "defer") != null, }; } @@ -742,34 +739,47 @@ pub const Page = struct { // > 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(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.?, "text/javascript")) return .javascript; - if (std.mem.eql(u8, stype.?, "module")) return .module; + fn parseKind(script_type_: ?[]const u8) ?Kind { + const script_type = script_type_ orelse return .javascript; + if (script_type.len == 0) { + return .javascript; + } - return .unknown; + 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: Script, page: *Page, body: []const u8) !void { + 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) { - .unknown => return error.UnknownScript, - .javascript => page.scope.exec(body, self.src), - .module => page.scope.module(body, self.src), + .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}", .{ self.src, msg }); + log.info("eval script {s}: {s}", .{ src, msg }); } - return FetchError.JsErr; + return error.JsErr; }; if (builtin.mode == .Debug) { const msg = try res.toString(page.arena); - log.debug("eval script {s}: {s}", .{ self.src, msg }); + 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; + }; } } }; diff --git a/src/browser/html/html.zig b/src/browser/html/html.zig index 6e576228..85eff4ec 100644 --- a/src/browser/html/html.zig +++ b/src/browser/html/html.zig @@ -23,6 +23,7 @@ const Window = @import("window.zig").Window; const Navigator = @import("navigator.zig").Navigator; const History = @import("history.zig").History; const Location = @import("location.zig").Location; +const MediaQueryList = @import("media_query_list.zig").MediaQueryList; pub const Interfaces = .{ HTMLDocument, @@ -34,4 +35,5 @@ pub const Interfaces = .{ Navigator, History, Location, + MediaQueryList, }; diff --git a/src/browser/html/media_query_list.zig b/src/browser/html/media_query_list.zig new file mode 100644 index 00000000..b38c1b26 --- /dev/null +++ b/src/browser/html/media_query_list.zig @@ -0,0 +1,45 @@ +// Copyright (C) 2023-2025 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 parser = @import("../netsurf.zig"); +const Callback = @import("../env.zig").Callback; +const EventTarget = @import("../dom/event_target.zig").EventTarget; + +// https://drafts.csswg.org/cssom-view/#the-mediaquerylist-interface +pub const MediaQueryList = struct { + pub const prototype = *EventTarget; + + // Extend libdom event target for pure zig struct. + // This is not safe as it relies on a structure layout that isn't guaranteed + base: parser.EventTargetTBase = parser.EventTargetTBase{}, + + matches: bool, + media: []const u8, + + pub fn get_matches(self: *const MediaQueryList) bool { + return self.matches; + } + + pub fn get_media(self: *const MediaQueryList) []const u8 { + return self.media; + } + + pub fn _addListener(_: *const MediaQueryList, _: Callback) void {} + + pub fn _removeListener(_: *const MediaQueryList, _: Callback) void {} +}; diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index c1548b96..7064be29 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -29,6 +29,7 @@ const Location = @import("location.zig").Location; const Crypto = @import("../crypto/crypto.zig").Crypto; const Console = @import("../console/console.zig").Console; const EventTarget = @import("../dom/event_target.zig").EventTarget; +const MediaQueryList = @import("media_query_list.zig").MediaQueryList; const storage = @import("../storage/storage.zig"); @@ -149,7 +150,14 @@ pub const Window = struct { try state.loop.cancel(kv.value.loop_id); } - pub fn createTimeout(self: *Window, cbk: Callback, delay_: ?u32, state: *SessionState, comptime repeat: bool) !u32 { + pub fn _matchMedia(_: *const Window, media: []const u8, state: *SessionState) !MediaQueryList { + return .{ + .matches = false, // TODO? + .media = try state.arena.dupe(u8, media), + }; + } + + fn createTimeout(self: *Window, cbk: Callback, delay_: ?u32, state: *SessionState, comptime repeat: bool) !u32 { if (self.timers.count() > 512) { return error.TooManyTimeout; } diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 8a15361c..fbeb4f2c 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -536,7 +536,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { const ModuleLoader = struct { ptr: *anyopaque, - func: *const fn (ptr: *anyopaque, specifier: []const u8) anyerror![]const u8, + func: *const fn (ptr: *anyopaque, specifier: []const u8) anyerror!?[]const u8, }; // no init, started with executor.startScope() @@ -790,7 +790,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { const source = module_loader.func(module_loader.ptr, specifier) catch |err| { log.err("fetchModuleSource for '{s}' fetch error: {}", .{ specifier, err }); return null; - }; + } orelse return null; const m = compileModule(self.isolate, source, specifier) catch |err| { log.err("fetchModuleSource for '{s}' compile error: {}", .{ specifier, err }); @@ -2825,7 +2825,7 @@ const NoopInspector = struct { }; const ErrorModuleLoader = struct { - pub fn fetchModuleSource(_: *anyopaque, _: []const u8) ![]const u8 { + pub fn fetchModuleSource(_: *anyopaque, _: []const u8) !?[]const u8 { return error.NoModuleLoadConfigured; } };