diff --git a/src/browser/Page.zig b/src/browser/Page.zig index f0c914ad..0599bab5 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1010,6 +1010,14 @@ pub fn scriptAddedCallback(self: *Page, comptime from_parser: bool, script: *Ele 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| { log.err(.page, "page.scriptAddedCallback", .{ .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: // 1. from_parser=true, parse_mode=document -> no mutations (initial document parse) // 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 // nodeComplete() callback is executed. 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 @@ -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_is_connected = parent.isConnected(); if (!parent_in_shadow and !parent_is_connected) { return; diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 6f55f43b..2baeef8d 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -159,7 +159,6 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e // + + + + + + + + + diff --git a/src/browser/webapi/CData.zig b/src/browser/webapi/CData.zig index 4fb6de6f..4e806c85 100644 --- a/src/browser/webapi/CData.zig +++ b/src/browser/webapi/CData.zig @@ -151,8 +151,13 @@ pub fn asNode(self: *CData) *Node { pub fn is(self: *CData, comptime T: type) ?*T { inline for (@typeInfo(Type).@"union".fields) |f| { - if (f.type == T and @field(Type, f.name) == self._type) { - return &@field(self._type, f.name); + if (@field(Type, f.name) == self._type) { + if (f.type == T) { + return &@field(self._type, f.name); + } + if (f.type == *T) { + return @field(self._type, f.name); + } } } return null; diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 3673a1e7..2eca4047 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -285,6 +285,19 @@ pub fn getTextContentAlloc(self: *Node, allocator: Allocator) error{WriteFailed} 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 { switch (self._type) { .element => |el| { diff --git a/src/browser/webapi/element/html/Script.zig b/src/browser/webapi/element/html/Script.zig index ad3d9ef7..00860d5f 100644 --- a/src/browser/webapi/element/html/Script.zig +++ b/src/browser/webapi/element/html/Script.zig @@ -31,6 +31,8 @@ const Script = @This(); _proto: *HtmlElement, _src: []const u8 = "", _executed: bool = false, +// dynamic scripts are forced to be async by default +_force_async: bool = true, pub fn asElement(self: *Script) *Element { 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 { - 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 { + self._force_async = false; if (value) { try self.asElement().setAttributeSafe(comptime .wrap("async"), .wrap(""), page); } else { @@ -136,7 +139,12 @@ pub const JsApi = struct { try self.asNode().getTextContent(&buf.writer); 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 {