Execute onload for dynamic script

Add support for onerror for static and dynamic scripts.

Make script type checking case insensitive.
This commit is contained in:
Karl Seguin
2025-05-29 19:03:42 +08:00
parent 6adb46abd5
commit ef64fa3794
2 changed files with 111 additions and 21 deletions

View File

@@ -19,6 +19,7 @@ const std = @import("std");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const generate = @import("../../runtime/generate.zig"); const generate = @import("../../runtime/generate.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const URL = @import("../url/url.zig").URL; const URL = @import("../url/url.zig").URL;
@@ -746,6 +747,9 @@ pub const HTMLScriptElement = struct {
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const subtype = .node; pub const subtype = .node;
onload: ?Env.Function = null,
onerror: ?Env.Function = null,
pub fn get_src(self: *parser.Script) !?[]const u8 { pub fn get_src(self: *parser.Script) !?[]const u8 {
return try parser.elementGetAttribute( return try parser.elementGetAttribute(
parser.scriptToElt(self), parser.scriptToElt(self),
@@ -854,6 +858,26 @@ pub const HTMLScriptElement = struct {
return try parser.elementRemoveAttribute(parser.scriptToElt(self), "nomodule"); return try parser.elementRemoveAttribute(parser.scriptToElt(self), "nomodule");
} }
pub fn get_onload(script: *parser.Script, page: *Page) !?Env.Function {
const self = page.getNodeWrapper(HTMLScriptElement, @ptrCast(script)) orelse return null;
return self.onload;
}
pub fn set_onload(script: *parser.Script, function: ?Env.Function, page: *Page) !void {
const self = try page.getOrCreateNodeWrapper(HTMLScriptElement, @ptrCast(script));
self.onload = function;
}
pub fn get_onerror(script: *parser.Script, page: *Page) !?Env.Function {
const self = page.getNodeWrapper(HTMLScriptElement, @ptrCast(script)) orelse return null;
return self.onerror;
}
pub fn set_onerror(script: *parser.Script, function: ?Env.Function, page: *Page) !void {
const self = try page.getOrCreateNodeWrapper(HTMLScriptElement, @ptrCast(script));
self.onerror = function;
}
}; };
pub const HTMLSourceElement = struct { pub const HTMLSourceElement = struct {

View File

@@ -327,7 +327,7 @@ pub const Page = struct {
continue; continue;
} }
const script = try Script.init(e) orelse continue; const script = try Script.init(e, null) orelse continue;
// TODO use fetchpriority // TODO use fetchpriority
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#fetchpriority // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#fetchpriority
@@ -568,7 +568,7 @@ pub const Page = struct {
} }
pub fn getOrCreateNodeWrapper(self: *Page, comptime T: type, node: *parser.Node) !*T { pub fn getOrCreateNodeWrapper(self: *Page, comptime T: type, node: *parser.Node) !*T {
if (try self.getNodeWrapper(T, node)) |wrap| { if (self.getNodeWrapper(T, node)) |wrap| {
return wrap; return wrap;
} }
@@ -579,7 +579,7 @@ pub const Page = struct {
return wrap; return wrap;
} }
pub fn getNodeWrapper(_: *Page, comptime T: type, node: *parser.Node) !?*T { pub fn getNodeWrapper(_: *const Page, comptime T: type, node: *parser.Node) ?*T {
if (parser.nodeGetEmbedderData(node)) |wrap| { if (parser.nodeGetEmbedderData(node)) |wrap| {
return @alignCast(@ptrCast(wrap)); return @alignCast(@ptrCast(wrap));
} }
@@ -609,7 +609,8 @@ const Script = struct {
src: ?[]const u8, src: ?[]const u8,
element: *parser.Element, element: *parser.Element,
// The javascript to load after we successfully load the script // The javascript to load after we successfully load the script
onload: ?[]const u8, onload: ?Callback,
onerror: ?Callback,
// The javascript to load if we have an error executing the script // 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 // For now, we ignore this, since we still have a lot of errors that we
@@ -621,7 +622,12 @@ const Script = struct {
javascript, javascript,
}; };
fn init(e: *parser.Element) !?Script { const Callback = union(enum) {
string: []const u8,
function: Env.Function,
};
fn init(e: *parser.Element, page_: ?*const Page) !?Script {
if (try parser.elementGetAttribute(e, "nomodule") != null) { if (try parser.elementGetAttribute(e, "nomodule") != null) {
// these scripts should only be loaded if we don't support modules // these scripts should only be loaded if we don't support modules
// but since we do support modules, we can just skip them. // but since we do support modules, we can just skip them.
@@ -632,11 +638,42 @@ const Script = struct {
return null; return null;
}; };
var onload: ?Callback = null;
var onerror: ?Callback = null;
if (page_) |page| {
// If we're given the page, then it means the script is dynamic
// and we need to load the onload and onerror function (if there are
// any) from our WebAPI.
// This page == null is an optimization which isn't technically
// correct, as a static script could have a dynamic onload/onerror
// attached to it. But this seems quite unlikely and it does help
// optimize loading scripts, of which there can be hundreds for a
// page.
const HTMLScriptElement = @import("html/elements.zig").HTMLScriptElement;
if (page.getNodeWrapper(HTMLScriptElement, @ptrCast(e))) |se| {
if (se.onload) |function| {
onload = .{ .function = function };
}
if (se.onerror) |function| {
onerror = .{ .function = function };
}
}
} else {
if (try parser.elementGetAttribute(e, "onload")) |string| {
onload = .{ .string = string };
}
if (try parser.elementGetAttribute(e, "onerror")) |string| {
onerror = .{ .string = string };
}
}
return .{ return .{
.kind = kind, .kind = kind,
.element = e, .element = e,
.onload = onload,
.onerror = onerror,
.src = try parser.elementGetAttribute(e, "src"), .src = try parser.elementGetAttribute(e, "src"),
.onload = try parser.elementGetAttribute(e, "onload"),
.is_async = try parser.elementGetAttribute(e, "async") != null, .is_async = try parser.elementGetAttribute(e, "async") != null,
.is_defer = try parser.elementGetAttribute(e, "defer") != null, .is_defer = try parser.elementGetAttribute(e, "defer") != null,
}; };
@@ -653,9 +690,9 @@ const Script = struct {
return .javascript; return .javascript;
} }
if (std.mem.eql(u8, script_type, "application/javascript")) return .javascript; if (std.ascii.eqlIgnoreCase(script_type, "application/javascript")) return .javascript;
if (std.mem.eql(u8, script_type, "text/javascript")) return .javascript; if (std.ascii.eqlIgnoreCase(script_type, "text/javascript")) return .javascript;
if (std.mem.eql(u8, script_type, "module")) return .module; if (std.ascii.eqlIgnoreCase(script_type, "module")) return .module;
return null; return null;
} }
@@ -666,7 +703,7 @@ const Script = struct {
defer try_catch.deinit(); defer try_catch.deinit();
const src = self.src orelse "inline"; const src = self.src orelse "inline";
const res = switch (self.kind) { _ = switch (self.kind) {
.javascript => page.scope.exec(body, src), .javascript => page.scope.exec(body, src),
.module => blk: { .module => blk: {
switch (try page.scope.module(body, src)) { switch (try page.scope.module(body, src)) {
@@ -681,17 +718,46 @@ const Script = struct {
if (try try_catch.err(page.arena)) |msg| { if (try try_catch.err(page.arena)) |msg| {
log.warn(.page, "eval script", .{ .src = src, .err = msg }); log.warn(.page, "eval script", .{ .src = src, .err = msg });
} }
try self.executeCallback("onerror", page);
return error.JsErr; return error.JsErr;
}; };
_ = res; try self.executeCallback("onload", page);
}
if (self.onload) |onload| {
_ = page.scope.exec(onload, "script_on_load") catch { fn executeCallback(self: *const Script, comptime typ: []const u8, page: *Page) !void {
if (try try_catch.err(page.arena)) |msg| { const callback = @field(self, typ) orelse return;
log.warn(.page, "eval onload", .{ .src = src, .err = msg }); switch (callback) {
.string => |str| {
var try_catch: Env.TryCatch = undefined;
try_catch.init(page.scope);
defer try_catch.deinit();
_ = page.scope.exec(str, typ) catch {
if (try try_catch.err(page.arena)) |msg| {
log.warn(.page, "script callback", .{
.src = self.src,
.err = msg,
.type = typ,
.@"inline" = true,
});
} }
return error.JsErr;
}; };
},
.function => |f| {
const Event = @import("events/event.zig").Event;
const loadevt = try parser.eventCreate();
defer parser.eventDestroy(loadevt);
var result: Env.Function.Result = undefined;
f.tryCall(void, .{try Event.toInterface(loadevt)}, &result) catch {
log.warn(.page, "script callback", .{
.src = self.src,
.type = typ,
.err = result.exception,
.stack = result.stack,
.@"inline" = false,
});
};
},
} }
} }
}; };
@@ -719,11 +785,11 @@ fn timestamp() u32 {
// after the document is loaded, it's ok to execute any async and defer scripts // after the document is loaded, it's ok to execute any async and defer scripts
// immediately. // immediately.
pub export fn scriptAddedCallback(ctx: ?*anyopaque, element: ?*parser.Element) callconv(.C) void { pub export fn scriptAddedCallback(ctx: ?*anyopaque, element: ?*parser.Element) callconv(.C) void {
var script = Script.init(element.?) catch |err| { const self: *Page = @alignCast(@ptrCast(ctx.?));
var script = Script.init(element.?, self) catch |err| {
log.warn(.page, "script added init error", .{ .err = err }); log.warn(.page, "script added init error", .{ .err = err });
return; return;
} orelse return; } orelse return;
const self: *Page = @alignCast(@ptrCast(ctx.?));
self.evalScript(&script); self.evalScript(&script);
} }