Merge branch 'main' into semantic-tree

This commit is contained in:
Adrià Arrufat
2026-03-08 08:18:08 +09:00
27 changed files with 532 additions and 321 deletions

View File

@@ -5,6 +5,7 @@ env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }} AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }} AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
AWS_CF_DISTRIBUTION: ${{ vars.AWS_CF_DISTRIBUTION }}
LIGHTPANDA_DISABLE_TELEMETRY: true LIGHTPANDA_DISABLE_TELEMETRY: true
on: on:
@@ -73,7 +74,7 @@ jobs:
# use a self host runner. # use a self host runner.
runs-on: lpd-bench-hetzner runs-on: lpd-bench-hetzner
timeout-minutes: 120 timeout-minutes: 180
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
@@ -107,7 +108,7 @@ jobs:
run: | run: |
./wpt serve 2> /dev/null & echo $! > WPT.pid ./wpt serve 2> /dev/null & echo $! > WPT.pid
sleep 10s sleep 10s
./wptrunner -lpd-path ./lightpanda -json -concurrency 3 > wpt.json ./wptrunner -lpd-path ./lightpanda -json -concurrency 6 > wpt.json
kill `cat WPT.pid` kill `cat WPT.pid`
- name: write commit - name: write commit

View File

@@ -47,10 +47,17 @@ pub fn init(allocator: Allocator, config: *const Config) !*App {
const app = try allocator.create(App); const app = try allocator.create(App);
errdefer allocator.destroy(app); errdefer allocator.destroy(app);
app.config = config; app.* = .{
app.allocator = allocator; .config = config,
.allocator = allocator,
app.robots = RobotStore.init(allocator); .robots = RobotStore.init(allocator),
.http = undefined,
.platform = undefined,
.snapshot = undefined,
.app_dir_path = undefined,
.telemetry = undefined,
.arena_pool = undefined,
};
app.http = try Http.init(allocator, &app.robots, config); app.http = try Http.init(allocator, &app.robots, config);
errdefer app.http.deinit(); errdefer app.http.deinit();

View File

@@ -69,6 +69,7 @@ const ArenaPool = App.ArenaPool;
const timestamp = @import("../datetime.zig").timestamp; const timestamp = @import("../datetime.zig").timestamp;
const milliTimestamp = @import("../datetime.zig").milliTimestamp; const milliTimestamp = @import("../datetime.zig").milliTimestamp;
const IFrame = Element.Html.IFrame;
const WebApiURL = @import("webapi/URL.zig"); const WebApiURL = @import("webapi/URL.zig");
const GlobalEventHandlersLookup = @import("webapi/global_event_handlers.zig").Lookup; const GlobalEventHandlersLookup = @import("webapi/global_event_handlers.zig").Lookup;
@@ -223,7 +224,7 @@ _arena_pool_leak_track: (if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, struct {
parent: ?*Page, parent: ?*Page,
window: *Window, window: *Window,
document: *Document, document: *Document,
iframe: ?*Element.Html.IFrame = null, iframe: ?*IFrame = null,
frames: std.ArrayList(*Page) = .{}, frames: std.ArrayList(*Page) = .{},
frames_sorted: bool = true, frames_sorted: bool = true,
@@ -323,9 +324,9 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void
} }
} }
pub fn deinit(self: *Page) void { pub fn deinit(self: *Page, abort_http: bool) void {
for (self.frames.items) |frame| { for (self.frames.items) |frame| {
frame.deinit(); frame.deinit(abort_http);
} }
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
@@ -346,10 +347,16 @@ pub fn deinit(self: *Page) void {
session.browser.env.destroyContext(self.js); session.browser.env.destroyContext(self.js);
self._script_manager.shutdown = true; self._script_manager.shutdown = true;
if (self.parent == null) { if (self.parent == null) {
// only the root frame needs to abort this. It's more efficient this way
session.browser.http_client.abort(); session.browser.http_client.abort();
} else if (abort_http) {
// a small optimization, it's faster to abort _everything_ on the root
// page, so we prefer that. But if it's just the frame that's going
// away (a frame navigation) then we'll abort the frame-related requests
session.browser.http_client.abortFrame(self._frame_id);
} }
self._script_manager.deinit(); self._script_manager.deinit();
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
@@ -357,6 +364,9 @@ pub fn deinit(self: *Page) void {
while (it.next()) |value_ptr| { while (it.next()) |value_ptr| {
if (value_ptr.count > 0) { if (value_ptr.count > 0) {
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner, .type = self._type, .url = self.url }); log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner, .type = self._type, .url = self.url });
if (comptime builtin.is_test) {
@panic("ArenaPool Leak");
}
} }
} }
} }
@@ -429,6 +439,9 @@ pub fn releaseArena(self: *Page, allocator: Allocator) void {
const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?; const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?;
if (found.count != 1) { if (found.count != 1) {
log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count, .type = self._type, .url = self.url }); log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count, .type = self._type, .url = self.url });
if (comptime builtin.is_test) {
@panic("ArenaPool Double Free");
}
return; return;
} }
found.count = 0; found.count = 0;
@@ -459,16 +472,15 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
// if the url is about:blank, we load an empty HTML document in the // if the url is about:blank, we load an empty HTML document in the
// page and dispatch the events. // page and dispatch the events.
if (std.mem.eql(u8, "about:blank", request_url)) { if (std.mem.eql(u8, "about:blank", request_url)) {
self.url = "about:blank";
// Assume we parsed the document. // Assume we parsed the document.
// It's important to force a reset during the following navigation. // It's important to force a reset during the following navigation.
self._parse_state = .complete; self._parse_state = .complete;
{ self.document.injectBlank(self) catch |err| {
const parse_arena = try self.getArena(.{ .debug = "about:blank parse" }); log.err(.browser, "inject blank", .{ .err = err });
defer self.releaseArena(parse_arena); return error.InjectBlankFailed;
var parser = Parser.init(parse_arena, self.document.asNode(), self); };
parser.parse("<html><head></head><body></body></html>");
}
self.documentIsComplete(); self.documentIsComplete();
session.notification.dispatch(.page_navigate, &.{ session.notification.dispatch(.page_navigate, &.{
@@ -559,57 +571,89 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
}; };
} }
// We cannot navigate immediately as navigating will delete the DOM tree, // Navigation can happen in many places, such as executing a <script> tag or
// which holds this event's node. // a JavaScript callback, a CDP command, etc...It's rarely safe to do immediately
// As such we schedule the function to be called as soon as possible. // as the caller almost certainly does'nt expect the page to go away during the
pub fn scheduleNavigation(self: *Page, request_url: []const u8, opts: NavigateOpts, priority: NavigationPriority) !void { // call. So, we schedule the navigation for the next tick.
if (self.canScheduleNavigation(priority) == false) { pub fn scheduleNavigation(self: *Page, request_url: []const u8, opts: NavigateOpts, nt: Navigation) !void {
if (self.canScheduleNavigation(std.meta.activeTag(nt)) == false) {
return; return;
} }
const arena = try self.arena_pool.acquire(); const arena = try self.arena_pool.acquire();
errdefer self.arena_pool.release(arena); errdefer self.arena_pool.release(arena);
return self.scheduleNavigationWithArena(arena, request_url, opts, priority); return self.scheduleNavigationWithArena(arena, request_url, opts, nt);
} }
fn scheduleNavigationWithArena(self: *Page, arena: Allocator, request_url: []const u8, opts: NavigateOpts, priority: NavigationPriority) !void { // Don't name the first parameter "self", because the target of this navigation
const resolved_url = try URL.resolve( // might change inside the function. So the code should be explicit about the
arena, // page that it's acting on.
self.base(), fn scheduleNavigationWithArena(originator: *Page, arena: Allocator, request_url: []const u8, opts: NavigateOpts, nt: Navigation) !void {
request_url, const resolved_url, const is_about_blank = blk: {
.{ .always_dupe = true, .encode = true }, if (std.mem.eql(u8, request_url, "about:blank")) {
); // navigate will handle this special case
break :blk .{ "about:blank", true };
}
const u = try URL.resolve(
arena,
originator.base(),
request_url,
.{ .always_dupe = true, .encode = true },
);
break :blk .{ u, false };
};
const session = self._session; const target = switch (nt) {
if (!opts.force and URL.eqlDocument(self.url, resolved_url)) { .script => |p| p orelse originator,
self.arena_pool.release(arena); .iframe => |iframe| iframe._window.?._page, // only an frame with existing content (i.e. a window) can be navigated
.anchor, .form => |node| blk: {
const doc = node.ownerDocument(originator) orelse break :blk originator;
break :blk doc._page orelse originator;
},
};
self.url = try self.arena.dupeZ(u8, resolved_url); const session = target._session;
self.window._location = try Location.init(self.url, self); if (!opts.force and URL.eqlDocument(target.url, resolved_url)) {
self.document._location = self.window._location; target.url = try target.arena.dupeZ(u8, resolved_url);
return session.navigation.updateEntries(self.url, opts.kind, self, true); target.window._location = try Location.init(target.url, target);
target.document._location = target.window._location;
if (target.parent == null) {
try session.navigation.updateEntries(target.url, opts.kind, target, true);
}
// doin't defer this, the caller, the caller is responsible for freeing
// it on error
target.arena_pool.release(arena);
return;
} }
log.info(.browser, "schedule navigation", .{ log.info(.browser, "schedule navigation", .{
.url = resolved_url, .url = resolved_url,
.reason = opts.reason, .reason = opts.reason,
.target = resolved_url, .type = target._type,
.type = self._type,
}); });
session.browser.http_client.abort(); // This is a micro-optimization. Terminate any inflight request as early
// as we can. This will be more propery shutdown when we process the
// scheduled navigation.
if (target.parent == null) {
session.browser.http_client.abort();
} else {
// This doesn't terminate any inflight requests for nested frames, but
// again, this is just an optimization. We'll correctly shut down all
// nested inflight requests when we process the navigation.
session.browser.http_client.abortFrame(target._frame_id);
}
const qn = try arena.create(QueuedNavigation); const qn = try arena.create(QueuedNavigation);
qn.* = .{ qn.* = .{
.opts = opts, .opts = opts,
.arena = arena, .arena = arena,
.url = resolved_url, .url = resolved_url,
.priority = priority, .is_about_blank = is_about_blank,
.navigation_type = std.meta.activeTag(nt),
}; };
if (self._queued_navigation) |existing| { target._queued_navigation = qn;
self.arena_pool.release(existing.arena); return session.scheduleNavigation(target);
}
self._queued_navigation = qn;
} }
// A script can have multiple competing navigation events, say it starts off // A script can have multiple competing navigation events, say it starts off
@@ -617,23 +661,31 @@ fn scheduleNavigationWithArena(self: *Page, arena: Allocator, request_url: []con
// You might think that we just stop at the first one, but that doesn't seem // You might think that we just stop at the first one, but that doesn't seem
// to be what browsers do, and it isn't particularly well supported by v8 (i.e. // to be what browsers do, and it isn't particularly well supported by v8 (i.e.
// halting execution mid-script). // halting execution mid-script).
// From what I can tell, there are 3 "levels" of priority, in order: // From what I can tell, there are 4 "levels" of priority, in order:
// 1 - form submission // 1 - form submission
// 2 - JavaScript apis (e.g. top.location) // 2 - JavaScript apis (e.g. top.location)
// 3 - anchor clicks // 3 - anchor clicks
// 4 - iframe.src =
// Within, each category, it's last-one-wins. // Within, each category, it's last-one-wins.
fn canScheduleNavigation(self: *Page, priority: NavigationPriority) bool { fn canScheduleNavigation(self: *Page, new_target_type: NavigationType) bool {
const existing = self._queued_navigation orelse return true; if (self.parent) |parent| {
if (parent.isGoingAway()) {
return false;
}
}
if (existing.priority == priority) { const existing_target_type = (self._queued_navigation orelse return true).navigation_type;
if (existing_target_type == new_target_type) {
// same reason, than this latest one wins // same reason, than this latest one wins
return true; return true;
} }
return switch (existing.priority) { return switch (existing_target_type) {
.anchor => true, // everything is higher priority than an anchor .iframe => true, // everything is higher priority than iframe.src = "x"
.anchor => new_target_type != .iframe, // an anchor is only higher priority than an iframe
.form => false, // nothing is higher priority than a form .form => false, // nothing is higher priority than a form
.script => priority == .form, // a form is higher priority than a script .script => new_target_type == .form, // a form is higher priority than a script
}; };
} }
@@ -665,7 +717,7 @@ pub fn scriptsCompletedLoading(self: *Page) void {
self.pendingLoadCompleted(); self.pendingLoadCompleted();
} }
pub fn iframeCompletedLoading(self: *Page, iframe: *Element.Html.IFrame) void { pub fn iframeCompletedLoading(self: *Page, iframe: *IFrame) void {
blk: { blk: {
var ls: JS.Local.Scope = undefined; var ls: JS.Local.Scope = undefined;
self.js.localScope(&ls); self.js.localScope(&ls);
@@ -758,6 +810,8 @@ fn _documentIsComplete(self: *Page) !void {
} }
fn notifyParentLoadComplete(self: *Page) void { fn notifyParentLoadComplete(self: *Page) void {
const parent = self.parent orelse return;
if (self._parent_notified == true) { if (self._parent_notified == true) {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
std.debug.assert(false); std.debug.assert(false);
@@ -767,9 +821,7 @@ fn notifyParentLoadComplete(self: *Page) void {
} }
self._parent_notified = true; self._parent_notified = true;
if (self.parent) |p| { parent.iframeCompletedLoading(self.iframe.?);
p.iframeCompletedLoading(self.iframe.?);
}
} }
fn pageHeaderDoneCallback(transfer: *Http.Transfer) !bool { fn pageHeaderDoneCallback(transfer: *Http.Transfer) !bool {
@@ -949,7 +1001,11 @@ fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
} }
pub fn isGoingAway(self: *const Page) bool { pub fn isGoingAway(self: *const Page) bool {
return self._queued_navigation != null; if (self._queued_navigation != null) {
return true;
}
const parent = self.parent orelse return false;
return parent.isGoingAway();
} }
pub fn scriptAddedCallback(self: *Page, comptime from_parser: bool, script: *Element.Html.Script) !void { pub fn scriptAddedCallback(self: *Page, comptime from_parser: bool, script: *Element.Html.Script) !void {
@@ -968,7 +1024,7 @@ pub fn scriptAddedCallback(self: *Page, comptime from_parser: bool, script: *Ele
}; };
} }
pub fn iframeAddedCallback(self: *Page, iframe: *Element.Html.IFrame) !void { pub fn iframeAddedCallback(self: *Page, iframe: *IFrame) !void {
if (self.isGoingAway()) { if (self.isGoingAway()) {
// if we're planning on navigating to another page, don't load this iframe // if we're planning on navigating to another page, don't load this iframe
return; return;
@@ -977,34 +1033,43 @@ pub fn iframeAddedCallback(self: *Page, iframe: *Element.Html.IFrame) !void {
return; return;
} }
const src = iframe.asElement().getAttributeSafe(comptime .wrap("src")) orelse return; var src = iframe.asElement().getAttributeSafe(comptime .wrap("src")) orelse "";
if (src.len == 0) { if (src.len == 0) {
return; src = "about:blank";
}
if (iframe._window != null) {
// This frame is being re-navigated. We need to do this through a
// scheduleNavigation phase. We can't navigate immediately here, for
// the same reason that a "root" page can't immediately navigate:
// we could be in the middle of a JS callback or something else that
// doesn't exit the page to just suddenly go away.
return self.scheduleNavigation(src, .{
.reason = .script,
.kind = .{ .push = null },
}, .{ .iframe = iframe });
} }
iframe._executed = true; iframe._executed = true;
const session = self._session; const session = self._session;
// A frame can be re-navigated by setting the src.
const existing_window = iframe._content_window;
const page_frame = try self.arena.create(Page); const page_frame = try self.arena.create(Page);
const frame_id = blk: { const frame_id = session.nextFrameId();
if (existing_window) |w| {
const existing_frame_id = w._page._frame_id;
session.browser.http_client.abortFrame(existing_frame_id);
break :blk existing_frame_id;
}
break :blk session.nextFrameId();
};
try Page.init(page_frame, frame_id, session, self); try Page.init(page_frame, frame_id, session, self);
errdefer page_frame.deinit(); errdefer page_frame.deinit(true);
self._pending_loads += 1; self._pending_loads += 1;
page_frame.iframe = iframe; page_frame.iframe = iframe;
iframe._content_window = page_frame.window; iframe._window = page_frame.window;
errdefer iframe._content_window = null; errdefer iframe._window = null;
// on first load, dispatch frame_created evnet
self._session.notification.dispatch(.page_frame_created, &.{
.frame_id = frame_id,
.parent_id = self._frame_id,
.timestamp = timestamp(.monotonic),
});
const url = blk: { const url = blk: {
if (std.mem.eql(u8, src, "about:blank")) { if (std.mem.eql(u8, src, "about:blank")) {
@@ -1018,42 +1083,14 @@ pub fn iframeAddedCallback(self: *Page, iframe: *Element.Html.IFrame) !void {
); );
}; };
if (existing_window == null) {
// on first load, dispatch frame_created evnet
self._session.notification.dispatch(.page_frame_created, &.{
.frame_id = frame_id,
.parent_id = self._frame_id,
.timestamp = timestamp(.monotonic),
});
}
page_frame.navigate(url, .{ .reason = .initialFrameNavigation }) catch |err| { page_frame.navigate(url, .{ .reason = .initialFrameNavigation }) catch |err| {
log.warn(.page, "iframe navigate failure", .{ .url = url, .err = err }); log.warn(.page, "iframe navigate failure", .{ .url = url, .err = err });
self._pending_loads -= 1; self._pending_loads -= 1;
iframe._content_window = null; iframe._window = null;
page_frame.deinit(); page_frame.deinit(true);
return error.IFrameLoadError; return error.IFrameLoadError;
}; };
if (existing_window) |w| {
const existing_page = w._page;
if (existing_page._parent_notified == false) {
self._pending_loads -= 1;
}
for (self.frames.items, 0..) |p, i| {
if (p == existing_page) {
self.frames.items[i] = page_frame;
break;
}
} else {
lp.assert(false, "Existing frame not found", .{ .len = self.frames.items.len });
}
existing_page.deinit();
return;
}
// window[N] is based on document order. For now we'll just append the frame // window[N] is based on document order. For now we'll just append the frame
// at the end of our list and set frames_sorted == false. window.getFrame // at the end of our list and set frames_sorted == false. window.getFrame
// will check this flag to decide if it needs to sort the frames or not. // will check this flag to decide if it needs to sort the frames or not.
@@ -1969,7 +2006,7 @@ pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const
.{ ._proto = undefined }, .{ ._proto = undefined },
), ),
asUint("iframe") => return self.createHtmlElementT( asUint("iframe") => return self.createHtmlElementT(
Element.Html.IFrame, IFrame,
namespace, namespace,
attribute_iterator, attribute_iterator,
.{ ._proto = undefined }, .{ ._proto = undefined },
@@ -2880,12 +2917,7 @@ fn nodeIsReady(self: *Page, comptime from_parser: bool, node: *Node) !void {
log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "script", .type = self._type, .url = self.url }); log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "script", .type = self._type, .url = self.url });
return err; return err;
}; };
} else if (node.is(Element.Html.IFrame)) |iframe| { } else if (node.is(IFrame)) |iframe| {
if ((comptime from_parser == false) and iframe._src.len == 0) {
// iframe was added via JavaScript, but without a src
return;
}
self.iframeAddedCallback(iframe) catch |err| { self.iframeAddedCallback(iframe) catch |err| {
log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "iframe", .type = self._type, .url = self.url }); log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "iframe", .type = self._type, .url = self.url });
return err; return err;
@@ -3026,17 +3058,26 @@ pub const NavigatedOpts = struct {
method: Http.Method = .GET, method: Http.Method = .GET,
}; };
const NavigationPriority = enum { const NavigationType = enum {
form, form,
script, script,
anchor, anchor,
iframe,
};
const Navigation = union(NavigationType) {
form: *Node,
script: ?*Page,
anchor: *Node,
iframe: *IFrame,
}; };
pub const QueuedNavigation = struct { pub const QueuedNavigation = struct {
arena: Allocator, arena: Allocator,
url: [:0]const u8, url: [:0]const u8,
opts: NavigateOpts, opts: NavigateOpts,
priority: NavigationPriority, is_about_blank: bool,
navigation_type: NavigationType,
}; };
pub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void { pub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void {
@@ -3089,11 +3130,17 @@ pub fn handleClick(self: *Page, target: *Node) !void {
return; return;
} }
// TODO: We need to support targets properly, but this is the most
// common case: a click on an anchor navigates the page/frame that
// anchor is in.
// ownerDocument only returns null when `target` is a document, which
// it is NOT in this case. Even for a detched node, it'll return self.document
try element.focus(self); try element.focus(self);
try self.scheduleNavigation(href, .{ try self.scheduleNavigation(href, .{
.reason = .script, .reason = .script,
.kind = .{ .push = null }, .kind = .{ .push = null },
}, .anchor); }, .{ .anchor = target });
}, },
.input => |input| { .input => |input| {
try element.focus(self); try element.focus(self);
@@ -3114,7 +3161,11 @@ pub fn handleClick(self: *Page, target: *Node) !void {
pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void { pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
const event = keyboard_event.asEvent(); const event = keyboard_event.asEvent();
const element = self.window._document._active_element orelse return; const element = self.window._document._active_element orelse {
keyboard_event.deinit(false, self);
return;
};
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
log.debug(.page, "page keydown", .{ log.debug(.page, "page keydown", .{
.url = self.url, .url = self.url,
@@ -3224,7 +3275,7 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
} else { } else {
action = try URL.concatQueryString(arena, action, buf.written()); action = try URL.concatQueryString(arena, action, buf.written());
} }
return self.scheduleNavigationWithArena(arena, action, opts, .form); return self.scheduleNavigationWithArena(arena, action, opts, .{ .form = form_element.asNode() });
} }
// insertText is a shortcut to insert text into the active element. // insertText is a shortcut to insert text into the active element.

View File

@@ -30,6 +30,7 @@ const History = @import("webapi/History.zig");
const Page = @import("Page.zig"); const Page = @import("Page.zig");
const Browser = @import("Browser.zig"); const Browser = @import("Browser.zig");
const Notification = @import("../Notification.zig"); const Notification = @import("../Notification.zig");
const QueuedNavigation = Page.QueuedNavigation;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const IS_DEBUG = builtin.mode == .Debug; const IS_DEBUG = builtin.mode == .Debug;
@@ -43,6 +44,12 @@ const Session = @This();
browser: *Browser, browser: *Browser,
notification: *Notification, notification: *Notification,
queued_navigation: std.ArrayList(*Page),
// Temporary buffer for about:blank navigations during processing.
// We process async navigations first (safe from re-entrance), then sync
// about:blank navigations (which may add to queued_navigation).
queued_queued_navigation: std.ArrayList(*Page),
// Used to create our Inspector and in the BrowserContext. // Used to create our Inspector and in the BrowserContext.
arena: Allocator, arena: Allocator,
@@ -70,6 +77,8 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
.navigation = .{ ._proto = undefined }, .navigation = .{ ._proto = undefined },
.storage_shed = .{}, .storage_shed = .{},
.browser = browser, .browser = browser,
.queued_navigation = .{},
.queued_queued_navigation = .{},
.notification = notification, .notification = notification,
.cookie_jar = storage.Cookie.Jar.init(allocator), .cookie_jar = storage.Cookie.Jar.init(allocator),
}; };
@@ -79,9 +88,9 @@ pub fn deinit(self: *Session) void {
if (self.page != null) { if (self.page != null) {
self.removePage(); self.removePage();
} }
const browser = self.browser;
self.cookie_jar.deinit(); self.cookie_jar.deinit();
const browser = self.browser;
self.storage_shed.deinit(browser.app.allocator); self.storage_shed.deinit(browser.app.allocator);
browser.arena_pool.release(self.arena); browser.arena_pool.release(self.arena);
} }
@@ -113,7 +122,7 @@ pub fn removePage(self: *Session) void {
self.notification.dispatch(.page_remove, .{}); self.notification.dispatch(.page_remove, .{});
lp.assert(self.page != null, "Session.removePage - page is null", .{}); lp.assert(self.page != null, "Session.removePage - page is null", .{});
self.page.?.deinit(); self.page.?.deinit(false);
self.page = null; self.page = null;
self.navigation.onRemovePage(); self.navigation.onRemovePage();
@@ -133,7 +142,7 @@ pub fn replacePage(self: *Session) !*Page {
var current = self.page.?; var current = self.page.?;
const frame_id = current._frame_id; const frame_id = current._frame_id;
const parent = current.parent; const parent = current.parent;
current.deinit(); current.deinit(false);
self.browser.env.memoryPressureNotification(.moderate); self.browser.env.memoryPressureNotification(.moderate);
@@ -174,10 +183,11 @@ pub fn wait(self: *Session, wait_ms: u32) WaitResult {
switch (wait_result) { switch (wait_result) {
.done => { .done => {
if (page._queued_navigation == null) { if (self.queued_navigation.items.len == 0) {
return .done; return .done;
} }
page = self.processScheduledNavigation(page) catch return .done; self.processQueuedNavigation() catch return .done;
page = &self.page.?; // might have changed
}, },
else => |result| return result, else => |result| return result,
} }
@@ -229,7 +239,7 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
} }
}, },
.html, .complete => { .html, .complete => {
if (page._queued_navigation != null) { if (self.queued_navigation.items.len != 0) {
return .done; return .done;
} }
@@ -345,42 +355,134 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
} }
} }
fn processScheduledNavigation(self: *Session, current_page: *Page) !*Page { pub fn scheduleNavigation(self: *Session, page: *Page) !void {
const browser = self.browser; const list = &self.queued_navigation;
const qn = current_page._queued_navigation.?; // Check if page is already queued
// take ownership of the page's queued navigation for (list.items) |existing| {
current_page._queued_navigation = null; if (existing == page) {
// Already queued
return;
}
}
return list.append(self.arena, page);
}
fn processQueuedNavigation(self: *Session) !void {
const navigations = &self.queued_navigation;
if (self.page.?._queued_navigation != null) {
// This is both an optimization and a simplification of sorts. If the
// root page is navigating, then we don't need to process any other
// navigation. Also, the navigation for the root page and for a frame
// is different enough that have two distinct code blocks is, imo,
// better. Yes, there will be duplication.
navigations.clearRetainingCapacity();
return self.processRootQueuedNavigation();
}
const about_blank_queue = &self.queued_queued_navigation;
defer about_blank_queue.clearRetainingCapacity();
// First pass: process async navigations (non-about:blank)
// These cannot cause re-entrant navigation scheduling
for (navigations.items) |page| {
const qn = page._queued_navigation.?;
if (qn.is_about_blank) {
// Defer about:blank to second pass
try about_blank_queue.append(self.arena, page);
continue;
}
try self.processFrameNavigation(page, qn);
}
// Clear the queue after first pass
navigations.clearRetainingCapacity();
// Second pass: process synchronous navigations (about:blank)
// These may trigger new navigations which go into queued_navigation
for (about_blank_queue.items) |page| {
const qn = page._queued_navigation.?;
try self.processFrameNavigation(page, qn);
}
// Safety: Remove any about:blank navigations that were queued during the
// second pass to prevent infinite loops
var i: usize = 0;
while (i < navigations.items.len) {
const page = navigations.items[i];
if (page._queued_navigation) |qn| {
if (qn.is_about_blank) {
log.warn(.page, "recursive about blank", .{});
_ = navigations.swapRemove(i);
continue;
}
}
i += 1;
}
}
fn processFrameNavigation(self: *Session, page: *Page, qn: *QueuedNavigation) !void {
lp.assert(page.parent != null, "root queued navigation", .{});
const browser = self.browser;
const iframe = page.iframe.?;
const parent = page.parent.?;
page._queued_navigation = null;
defer browser.arena_pool.release(qn.arena); defer browser.arena_pool.release(qn.arena);
const frame_id, const parent = blk: { errdefer iframe._window = null;
const page = &self.page.?;
const frame_id = page._frame_id;
const parent = page.parent;
browser.http_client.abort(); if (page._parent_notified) {
self.removePage(); // we already notified the parent that we had loaded
parent._pending_loads += 1;
}
break :blk .{ frame_id, parent }; const frame_id = page._frame_id;
}; page.deinit(true);
page.* = undefined;
self.page = @as(Page, undefined);
const page = &self.page.?;
try Page.init(page, frame_id, self, parent); try Page.init(page, frame_id, self, parent);
errdefer page.deinit(true);
page.iframe = iframe;
iframe._window = page.window;
page.navigate(qn.url, qn.opts) catch |err| {
log.err(.browser, "queued frame navigation error", .{ .err = err });
return err;
};
}
fn processRootQueuedNavigation(self: *Session) !void {
const current_page = &self.page.?;
const frame_id = current_page._frame_id;
// create a copy before the page is cleared
const qn = current_page._queued_navigation.?;
current_page._queued_navigation = null;
defer self.browser.arena_pool.release(qn.arena);
self.removePage();
self.page = @as(Page, undefined);
const new_page = &self.page.?;
try Page.init(new_page, frame_id, self, null);
// Creates a new NavigationEventTarget for this page. // Creates a new NavigationEventTarget for this page.
try self.navigation.onNewPage(page); try self.navigation.onNewPage(new_page);
// start JS env // start JS env
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well // Inform CDP the main page has been created such that additional context for other Worlds can be created as well
self.notification.dispatch(.page_created, page); self.notification.dispatch(.page_created, new_page);
page.navigate(qn.url, qn.opts) catch |err| { new_page.navigate(qn.url, qn.opts) catch |err| {
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url }); log.err(.browser, "queued navigation error", .{ .err = err });
return err; return err;
}; };
return page;
} }
pub fn nextFrameId(self: *Session) u32 { pub fn nextFrameId(self: *Session) u32 {

View File

@@ -153,7 +153,7 @@ pub fn fromIsolate(isolate: js.Isolate) *Context {
} }
pub fn deinit(self: *Context) void { pub fn deinit(self: *Context) void {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG and @import("builtin").is_test == false) {
var it = self.unknown_properties.iterator(); var it = self.unknown_properties.iterator();
while (it.next()) |kv| { while (it.next()) |kv| {
log.debug(.unknown_prop, "unknown property", .{ log.debug(.unknown_prop, "unknown property", .{

View File

@@ -160,8 +160,8 @@ fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args
try_catch.rethrow(); try_catch.rethrow();
return error.TryCatchRethrow; return error.TryCatchRethrow;
} }
caught.* = try_catch.caughtOrError(local.call_arena, error.JSExecCallback); caught.* = try_catch.caughtOrError(local.call_arena, error.JsException);
return error.JSExecCallback; return error.JsException;
}; };
if (@typeInfo(T) == .void) { if (@typeInfo(T) == .void) {

View File

@@ -137,7 +137,7 @@ pub fn compileAndRun(self: *const Local, src: []const u8, name: ?[]const u8) !js
) orelse return error.CompilationError; ) orelse return error.CompilationError;
// Run the script // Run the script
const result = v8.v8__Script__Run(v8_script, self.handle) orelse return error.ExecutionError; const result = v8.v8__Script__Run(v8_script, self.handle) orelse return error.JsException;
return .{ .local = self, .handle = result }; return .{ .local = self, .handle = result };
} }

View File

@@ -72,3 +72,59 @@
testing.expectEqual(2, calls); testing.expectEqual(2, calls);
} }
</script> </script>
<div id=fragment_clone_container></div>
<script id=clone_fragment>
{
let calls = 0;
class MyFragmentCloneElement extends HTMLElement {
constructor() {
super();
calls += 1;
$('#fragment_clone_container').appendChild(this);
}
}
customElements.define('my-fragment-clone-element', MyFragmentCloneElement);
// Create a DocumentFragment with a custom element
const fragment = document.createDocumentFragment();
const customEl = document.createElement('my-fragment-clone-element');
fragment.appendChild(customEl);
// Clone the fragment - this should trigger the crash
// because the constructor will attach the element during cloning
const clonedFragment = fragment.cloneNode(true);
testing.expectEqual(2, calls);
}
</script>
<div id=range_clone_container></div>
<script id=clone_range>
{
let calls = 0;
class MyRangeCloneElement extends HTMLElement {
constructor() {
super();
calls += 1;
$('#range_clone_container').appendChild(this);
}
}
customElements.define('my-range-clone-element', MyRangeCloneElement);
// Create a container with a custom element
const container = document.createElement('div');
const customEl = document.createElement('my-range-clone-element');
container.appendChild(customEl);
// Create a range that includes the custom element
const range = document.createRange();
range.selectNodeContents(container);
// Clone the range contents - this should trigger the crash
// because the constructor will attach the element during cloning
const clonedContents = range.cloneContents();
testing.expectEqual(2, calls);
}
</script>

View File

@@ -7,45 +7,59 @@
} }
</script> </script>
<iframe id=f1 onload="frame1Onload" src="support/sub 1.html"></iframe> <iframe id=f0></iframe>
<iframe id=f1 onload="frame1Onload()" src="support/sub 1.html"></iframe>
<iframe id=f2 src="support/sub2.html"></iframe> <iframe id=f2 src="support/sub2.html"></iframe>
<script id=empty>
{
const blank = document.createElement('iframe');
testing.expectEqual(null, blank.contentDocument);
document.documentElement.appendChild(blank);
testing.expectEqual('<html><head></head><body></body></html>', blank.contentDocument.documentElement.outerHTML);
const f0 = $('#f0')
testing.expectEqual('<html><head></head><body></body></html>', f0.contentDocument.documentElement.outerHTML);
}
</script>
<script id="basic"> <script id="basic">
// reload it // reload it
$('#f2').src = 'support/sub2.html'; $('#f2').src = 'support/sub2.html';
testing.expectEqual(true, true);
testing.eventually(() => { testing.eventually(() => {
testing.expectEqual(undefined, window[10]); testing.expectEqual(undefined, window[20]);
testing.expectEqual(window, window[0].top);
testing.expectEqual(window, window[0].parent);
testing.expectEqual(false, window === window[0]);
testing.expectEqual(window, window[1].top); testing.expectEqual(window, window[1].top);
testing.expectEqual(window, window[1].parent); testing.expectEqual(window, window[1].parent);
testing.expectEqual(false, window === window[1]); testing.expectEqual(false, window === window[1]);
testing.expectEqual(false, window[0] === window[1]);
testing.expectEqual(window, window[2].top);
testing.expectEqual(window, window[2].parent);
testing.expectEqual(false, window === window[2]);
testing.expectEqual(false, window[1] === window[2]);
testing.expectEqual(0, $('#f1').childNodes.length); testing.expectEqual(0, $('#f1').childNodes.length);
testing.expectEqual(testing.BASE_URL + 'frames/support/sub%201.html', $('#f1').src); testing.expectEqual(testing.BASE_URL + 'frames/support/sub%201.html', $('#f1').src);
testing.expectEqual(window[0], $('#f1').contentWindow); testing.expectEqual(window[1], $('#f1').contentWindow);
testing.expectEqual(window[1], $('#f2').contentWindow); testing.expectEqual(window[2], $('#f2').contentWindow);
testing.expectEqual(window[0].document, $('#f1').contentDocument); testing.expectEqual(window[1].document, $('#f1').contentDocument);
testing.expectEqual(window[1].document, $('#f2').contentDocument); testing.expectEqual(window[2].document, $('#f2').contentDocument);
// sibling frames share the same top // sibling frames share the same top
testing.expectEqual(window[0].top, window[1].top); testing.expectEqual(window[1].top, window[2].top);
// child frames have no sub-frames // child frames have no sub-frames
testing.expectEqual(0, window[0].length);
testing.expectEqual(0, window[1].length); testing.expectEqual(0, window[1].length);
testing.expectEqual(0, window[2].length);
// self and window are self-referential on child frames // self and window are self-referential on child frames
testing.expectEqual(window[0], window[0].self);
testing.expectEqual(window[0], window[0].window);
testing.expectEqual(window[1], window[1].self); testing.expectEqual(window[1], window[1].self);
testing.expectEqual(window[1], window[1].window);
testing.expectEqual(window[2], window[2].self);
// child frame's top.parent is itself (root has no parent) // child frame's top.parent is itself (root has no parent)
testing.expectEqual(window, window[0].top.parent); testing.expectEqual(window, window[0].top.parent);
@@ -62,6 +76,7 @@
{ {
let f3_load_event = false; let f3_load_event = false;
let f3 = document.createElement('iframe'); let f3 = document.createElement('iframe');
f3.id = 'f3';
f3.addEventListener('load', () => { f3.addEventListener('load', () => {
f3_load_event = true; f3_load_event = true;
}); });
@@ -75,9 +90,10 @@
} }
</script> </script>
<script id=onload> <script id=about_blank>
{ {
let f4 = document.createElement('iframe'); let f4 = document.createElement('iframe');
f4.id = 'f4';
f4.src = "about:blank"; f4.src = "about:blank";
document.documentElement.appendChild(f4); document.documentElement.appendChild(f4);
@@ -87,8 +103,43 @@
} }
</script> </script>
<script id=count> <script id=about_blank_renavigate>
testing.eventually(() => { {
testing.expectEqual(4, window.length); let f5 = document.createElement('iframe');
f5.id = 'f5';
f5.src = "support/sub 1.html";
document.documentElement.appendChild(f5);
f5.src = "about:blank";
testing.eventually(() => {
testing.expectEqual("<html><head></head><body></body></html>", f5.contentDocument.documentElement.outerHTML);
});
}
</script>
<script id=link_click>
testing.async(async (restore) => {
await new Promise((resolve) => {
let count = 0;
let f6 = document.createElement('iframe');
f6.id = 'f6';
f6.addEventListener('load', () => {
if (++count == 2) {
resolve();
return;
}
f6.contentDocument.querySelector('#link').click();
});
f6.src = "support/with_link.html";
document.documentElement.appendChild(f6);
});
restore();
testing.expectEqual("<html><head></head><body>It was clicked!\n</body></html>", f6.contentDocument.documentElement.outerHTML);
});
</script>
<script id=count>
testing.eventually(() => {
testing.expectEqual(8, window.length);
}); });
</script> </script>

View File

@@ -0,0 +1,2 @@
<!DOCTYPE html>
It was clicked!

View File

@@ -0,0 +1,2 @@
<!DOCTYPE html>
<a href="support/after_link.html" id=link>a link</a>

View File

@@ -40,6 +40,8 @@ const Selection = @import("Selection.zig");
pub const XMLDocument = @import("XMLDocument.zig"); pub const XMLDocument = @import("XMLDocument.zig");
pub const HTMLDocument = @import("HTMLDocument.zig"); pub const HTMLDocument = @import("HTMLDocument.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const Document = @This(); const Document = @This();
_type: Type, _type: Type,
@@ -937,6 +939,32 @@ fn validateElementName(name: []const u8) !void {
} }
} }
// When a page or frame's URL is about:blank, or as soon as a frame is
// programmatically created, it has this default "blank" content
pub fn injectBlank(self: *Document, page: *Page) error{InjectBlankError}!void {
self._injectBlank(page) catch |err| {
// we wrap _injectBlank like this so that injectBlank can only return an
// InjectBlankError. injectBlank is used in when nodes are inserted
// as since it inserts node itself, Zig can't infer the error set.
log.err(.browser, "inject blank", .{ .err = err });
return error.InjectBlankError;
};
}
fn _injectBlank(self: *Document, page: *Page) !void {
if (comptime IS_DEBUG) {
// should only be called on an empty document
std.debug.assert(self.asNode()._children == null);
}
const html = try page.createElementNS(.html, "html", null);
const head = try page.createElementNS(.html, "head", null);
const body = try page.createElementNS(.html, "body", null);
try page.appendNode(html, head, .{});
try page.appendNode(html, body, .{});
try page.appendNode(self.asNode(), html, .{});
}
const ReadyState = enum { const ReadyState = enum {
loading, loading,
interactive, interactive,

View File

@@ -195,8 +195,9 @@ pub fn cloneFragment(self: *DocumentFragment, deep: bool, page: *Page) !*Node {
var child_it = node.childrenIterator(); var child_it = node.childrenIterator();
while (child_it.next()) |child| { while (child_it.next()) |child| {
const cloned_child = try child.cloneNode(true, page); if (try child.cloneNodeForAppending(true, page)) |cloned_child| {
try page.appendNode(fragment_node, cloned_child, .{ .child_already_connected = self_is_connected }); try page.appendNode(fragment_node, cloned_child, .{ .child_already_connected = self_is_connected });
}
} }
} }

View File

@@ -1328,20 +1328,12 @@ pub fn clone(self: *Element, deep: bool, page: *Page) !*Node {
if (deep) { if (deep) {
var child_it = self.asNode().childrenIterator(); var child_it = self.asNode().childrenIterator();
while (child_it.next()) |child| { while (child_it.next()) |child| {
const cloned_child = try child.cloneNode(true, page); if (try child.cloneNodeForAppending(true, page)) |cloned_child| {
if (cloned_child._parent != null) { // We pass `true` to `child_already_connected` as a hacky optimization
// This is almost always false, the only case where a cloned // We _know_ this child isn't connected (Because the parent isn't connected)
// node would already have a parent is with a custom element // setting this to `true` skips all connection checks.
// that has a constructor (which is called during cloning) which try page.appendNode(node, cloned_child, .{ .child_already_connected = true });
// inserts it somewhere. In that case, whatever parent was set
// in the constructor should not be changed.
continue;
} }
// We pass `true` to `child_already_connected` as a hacky optimization
// We _know_ this child isn't connected (Because the parent isn't connected)
// setting this to `true` skips all connection checks.
try page.appendNode(node, cloned_child, .{ .child_already_connected = true });
} }
} }

View File

@@ -180,8 +180,8 @@ pub fn getLocation(self: *const HTMLDocument) ?*@import("Location.zig") {
return self._proto._location; return self._proto._location;
} }
pub fn setLocation(_: *const HTMLDocument, url: [:0]const u8, page: *Page) !void { pub fn setLocation(self: *HTMLDocument, url: [:0]const u8, page: *Page) !void {
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .script); return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = self._proto._page });
} }
pub fn getAll(self: *HTMLDocument, page: *Page) !*collections.HTMLAllCollection { pub fn getAll(self: *HTMLDocument, page: *Page) !*collections.HTMLAllCollection {

View File

@@ -83,19 +83,19 @@ pub fn setHash(_: *const Location, hash: []const u8, page: *Page) !void {
return page.scheduleNavigation(normalized_hash, .{ return page.scheduleNavigation(normalized_hash, .{
.reason = .script, .reason = .script,
.kind = .{ .replace = null }, .kind = .{ .replace = null },
}, .script); }, .{ .script = page });
} }
pub fn assign(_: *const Location, url: [:0]const u8, page: *Page) !void { pub fn assign(_: *const Location, url: [:0]const u8, page: *Page) !void {
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .script); return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = page });
} }
pub fn replace(_: *const Location, url: [:0]const u8, page: *Page) !void { pub fn replace(_: *const Location, url: [:0]const u8, page: *Page) !void {
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .replace = null } }, .script); return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .replace = null } }, .{ .script = page });
} }
pub fn reload(_: *const Location, page: *Page) !void { pub fn reload(_: *const Location, page: *Page) !void {
return page.scheduleNavigation(page.url, .{ .reason = .script, .kind = .reload }, .script); return page.scheduleNavigation(page.url, .{ .reason = .script, .kind = .reload }, .{ .script = page });
} }
pub fn toString(self: *const Location, page: *const Page) ![:0]const u8 { pub fn toString(self: *const Location, page: *const Page) ![:0]const u8 {

View File

@@ -724,6 +724,9 @@ const CloneError = error{
TooManyContexts, TooManyContexts,
LinkLoadError, LinkLoadError,
StyleLoadError, StyleLoadError,
TypeError,
CompilationError,
JsException,
}; };
pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) CloneError!*Node { pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) CloneError!*Node {
const deep = deep_ orelse false; const deep = deep_ orelse false;
@@ -751,6 +754,29 @@ pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) CloneError!*Node {
} }
} }
/// Clone a node for the purpose of appending to a parent.
/// Returns null if the cloned node was already attached somewhere by a custom element
/// constructor, indicating that the constructor's decision should be respected.
///
/// This helper is used when iterating over children to clone them. The typical pattern is:
/// while (child_it.next()) |child| {
/// if (try child.cloneNodeForAppending(true, page)) |cloned| {
/// try page.appendNode(parent, cloned, opts);
/// }
/// }
///
/// The only case where a cloned node would already have a parent is when a custom element
/// constructor (which runs during cloning per the HTML spec) explicitly attaches the element
/// somewhere. In that case, we respect the constructor's decision and return null to signal
/// that the cloned node should not be appended to our intended parent.
pub fn cloneNodeForAppending(self: *Node, deep: bool, page: *Page) CloneError!?*Node {
const cloned = try self.cloneNode(deep, page);
if (cloned._parent != null) {
return null;
}
return cloned;
}
pub fn compareDocumentPosition(self: *Node, other: *Node) u16 { pub fn compareDocumentPosition(self: *Node, other: *Node) u16 {
const DISCONNECTED: u16 = 0x01; const DISCONNECTED: u16 = 0x01;
const PRECEDING: u16 = 0x02; const PRECEDING: u16 = 0x02;

View File

@@ -446,8 +446,9 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
var offset = self._proto._start_offset; var offset = self._proto._start_offset;
while (offset < self._proto._end_offset) : (offset += 1) { while (offset < self._proto._end_offset) : (offset += 1) {
if (self._proto._start_container.getChildAt(offset)) |child| { if (self._proto._start_container.getChildAt(offset)) |child| {
const cloned = try child.cloneNode(true, page); if (try child.cloneNodeForAppending(true, page)) |cloned| {
_ = try fragment.asNode().appendChild(cloned, page); _ = try fragment.asNode().appendChild(cloned, page);
}
} }
} }
} }
@@ -468,9 +469,11 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
if (self._proto._start_container.parentNode() == self._proto._end_container.parentNode()) { if (self._proto._start_container.parentNode() == self._proto._end_container.parentNode()) {
var current = self._proto._start_container.nextSibling(); var current = self._proto._start_container.nextSibling();
while (current != null and current != self._proto._end_container) { while (current != null and current != self._proto._end_container) {
const cloned = try current.?.cloneNode(true, page); const next = current.?.nextSibling();
_ = try fragment.asNode().appendChild(cloned, page); if (try current.?.cloneNodeForAppending(true, page)) |cloned| {
current = current.?.nextSibling(); _ = try fragment.asNode().appendChild(cloned, page);
}
current = next;
} }
} }

