Skip to content

Commit a1a7919

Browse files
committed
Better script handling.
Dynamic scripts have script.async == true by default (we handled this correctly in the ScriptManager, but we didn't return the right value when .async was accessed). Inline scripts only consider direct children, not the entire tree. Empty inline scripts are executed at a later time if text is inserted into them
1 parent dd35bdf commit a1a7919

File tree

6 files changed

+123
-7
lines changed

6 files changed

+123
-7
lines changed

src/browser/Page.zig

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,14 @@ pub fn scriptAddedCallback(self: *Page, comptime from_parser: bool, script: *Ele
10101010
return;
10111011
}
10121012

1013+
if (comptime from_parser) {
1014+
// parser-inserted scripts have force-async set to false, but only if
1015+
// they have src or non-empty content
1016+
if (script._src.len > 0 or script.asNode().firstChild() != null) {
1017+
script._force_async = false;
1018+
}
1019+
}
1020+
10131021
self._script_manager.addFromElement(from_parser, script, "parsing") catch |err| {
10141022
log.err(.page, "page.scriptAddedCallback", .{
10151023
.err = err,
@@ -2643,6 +2651,8 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
26432651
}
26442652
}
26452653

2654+
const parent_is_connected = parent.isConnected();
2655+
26462656
// Tri-state behavior for mutations:
26472657
// 1. from_parser=true, parse_mode=document -> no mutations (initial document parse)
26482658
// 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
26582668
// When the parser adds the node, nodeIsReady is only called when the
26592669
// nodeComplete() callback is executed.
26602670
try self.nodeIsReady(false, child);
2671+
2672+
// Check if text was added to a script that hasn't started yet.
2673+
if (child._type == .cdata and parent_is_connected) {
2674+
if (parent.is(Element.Html.Script)) |script| {
2675+
if (!script._executed) {
2676+
try self.nodeIsReady(false, parent);
2677+
}
2678+
}
2679+
}
26612680
}
26622681

26632682
// Notify mutation observers about childList change
@@ -2696,7 +2715,6 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
26962715
}
26972716

26982717
const parent_in_shadow = parent.is(ShadowRoot) != null or parent.isInShadowTree();
2699-
const parent_is_connected = parent.isConnected();
27002718

27012719
if (!parent_in_shadow and !parent_is_connected) {
27022720
return;

src/browser/ScriptManager.zig

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
159159
// <script> has already been processed.
160160
return;
161161
}
162-
script_element._executed = true;
163162

164163
const element = script_element.asElement();
165164
if (element.getAttributeSafe(comptime .wrap("nomodule")) != null) {
@@ -204,10 +203,22 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
204203
source = .{ .remote = .{} };
205204
}
206205
} else {
207-
const inline_source = try element.asNode().getTextContentAlloc(page.arena);
206+
var buf = std.Io.Writer.Allocating.init(page.arena);
207+
try element.asNode().getChildTextContent(&buf.writer);
208+
try buf.writer.writeByte(0);
209+
const data = buf.written();
210+
const inline_source: [:0]const u8 = data[0 .. data.len - 1 :0];
211+
if (inline_source.len == 0) {
212+
// we haven't set script_element._executed = true yet, which is good.
213+
// If content is appended to the script, we will execute it then.
214+
return;
215+
}
208216
source = .{ .@"inline" = inline_source };
209217
}
210218

