Skip to content

Commit be8ba53

Browse files
authored
Merge pull request #1811 from lightpanda-io/script_handling
Better script handling.
2 parents 043d48d + a1a7919 commit be8ba53

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)