View File

@@ -160,8 +160,8 @@ pub fn getSelection(self: *const Window) *Selection {
return &self._document._selection; return &self._document._selection;
} }
pub fn setLocation(_: *const Window, url: [:0]const u8, page: *Page) !void { pub fn setLocation(self: *Window, url: [:0]const u8, page: *Page) !void {
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .script); return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = self._page });
} }
pub fn getHistory(_: *Window, page: *Page) *History { pub fn getHistory(_: *Window, page: *Page) *History {

View File

@@ -30,7 +30,7 @@ const IFrame = @This();
_proto: *HtmlElement, _proto: *HtmlElement,
_src: []const u8 = "", _src: []const u8 = "",
_executed: bool = false, _executed: bool = false,
_content_window: ?*Window = null, _window: ?*Window = null,
pub fn asElement(self: *IFrame) *Element { pub fn asElement(self: *IFrame) *Element {
return self._proto._proto; return self._proto._proto;
@@ -40,11 +40,11 @@ pub fn asNode(self: *IFrame) *Node {
} }
pub fn getContentWindow(self: *const IFrame) ?*Window { pub fn getContentWindow(self: *const IFrame) ?*Window {
return self._content_window; return self._window;
} }
pub fn getContentDocument(self: *const IFrame) ?*Document { pub fn getContentDocument(self: *const IFrame) ?*Document {
const window = self._content_window orelse return null; const window = self._window orelse return null;
return window._document; return window._document;
} }

View File

@@ -72,14 +72,6 @@ pub fn init(label_: ?[]const u8, opts_: ?InitOpts, page: *Page) !TextDecoderStre
}; };
} }
pub fn acquireRef(self: *TextDecoderStream) void {
self._transform.acquireRef();
}
pub fn deinit(self: *TextDecoderStream, shutdown: bool, page: *Page) void {
self._transform.deinit(shutdown, page);
}
fn decodeTransform(controller: *TransformStream.DefaultController, chunk: js.Value, ignoreBOM: bool) !void { fn decodeTransform(controller: *TransformStream.DefaultController, chunk: js.Value, ignoreBOM: bool) !void {
// chunk should be a Uint8Array; decode it as UTF-8 string // chunk should be a Uint8Array; decode it as UTF-8 string
const typed_array = try chunk.toZig(js.TypedArray(u8)); const typed_array = try chunk.toZig(js.TypedArray(u8));
@@ -119,8 +111,6 @@ pub const JsApi = struct {
pub const name = "TextDecoderStream"; pub const name = "TextDecoderStream";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(TextDecoderStream.deinit);
}; };
pub const constructor = bridge.constructor(TextDecoderStream.init, .{}); pub const constructor = bridge.constructor(TextDecoderStream.init, .{});

View File

@@ -34,14 +34,6 @@ pub fn init(page: *Page) !TextEncoderStream {
}; };
} }
pub fn acquireRef(self: *TextEncoderStream) void {
self._transform.acquireRef();
}
pub fn deinit(self: *TextEncoderStream, shutdown: bool, page: *Page) void {
self._transform.deinit(shutdown, page);
}
fn encodeTransform(controller: *TransformStream.DefaultController, chunk: js.Value) !void { fn encodeTransform(controller: *TransformStream.DefaultController, chunk: js.Value) !void {
// chunk should be a JS string; encode it as UTF-8 bytes (Uint8Array) // chunk should be a JS string; encode it as UTF-8 bytes (Uint8Array)
const str = chunk.isString() orelse return error.InvalidChunk; const str = chunk.isString() orelse return error.InvalidChunk;
@@ -64,8 +56,6 @@ pub const JsApi = struct {
pub const name = "TextEncoderStream"; pub const name = "TextEncoderStream";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(TextEncoderStream.deinit);
}; };
pub const constructor = bridge.constructor(TextEncoderStream.init, .{}); pub const constructor = bridge.constructor(TextEncoderStream.init, .{});

View File

@@ -308,7 +308,7 @@ pub fn navigateInner(
_ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, true); _ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, true);
} else { } else {
try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .script); try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .{ .script = page });
} }
}, },
.replace => |state| { .replace => |state| {
@@ -321,7 +321,7 @@ pub fn navigateInner(
_ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, true); _ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, true);
} else { } else {
try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .script); try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .{ .script = page });
} }
}, },
.traverse => |index| { .traverse => |index| {
@@ -334,11 +334,11 @@ pub fn navigateInner(
// todo: Fire navigate event // todo: Fire navigate event
finished.resolve("navigation traverse", {}); finished.resolve("navigation traverse", {});
} else { } else {
try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .script); try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .{ .script = page });
} }
}, },
.reload => { .reload => {
try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .script); try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .{ .script = page });
}, },
} }

