mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
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
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:
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
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 {
|
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) {
|
||||||
return &@field(self._type, f.name);
|
if (f.type == T) {
|
||||||
|
return &@field(self._type, f.name);
|
||||||
|
}
|
||||||
|
if (f.type == *T) {
|
||||||
|
return @field(self._type, f.name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -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| {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user