Skip to content

Commit 7fa7f4e

Browse files
committed
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 <scripts> 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.
1 parent 3466325 commit 7fa7f4e

File tree

5 files changed

+155
-90
lines changed

5 files changed

+155
-90
lines changed

src/browser/browser.zig

Lines changed: 96 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ pub const Page = struct {
289289
try Dump.writeHTML(self.doc.?, out);
290290
}
291291

292-
pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 {
292+
pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) !?[]const u8 {
293293
const self: *Page = @ptrCast(@alignCast(ctx));
294294

295295
log.debug("fetch module: specifier: {s}", .{specifier});
@@ -435,10 +435,18 @@ pub const Page = struct {
435435
// TODO fetch the script resources concurrently but execute them in the
436436
// declaration order for synchronous ones.
437437

438-
// sasync stores scripts which can be run asynchronously.
438+
// async_scripts stores scripts which can be run asynchronously.
439439
// for now they are just run after the non-async one in order to
440440
// dispatch DOMContentLoaded the sooner as possible.
441-
var sasync: std.ArrayListUnmanaged(Script) = .{};
441+
var async_scripts: std.ArrayListUnmanaged(Script) = .{};
442+
443+
// defer_scripts stores scripts which are meant to be deferred. For now
444+
// this doesn't have a huge impact, since normal scripts are parsed
445+
// after the document is loaded. But (a) we should fix that and (b)
446+
// this results in JavaScript being loaded in the same order as browsers
447+
// which can help debug issues (and might actually fix issues if websites
448+
// are expecting this execution order)
449+
var defer_scripts: std.ArrayListUnmanaged(Script) = .{};
442450

443451
const root = parser.documentToNode(doc);
444452
const walker = Walker{};
@@ -455,11 +463,6 @@ pub const Page = struct {
455463

456464
// ignore non-js script.
457465
const script = try Script.init(e) orelse continue;
458-
if (script.kind == .unknown) continue;
459-
460-
// Ignore the defer attribute b/c we analyze all script
461-
// after the document has been parsed.
462-
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer
463466

464467
// TODO use fetchpriority
465468
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#fetchpriority
@@ -470,22 +473,18 @@ pub const Page = struct {
470473
// > parsing and evaluated as soon as it is available.
471474
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async
472475
if (script.is_async) {
473-
try sasync.append(arena, script);
476+
try async_scripts.append(arena, script);
477+
continue;
478+
}
479+
480+
if (script.is_defer) {
481+
try defer_scripts.append(arena, script);
474482
continue;
475483
}
476484

477485
// TODO handle for attribute
478486
// TODO handle event attribute
479487

480-
// TODO defer
481-
// > This Boolean attribute is set to indicate to a browser
482-
// > that the script is meant to be executed after the
483-
// > document has been parsed, but before firing
484-
// > DOMContentLoaded.
485-
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer
486-
// defer allow us to load a script w/o blocking the rest of
487-
// evaluations.
488-
489488
// > Scripts without async, defer or type="module"
490489
// > attributes, as well as inline scripts without the
491490
// > type="module" attribute, are fetched and executed
@@ -497,7 +496,11 @@ pub const Page = struct {
497496
try parser.documentHTMLSetCurrentScript(html_doc, null);
498497
}
499498

500-
// TODO wait for deferred scripts
499+
for (defer_scripts.items) |s| {
500+
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(s.element));
501+
self.evalScript(&s) catch |err| log.warn("evaljs: {any}", .{err});
502+
try parser.documentHTMLSetCurrentScript(html_doc, null);
503+
}
501504

502505
// dispatch DOMContentLoaded before the transition to "complete",
503506
// at the point where all subresources apart from async script elements
@@ -510,7 +513,7 @@ pub const Page = struct {
510513
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt);
511514

512515
// eval async scripts.
513-
for (sasync.items) |s| {
516+
for (async_scripts.items) |s| {
514517
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(s.element));
515518
self.evalScript(&s) catch |err| log.warn("evaljs: {any}", .{err});
516519
try parser.documentHTMLSetCurrentScript(html_doc, null);
@@ -534,57 +537,42 @@ pub const Page = struct {
534537
// evalScript evaluates the src in priority.
535538
// if no src is present, we evaluate the text source.
536539
// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model
537-
fn evalScript(self: *Page, s: *const Script) !void {
538-
self.current_script = s;
539-
defer self.current_script = null;
540-
541-
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
542-
const opt_src = try parser.elementGetAttribute(s.element, "src");
543-
if (opt_src) |src| {
544-
log.debug("starting GET {s}", .{src});
545-
546-
self.fetchScript(s) catch |err| {
547-
switch (err) {
548-
FetchError.BadStatusCode => return err,
549-
550-
// TODO If el's result is null, then fire an event named error at
551-
// el, and return.
552-
FetchError.NoBody => return,
540+
fn evalScript(self: *Page, script: *const Script) !void {
541+
const src = script.src orelse {
542+
// source is inline
543+
// TODO handle charset attribute
544+
if (try parser.nodeTextContent(parser.elementToNode(script.element))) |text| {
545+
try script.eval(self, text);
546+
}
547+
return;
548+
};
553549

554-
FetchError.JsErr => {}, // nothing to do here.
555-
else => return err,
556-
}
557-
};
550+
self.current_script = script;
551+
defer self.current_script = null;
558552

559-
// TODO If el's from an external file is true, then fire an event
560-
// named load at el.
553+
log.debug("starting GET {s}", .{src});
561554

555+
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
556+
const body = (try self.fetchData(src, null)) orelse {
557+
// TODO If el's result is null, then fire an event named error at
558+
// el, and return
562559
return;
563-
}
560+
};
564561

565-
// TODO handle charset attribute
566-
const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element));
567-
if (opt_text) |text| {
568-
try s.eval(self, text);
569-
return;
570-
}
562+
script.eval(self, body) catch |err| switch (err) {
563+
error.JsErr => {}, // nothing to do here.
564+
else => return err,
565+
};
571566

572-
// nothing has been loaded.
573-
// TODO If el's result is null, then fire an event named error at
574-
// el, and return.
567+
// TODO If el's from an external file is true, then fire an event
568+
// named load at el.
575569
}
576570

577-
const FetchError = error{
578-
BadStatusCode,
579-
NoBody,
580-
JsErr,
581-
};
582-
583571
// fetchData returns the data corresponding to the src target.
584572
// It resolves src using the page's uri.
585573
// If a base path is given, src is resolved according to the base first.
586574
// the caller owns the returned string
587-
fn fetchData(self: *const Page, src: []const u8, base: ?[]const u8) ![]const u8 {
575+
fn fetchData(self: *const Page, src: []const u8, base: ?[]const u8) !?[]const u8 {
588576
log.debug("starting fetch {s}", .{src});
589577

590578
const arena = self.arena;
@@ -619,7 +607,7 @@ pub const Page = struct {
619607
log.info("fetch {any}: {d}", .{ url, header.status });
620608

621609
if (header.status != 200) {
622-
return FetchError.BadStatusCode;
610+
return error.BadStatusCode;
623611
}
624612

625613
var arr: std.ArrayListUnmanaged(u8) = .{};
@@ -631,17 +619,12 @@ pub const Page = struct {
631619

632620
// check no body
633621
if (arr.items.len == 0) {
634-
return FetchError.NoBody;
622+
return null;
635623
}
636624

637625
return arr.items;
638626
}
639627

640-
fn fetchScript(self: *Page, s: *const Script) !void {
641-
const body = try self.fetchData(s.src, null);
642-
try s.eval(self, body);
643-
}
644-
645628
fn newHTTPRequest(self: *const Page, method: http.Request.Method, url: *const URL, opts: storage.cookie.LookupOpts) !http.Request {
646629
var request = try self.state.http_client.request(method, &url.uri);
647630
errdefer request.deinit();
@@ -712,28 +695,42 @@ pub const Page = struct {
712695
}
713696

714697
const Script = struct {
715-
element: *parser.Element,
716698
kind: Kind,
717699
is_async: bool,
700+
is_defer: bool,
701+
src: ?[]const u8,
702+
element: *parser.Element,
703+
// The javascript to load after we successfully load the script
704+
onload: ?[]const u8,
718705

719-
src: []const u8,
706+
// The javascript to load if we have an error executing the script
707+
// For now, we ignore this, since we still have a lot of errors that we
708+
// shouldn't
709+
//onerror: ?[]const u8,
720710

721711
const Kind = enum {
722-
unknown,
723-
javascript,
724712
module,
713+
javascript,
725714
};
726715

727716
fn init(e: *parser.Element) !?Script {
728717
// ignore non-script tags
729718
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
730-
if (tag != .script) return null;
719+
if (tag != .script) {
720+
return null;
721+
}
722+
723+
const kind = parseKind(try parser.elementGetAttribute(e, "type")) orelse {
724+
return null;
725+
};
731726

732727
return .{
728+
.kind = kind,
733729
.element = e,
734-
.kind = parseKind(try parser.elementGetAttribute(e, "type")),
730+
.src = try parser.elementGetAttribute(e, "src"),
731+
.onload = try parser.elementGetAttribute(e, "onload"),
735732
.is_async = try parser.elementGetAttribute(e, "async") != null,
736-
.src = try parser.elementGetAttribute(e, "src") orelse "inline",
733+
.is_defer = try parser.elementGetAttribute(e, "defer") != null,
737734
};
738735
}
739736

@@ -742,34 +739,47 @@ pub const Page = struct {
742739
// > type indicates that the script is a "classic script", containing
743740
// > JavaScript code.
744741
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
745-
fn parseKind(stype: ?[]const u8) Kind {
746-
if (stype == null or stype.?.len == 0) return .javascript;
747-
if (std.mem.eql(u8, stype.?, "application/javascript")) return .javascript;
748-
if (std.mem.eql(u8, stype.?, "text/javascript")) return .javascript;
749-
if (std.mem.eql(u8, stype.?, "module")) return .module;
742+
fn parseKind(script_type_: ?[]const u8) ?Kind {
743+
const script_type = script_type_ orelse return .javascript;
744+
if (script_type.len == 0) {
745+
return .javascript;
746+
}
747+
748+
if (std.mem.eql(u8, script_type, "application/javascript")) return .javascript;
749+
if (std.mem.eql(u8, script_type, "text/javascript")) return .javascript;
750+
if (std.mem.eql(u8, script_type, "module")) return .module;
750751

751-
return .unknown;
752+
return null;
752753
}
753754

754-
fn eval(self: Script, page: *Page, body: []const u8) !void {
755+
fn eval(self: *const Script, page: *Page, body: []const u8) !void {
755756
var try_catch: Env.TryCatch = undefined;
756757
try_catch.init(page.scope);
757758
defer try_catch.deinit();
758759

760+
const src = self.src orelse "inline";
759761
const res = switch (self.kind) {
760-
.unknown => return error.UnknownScript,
761-
.javascript => page.scope.exec(body, self.src),
762-
.module => page.scope.module(body, self.src),
762+
.javascript => page.scope.exec(body, src),
763+
.module => page.scope.module(body, src),
763764
} catch {
764765
if (try try_catch.err(page.arena)) |msg| {
765-
log.info("eval script {s}: {s}", .{ self.src, msg });
766+
log.info("eval script {s}: {s}", .{ src, msg });
766767
}
767-
return FetchError.JsErr;
768+
return error.JsErr;
768769
};
769770

770771
if (builtin.mode == .Debug) {
771772
const msg = try res.toString(page.arena);
772-
log.debug("eval script {s}: {s}", .{ self.src, msg });
773+
log.debug("eval script {s}: {s}", .{ src, msg });
774+
}
775+
776+
if (self.onload) |onload| {
777+
_ = page.scope.exec(onload, "script_on_load") catch {
778+
if (try try_catch.err(page.arena)) |msg| {
779+
log.info("eval script onload {s}: {s}", .{ src, msg });
780+
}
781+
return error.JsErr;
782+
};
773783
}
774784
}
775785
};

src/browser/html/html.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const Window = @import("window.zig").Window;
2323
const Navigator = @import("navigator.zig").Navigator;
2424
const History = @import("history.zig").History;
2525
const Location = @import("location.zig").Location;
26+
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
2627

2728
pub const Interfaces = .{
2829
HTMLDocument,
@@ -34,4 +35,5 @@ pub const Interfaces = .{
3435
Navigator,
3536
History,
3637
Location,
38+
MediaQueryList,
3739
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <[email protected]>
4+
// Pierre Tachoire <[email protected]>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
const parser = @import("../netsurf.zig");
20+
const Callback = @import("../env.zig").Callback;
21+
const EventTarget = @import("../dom/event_target.zig").EventTarget;
22+
23+
// https://drafts.csswg.org/cssom-view/#the-mediaquerylist-interface
24+
pub const MediaQueryList = struct {
25+
pub const prototype = *EventTarget;
26+
27+
// Extend libdom event target for pure zig struct.
28+
// This is not safe as it relies on a structure layout that isn't guaranteed
29+
base: parser.EventTargetTBase = parser.EventTargetTBase{},
30+
31+
matches: bool,
32+
media: []const u8,
33+
34+
pub fn get_matches(self: *const MediaQueryList) bool {
35+
return self.matches;
36+
}
37+
38+
pub fn get_media(self: *const MediaQueryList) []const u8 {
39+
return self.media;
40+
}
41+
42+
pub fn _addListener(_: *const MediaQueryList, _: Callback) void {}
43+
44+
pub fn _removeListener(_: *const MediaQueryList, _: Callback) void {}
45+
};

src/browser/html/window.zig

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const Location = @import("location.zig").Location;
2929
const Crypto = @import("../crypto/crypto.zig").Crypto;
3030
const Console = @import("../console/console.zig").Console;
3131
const EventTarget = @import("../dom/event_target.zig").EventTarget;
32+
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
3233

3334
const storage = @import("../storage/storage.zig");
3435

@@ -149,7 +150,14 @@ pub const Window = struct {
149150
try state.loop.cancel(kv.value.loop_id);
150151
}
151152

152-
pub fn createTimeout(self: *Window, cbk: Callback, delay_: ?u32, state: *SessionState, comptime repeat: bool) !u32 {
153+
pub fn _matchMedia(_: *const Window, media: []const u8, state: *SessionState) !MediaQueryList {
154+
return .{
155+
.matches = false, // TODO?
156+
.media = try state.arena.dupe(u8, media),
157+
};
158+
}
159+
160+
fn createTimeout(self: *Window, cbk: Callback, delay_: ?u32, state: *SessionState, comptime repeat: bool) !u32 {
153161
if (self.timers.count() > 512) {
154162
return error.TooManyTimeout;
155163
}

0 commit comments

Comments
 (0)