Merge pull request #1811 from lightpanda-io/script_handling
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / zig build release (push) Has been cancelled
wpt / build wpt runner (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (push) Has been cancelled

Better script handling.
This commit is contained in:
Karl Seguin
2026-03-13 21:40:19 +08:00
committed by GitHub
6 changed files with 123 additions and 7 deletions

View File

@@ -1010,6 +1010,14 @@ pub fn scriptAddedCallback(self: *Page, comptime from_parser: bool, script: *Ele
return; return;
} }
if (comptime from_parser) {
// parser-inserted scripts have force-async set to false, but only if
// they have src or non-empty content
if (script._src.len > 0 or script.asNode().firstChild() != null) {
script._force_async = false;
}
}
self._script_manager.addFromElement(from_parser, script, "parsing") catch |err| { self._script_manager.addFromElement(from_parser, script, "parsing") catch |err| {
log.err(.page, "page.scriptAddedCallback", .{ log.err(.page, "page.scriptAddedCallback", .{
.err = err, .err = err,
@@ -2643,6 +2651,8 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
} }
} }
const parent_is_connected = parent.isConnected();
// Tri-state behavior for mutations: // Tri-state behavior for mutations:
// 1. from_parser=true, parse_mode=document -> no mutations (initial document parse) // 1. from_parser=true, parse_mode=document -> no mutations (initial document parse)
// 2. from_parser=true, parse_mode=fragment -> mutations (innerHTML additions) // 2. from_parser=true, parse_mode=fragment -> mutations (innerHTML additions)
@@ -2658,6 +2668,15 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
// When the parser adds the node, nodeIsReady is only called when the // When the parser adds the node, nodeIsReady is only called when the
// nodeComplete() callback is executed. // nodeComplete() callback is executed.
try self.nodeIsReady(false, child); try self.nodeIsReady(false, child);
// Check if text was added to a script that hasn't started yet.
if (child._type == .cdata and parent_is_connected) {
if (parent.is(Element.Html.Script)) |script| {
if (!script._executed) {
try self.nodeIsReady(false, parent);
}
}
}
} }
// Notify mutation observers about childList change // Notify mutation observers about childList change
@@ -2696,7 +2715,6 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
} }
const parent_in_shadow = parent.is(ShadowRoot) != null or parent.isInShadowTree(); const parent_in_shadow = parent.is(ShadowRoot) != null or parent.isInShadowTree();
const parent_is_connected = parent.isConnected();
if (!parent_in_shadow and !parent_is_connected) { if (!parent_in_shadow and !parent_is_connected) {
return; return;

View File

@@ -159,7 +159,6 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
// <script> has already been processed. // <script> has already been processed.
return; return;
} }
script_element._executed = true;
const element = script_element.asElement(); const element = script_element.asElement();
if (element.getAttributeSafe(comptime .wrap("nomodule")) != null) { if (element.getAttributeSafe(comptime .wrap("nomodule")) != null) {
@@ -204,10 +203,22 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
source = .{ .remote = .{} }; source = .{ .remote = .{} };
} }
} else { } else {
const inline_source = try element.asNode().getTextContentAlloc(page.arena); var buf = std.Io.Writer.Allocating.init(page.arena);
try element.asNode().getChildTextContent(&buf.writer);
try buf.writer.writeByte(0);
const data = buf.written();
const inline_source: [:0]const u8 = data[0 .. data.len - 1 :0];
if (inline_source.len == 0) {
// we haven't set script_element._executed = true yet, which is good.
// If content is appended to the script, we will execute it then.
return;
}
source = .{ .@"inline" = inline_source }; source = .{ .@"inline" = inline_source };
} }
// Only set _executed (already-started) when we actually have content to execute
script_element._executed = true;
const script = try self.script_pool.create(); const script = try self.script_pool.create();
errdefer self.script_pool.destroy(script); errdefer self.script_pool.destroy(script);

