mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-21 20:24:42 +00:00
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
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -159,7 +159,6 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
||||
// <script> has already been processed.
|
||||
return;
|
||||
}
|
||||
script_element._executed = true;
|
||||
|
||||
const element = script_element.asElement();
|
||||
if (element.getAttributeSafe(comptime .wrap("nomodule")) != null) {
|
||||
@@ -204,10 +203,22 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
||||
source = .{ .remote = .{} };
|
||||
}
|
||||
} 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 };
|
||||
}
|
||||
|
||||
// Only set _executed (already-started) when we actually have content to execute
|
||||
script_element._executed = true;
|
||||
|
||||
const script = try self.script_pool.create();
|
||||
errdefer self.script_pool.destroy(script);
|
||||
|
||||
|
||||
61
src/browser/tests/element/html/script/async_text.html
Normal file
61
src/browser/tests/element/html/script/async_text.html
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user