Merge pull request #975 from lightpanda-io/dynamic_script
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled

Support dynamic scripts which are added to the DOM before src is set
This commit is contained in:
Karl Seguin
2025-08-27 05:59:28 +08:00
committed by GitHub
4 changed files with 44 additions and 37 deletions

View File

@@ -143,32 +143,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
return; return;
}; };
var onload: ?Script.Callback = null;
var onerror: ?Script.Callback = null;
const page = self.page; const page = self.page;
if (page.getNodeState(@ptrCast(element))) |se| {
// if the script has a node state, then it was dynamically added and thus
// the onload/onerror were saved in the state (if there are any)
if (se.onload) |function| {
onload = .{ .function = function };
}
if (se.onerror) |function| {
onerror = .{ .function = function };
}
} else {
// if the script has no node state, then it could still be dynamically
// added (could have been dynamically added, but no attributes were set
// which required a node state to be created) or it could be a inline
// <script>.
if (try parser.elementGetAttribute(element, "onload")) |string| {
onload = .{ .string = string };
}
if (try parser.elementGetAttribute(element, "onerror")) |string| {
onerror = .{ .string = string };
}
}
var source: Script.Source = undefined; var source: Script.Source = undefined;
var remote_url: ?[:0]const u8 = null; var remote_url: ?[:0]const u8 = null;
if (try parser.elementGetAttribute(element, "src")) |src| { if (try parser.elementGetAttribute(element, "src")) |src| {
@@ -184,8 +159,6 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
var script = Script{ var script = Script{
.kind = kind, .kind = kind,
.onload = onload,
.onerror = onerror,
.element = element, .element = element,
.source = source, .source = source,
.url = remote_url orelse page.url.raw, .url = remote_url orelse page.url.raw,
@@ -558,8 +531,6 @@ const Script = struct {
is_async: bool, is_async: bool,
is_defer: bool, is_defer: bool,
source: Source, source: Source,
onload: ?Callback,
onerror: ?Callback,
element: *parser.Element, element: *parser.Element,
const Kind = enum { const Kind = enum {
@@ -644,7 +615,7 @@ const Script = struct {
} }
fn executeCallback(self: *const Script, comptime typ: []const u8, page: *Page) void { fn executeCallback(self: *const Script, comptime typ: []const u8, page: *Page) void {
const callback = @field(self, typ) orelse return; const callback = self.getCallback(typ, page) orelse return;
switch (callback) { switch (callback) {
.string => |str| { .string => |str| {
@@ -687,6 +658,23 @@ const Script = struct {
}, },
} }
} }
fn getCallback(self: *const Script, comptime typ: []const u8, page: *Page) ?Callback {
const element = self.element;
// first we check if there was an el.onload set directly on the
// element in JavaScript (if so, it'd be stored in the node state)
if (page.getNodeState(@ptrCast(element))) |se| {
if (@field(se, typ)) |function| {
return .{ .function = function };
}
}
// if we have no node state, or if the node state has no onload/onerror
// then check for the onload/onerror attribute
if (parser.elementGetAttribute(element, typ) catch null) |string| {
return .{ .string = string };
}
return null;
}
}; };
const BufferPool = struct { const BufferPool = struct {

View File

@@ -262,7 +262,6 @@ pub const EventHandler = struct {
}, },
} }
} }
const callback = (try listener.callback(target)) orelse return null; const callback = (try listener.callback(target)) orelse return null;
if (signal) |s| { if (signal) |s| {

View File

@@ -862,12 +862,23 @@ pub const HTMLScriptElement = struct {
) orelse ""; ) orelse "";
} }
pub fn set_src(self: *parser.Script, v: []const u8) !void { pub fn set_src(self: *parser.Script, v: []const u8, page: *Page) !void {
return try parser.elementSetAttribute( try parser.elementSetAttribute(
parser.scriptToElt(self), parser.scriptToElt(self),
"src", "src",
v, v,
); );
if (try Node.get_isConnected(@alignCast(@ptrCast(self)))) {
// There are sites which do set the src AFTER appending the script
// tag to the document:
// const s = document.createElement('script');
// document.getElementsByTagName('body')[0].appendChild(s);
// s.src = '...';
// This should load the script.
// addFromElement protects against double execution.
try page.script_manager.addFromElement(@alignCast(@ptrCast(self)));
}
} }
pub fn get_type(self: *parser.Script) !?[]const u8 { pub fn get_type(self: *parser.Script) !?[]const u8 {
@@ -878,7 +889,7 @@ pub const HTMLScriptElement = struct {
} }
pub fn set_type(self: *parser.Script, v: []const u8) !void { pub fn set_type(self: *parser.Script, v: []const u8) !void {
return try parser.elementSetAttribute( try parser.elementSetAttribute(
parser.scriptToElt(self), parser.scriptToElt(self),
"type", "type",
v, v,
@@ -893,7 +904,7 @@ pub const HTMLScriptElement = struct {
} }
pub fn set_text(self: *parser.Script, v: []const u8) !void { pub fn set_text(self: *parser.Script, v: []const u8) !void {
return try parser.elementSetAttribute( try parser.elementSetAttribute(
parser.scriptToElt(self), parser.scriptToElt(self),
"text", "text",
v, v,
@@ -908,7 +919,7 @@ pub const HTMLScriptElement = struct {
} }
pub fn set_integrity(self: *parser.Script, v: []const u8) !void { pub fn set_integrity(self: *parser.Script, v: []const u8) !void {
return try parser.elementSetAttribute( try parser.elementSetAttribute(
parser.scriptToElt(self), parser.scriptToElt(self),
"integrity", "integrity",
v, v,

View File

@@ -1110,13 +1110,22 @@ fn timestamp() u32 {
// so that's handled. And because we're only executing the inline <script> tags // so that's handled. And because we're only executing the inline <script> tags
// 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 {
const self: *Page = @alignCast(@ptrCast(ctx.?)); const self: *Page = @alignCast(@ptrCast(ctx.?));
if (self.delayed_navigation) { if (self.delayed_navigation) {
// if we're planning on navigating to another page, don't run this script // if we're planning on navigating to another page, don't run this script
return; return;
} }
// It's posisble for a script to be dynamically added without a src.
// const s = document.createElement('script');
// document.getElementsByTagName('body')[0].appendChild(s);
// The src can be set after. We handle that in HTMLScriptElement.set_src,
// but it's important we don't pass such elements to the script_manager
// here, else the script_manager will flag it as already-processed.
_ = parser.elementGetAttribute(element.?, "src") catch return orelse return;
self.script_manager.addFromElement(element.?) catch |err| { self.script_manager.addFromElement(element.?) catch |err| {
log.warn(.browser, "dynamic script", .{ .err = err }); log.warn(.browser, "dynamic script", .{ .err = err });
}; };