View File

@@ -52,8 +52,6 @@ _pull_fn: ?js.Function.Global = null,
_pulling: bool = false, _pulling: bool = false,
_pull_again: bool = false, _pull_again: bool = false,
_cancel: ?Cancel = null, _cancel: ?Cancel = null,
_arena: std.mem.Allocator,
_rc: usize = 0,
const UnderlyingSource = struct { const UnderlyingSource = struct {
start: ?js.Function = null, start: ?js.Function = null,
@@ -70,18 +68,13 @@ const QueueingStrategy = struct {
pub fn init(src_: ?UnderlyingSource, strategy_: ?QueueingStrategy, page: *Page) !*ReadableStream { pub fn init(src_: ?UnderlyingSource, strategy_: ?QueueingStrategy, page: *Page) !*ReadableStream {
const strategy: QueueingStrategy = strategy_ orelse .{}; const strategy: QueueingStrategy = strategy_ orelse .{};
const arena = try page.getArena(.{ .debug = "ReadableStream" }); const self = try page._factory.create(ReadableStream{
errdefer page.releaseArena(arena);
const self = try arena.create(ReadableStream);
self.* = .{
._page = page, ._page = page,
._state = .readable, ._state = .readable,
._arena = arena,
._reader = null, ._reader = null,
._controller = undefined, ._controller = undefined,
._stored_error = null, ._stored_error = null,
}; });
self._controller = try ReadableStreamDefaultController.init(self, strategy.highWaterMark, page); self._controller = try ReadableStreamDefaultController.init(self, strategy.highWaterMark, page);
@@ -115,23 +108,6 @@ pub fn initWithData(data: []const u8, page: *Page) !*ReadableStream {
return stream; return stream;
} }
pub fn deinit(self: *ReadableStream, _: bool, page: *Page) void {
const rc = self._rc;
if (comptime IS_DEBUG) {
std.debug.assert(rc != 0);
}
if (rc == 1) {
page.releaseArena(self._arena);
} else {
self._rc = rc - 1;
}
}
pub fn acquireRef(self: *ReadableStream) void {
self._rc += 1;
}
pub fn getReader(self: *ReadableStream, page: *Page) !*ReadableStreamDefaultReader { pub fn getReader(self: *ReadableStream, page: *Page) !*ReadableStreamDefaultReader {
if (self.getLocked()) { if (self.getLocked()) {
return error.ReaderLocked; return error.ReaderLocked;
@@ -144,12 +120,6 @@ pub fn getReader(self: *ReadableStream, page: *Page) !*ReadableStreamDefaultRead
pub fn releaseReader(self: *ReadableStream) void { pub fn releaseReader(self: *ReadableStream) void {
self._reader = null; self._reader = null;
const rc = self._rc;
if (comptime IS_DEBUG) {
std.debug.assert(rc != 0);
}
self._rc = rc - 1;
} }
pub fn getAsyncIterator(self: *ReadableStream, page: *Page) !*AsyncIterator { pub fn getAsyncIterator(self: *ReadableStream, page: *Page) !*AsyncIterator {
@@ -397,8 +367,6 @@ pub const JsApi = struct {
pub const name = "ReadableStream"; pub const name = "ReadableStream";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(ReadableStream.deinit);
}; };
pub const constructor = bridge.constructor(ReadableStream.init, .{}); pub const constructor = bridge.constructor(ReadableStream.init, .{});
@@ -422,14 +390,6 @@ pub const AsyncIterator = struct {
}); });
} }
pub fn acquireRef(self: *AsyncIterator) void {
self._stream.acquireRef();
}
pub fn deinit(self: *AsyncIterator, shutdown: bool, page: *Page) void {
self._stream.deinit(shutdown, page);
}
pub fn next(self: *AsyncIterator, page: *Page) !js.Promise { pub fn next(self: *AsyncIterator, page: *Page) !js.Promise {
return self._reader.read(page); return self._reader.read(page);
} }
@@ -446,8 +406,6 @@ pub const AsyncIterator = struct {
pub const name = "ReadableStreamAsyncIterator"; pub const name = "ReadableStreamAsyncIterator";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(AsyncIterator.deinit);
}; };
pub const next = bridge.function(ReadableStream.AsyncIterator.next, .{}); pub const next = bridge.function(ReadableStream.AsyncIterator.next, .{});

View File

@@ -27,8 +27,6 @@ const ReadableStreamDefaultReader = @import("ReadableStreamDefaultReader.zig");
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = @import("builtin").mode == .Debug;
/// ReadableStreamDefaultController uses ReadableStream's arena to make
/// allocation. Indeed, the controller is owned by its ReadableStream.
const ReadableStreamDefaultController = @This(); const ReadableStreamDefaultController = @This();
pub const Chunk = union(enum) { pub const Chunk = union(enum) {
@@ -48,6 +46,7 @@ pub const Chunk = union(enum) {
_page: *Page, _page: *Page,
_stream: *ReadableStream, _stream: *ReadableStream,
_arena: std.mem.Allocator,
_queue: std.ArrayList(Chunk), _queue: std.ArrayList(Chunk),
_pending_reads: std.ArrayList(js.PromiseResolver.Global), _pending_reads: std.ArrayList(js.PromiseResolver.Global),
_high_water_mark: u32, _high_water_mark: u32,
@@ -57,22 +56,15 @@ pub fn init(stream: *ReadableStream, high_water_mark: u32, page: *Page) !*Readab
._page = page, ._page = page,
._queue = .empty, ._queue = .empty,
._stream = stream, ._stream = stream,
._arena = page.arena,
._pending_reads = .empty, ._pending_reads = .empty,
._high_water_mark = high_water_mark, ._high_water_mark = high_water_mark,
}); });
} }
pub fn acquireRef(self: *ReadableStreamDefaultController) void {
self._stream.acquireRef();
}
pub fn deinit(self: *ReadableStreamDefaultController, shutdown: bool, page: *Page) void {
self._stream.deinit(shutdown, page);
}
pub fn addPendingRead(self: *ReadableStreamDefaultController, page: *Page) !js.Promise { pub fn addPendingRead(self: *ReadableStreamDefaultController, page: *Page) !js.Promise {
const resolver = page.js.local.?.createPromiseResolver(); const resolver = page.js.local.?.createPromiseResolver();
try self._pending_reads.append(self._stream._arena, try resolver.persist()); try self._pending_reads.append(self._arena, try resolver.persist());
return resolver.promise(); return resolver.promise();
} }
@@ -82,8 +74,8 @@ pub fn enqueue(self: *ReadableStreamDefaultController, chunk: Chunk) !void {
} }
if (self._pending_reads.items.len == 0) { if (self._pending_reads.items.len == 0) {
const chunk_copy = try chunk.dupe(self._stream._arena); const chunk_copy = try chunk.dupe(self._page.arena);
return self._queue.append(self._stream._arena, chunk_copy); return self._queue.append(self._arena, chunk_copy);
} }
// I know, this is ouch! But we expect to have very few (if any) // I know, this is ouch! But we expect to have very few (if any)
@@ -117,7 +109,7 @@ pub fn enqueueValue(self: *ReadableStreamDefaultController, value: js.Value) !vo
if (self._pending_reads.items.len == 0) { if (self._pending_reads.items.len == 0) {
const persisted = try value.persist(); const persisted = try value.persist();
try self._queue.append(self._stream._arena, .{ .js_value = persisted }); try self._queue.append(self._arena, .{ .js_value = persisted });
return; return;
} }
@@ -178,7 +170,7 @@ pub fn doError(self: *ReadableStreamDefaultController, err: []const u8) !void {
} }
self._stream._state = .errored; self._stream._state = .errored;
self._stream._stored_error = try self._stream._arena.dupe(u8, err); self._stream._stored_error = try self._page.arena.dupe(u8, err);
// Reject all pending reads // Reject all pending reads
for (self._pending_reads.items) |resolver| { for (self._pending_reads.items) |resolver| {
@@ -218,8 +210,6 @@ pub const JsApi = struct {
pub const name = "ReadableStreamDefaultController"; pub const name = "ReadableStreamDefaultController";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(ReadableStreamDefaultController.deinit);
}; };
pub const enqueue = bridge.function(ReadableStreamDefaultController.enqueueValue, .{}); pub const enqueue = bridge.function(ReadableStreamDefaultController.enqueueValue, .{});

View File

@@ -19,8 +19,6 @@
const std = @import("std"); const std = @import("std");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const ReadableStream = @import("ReadableStream.zig"); const ReadableStream = @import("ReadableStream.zig");
const ReadableStreamDefaultController = @import("ReadableStreamDefaultController.zig"); const ReadableStreamDefaultController = @import("ReadableStreamDefaultController.zig");
@@ -37,21 +35,6 @@ pub fn init(stream: *ReadableStream, page: *Page) !*ReadableStreamDefaultReader
}); });
} }
pub fn acquireRef(self: *ReadableStreamDefaultReader) void {
const stream = self._stream orelse {
if (comptime IS_DEBUG) {
std.debug.assert(false);
}
return;
};
stream.acquireRef();
}
pub fn deinit(self: *ReadableStreamDefaultReader, shutdown: bool, page: *Page) void {
const stream = self._stream orelse return;
stream.deinit(shutdown, page);
}
pub const ReadResult = struct { pub const ReadResult = struct {
done: bool, done: bool,
value: Chunk, value: Chunk,
@@ -127,8 +110,6 @@ pub const JsApi = struct {
pub const name = "ReadableStreamDefaultReader"; pub const name = "ReadableStreamDefaultReader";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(ReadableStreamDefaultReader.deinit);
}; };
pub const read = bridge.function(ReadableStreamDefaultReader.read, .{}); pub const read = bridge.function(ReadableStreamDefaultReader.read, .{});

View File

@@ -85,14 +85,6 @@ pub fn initWithZigTransform(zig_transform: ZigTransformFn, page: *Page) !*Transf
return self; return self;
} }
pub fn acquireRef(self: *TransformStream) void {
self._readable.acquireRef();
}
pub fn deinit(self: *TransformStream, shutdown: bool, page: *Page) void {
self._readable.deinit(shutdown, page);
}
pub fn transformWrite(self: *TransformStream, chunk: js.Value, page: *Page) !void { pub fn transformWrite(self: *TransformStream, chunk: js.Value, page: *Page) !void {
if (self._controller._zig_transform_fn) |zig_fn| { if (self._controller._zig_transform_fn) |zig_fn| {
// Zig-level transform (used by TextEncoderStream etc.) // Zig-level transform (used by TextEncoderStream etc.)
@@ -138,8 +130,6 @@ pub const JsApi = struct {
pub const name = "TransformStream"; pub const name = "TransformStream";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(TransformStream.deinit);
}; };
pub const constructor = bridge.constructor(TransformStream.init, .{}); pub const constructor = bridge.constructor(TransformStream.init, .{});
@@ -175,14 +165,6 @@ pub const TransformStreamDefaultController = struct {
}); });
} }
pub fn acquireRef(self: *TransformStreamDefaultController) void {
self._stream.acquireRef();
}
pub fn deinit(self: *TransformStreamDefaultController, shutdown: bool, page: *Page) void {
self._stream.deinit(shutdown, page);
}
pub fn enqueue(self: *TransformStreamDefaultController, chunk: ReadableStreamDefaultController.Chunk) !void { pub fn enqueue(self: *TransformStreamDefaultController, chunk: ReadableStreamDefaultController.Chunk) !void {
try self._stream._readable._controller.enqueue(chunk); try self._stream._readable._controller.enqueue(chunk);
} }
@@ -207,8 +189,6 @@ pub const TransformStreamDefaultController = struct {
pub const name = "TransformStreamDefaultController"; pub const name = "TransformStreamDefaultController";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(TransformStreamDefaultController.deinit);
}; };
pub const enqueue = bridge.function(TransformStreamDefaultController.enqueueValue, .{}); pub const enqueue = bridge.function(TransformStreamDefaultController.enqueueValue, .{});