219+
// Only set _executed (already-started) when we actually have content to execute
220+
script_element._executed = true;
221+
211222
const script = try self.script_pool.create();
212223
errdefer self.script_pool.destroy(script);
213224

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<!DOCTYPE html>
2+
<script src="../../../testing.js"></script>
3+
4+
<script id=force_async>
5+
{
6+
// Dynamically created scripts have async=true by default
7+
let s = document.createElement('script');
8+
testing.expectEqual(true, s.async);
9+
10+
// Setting async=false clears the force async flag and removes attribute
11+
s.async = false;
12+
testing.expectEqual(false, s.async);
13+
testing.expectEqual(false, s.hasAttribute('async'));
14+
15+
// Setting async=true adds the attribute
16+
s.async = true;
17+
testing.expectEqual(true, s.async);
18+
testing.expectEqual(true, s.hasAttribute('async'));
19+
}
20+
</script>
21+
22+
<script></script>
23+
<script id=empty>
24+
{
25+
// Empty parser-inserted script should have async=true (force async retained)
26+
let scripts = document.getElementsByTagName('script');
27+
let emptyScript = scripts[scripts.length - 2];
28+
testing.expectEqual(true, emptyScript.async);
29+
}
30+
</script>
31+
32+
<script id=text_content>
33+
{
34+
let s = document.createElement('script');
35+
s.appendChild(document.createComment('COMMENT'));
36+
s.appendChild(document.createTextNode(' TEXT '));
37+
s.appendChild(document.createProcessingInstruction('P', 'I'));
38+
let a = s.appendChild(document.createElement('a'));
39+
a.appendChild(document.createTextNode('ELEMENT'));
40+
41+
// script.text should return only direct Text node children
42+
testing.expectEqual(' TEXT ', s.text);
43+
// script.textContent should return all descendant text
44+
testing.expectEqual(' TEXT ELEMENT', s.textContent);
45+
}
46+
</script>
47+
48+
<script id=lazy_inline>
49+
{
50+
// Empty script in DOM, then append text - should execute
51+
window.lazyScriptRan = false;
52+
let s = document.createElement('script');
53+
document.head.appendChild(s);
54+
// Script is in DOM but empty, so not yet executed
55+
testing.expectEqual(false, window.lazyScriptRan);
56+
// Append text node with code
57+
s.appendChild(document.createTextNode('window.lazyScriptRan = true;'));
58+
// Now it should have executed
59+
testing.expectEqual(true, window.lazyScriptRan);
60+
}
61+
</script>

src/browser/webapi/CData.zig

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,13 @@ pub fn asNode(self: *CData) *Node {
151151

152152
pub fn is(self: *CData, comptime T: type) ?*T {
153153
inline for (@typeInfo(Type).@"union".fields) |f| {
154-
if (f.type == T and @field(Type, f.name) == self._type) {
155-
return &@field(self._type, f.name);
154+
if (@field(Type, f.name) == self._type) {
155+
if (f.type == T) {
156+
return &@field(self._type, f.name);
157+
}
158+
if (f.type == *T) {
159+
return @field(self._type, f.name);
160+
}
156161
}
157162
}
158163
return null;

src/browser/webapi/Node.zig

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,19 @@ pub fn getTextContentAlloc(self: *Node, allocator: Allocator) error{WriteFailed}
285285
return data[0 .. data.len - 1 :0];
286286
}
287287

288+
/// Returns the "child text content" which is the concatenation of the data
289+
/// of all the Text node children of the node, in tree order.
290+
/// This differs from textContent which includes all descendant text.
291+
/// See: https://dom.spec.whatwg.org/#concept-child-text-content
292+
pub fn getChildTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFailed}!void {
293+
var it = self.childrenIterator();
294+
while (it.next()) |child| {
295+
if (child.is(CData.Text)) |text| {
296+
try writer.writeAll(text._proto._data.str());
297+
}
298+
}
299+
}
300+
288301
pub fn setTextContent(self: *Node, data: []const u8, page: *Page) !void {
289302
switch (self._type) {
290303
.element => |el| {

src/browser/webapi/element/html/Script.zig

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const Script = @This();
3131
_proto: *HtmlElement,
3232
_src: []const u8 = "",
3333
_executed: bool = false,
34+
// dynamic scripts are forced to be async by default
35+
_force_async: bool = true,
3436

3537
pub fn asElement(self: *Script) *Element {
3638
return self._proto._proto;
@@ -83,10 +85,11 @@ pub fn setCharset(self: *Script, value: []const u8, page: *Page) !void {
8385
}
8486

8587
pub fn getAsync(self: *const Script) bool {
86-
return self.asConstElement().getAttributeSafe(comptime .wrap("async")) != null;
88+
return self._force_async or self.asConstElement().getAttributeSafe(comptime .wrap("async")) != null;
8789
}
8890

8991
pub fn setAsync(self: *Script, value: bool, page: *Page) !void {
92+
self._force_async = false;
9093
if (value) {
9194
try self.asElement().setAttributeSafe(comptime .wrap("async"), .wrap(""), page);
9295
} else {
@@ -136,7 +139,12 @@ pub const JsApi = struct {
136139
try self.asNode().getTextContent(&buf.writer);
137140
return buf.written();
138141
}
139-
pub const text = bridge.accessor(_innerText, Script.setInnerText, .{});
142+
pub const text = bridge.accessor(_text, Script.setInnerText, .{});
143+
fn _text(self: *Script, page: *const Page) ![]const u8 {
144+
var buf = std.Io.Writer.Allocating.init(page.call_arena);
145+
try self.asNode().getChildTextContent(&buf.writer);
146+
return buf.written();
147+
}
140148
};
141149

142150
pub const Build = struct {

0 commit comments

Comments
 (0)