View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<script src="../../../testing.js"></script>
<script id=force_async>
{
// Dynamically created scripts have async=true by default
let s = document.createElement('script');
testing.expectEqual(true, s.async);
// Setting async=false clears the force async flag and removes attribute
s.async = false;
testing.expectEqual(false, s.async);
testing.expectEqual(false, s.hasAttribute('async'));
// Setting async=true adds the attribute
s.async = true;
testing.expectEqual(true, s.async);
testing.expectEqual(true, s.hasAttribute('async'));
}
</script>
<script></script>
<script id=empty>
{
// Empty parser-inserted script should have async=true (force async retained)
let scripts = document.getElementsByTagName('script');
let emptyScript = scripts[scripts.length - 2];
testing.expectEqual(true, emptyScript.async);
}
</script>
<script id=text_content>
{
let s = document.createElement('script');
s.appendChild(document.createComment('COMMENT'));
s.appendChild(document.createTextNode(' TEXT '));
s.appendChild(document.createProcessingInstruction('P', 'I'));
let a = s.appendChild(document.createElement('a'));
a.appendChild(document.createTextNode('ELEMENT'));
// script.text should return only direct Text node children
testing.expectEqual(' TEXT ', s.text);
// script.textContent should return all descendant text
testing.expectEqual(' TEXT ELEMENT', s.textContent);
}
</script>
<script id=lazy_inline>
{
// Empty script in DOM, then append text - should execute
window.lazyScriptRan = false;
let s = document.createElement('script');
document.head.appendChild(s);
// Script is in DOM but empty, so not yet executed
testing.expectEqual(false, window.lazyScriptRan);
// Append text node with code
s.appendChild(document.createTextNode('window.lazyScriptRan = true;'));
// Now it should have executed
testing.expectEqual(true, window.lazyScriptRan);
}
</script>

View File

@@ -151,9 +151,14 @@ pub fn asNode(self: *CData) *Node {
pub fn is(self: *CData, comptime T: type) ?*T { pub fn is(self: *CData, comptime T: type) ?*T {
inline for (@typeInfo(Type).@"union".fields) |f| { inline for (@typeInfo(Type).@"union".fields) |f| {
if (f.type == T and @field(Type, f.name) == self._type) { if (@field(Type, f.name) == self._type) {
if (f.type == T) {
return &@field(self._type, f.name); return &@field(self._type, f.name);
} }
if (f.type == *T) {
return @field(self._type, f.name);
}
}
} }
return null; return null;
} }

View File

@@ -285,6 +285,19 @@ pub fn getTextContentAlloc(self: *Node, allocator: Allocator) error{WriteFailed}
return data[0 .. data.len - 1 :0]; return data[0 .. data.len - 1 :0];
} }
/// Returns the "child text content" which is the concatenation of the data
/// of all the Text node children of the node, in tree order.
/// This differs from textContent which includes all descendant text.
/// See: https://dom.spec.whatwg.org/#concept-child-text-content
pub fn getChildTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFailed}!void {
var it = self.childrenIterator();
while (it.next()) |child| {
if (child.is(CData.Text)) |text| {
try writer.writeAll(text._proto._data.str());
}
}
}
pub fn setTextContent(self: *Node, data: []const u8, page: *Page) !void { pub fn setTextContent(self: *Node, data: []const u8, page: *Page) !void {
switch (self._type) { switch (self._type) {
.element => |el| { .element => |el| {

View File

@@ -31,6 +31,8 @@ const Script = @This();
_proto: *HtmlElement, _proto: *HtmlElement,
_src: []const u8 = "", _src: []const u8 = "",
_executed: bool = false, _executed: bool = false,
// dynamic scripts are forced to be async by default
_force_async: bool = true,
pub fn asElement(self: *Script) *Element { pub fn asElement(self: *Script) *Element {
return self._proto._proto; return self._proto._proto;
@@ -83,10 +85,11 @@ pub fn setCharset(self: *Script, value: []const u8, page: *Page) !void {
} }
pub fn getAsync(self: *const Script) bool { pub fn getAsync(self: *const Script) bool {
return self.asConstElement().getAttributeSafe(comptime .wrap("async")) != null; return self._force_async or self.asConstElement().getAttributeSafe(comptime .wrap("async")) != null;
} }
pub fn setAsync(self: *Script, value: bool, page: *Page) !void { pub fn setAsync(self: *Script, value: bool, page: *Page) !void {
self._force_async = false;
if (value) { if (value) {
try self.asElement().setAttributeSafe(comptime .wrap("async"), .wrap(""), page); try self.asElement().setAttributeSafe(comptime .wrap("async"), .wrap(""), page);
} else { } else {
@@ -136,7 +139,12 @@ pub const JsApi = struct {
try self.asNode().getTextContent(&buf.writer); try self.asNode().getTextContent(&buf.writer);
return buf.written(); return buf.written();
} }
pub const text = bridge.accessor(_innerText, Script.setInnerText, .{}); pub const text = bridge.accessor(_text, Script.setInnerText, .{});
fn _text(self: *Script, page: *const Page) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try self.asNode().getChildTextContent(&buf.writer);
return buf.written();
}
}; };
pub const Build = struct { pub const Build = struct {