mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 17:46:32 -04:00
Merge branch 'main' into agent
This commit is contained in:
1186
src/Config.zig
1186
src/Config.zig
File diff suppressed because it is too large
Load Diff
@@ -726,7 +726,7 @@ test "SemanticTree backendDOMNodeId" {
|
||||
|
||||
var frame = try testing.pageTest("cdp/registry1.html", .{});
|
||||
defer testing.reset();
|
||||
defer frame._session.removeFrame();
|
||||
defer frame._session.removePage();
|
||||
|
||||
const st: Self = .{
|
||||
.dom_node = frame.window._document.asNode(),
|
||||
@@ -750,7 +750,7 @@ test "SemanticTree max_depth" {
|
||||
|
||||
var frame = try testing.pageTest("cdp/registry1.html", .{});
|
||||
defer testing.reset();
|
||||
defer frame._session.removeFrame();
|
||||
defer frame._session.removePage();
|
||||
|
||||
const st: Self = .{
|
||||
.dom_node = frame.window._document.asNode(),
|
||||
|
||||
@@ -172,7 +172,7 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Self
|
||||
.self_heal = opts.self_heal,
|
||||
.interactive = opts.interactive,
|
||||
.one_shot_task = opts.task,
|
||||
.one_shot_attachments = opts.task_attachments,
|
||||
.one_shot_attachments = if (opts.task_attachments.items.len == 0) null else opts.task_attachments.items,
|
||||
};
|
||||
|
||||
self.cmd_executor = CommandExecutor.init(allocator, tool_executor, &self.terminal);
|
||||
|
||||
@@ -104,7 +104,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat
|
||||
|
||||
pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, comptime opts: DispatchOpts) DispatchError!void {
|
||||
event.acquireRef();
|
||||
defer _ = event.releaseRef(self.frame._session);
|
||||
defer _ = event.releaseRef(self.frame._page);
|
||||
|
||||
// Increment event count for Event Timing API
|
||||
self.frame.window._performance._event_counts.increment(event._type_string.str());
|
||||
@@ -124,7 +124,7 @@ pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, co
|
||||
// property is just a shortcut for calling addEventListener, but they are distinct.
|
||||
// An event set via property cannot be removed by removeEventListener. If you
|
||||
// set both the property and add a listener, they both execute.
|
||||
const DispatchDirectOptions = EventManagerBase.DispatchDirectOptions;
|
||||
pub const DispatchDirectOptions = EventManagerBase.DispatchDirectOptions;
|
||||
|
||||
// Direct dispatch for non-DOM targets (Window, XHR, AbortSignal) or DOM nodes with
|
||||
// property handlers. No propagation - just calls the handler and registered listeners.
|
||||
@@ -138,7 +138,7 @@ pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event,
|
||||
window._current_event = event;
|
||||
defer window._current_event = prev_event;
|
||||
|
||||
try self.base.dispatchDirect(frame.call_arena, frame.js, target, event, handler, frame._session, opts);
|
||||
try self.base.dispatchDirect(frame.call_arena, frame.js, target, event, handler, frame._page, opts);
|
||||
}
|
||||
|
||||
/// Check if there are any listeners for a direct dispatch (non-DOM target).
|
||||
@@ -617,7 +617,7 @@ const ActivationState = struct {
|
||||
const event = try Event.initTrusted(comptime .wrap(typ), .{
|
||||
.bubbles = true,
|
||||
.cancelable = false,
|
||||
}, frame);
|
||||
}, frame._page);
|
||||
|
||||
const target = input.asElement().asEventTarget();
|
||||
try frame._event_manager.dispatch(target, event);
|
||||
|
||||
@@ -21,7 +21,7 @@ const lp = @import("lightpanda");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const js = @import("js/js.zig");
|
||||
const Session = @import("Session.zig");
|
||||
const Page = @import("Page.zig");
|
||||
|
||||
const Event = @import("webapi/Event.zig");
|
||||
const EventTarget = @import("webapi/EventTarget.zig");
|
||||
@@ -216,7 +216,7 @@ pub fn dispatchDirect(
|
||||
target: *EventTarget,
|
||||
event: *Event,
|
||||
handler: anytype,
|
||||
session: *Session,
|
||||
page: *Page,
|
||||
comptime opts: DispatchDirectOptions,
|
||||
) DispatchError!void {
|
||||
if (comptime IS_DEBUG) {
|
||||
@@ -224,7 +224,7 @@ pub fn dispatchDirect(
|
||||
}
|
||||
|
||||
event.acquireRef();
|
||||
defer _ = event.releaseRef(session);
|
||||
defer _ = event.releaseRef(page);
|
||||
|
||||
if (comptime opts.inject_target) {
|
||||
event._target = target;
|
||||
|
||||
@@ -22,6 +22,7 @@ const builtin = @import("builtin");
|
||||
|
||||
const JS = @import("js/js.zig");
|
||||
const Mime = @import("Mime.zig");
|
||||
const Page = @import("Page.zig");
|
||||
const Factory = @import("Factory.zig");
|
||||
const Session = @import("Session.zig");
|
||||
const EventManager = @import("EventManager.zig");
|
||||
@@ -35,6 +36,7 @@ const URL = @import("URL.zig");
|
||||
const Blob = @import("webapi/Blob.zig");
|
||||
const Node = @import("webapi/Node.zig");
|
||||
const Event = @import("webapi/Event.zig");
|
||||
const EventTarget = @import("webapi/EventTarget.zig");
|
||||
const CData = @import("webapi/CData.zig");
|
||||
const Element = @import("webapi/Element.zig");
|
||||
const HtmlElement = @import("webapi/element/Html.zig");
|
||||
@@ -86,6 +88,8 @@ _frame_id: u32,
|
||||
// navigate.
|
||||
_loader_id: u32,
|
||||
|
||||
_page: *Page,
|
||||
|
||||
_session: *Session,
|
||||
|
||||
_event_manager: EventManager,
|
||||
@@ -255,35 +259,39 @@ _req_id: u32 = 0,
|
||||
_console_messages: std.ArrayListUnmanaged(ConsoleMessage) = .{},
|
||||
_navigated_options: ?NavigatedOpts = null,
|
||||
|
||||
pub fn init(self: *Frame, frame_id: u32, session: *Session, parent: ?*Frame) !void {
|
||||
pub fn init(self: *Frame, frame_id: u32, page: *Page, parent: ?*Frame) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.frame, "frame.init", .{});
|
||||
}
|
||||
|
||||
const session = page.session;
|
||||
const call_arena = try session.getArena(.medium, "call_arena");
|
||||
errdefer session.releaseArena(call_arena);
|
||||
|
||||
const factory = &session.factory;
|
||||
const factory = &page.factory;
|
||||
const document = (try factory.document(Node.Document.HTMLDocument{
|
||||
._proto = undefined,
|
||||
})).asDocument();
|
||||
|
||||
const arena = page.frame_arena;
|
||||
|
||||
self.* = .{
|
||||
.js = undefined,
|
||||
.arena = arena,
|
||||
.parent = parent,
|
||||
.arena = session.frame_arena,
|
||||
.document = document,
|
||||
.window = undefined,
|
||||
.call_arena = call_arena,
|
||||
._frame_id = frame_id,
|
||||
._loader_id = session.nextLoaderId(),
|
||||
._page = page,
|
||||
._session = session,
|
||||
._loader_id = session.nextLoaderId(),
|
||||
._factory = factory,
|
||||
._pending_loads = 1, // always 1 for the ScriptManager
|
||||
._type = if (parent == null) .root else .frame,
|
||||
._style_manager = undefined,
|
||||
._script_manager = undefined,
|
||||
._event_manager = EventManager.init(session.frame_arena, self),
|
||||
._event_manager = EventManager.init(arena, self),
|
||||
};
|
||||
self._to_load = &self._to_load_1;
|
||||
|
||||
@@ -322,8 +330,8 @@ pub fn init(self: *Frame, frame_id: u32, session: *Session, parent: ?*Frame) !vo
|
||||
errdefer self._script_manager.deinit();
|
||||
|
||||
self.js = try browser.env.createContext(self, .{
|
||||
.identity = &session.identity,
|
||||
.identity_arena = session.frame_arena,
|
||||
.identity = &page.identity,
|
||||
.identity_arena = arena,
|
||||
.call_arena = self.call_arena,
|
||||
});
|
||||
errdefer browser.env.destroyContext(self.js);
|
||||
@@ -363,10 +371,10 @@ pub fn deinit(self: *Frame, abort_http: bool) void {
|
||||
// stats.print(&stream) catch unreachable;
|
||||
}
|
||||
|
||||
const session = self._session;
|
||||
const page = self._page;
|
||||
|
||||
if (self._queued_navigation) |qn| {
|
||||
session.releaseArena(qn.arena);
|
||||
page.releaseArena(qn.arena);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -374,7 +382,7 @@ pub fn deinit(self: *Frame, abort_http: bool) void {
|
||||
{
|
||||
var it = self._blob_urls.valueIterator();
|
||||
while (it.next()) |blob| {
|
||||
blob.*.releaseRef(session);
|
||||
blob.*.releaseRef(page);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,39 +391,40 @@ pub fn deinit(self: *Frame, abort_http: bool) void {
|
||||
while (node) |n| {
|
||||
node = n.next; // capture before we potentially delete observer
|
||||
const observer: *MutationObserver = @fieldParentPtr("node", n);
|
||||
observer.releaseRef(session);
|
||||
observer.releaseRef(page);
|
||||
}
|
||||
}
|
||||
|
||||
for (self._intersection_observers.items) |observer| {
|
||||
observer.releaseRef(session);
|
||||
observer.releaseRef(page);
|
||||
}
|
||||
|
||||
var document = self.window._document;
|
||||
document._selection.releaseRef(session);
|
||||
document._selection.releaseRef(page);
|
||||
|
||||
if (document._fonts) |f| {
|
||||
f.releaseRef(session);
|
||||
f.releaseRef(page);
|
||||
}
|
||||
}
|
||||
|
||||
session.browser.env.destroyContext(self.js);
|
||||
const browser = page.session.browser;
|
||||
browser.env.destroyContext(self.js);
|
||||
|
||||
self._script_manager.shutdown = true;
|
||||
|
||||
if (self.parent == null) {
|
||||
session.browser.http_client.abort();
|
||||
browser.http_client.abort();
|
||||
} else if (abort_http) {
|
||||
// a small optimization, it's faster to abort _everything_ on the root
|
||||
// frame, 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);
|
||||
browser.http_client.abortFrame(self._frame_id);
|
||||
}
|
||||
|
||||
self._script_manager.deinit();
|
||||
self._style_manager.deinit();
|
||||
|
||||
session.releaseArena(self.call_arena);
|
||||
page.releaseArena(self.call_arena);
|
||||
}
|
||||
|
||||
pub fn trackWorker(self: *Frame, worker: *Worker) !void {
|
||||
@@ -801,13 +810,14 @@ pub fn documentIsLoaded(self: *Frame) void {
|
||||
|
||||
self._load_state = .load;
|
||||
self.document._ready_state = .interactive;
|
||||
self._documentIsLoaded() catch |err| {
|
||||
log.err(.frame, "document is loaded", .{ .err = err, .type = self._type, .url = self.url });
|
||||
self._documentIsLoaded() catch |err| switch (err) {
|
||||
error.JsException => {}, // already logged
|
||||
else => log.err(.frame, "document is loaded2", .{ .err = err, .type = self._type, .url = self.url }),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _documentIsLoaded(self: *Frame) !void {
|
||||
const event = try Event.initTrusted(.wrap("DOMContentLoaded"), .{ .bubbles = true }, self);
|
||||
const event = try Event.initTrusted(.wrap("DOMContentLoaded"), .{ .bubbles = true }, self._page);
|
||||
try self._event_manager.dispatch(
|
||||
self.document.asEventTarget(),
|
||||
event,
|
||||
@@ -834,7 +844,7 @@ pub fn iframeCompletedLoading(self: *Frame, iframe: *IFrame) void {
|
||||
defer entered.exit();
|
||||
|
||||
blk: {
|
||||
const event = Event.initTrusted(comptime .wrap("load"), .{}, self) catch |err| {
|
||||
const event = Event.initTrusted(comptime .wrap("load"), .{}, self._page) catch |err| {
|
||||
log.err(.frame, "iframe event init", .{ .err = err, .url = iframe._src });
|
||||
break :blk;
|
||||
};
|
||||
@@ -874,8 +884,9 @@ pub fn documentIsComplete(self: *Frame) void {
|
||||
}
|
||||
|
||||
self._load_state = .complete;
|
||||
self._documentIsComplete() catch |err| {
|
||||
log.err(.frame, "document is complete", .{ .err = err, .type = self._type, .url = self.url });
|
||||
self._documentIsComplete() catch |err| switch (err) {
|
||||
error.JsException => {}, // already logged
|
||||
else => log.err(.frame, "document is complete", .{ .err = err, .type = self._type, .url = self.url }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -888,7 +899,7 @@ fn _documentIsComplete(self: *Frame) !void {
|
||||
// Dispatch window.load event.
|
||||
const window_target = self.window.asEventTarget();
|
||||
if (self._event_manager.hasDirectListeners(window_target, "load", self.window._on_load)) {
|
||||
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self);
|
||||
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self._page);
|
||||
// This event is weird, it's dispatched directly on the window, but
|
||||
// with the document as the target.
|
||||
event._target = self.document.asEventTarget();
|
||||
@@ -1213,7 +1224,7 @@ pub fn iframeAddedCallback(self: *Frame, iframe: *IFrame) !void {
|
||||
const new_frame = try self.arena.create(Frame);
|
||||
const frame_id = session.nextFrameId();
|
||||
|
||||
try Frame.init(new_frame, frame_id, session, self);
|
||||
try Frame.init(new_frame, frame_id, self._page, self);
|
||||
errdefer new_frame.deinit(true);
|
||||
|
||||
self._pending_loads += 1;
|
||||
@@ -1431,7 +1442,7 @@ pub fn registerMutationObserver(self: *Frame, observer: *MutationObserver) !void
|
||||
}
|
||||
|
||||
pub fn unregisterMutationObserver(self: *Frame, observer: *MutationObserver) void {
|
||||
observer.releaseRef(self._session);
|
||||
observer.releaseRef(self._page);
|
||||
self._mutation_observers.remove(&observer.node);
|
||||
}
|
||||
|
||||
@@ -1443,7 +1454,7 @@ pub fn registerIntersectionObserver(self: *Frame, observer: *IntersectionObserve
|
||||
pub fn unregisterIntersectionObserver(self: *Frame, observer: *IntersectionObserver) void {
|
||||
for (self._intersection_observers.items, 0..) |obs, i| {
|
||||
if (obs == observer) {
|
||||
observer.releaseRef(self._session);
|
||||
observer.releaseRef(self._page);
|
||||
_ = self._intersection_observers.swapRemove(i);
|
||||
return;
|
||||
}
|
||||
@@ -1468,7 +1479,7 @@ pub fn dispatchLoad(self: *Frame) !void {
|
||||
|
||||
for (to_process.items) |html_element| {
|
||||
if (has_dom_load_listener or html_element.hasAttributeFunction(.onload, self)) {
|
||||
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self);
|
||||
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self._page);
|
||||
try self._event_manager.dispatch(html_element.asEventTarget(), event);
|
||||
}
|
||||
}
|
||||
@@ -1579,7 +1590,7 @@ pub fn deliverSlotchangeEvents(self: *Frame) void {
|
||||
self._slots_pending_slotchange.clearRetainingCapacity();
|
||||
|
||||
for (slots) |slot| {
|
||||
const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, self) catch |err| {
|
||||
const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, self._page) catch |err| {
|
||||
log.err(.frame, "deliverSlotchange.init", .{ .err = err, .type = self._type, .url = self.url });
|
||||
continue;
|
||||
};
|
||||
@@ -2625,6 +2636,19 @@ pub fn dupeString(self: *Frame, value: []const u8) ![]const u8 {
|
||||
return self.arena.dupe(u8, value);
|
||||
}
|
||||
|
||||
// Direct (non-propagating) dispatch of an event. Mirrors WorkerGlobalScope.dispatch
|
||||
// so worker-compatible APIs can uniformly call `global.dispatch(...)` across both
|
||||
// Frame and Worker contexts.
|
||||
pub fn dispatch(
|
||||
self: *Frame,
|
||||
target: *EventTarget,
|
||||
event: *Event,
|
||||
handler: anytype,
|
||||
comptime opts: EventManager.DispatchDirectOptions,
|
||||
) !void {
|
||||
return self._event_manager.dispatchDirect(target, event, handler, opts);
|
||||
}
|
||||
|
||||
pub fn dupeSSO(self: *Frame, value: []const u8) !String {
|
||||
return String.init(self.arena, value, .{ .dupe = true });
|
||||
}
|
||||
@@ -3534,7 +3558,7 @@ pub fn handleClick(self: *Frame, target: *Node) !void {
|
||||
pub fn triggerKeyboard(self: *Frame, keyboard_event: *KeyboardEvent) !void {
|
||||
const event = keyboard_event.asEvent();
|
||||
const element = self.window._document._active_element orelse {
|
||||
event.deinit(self._session);
|
||||
event.deinit(self._page);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -3630,7 +3654,7 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For
|
||||
|
||||
// so submit_event is still valid when we check _prevent_default
|
||||
submit_event.acquireRef();
|
||||
defer _ = submit_event.releaseRef(self._session);
|
||||
defer _ = submit_event.releaseRef(self._page);
|
||||
|
||||
try self._event_manager.dispatch(form_element.asEventTarget(), submit_event);
|
||||
// If the submit event was prevented, don't submit the form
|
||||
|
||||
232
src/browser/Page.zig
Normal file
232
src/browser/Page.zig
Normal file
@@ -0,0 +1,232 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const App = @import("../App.zig");
|
||||
|
||||
const js = @import("js/js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const Frame = @import("Frame.zig");
|
||||
const Session = @import("Session.zig");
|
||||
const Factory = @import("Factory.zig");
|
||||
|
||||
const log = lp.log;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
// A Page is the container for a root Frame and all of its descendants
|
||||
// (nested iframes). It owns the resources that share the lifetime of the root
|
||||
// document: the DOM factory, the per-page arena, the JS identity map, shared
|
||||
// origins, v8 global handles, and queued navigation buffers.
|
||||
//
|
||||
// In the future, a Session may hold multiple Pages at once (e.g. during a
|
||||
// navigation, while the old Page is retiring and the new one is provisional).
|
||||
// For now, Session still holds a single Page.
|
||||
const Page = @This();
|
||||
|
||||
session: *Session,
|
||||
|
||||
// DOM object factory scoped to this Page's documents.
|
||||
factory: Factory,
|
||||
|
||||
// The arena for this Page's lifetime. Document / Frame / Factory / DOM
|
||||
// objects allocate out of this.
|
||||
frame_arena: Allocator,
|
||||
|
||||
// Origin map for same-origin context sharing. Entries live for the Page's
|
||||
// lifetime.
|
||||
origins: std.StringHashMapUnmanaged(*js.Origin) = .empty,
|
||||
|
||||
// Identity tracking for the main world. All main-world contexts in this Page
|
||||
// share this, ensuring object identity works across same-origin frames.
|
||||
identity: js.Identity = .{},
|
||||
|
||||
// Finalizer callbacks for Zig instances exposed to v8 in this Page. Keyed by
|
||||
// Zig instance ptr. The backing FinalizerCallback.Identity structs come from
|
||||
// Session.fc_identity_pool so they outlive the Page for v8 weak-callback
|
||||
// safety.
|
||||
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *Session.FinalizerCallback) = .empty,
|
||||
|
||||
// Tracked global v8 objects that need to be released when the Page tears down.
|
||||
globals: std.ArrayList(v8.Global) = .empty,
|
||||
|
||||
// Temporary v8 globals that can be released early. Key is global.data_ptr.
|
||||
temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
||||
|
||||
// Double buffered so that, as we process one list of queued navigations, new
|
||||
// entries are added to the separate buffer. Prevents endless navigation loops
|
||||
// and invalidation of the list during iteration.
|
||||
queued_navigation_1: std.ArrayList(*Frame) = .empty,
|
||||
queued_navigation_2: std.ArrayList(*Frame) = .empty,
|
||||
// pointer to either queued_navigation_1 or queued_navigation_2
|
||||
queued_navigation: *std.ArrayList(*Frame) = undefined,
|
||||
|
||||
// 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(*Frame) = .empty,
|
||||
|
||||
// The root Frame of this Page. Non-optional — a Page always has a root frame.
|
||||
frame: Frame,
|
||||
|
||||
// Initialize a Page and its root Frame.
|
||||
pub fn init(self: *Page, session: *Session, frame_id: u32) !void {
|
||||
const frame_arena = try session.arena_pool.acquire(.large, "Page.frame_arena");
|
||||
errdefer session.arena_pool.release(frame_arena);
|
||||
|
||||
self.* = .{
|
||||
.session = session,
|
||||
.frame = undefined,
|
||||
.frame_arena = frame_arena,
|
||||
.factory = Factory.init(frame_arena),
|
||||
};
|
||||
self.queued_navigation = &self.queued_navigation_1;
|
||||
|
||||
try Frame.init(&self.frame, frame_id, self, null);
|
||||
}
|
||||
|
||||
// Tear down the Page and its root Frame. Equivalent to the old
|
||||
// Session.removePage + Session.resetFrameResources.
|
||||
pub fn deinit(self: *Page, abort_http: bool) void {
|
||||
self.frame.deinit(abort_http);
|
||||
|
||||
const session = self.session;
|
||||
defer session.browser.env.memoryPressureNotification(.moderate);
|
||||
|
||||
self.identity.deinit();
|
||||
self.identity = .{};
|
||||
|
||||
// Force cleanup all remaining finalized objects.
|
||||
{
|
||||
var it = self.finalizer_callbacks.valueIterator();
|
||||
while (it.next()) |fc| {
|
||||
fc.*.deinit(self);
|
||||
}
|
||||
self.finalizer_callbacks = .empty;
|
||||
}
|
||||
|
||||
{
|
||||
for (self.globals.items) |*global| {
|
||||
v8.v8__Global__Reset(global);
|
||||
}
|
||||
self.globals = .empty;
|
||||
}
|
||||
|
||||
{
|
||||
var it = self.temps.valueIterator();
|
||||
while (it.next()) |global| {
|
||||
v8.v8__Global__Reset(global);
|
||||
}
|
||||
self.temps = .empty;
|
||||
}
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(self.origins.count() == 0);
|
||||
}
|
||||
// Defensive cleanup in case origins leaked.
|
||||
{
|
||||
const app = session.browser.app;
|
||||
var it = self.origins.valueIterator();
|
||||
while (it.next()) |value| {
|
||||
value.*.deinit(app);
|
||||
}
|
||||
self.origins = .empty;
|
||||
}
|
||||
|
||||
session.arena_pool.release(self.frame_arena);
|
||||
}
|
||||
|
||||
pub fn getArena(self: *Page, size_or_bucket: anytype, debug: []const u8) !Allocator {
|
||||
return self.session.getArena(size_or_bucket, debug);
|
||||
}
|
||||
|
||||
pub fn releaseArena(self: *Page, allocator: Allocator) void {
|
||||
return self.session.releaseArena(allocator);
|
||||
}
|
||||
|
||||
pub fn getOrCreateOrigin(self: *Page, key_: ?[]const u8) !*js.Origin {
|
||||
const session = self.session;
|
||||
const key = key_ orelse {
|
||||
var opaque_origin: [36]u8 = undefined;
|
||||
@import("../id.zig").uuidv4(&opaque_origin);
|
||||
// Origin.init will dupe opaque_origin. It's fine that this doesn't
|
||||
// get added to self.origins. In fact, it further isolates it. When the
|
||||
// context is freed, it'll call Page.releaseOrigin which will free it.
|
||||
return js.Origin.init(session.browser.app, session.browser.env.isolate, &opaque_origin);
|
||||
};
|
||||
|
||||
const gop = try self.origins.getOrPut(session.arena, key);
|
||||
if (gop.found_existing) {
|
||||
const origin = gop.value_ptr.*;
|
||||
origin.rc += 1;
|
||||
return origin;
|
||||
}
|
||||
|
||||
errdefer _ = self.origins.remove(key);
|
||||
|
||||
const origin = try js.Origin.init(session.browser.app, session.browser.env.isolate, key);
|
||||
gop.key_ptr.* = origin.key;
|
||||
gop.value_ptr.* = origin;
|
||||
return origin;
|
||||
}
|
||||
|
||||
pub fn releaseOrigin(self: *Page, origin: *js.Origin) void {
|
||||
const rc = origin.rc;
|
||||
if (rc == 1) {
|
||||
_ = self.origins.remove(origin.key);
|
||||
origin.deinit(self.session.browser.app);
|
||||
} else {
|
||||
origin.rc = rc - 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scheduleNavigation(self: *Page, frame: *Frame) !void {
|
||||
const list = self.queued_navigation;
|
||||
|
||||
// Check if frame is already queued
|
||||
for (list.items) |existing| {
|
||||
if (existing == frame) {
|
||||
// Already queued
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return list.append(self.session.arena, frame);
|
||||
}
|
||||
|
||||
pub fn findFrameByFrameId(self: *Page, frame_id: u32) ?*Frame {
|
||||
return findFrameBy(&self.frame, "_frame_id", frame_id);
|
||||
}
|
||||
|
||||
pub fn findFrameByLoaderId(self: *Page, loader_id: u32) ?*Frame {
|
||||
return findFrameBy(&self.frame, "_loader_id", loader_id);
|
||||
}
|
||||
|
||||
fn findFrameBy(frame: *Frame, comptime field: []const u8, id: u32) ?*Frame {
|
||||
if (@field(frame, field) == id) return frame;
|
||||
for (frame.child_frames.items) |f| {
|
||||
if (findFrameBy(f, field, id)) |found| {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -40,7 +40,7 @@ http_client: *HttpClient,
|
||||
pub const Opts = struct {};
|
||||
|
||||
pub fn init(session: *Session, _: Opts) !Runner {
|
||||
const frame = &(session.frame orelse return error.NoPage);
|
||||
const frame = session.currentFrame() orelse return error.NoPage;
|
||||
|
||||
return .{
|
||||
.frame = frame,
|
||||
@@ -150,10 +150,12 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !CDPTickResult {
|
||||
},
|
||||
.html, .complete => {
|
||||
const session = self.session;
|
||||
if (session.queued_navigation.items.len != 0) {
|
||||
try session.processQueuedNavigation();
|
||||
self.frame = &session.frame.?; // might have changed
|
||||
return .{ .ok = 0 };
|
||||
if (session.currentPage()) |page| {
|
||||
if (page.queued_navigation.items.len != 0) {
|
||||
try session.processQueuedNavigation();
|
||||
self.frame = session.currentFrame().?; // might have changed
|
||||
return .{ .ok = 0 };
|
||||
}
|
||||
}
|
||||
const browser = session.browser;
|
||||
|
||||
@@ -323,7 +325,7 @@ test "Runner: no page" {
|
||||
|
||||
test "Runner: waitForSelector timeout" {
|
||||
const frame = try testing.pageTest("runner/runner1.html", .{});
|
||||
defer frame._session.removeFrame();
|
||||
defer frame._session.removePage();
|
||||
|
||||
var runner = try frame._session.runner(.{});
|
||||
try testing.expectError(error.Timeout, runner.waitForSelector("#nope", 10));
|
||||
@@ -332,7 +334,7 @@ test "Runner: waitForSelector timeout" {
|
||||
test "Runner: waitForSelector" {
|
||||
defer testing.reset();
|
||||
const frame = try testing.pageTest("runner/runner1.html", .{});
|
||||
defer frame._session.removeFrame();
|
||||
defer frame._session.removePage();
|
||||
|
||||
var runner = try frame._session.runner(.{});
|
||||
const el = try runner.waitForSelector("#sel1", 10);
|
||||
@@ -341,7 +343,7 @@ test "Runner: waitForSelector" {
|
||||
|
||||
test "Runner: waitForScript timeout" {
|
||||
const frame = try testing.pageTest("runner/runner1.html", .{});
|
||||
defer frame._session.removeFrame();
|
||||
defer frame._session.removePage();
|
||||
|
||||
var runner = try frame._session.runner(.{});
|
||||
try testing.expectError(error.Timeout, runner.waitForScript("document.querySelector('#nope')", 10));
|
||||
@@ -349,7 +351,7 @@ test "Runner: waitForScript timeout" {
|
||||
|
||||
test "Runner: waitForScript" {
|
||||
const frame = try testing.pageTest("runner/runner1.html", .{});
|
||||
defer frame._session.removeFrame();
|
||||
defer frame._session.removePage();
|
||||
|
||||
var runner = try frame._session.runner(.{});
|
||||
try runner.waitForScript("document.querySelector('#sel1')", 10);
|
||||
|
||||
@@ -949,7 +949,7 @@ pub const Script = struct {
|
||||
|
||||
fn executeCallback(self: *const Script, typ: String, frame: *Frame) void {
|
||||
const Event = @import("webapi/Event.zig");
|
||||
const event = Event.initTrusted(typ, .{}, frame) catch |err| {
|
||||
const event = Event.initTrusted(typ, .{}, frame._page) catch |err| {
|
||||
log.warn(.js, "script internal callback", .{
|
||||
.url = self.url,
|
||||
.type = typ,
|
||||
|
||||
@@ -29,9 +29,9 @@ const Navigation = @import("webapi/navigation/Navigation.zig");
|
||||
const History = @import("webapi/History.zig");
|
||||
|
||||
const Frame = @import("Frame.zig");
|
||||
const Page = @import("Page.zig");
|
||||
pub const Runner = @import("Runner.zig");
|
||||
const Browser = @import("Browser.zig");
|
||||
const Factory = @import("Factory.zig");
|
||||
const Notification = @import("../Notification.zig");
|
||||
const QueuedNavigation = Frame.QueuedNavigation;
|
||||
|
||||
@@ -40,17 +40,16 @@ const ArenaPool = App.ArenaPool;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
// You can create successively multiple frames for a session, but you must
|
||||
// deinit a frame before running another one. It manages two distinct lifetimes.
|
||||
// A Session represents a browsing context group (cookie jar, session storage,
|
||||
// navigation history) within a Browser. It hosts one Page at a time — the
|
||||
// root Frame and all of its descendants — and is responsible for Page
|
||||
// lifecycle (create, remove, replace on root navigation).
|
||||
//
|
||||
// The first is the lifetime of the Session itself, where frames are created and
|
||||
// removed, but share the same cookie jar and navigation history (etc...)
|
||||
//
|
||||
// The second is as a container the data needed by the full frame hierarchy, i.e. \
|
||||
// the root frame and all of its frames (and all of their frames.)
|
||||
// Multiple concurrent Pages (e.g. an old Page retiring while a new provisional
|
||||
// Page is loading) are not yet supported; see Page.zig for the intended
|
||||
// direction.
|
||||
const Session = @This();
|
||||
|
||||
// These are the fields that remain intact for the duration of the Session
|
||||
browser: *Browser,
|
||||
arena: Allocator,
|
||||
history: History,
|
||||
@@ -59,54 +58,18 @@ storage_shed: storage.Shed,
|
||||
notification: *Notification,
|
||||
cookie_jar: storage.Cookie.Jar,
|
||||
|
||||
// These are the fields that get reset whenever the Session's frame (the root) is reset.
|
||||
factory: Factory,
|
||||
|
||||
frame_arena: Allocator,
|
||||
|
||||
// Origin map for same-origin context sharing. Scoped to the root frame lifetime.
|
||||
origins: std.StringHashMapUnmanaged(*js.Origin) = .empty,
|
||||
|
||||
// Identity tracking for the main world. All main world contexts share this,
|
||||
// ensuring object identity works across same-origin frames.
|
||||
identity: js.Identity = .{},
|
||||
|
||||
// Shared finalizer callbacks across all Identities. Keyed by Zig instance ptr.
|
||||
// This ensures objects are only freed when ALL v8 wrappers are gone.
|
||||
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
|
||||
|
||||
// Pool for FinalizerCallback.Identity structs. These must survive frame resets
|
||||
// so V8 weak callbacks can validate the FC before dereferencing it.
|
||||
fc_identity_pool: std.heap.MemoryPool(FinalizerCallback.Identity),
|
||||
|
||||
// Tracked global v8 objects that need to be released on cleanup.
|
||||
// Lives at Session level so objects can outlive individual Identities.
|
||||
globals: std.ArrayList(v8.Global) = .empty,
|
||||
|
||||
// Temporary v8 globals that can be released early. Key is global.data_ptr.
|
||||
// Lives at Session level so objects holding Temps can outlive individual Identities.
|
||||
temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
||||
|
||||
// Shared resources for all frames in this session.
|
||||
// These live for the duration of the frame tree (root + frames).
|
||||
// Shared allocator. Used by Session itself and borrowed by Pages.
|
||||
arena_pool: *ArenaPool,
|
||||
|
||||
frame: ?Frame,
|
||||
// Pool for FinalizerCallback.Identity structs. These must survive Page
|
||||
// teardowns so V8 weak callbacks can validate the FC before dereferencing it.
|
||||
fc_identity_pool: std.heap.MemoryPool(FinalizerCallback.Identity),
|
||||
|
||||
// Double buffer so that, as we process one list of queued navigations, new entries
|
||||
// are added to the separate buffer. This ensures that we don't end up with
|
||||
// endless navigation loops AND that we don't invalidate the list while iterating
|
||||
// if a new entry gets appended
|
||||
queued_navigation_1: std.ArrayList(*Frame),
|
||||
queued_navigation_2: std.ArrayList(*Frame),
|
||||
// pointer to either queued_navigation_1 or queued_navigation_2
|
||||
queued_navigation: *std.ArrayList(*Frame),
|
||||
|
||||
// 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(*Frame),
|
||||
// The currently-active Page. Null when no Page exists (between removePage
|
||||
// and createPage, or at startup).
|
||||
page: ?Page,
|
||||
|
||||
// IDs. Kept at Session level so IDs can remain unique across Page replacements.
|
||||
frame_id_gen: u32 = 0,
|
||||
loader_id_gen: u32 = 0,
|
||||
|
||||
@@ -117,57 +80,47 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
|
||||
const arena = try arena_pool.acquire(.small, "Session");
|
||||
errdefer arena_pool.release(arena);
|
||||
|
||||
const frame_arena = try arena_pool.acquire(.large, "Session.frame_arena");
|
||||
errdefer arena_pool.release(frame_arena);
|
||||
|
||||
self.* = .{
|
||||
.frame = null,
|
||||
.page = null,
|
||||
.arena = arena,
|
||||
.arena_pool = arena_pool,
|
||||
.frame_arena = frame_arena,
|
||||
.factory = Factory.init(frame_arena),
|
||||
.history = .{},
|
||||
// The prototype (EventTarget) for Navigation is created when a Frame is created.
|
||||
.navigation = .{ ._proto = undefined },
|
||||
.storage_shed = .{},
|
||||
.browser = browser,
|
||||
.queued_navigation = undefined,
|
||||
.queued_navigation_1 = .{},
|
||||
.queued_navigation_2 = .{},
|
||||
.queued_queued_navigation = .{},
|
||||
.notification = notification,
|
||||
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
||||
.fc_identity_pool = .init(allocator),
|
||||
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
||||
};
|
||||
self.queued_navigation = &self.queued_navigation_1;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Session) void {
|
||||
if (self.frame != null) {
|
||||
self.removeFrame();
|
||||
if (self.page != null) {
|
||||
self.removePage();
|
||||
}
|
||||
self.cookie_jar.deinit();
|
||||
self.fc_identity_pool.deinit();
|
||||
|
||||
self.storage_shed.deinit(self.browser.app.allocator);
|
||||
self.arena_pool.release(self.frame_arena);
|
||||
self.arena_pool.release(self.arena);
|
||||
}
|
||||
|
||||
// NOTE: the caller is not the owner of the returned value,
|
||||
// the pointer on Frame is just returned as a convenience
|
||||
pub fn createFrame(self: *Session) !*Frame {
|
||||
lp.assert(self.frame == null, "Session.createFrame - frame not null", .{});
|
||||
pub fn createPage(self: *Session) !*Frame {
|
||||
lp.assert(self.page == null, "Session.createPage - page not null", .{});
|
||||
|
||||
self.frame = @as(Frame, undefined);
|
||||
const frame = &self.frame.?;
|
||||
try Frame.init(frame, self.nextFrameId(), self, null);
|
||||
self.page = @as(Page, undefined);
|
||||
const page = &self.page.?;
|
||||
try Page.init(page, self, self.nextFrameId());
|
||||
const frame = &page.frame;
|
||||
|
||||
// Creates a new NavigationEventTarget for this frame.
|
||||
try self.navigation.onNewFrame(frame);
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.browser, "create frame", .{});
|
||||
log.debug(.browser, "create page", .{});
|
||||
}
|
||||
// start JS env
|
||||
// Inform CDP the main frame has been created such that additional context for other Worlds can be created as well
|
||||
@@ -176,19 +129,22 @@ pub fn createFrame(self: *Session) !*Frame {
|
||||
return frame;
|
||||
}
|
||||
|
||||
pub fn removeFrame(self: *Session) void {
|
||||
pub fn removePage(self: *Session) void {
|
||||
// Inform CDP the frame is going to be removed, allowing other worlds to remove themselves before the main one
|
||||
self.notification.dispatch(.frame_remove, .{});
|
||||
lp.assert(self.frame != null, "Session.removeFrame - frame is null", .{});
|
||||
lp.assert(self.page != null, "Session.removePage - page is null", .{});
|
||||
|
||||
self.frame.?.deinit(false);
|
||||
self.frame = null;
|
||||
self.page.?.deinit(false);
|
||||
self.page = null;
|
||||
|
||||
self.navigation.onRemoveFrame();
|
||||
self.resetFrameResources();
|
||||
|
||||
// resetting frame_id_gen preserves previous behavior where removing the
|
||||
// root page returned us to a clean-slate state.
|
||||
self.frame_id_gen = 0;
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.browser, "remove frame", .{});
|
||||
log.debug(.browser, "remove page", .{});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,132 +157,49 @@ pub fn releaseArena(self: *Session, allocator: Allocator) void {
|
||||
}
|
||||
|
||||
pub fn getOrCreateOrigin(self: *Session, key_: ?[]const u8) !*js.Origin {
|
||||
const key = key_ orelse {
|
||||
var opaque_origin: [36]u8 = undefined;
|
||||
@import("../id.zig").uuidv4(&opaque_origin);
|
||||
// Origin.init will dupe opaque_origin. It's fine that this doesn't
|
||||
// get added to self.origins. In fact, it further isolates it. When the
|
||||
// context is freed, it'll call session.releaseOrigin which will free it.
|
||||
return js.Origin.init(self.browser.app, self.browser.env.isolate, &opaque_origin);
|
||||
};
|
||||
|
||||
const gop = try self.origins.getOrPut(self.arena, key);
|
||||
if (gop.found_existing) {
|
||||
const origin = gop.value_ptr.*;
|
||||
origin.rc += 1;
|
||||
return origin;
|
||||
}
|
||||
|
||||
errdefer _ = self.origins.remove(key);
|
||||
|
||||
const origin = try js.Origin.init(self.browser.app, self.browser.env.isolate, key);
|
||||
gop.key_ptr.* = origin.key;
|
||||
gop.value_ptr.* = origin;
|
||||
return origin;
|
||||
return self.page.?.getOrCreateOrigin(key_);
|
||||
}
|
||||
|
||||
pub fn releaseOrigin(self: *Session, origin: *js.Origin) void {
|
||||
const rc = origin.rc;
|
||||
if (rc == 1) {
|
||||
_ = self.origins.remove(origin.key);
|
||||
origin.deinit(self.browser.app);
|
||||
} else {
|
||||
origin.rc = rc - 1;
|
||||
}
|
||||
return self.page.?.releaseOrigin(origin);
|
||||
}
|
||||
|
||||
/// Reset frame_arena and factory for a clean slate.
|
||||
/// Called when root frame is removed.
|
||||
fn resetFrameResources(self: *Session) void {
|
||||
defer self.browser.env.memoryPressureNotification(.moderate);
|
||||
|
||||
self.identity.deinit();
|
||||
self.identity = .{};
|
||||
|
||||
// Force cleanup all remaining finalized objects
|
||||
{
|
||||
var it = self.finalizer_callbacks.valueIterator();
|
||||
while (it.next()) |fc| {
|
||||
fc.*.deinit(self);
|
||||
}
|
||||
self.finalizer_callbacks = .empty;
|
||||
}
|
||||
|
||||
{
|
||||
for (self.globals.items) |*global| {
|
||||
v8.v8__Global__Reset(global);
|
||||
}
|
||||
self.globals = .empty;
|
||||
}
|
||||
|
||||
{
|
||||
var it = self.temps.valueIterator();
|
||||
while (it.next()) |global| {
|
||||
v8.v8__Global__Reset(global);
|
||||
}
|
||||
self.temps = .empty;
|
||||
}
|
||||
|
||||
pub fn replacePage(self: *Session) !*Frame {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(self.origins.count() == 0);
|
||||
}
|
||||
// Defensive cleanup in case origins leaked
|
||||
{
|
||||
const app = self.browser.app;
|
||||
var it = self.origins.valueIterator();
|
||||
while (it.next()) |value| {
|
||||
value.*.deinit(app);
|
||||
}
|
||||
self.origins = .empty;
|
||||
log.debug(.browser, "replace page", .{});
|
||||
}
|
||||
|
||||
self.frame_id_gen = 0;
|
||||
self.arena_pool.reset(self.frame_arena, 64 * 1024);
|
||||
self.factory = Factory.init(self.frame_arena);
|
||||
}
|
||||
lp.assert(self.page != null, "Session.replacePage null page", .{});
|
||||
const current = &self.page.?;
|
||||
lp.assert(current.frame.parent == null, "Session.replacePage with parent", .{});
|
||||
|
||||
pub fn replaceFrame(self: *Session) !*Frame {
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.browser, "replace frame", .{});
|
||||
}
|
||||
|
||||
lp.assert(self.frame != null, "Session.replaceFrame null frame", .{});
|
||||
lp.assert(self.frame.?.parent == null, "Session.replaceFrame with parent", .{});
|
||||
|
||||
var current = self.frame.?;
|
||||
const frame_id = current._frame_id;
|
||||
const frame_id = current.frame._frame_id;
|
||||
current.deinit(true);
|
||||
self.page = null;
|
||||
|
||||
self.resetFrameResources();
|
||||
// Preserve prior behavior: frame_id_gen reset on root replacement so a
|
||||
// subsequent createPage starts from id 1. The captured frame_id is
|
||||
// passed into Page.init explicitly, so it isn't affected.
|
||||
self.frame_id_gen = 0;
|
||||
|
||||
self.frame = @as(Frame, undefined);
|
||||
const frame = &self.frame.?;
|
||||
try Frame.init(frame, frame_id, self, null);
|
||||
return frame;
|
||||
self.page = @as(Page, undefined);
|
||||
const page = &self.page.?;
|
||||
try Page.init(page, self, frame_id);
|
||||
return &page.frame;
|
||||
}
|
||||
|
||||
pub fn currentPage(self: *Session) ?*Page {
|
||||
return &(self.page orelse return null);
|
||||
}
|
||||
|
||||
pub fn currentFrame(self: *Session) ?*Frame {
|
||||
return &(self.frame orelse return null);
|
||||
const page = self.currentPage() orelse return null;
|
||||
return &page.frame;
|
||||
}
|
||||
|
||||
pub fn findFrameByFrameId(self: *Session, frame_id: u32) ?*Frame {
|
||||
const frame = self.currentFrame() orelse return null;
|
||||
return findFrameBy(frame, "_frame_id", frame_id);
|
||||
}
|
||||
|
||||
pub fn findFrameByLoaderId(self: *Session, loader_id: u32) ?*Frame {
|
||||
const frame = self.currentFrame() orelse return null;
|
||||
return findFrameBy(frame, "_loader_id", loader_id);
|
||||
}
|
||||
|
||||
fn findFrameBy(frame: *Frame, comptime field: []const u8, id: u32) ?*Frame {
|
||||
if (@field(frame, field) == id) return frame;
|
||||
for (frame.child_frames.items) |f| {
|
||||
if (findFrameBy(f, field, id)) |found| {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
const page = self.currentPage() orelse return null;
|
||||
return page.findFrameByFrameId(frame_id);
|
||||
}
|
||||
|
||||
pub fn runner(self: *Session, opts: Runner.Opts) !Runner {
|
||||
@@ -334,28 +207,19 @@ pub fn runner(self: *Session, opts: Runner.Opts) !Runner {
|
||||
}
|
||||
|
||||
pub fn scheduleNavigation(self: *Session, frame: *Frame) !void {
|
||||
const list = self.queued_navigation;
|
||||
|
||||
// Check if frame is already queued
|
||||
for (list.items) |existing| {
|
||||
if (existing == frame) {
|
||||
// Already queued
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return list.append(self.arena, frame);
|
||||
return self.page.?.scheduleNavigation(frame);
|
||||
}
|
||||
|
||||
pub fn processQueuedNavigation(self: *Session) !void {
|
||||
const navigations = self.queued_navigation;
|
||||
if (self.queued_navigation == &self.queued_navigation_1) {
|
||||
self.queued_navigation = &self.queued_navigation_2;
|
||||
const page = self.currentPage() orelse return;
|
||||
const navigations = page.queued_navigation;
|
||||
if (page.queued_navigation == &page.queued_navigation_1) {
|
||||
page.queued_navigation = &page.queued_navigation_2;
|
||||
} else {
|
||||
self.queued_navigation = &self.queued_navigation_1;
|
||||
page.queued_navigation = &page.queued_navigation_1;
|
||||
}
|
||||
|
||||
if (self.frame.?._queued_navigation != null) {
|
||||
if (page.frame._queued_navigation != null) {
|
||||
// This is both an optimization and a simplification of sorts. If the
|
||||
// root frame is navigating, then we don't need to process any other
|
||||
// navigation. Also, the navigation for the root frame and for a frame
|
||||
@@ -365,7 +229,7 @@ pub fn processQueuedNavigation(self: *Session) !void {
|
||||
return self.processRootQueuedNavigation();
|
||||
}
|
||||
|
||||
const about_blank_queue = &self.queued_queued_navigation;
|
||||
const about_blank_queue = &page.queued_queued_navigation;
|
||||
defer about_blank_queue.clearRetainingCapacity();
|
||||
|
||||
// First pass: process async navigations (non-about:blank)
|
||||
@@ -401,14 +265,14 @@ pub fn processQueuedNavigation(self: *Session) !void {
|
||||
// Safety: Remove any about:blank navigations that were queued during
|
||||
// processing to prevent infinite loops. New navigations have been queued
|
||||
// in the other buffer.
|
||||
const new_navigations = self.queued_navigation;
|
||||
const new_navigations = page.queued_navigation;
|
||||
var i: usize = 0;
|
||||
while (i < new_navigations.items.len) {
|
||||
const frame = new_navigations.items[i];
|
||||
if (frame._queued_navigation) |qn| {
|
||||
if (qn.is_about_blank) {
|
||||
log.warn(.frame, "recursive about blank", .{});
|
||||
_ = self.queued_navigation.swapRemove(i);
|
||||
_ = page.queued_navigation.swapRemove(i);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -434,10 +298,11 @@ fn processFrameNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation)
|
||||
}
|
||||
|
||||
const frame_id = frame._frame_id;
|
||||
const page = self.currentPage().?;
|
||||
frame.deinit(true);
|
||||
frame.* = undefined;
|
||||
|
||||
try Frame.init(frame, frame_id, self, parent);
|
||||
try Frame.init(frame, frame_id, page, parent);
|
||||
errdefer {
|
||||
for (parent.child_frames.items, 0..) |f, i| {
|
||||
if (f == frame) {
|
||||
@@ -462,7 +327,7 @@ fn processFrameNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation)
|
||||
}
|
||||
|
||||
fn processRootQueuedNavigation(self: *Session) !void {
|
||||
const current_frame = &self.frame.?;
|
||||
const current_frame = &self.page.?.frame;
|
||||
const frame_id = current_frame._frame_id;
|
||||
|
||||
// create a copy before the frame is cleared
|
||||
@@ -471,11 +336,21 @@ fn processRootQueuedNavigation(self: *Session) !void {
|
||||
|
||||
defer self.arena_pool.release(qn.arena);
|
||||
|
||||
self.removeFrame();
|
||||
// Dispatch frame_remove (same as removePage) then replace the Page
|
||||
// in-place, keeping the frame_id stable.
|
||||
self.notification.dispatch(.frame_remove, .{});
|
||||
self.page.?.deinit(true);
|
||||
self.page = null;
|
||||
|
||||
self.frame = @as(Frame, undefined);
|
||||
const new_frame = &self.frame.?;
|
||||
try Frame.init(new_frame, frame_id, self, null);
|
||||
self.navigation.onRemoveFrame();
|
||||
|
||||
// Preserve prior behavior: the old resetFrameResources reset frame_id_gen.
|
||||
self.frame_id_gen = 0;
|
||||
|
||||
self.page = @as(Page, undefined);
|
||||
const page = &self.page.?;
|
||||
try Page.init(page, self, frame_id);
|
||||
const new_frame = &page.frame;
|
||||
|
||||
// Creates a new NavigationEventTarget for this frame.
|
||||
try self.navigation.onNewFrame(new_frame);
|
||||
@@ -503,14 +378,14 @@ pub fn nextLoaderId(self: *Session) u32 {
|
||||
}
|
||||
|
||||
// Every finalizable instance of Zig gets 1 FinalizerCallback registered in the
|
||||
// session. This is to ensure that, if v8 doesn't finalize the value, we can
|
||||
// release on frame reset.
|
||||
// Page. This is to ensure that, if v8 doesn't finalize the value, we can
|
||||
// release on Page teardown.
|
||||
pub const FinalizerCallback = struct {
|
||||
page: *Page,
|
||||
arena: Allocator,
|
||||
session: *Session,
|
||||
resolved_ptr_id: usize,
|
||||
finalizer_ptr_id: usize,
|
||||
release_ref: *const fn (ptr_id: usize, session: *Session) void,
|
||||
release_ref: *const fn (ptr_id: usize, page: *Page) void,
|
||||
|
||||
// Linked list of Identities referencing this FC.
|
||||
identities: ?*Identity = null,
|
||||
@@ -519,10 +394,14 @@ pub const FinalizerCallback = struct {
|
||||
|
||||
// For every FinalizerCallback we'll have 1+ FinalizerCallback.Identity: one
|
||||
// for every identity that gets the instance. In most cases, that'll be 1.
|
||||
// Allocated from Session.fc_identity_pool so it survives frame resets and
|
||||
// Allocated from Session.fc_identity_pool so it survives Page teardowns and
|
||||
// allows the weak callback to safely check the done flag.
|
||||
pub const Identity = struct {
|
||||
session: *Session,
|
||||
// The Page that owns the FinalizerCallback this Identity references.
|
||||
// Only safe to dereference when `done == false`. When done is true,
|
||||
// the Page may have been torn down and this pointer is stale.
|
||||
page: *Page,
|
||||
identity: *js.Identity,
|
||||
finalizer_ptr_id: usize,
|
||||
resolved_ptr_id: usize,
|
||||
@@ -530,8 +409,8 @@ pub const FinalizerCallback = struct {
|
||||
done: bool = false,
|
||||
};
|
||||
|
||||
// Called during frame reset to force cleanup regardless of identities.
|
||||
fn deinit(self: *FinalizerCallback, session: *Session) void {
|
||||
// Called during Page teardown to force cleanup regardless of identities.
|
||||
pub fn deinit(self: *FinalizerCallback, page: *Page) void {
|
||||
// Mark all identities as done so stale V8 weak callbacks
|
||||
// won't find the wrong FC if resolved_ptr_id is reused.
|
||||
var id = self.identities;
|
||||
@@ -539,7 +418,7 @@ pub const FinalizerCallback = struct {
|
||||
identity.done = true;
|
||||
id = identity.next;
|
||||
}
|
||||
self.release_ref(self.finalizer_ptr_id, session);
|
||||
session.releaseArena(self.arena);
|
||||
self.release_ref(self.finalizer_ptr_id, page);
|
||||
page.releaseArena(self.arena);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -28,12 +28,12 @@ const Session = @import("Session.zig");
|
||||
const Selector = @import("webapi/selector/Selector.zig");
|
||||
|
||||
fn dispatchInputAndChangeEvents(el: *Element, frame: *Frame) !void {
|
||||
const input_evt: *Event = try .initTrusted(comptime .wrap("input"), .{ .bubbles = true }, frame);
|
||||
const input_evt: *Event = try .initTrusted(comptime .wrap("input"), .{ .bubbles = true }, frame._page);
|
||||
frame._event_manager.dispatch(el.asEventTarget(), input_evt) catch |err| {
|
||||
lp.log.err(.app, "dispatch input event failed", .{ .err = err });
|
||||
};
|
||||
|
||||
const change_evt: *Event = try .initTrusted(comptime .wrap("change"), .{ .bubbles = true }, frame);
|
||||
const change_evt: *Event = try .initTrusted(comptime .wrap("change"), .{ .bubbles = true }, frame._page);
|
||||
frame._event_manager.dispatch(el.asEventTarget(), change_evt) catch |err| {
|
||||
lp.log.err(.app, "dispatch change event failed", .{ .err = err });
|
||||
};
|
||||
@@ -196,7 +196,7 @@ pub fn scroll(node: ?*DOMNode, x: ?i32, y: ?i32, frame: *Frame) !void {
|
||||
};
|
||||
}
|
||||
|
||||
const scroll_evt: *Event = try .initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, frame);
|
||||
const scroll_evt: *Event = try .initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, frame._page);
|
||||
frame._event_manager.dispatch(el.asEventTarget(), scroll_evt) catch |err| {
|
||||
lp.log.err(.app, "dispatch scroll event failed", .{ .err = err });
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ pub const Opts = struct {
|
||||
strip: Opts.Strip = .{},
|
||||
shadow: Opts.Shadow = .rendered,
|
||||
|
||||
pub const Strip = struct {
|
||||
pub const Strip = packed struct(u3) {
|
||||
js: bool = false,
|
||||
ui: bool = false,
|
||||
css: bool = false,
|
||||
|
||||
@@ -278,7 +278,7 @@ fn collectSelectOptions(
|
||||
const testing = @import("../testing.zig");
|
||||
|
||||
fn testForms(html: []const u8) ![]FormInfo {
|
||||
const frame = try testing.test_session.createFrame();
|
||||
const frame = try testing.test_session.createPage();
|
||||
|
||||
const doc = frame.window._document;
|
||||
const div = try doc.createElement("div", null, frame);
|
||||
@@ -289,7 +289,7 @@ fn testForms(html: []const u8) ![]FormInfo {
|
||||
|
||||
test "browser.forms: login form" {
|
||||
defer testing.reset();
|
||||
defer testing.test_session.removeFrame();
|
||||
defer testing.test_session.removePage();
|
||||
const forms = try testForms(
|
||||
\\<form action="/login" method="POST">
|
||||
\\ <input type="email" name="email" required placeholder="Email">
|
||||
@@ -310,7 +310,7 @@ test "browser.forms: login form" {
|
||||
|
||||
test "browser.forms: form with select" {
|
||||
defer testing.reset();
|
||||
defer testing.test_session.removeFrame();
|
||||
defer testing.test_session.removePage();
|
||||
const forms = try testForms(
|
||||
\\<form>
|
||||
\\ <select name="color">
|
||||
@@ -329,7 +329,7 @@ test "browser.forms: form with select" {
|
||||
|
||||
test "browser.forms: form with textarea" {
|
||||
defer testing.reset();
|
||||
defer testing.test_session.removeFrame();
|
||||
defer testing.test_session.removePage();
|
||||
const forms = try testForms(
|
||||
\\<form method="POST">
|
||||
\\ <textarea name="message" placeholder="Your message"></textarea>
|
||||
@@ -343,7 +343,7 @@ test "browser.forms: form with textarea" {
|
||||
|
||||
test "browser.forms: empty form skipped" {
|
||||
defer testing.reset();
|
||||
defer testing.test_session.removeFrame();
|
||||
defer testing.test_session.removePage();
|
||||
const forms = try testForms(
|
||||
\\<form action="/empty">
|
||||
\\ <p>No fields here</p>
|
||||
@@ -354,7 +354,7 @@ test "browser.forms: empty form skipped" {
|
||||
|
||||
test "browser.forms: hidden inputs excluded" {
|
||||
defer testing.reset();
|
||||
defer testing.test_session.removeFrame();
|
||||
defer testing.test_session.removePage();
|
||||
const forms = try testForms(
|
||||
\\<form>
|
||||
\\ <input type="hidden" name="csrf" value="token123">
|
||||
@@ -368,7 +368,7 @@ test "browser.forms: hidden inputs excluded" {
|
||||
|
||||
test "browser.forms: multiple forms" {
|
||||
defer testing.reset();
|
||||
defer testing.test_session.removeFrame();
|
||||
defer testing.test_session.removePage();
|
||||
const forms = try testForms(
|
||||
\\<form action="/search" method="GET">
|
||||
\\ <input type="text" name="q" placeholder="Search">
|
||||
@@ -385,7 +385,7 @@ test "browser.forms: multiple forms" {
|
||||
|
||||
test "browser.forms: disabled fields flagged" {
|
||||
defer testing.reset();
|
||||
defer testing.test_session.removeFrame();
|
||||
defer testing.test_session.removePage();
|
||||
const forms = try testForms(
|
||||
\\<form>
|
||||
\\ <input type="text" name="enabled_field">
|
||||
@@ -400,7 +400,7 @@ test "browser.forms: disabled fields flagged" {
|
||||
|
||||
test "browser.forms: disabled fieldset" {
|
||||
defer testing.reset();
|
||||
defer testing.test_session.removeFrame();
|
||||
defer testing.test_session.removePage();
|
||||
const forms = try testForms(
|
||||
\\<form>
|
||||
\\ <fieldset disabled>
|
||||
@@ -417,7 +417,7 @@ test "browser.forms: disabled fieldset" {
|
||||
|
||||
test "browser.forms: external field via form attribute" {
|
||||
defer testing.reset();
|
||||
defer testing.test_session.removeFrame();
|
||||
defer testing.test_session.removePage();
|
||||
const forms = try testForms(
|
||||
\\<input type="text" name="external" form="myform">
|
||||
\\<form id="myform" action="/submit">
|
||||
@@ -430,7 +430,7 @@ test "browser.forms: external field via form attribute" {
|
||||
|
||||
test "browser.forms: checkbox and radio return value attribute" {
|
||||
defer testing.reset();
|
||||
defer testing.test_session.removeFrame();
|
||||
defer testing.test_session.removePage();
|
||||
const forms = try testForms(
|
||||
\\<form>
|
||||
\\ <input type="checkbox" name="agree" value="yes" checked>
|
||||
@@ -447,7 +447,7 @@ test "browser.forms: checkbox and radio return value attribute" {
|
||||
|
||||
test "browser.forms: form without action or method" {
|
||||
defer testing.reset();
|
||||
defer testing.test_session.removeFrame();
|
||||
defer testing.test_session.removePage();
|
||||
const forms = try testForms(
|
||||
\\<form>
|
||||
\\ <input type="text" name="q">
|
||||
|
||||
@@ -452,8 +452,8 @@ fn getListenerTypes(target: *EventTarget, listener_targets: ListenerTargetMap) [
|
||||
const testing = @import("../testing.zig");
|
||||
|
||||
fn testInteractive(html: []const u8) ![]InteractiveElement {
|
||||
const frame = try testing.test_session.createFrame();
|
||||
defer testing.test_session.removeFrame();
|
||||
const frame = try testing.test_session.createPage();
|
||||
defer testing.test_session.removePage();
|
||||
|
||||
const doc = frame.window._document;
|
||||
const div = try doc.createElement("div", null, frame);
|
||||
|
||||
@@ -21,6 +21,7 @@ const lp = @import("lightpanda");
|
||||
const string = @import("../../string.zig");
|
||||
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const Session = @import("../Session.zig");
|
||||
const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig");
|
||||
|
||||
@@ -447,10 +448,14 @@ fn tupleFieldName(comptime i: usize) [:0]const u8 {
|
||||
};
|
||||
}
|
||||
|
||||
fn isPage(comptime T: type) bool {
|
||||
fn isFrame(comptime T: type) bool {
|
||||
return T == *Frame or T == *const Frame;
|
||||
}
|
||||
|
||||
fn isPage(comptime T: type) bool {
|
||||
return T == *Page or T == *const Page;
|
||||
}
|
||||
|
||||
fn isSession(comptime T: type) bool {
|
||||
return T == *Session or T == *const Session;
|
||||
}
|
||||
@@ -460,19 +465,19 @@ fn isExecution(comptime T: type) bool {
|
||||
}
|
||||
|
||||
fn getGlobalArg(comptime T: type, ctx: *Context) T {
|
||||
if (comptime isPage(T)) {
|
||||
if (comptime isFrame(T)) {
|
||||
return switch (ctx.global) {
|
||||
.frame => |frame| frame,
|
||||
.worker => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
if (comptime isExecution(T)) {
|
||||
return &ctx.execution;
|
||||
if (comptime isPage(T)) {
|
||||
return ctx.page;
|
||||
}
|
||||
|
||||
if (comptime isSession(T)) {
|
||||
return ctx.session;
|
||||
if (comptime isExecution(T)) {
|
||||
return &ctx.execution;
|
||||
}
|
||||
|
||||
@compileError("Unsupported global arg type: " ++ @typeName(T));
|
||||
@@ -761,17 +766,17 @@ fn getArgs(comptime F: type, comptime offset: usize, local: *const Local, info:
|
||||
return args;
|
||||
}
|
||||
|
||||
// If the last parameter is the Page or Worker, set it, and exclude it
|
||||
// from our params slice, because we don't want to bind it to
|
||||
// a JS argument
|
||||
// If the last parameter is Frame/Page/Session/Execution, set it from
|
||||
// context and exclude it from our params slice, because we don't want
|
||||
// to bind it to a JS argument.
|
||||
const LastParamType = params[params.len - 1].type.?;
|
||||
if (comptime isPage(LastParamType) or isExecution(LastParamType) or isSession(LastParamType)) {
|
||||
if (comptime isFrame(LastParamType) or isPage(LastParamType) or isExecution(LastParamType) or isSession(LastParamType)) {
|
||||
@field(args, tupleFieldName(params.len - 1 + offset)) = getGlobalArg(LastParamType, local.ctx);
|
||||
break :blk params[0 .. params.len - 1];
|
||||
}
|
||||
|
||||
// we have neither a Page, Execution, nor a JsObject. All params must be
|
||||
// bound to a JavaScript value.
|
||||
// we have neither a Frame/Page/Session/Execution nor a JsObject.
|
||||
// All params must be bound to a JavaScript value.
|
||||
break :blk params;
|
||||
};
|
||||
|
||||
@@ -818,7 +823,9 @@ fn getArgs(comptime F: type, comptime offset: usize, local: *const Local, info:
|
||||
}
|
||||
}
|
||||
|
||||
if (comptime isPage(param.type.?)) {
|
||||
if (comptime isFrame(param.type.?)) {
|
||||
@compileError("Frame must be the last parameter: " ++ @typeName(F));
|
||||
} else if (comptime isPage(param.type.?)) {
|
||||
@compileError("Page must be the last parameter: " ++ @typeName(F));
|
||||
} else if (comptime isExecution(param.type.?)) {
|
||||
@compileError("Execution must be the last parameter: " ++ @typeName(F));
|
||||
|
||||
@@ -27,6 +27,7 @@ const Scheduler = @import("Scheduler.zig");
|
||||
const Execution = @import("Execution.zig");
|
||||
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const Session = @import("../Session.zig");
|
||||
const ScriptManager = @import("../ScriptManager.zig");
|
||||
const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig");
|
||||
@@ -69,7 +70,14 @@ pub const GlobalScope = union(enum) {
|
||||
id: usize,
|
||||
env: *Env,
|
||||
global: GlobalScope,
|
||||
session: *Session,
|
||||
|
||||
// The Page this Context belongs to. For main-world frame contexts, this is
|
||||
// the Page of the frame. For worker contexts, this is the Page of the
|
||||
// worker's parent frame — a worker's v8 globals and identity tracking live
|
||||
// on the same Page as its owning frame (worker dies with its page). The
|
||||
// Session is always reachable via `page.session`.
|
||||
page: *Page,
|
||||
|
||||
isolate: js.Isolate,
|
||||
|
||||
// Per-context microtask queue for isolation between contexts
|
||||
@@ -114,7 +122,7 @@ origin: *Origin,
|
||||
identity: *js.Identity,
|
||||
|
||||
// Allocator to use for identity map operations. For main world contexts this is
|
||||
// session.frame_arena, for isolated worlds it's the isolated world's arena.
|
||||
// page.frame_arena, for isolated worlds it's the isolated world's arena.
|
||||
identity_arena: Allocator,
|
||||
|
||||
// Unlike other v8 types, like functions or objects, modules are not shared
|
||||
@@ -207,7 +215,7 @@ pub fn deinit(self: *Context) void {
|
||||
v8.v8__Global__Reset(global);
|
||||
}
|
||||
|
||||
self.session.releaseOrigin(self.origin);
|
||||
self.page.releaseOrigin(self.origin);
|
||||
|
||||
// Clear the embedder data so that if V8 keeps this context alive
|
||||
// (because objects created in it are still referenced), we don't
|
||||
@@ -234,9 +242,9 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
|
||||
lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc });
|
||||
}
|
||||
|
||||
const origin = try self.session.getOrCreateOrigin(key);
|
||||
const origin = try self.page.getOrCreateOrigin(key);
|
||||
|
||||
self.session.releaseOrigin(self.origin);
|
||||
self.page.releaseOrigin(self.origin);
|
||||
self.origin = origin;
|
||||
|
||||
{
|
||||
@@ -252,11 +260,11 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
|
||||
}
|
||||
|
||||
pub fn trackGlobal(self: *Context, global: v8.Global) !void {
|
||||
return self.session.globals.append(self.session.frame_arena, global);
|
||||
return self.page.globals.append(self.page.frame_arena, global);
|
||||
}
|
||||
|
||||
pub fn trackTemp(self: *Context, global: v8.Global) !void {
|
||||
return self.session.temps.put(self.session.frame_arena, global.data_ptr, global);
|
||||
return self.page.temps.put(self.page.frame_arena, global.data_ptr, global);
|
||||
}
|
||||
|
||||
pub const IdentityResult = struct {
|
||||
|
||||
@@ -275,9 +275,9 @@ fn _createContext(self: *Env, global: anytype, params: ContextParams) !*Context
|
||||
const context_id = self.context_id;
|
||||
self.context_id = context_id + 1;
|
||||
|
||||
const session = global._session;
|
||||
const origin = try session.getOrCreateOrigin(null);
|
||||
errdefer session.releaseOrigin(origin);
|
||||
const page = global._page;
|
||||
const origin = try page.getOrCreateOrigin(null);
|
||||
errdefer page.releaseOrigin(origin);
|
||||
|
||||
const context = try context_arena.create(Context);
|
||||
context.* = .{
|
||||
@@ -285,7 +285,7 @@ fn _createContext(self: *Env, global: anytype, params: ContextParams) !*Context
|
||||
.global = if (comptime is_frame) .{ .frame = global } else .{ .worker = global },
|
||||
.origin = origin,
|
||||
.id = context_id,
|
||||
.session = session,
|
||||
.page = page,
|
||||
.isolate = isolate,
|
||||
.arena = context_arena,
|
||||
.handle = context_global,
|
||||
@@ -558,8 +558,8 @@ const PrivateSymbols = struct {
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Env: Worker context " {
|
||||
const session = testing.test_session;
|
||||
const frame = try session.createFrame();
|
||||
defer session.removeFrame();
|
||||
const frame = try session.createPage();
|
||||
defer session.removePage();
|
||||
|
||||
const worker = try @import("../webapi/Worker.zig").init("http://localhost:9582/src/browser/tests/testing.js", &frame.js.execution);
|
||||
|
||||
@@ -573,8 +573,8 @@ test "Env: Worker context " {
|
||||
|
||||
test "Env: Frame context" {
|
||||
const session = testing.test_session;
|
||||
const frame = try session.createFrame();
|
||||
defer session.removeFrame();
|
||||
const frame = try session.createPage();
|
||||
defer session.removePage();
|
||||
|
||||
// Frame already has a context created, use it directly
|
||||
const ctx = frame.js;
|
||||
|
||||
@@ -26,10 +26,13 @@
|
||||
//! whether it's a Page context or a Worker context.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const Context = @import("Context.zig");
|
||||
const Scheduler = @import("Scheduler.zig");
|
||||
const Factory = @import("../Factory.zig");
|
||||
|
||||
const String = lp.String;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Execution = @This();
|
||||
@@ -53,3 +56,10 @@ charset: *[]const u8,
|
||||
pub fn base(self: *const Execution) [:0]const u8 {
|
||||
return self.context.global.base();
|
||||
}
|
||||
|
||||
pub fn dupeString(self: *const Execution, value: []const u8) ![]const u8 {
|
||||
if (String.intern(value)) |v| {
|
||||
return v;
|
||||
}
|
||||
return self.arena.dupe(u8, value);
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Gl
|
||||
return .{ .handle = global, .temps = {} };
|
||||
}
|
||||
try ctx.trackTemp(global);
|
||||
return .{ .handle = global, .temps = &ctx.session.temps };
|
||||
return .{ .handle = global, .temps = &ctx.page.temps };
|
||||
}
|
||||
|
||||
pub fn tempWithThis(self: *const Function, value: anytype) !Temp {
|
||||
|
||||
@@ -20,7 +20,8 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
const string = @import("../../string.zig");
|
||||
|
||||
const Session = @import("../Session.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const FinalizerCallback = @import("../Session.zig").FinalizerCallback;
|
||||
|
||||
const js = @import("js.zig");
|
||||
const bridge = @import("bridge.zig");
|
||||
@@ -270,19 +271,21 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
|
||||
if (resolved.finalizer) |finalizer| {
|
||||
const finalizer_ptr_id = finalizer.ptr_id;
|
||||
|
||||
const session = ctx.session;
|
||||
const finalizer_gop = try session.finalizer_callbacks.getOrPut(session.frame_arena, finalizer_ptr_id);
|
||||
const page = ctx.page;
|
||||
const session = page.session;
|
||||
const finalizer_gop = try page.finalizer_callbacks.getOrPut(page.frame_arena, finalizer_ptr_id);
|
||||
if (finalizer_gop.found_existing == false) {
|
||||
// This is the first context (and very likely only one) to
|
||||
// see this Zig instance. We need to create the FinalizerCallback
|
||||
// so that we can cleanup on frame reset if v8 doesn't finalize.
|
||||
errdefer _ = session.finalizer_callbacks.remove(finalizer_ptr_id);
|
||||
// so that we can cleanup on Page teardown if v8 doesn't finalize.
|
||||
errdefer _ = page.finalizer_callbacks.remove(finalizer_ptr_id);
|
||||
finalizer.acquire_ref(finalizer_ptr_id);
|
||||
finalizer_gop.value_ptr.* = try self.createFinalizerCallback(resolved_ptr_id, finalizer_ptr_id, finalizer.release_ref_from_zig);
|
||||
}
|
||||
const fc = finalizer_gop.value_ptr.*;
|
||||
const identity_finalizer = try session.fc_identity_pool.create();
|
||||
identity_finalizer.* = .{
|
||||
.page = page,
|
||||
.session = session,
|
||||
.identity = ctx.identity,
|
||||
.finalizer_ptr_id = finalizer_ptr_id,
|
||||
@@ -1178,7 +1181,7 @@ const Resolved = struct {
|
||||
ptr_id: usize,
|
||||
acquire_ref: *const fn (ptr_id: usize) void,
|
||||
release_ref: *const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void,
|
||||
release_ref_from_zig: *const fn (ptr_id: usize, session: *Session) void,
|
||||
release_ref_from_zig: *const fn (ptr_id: usize, page: *Page) void,
|
||||
};
|
||||
};
|
||||
pub fn resolveValue(value: anytype) Resolved {
|
||||
@@ -1224,12 +1227,12 @@ fn resolveT(comptime T: type, value: *T) Resolved {
|
||||
|
||||
fn releaseRef(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
|
||||
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
|
||||
const identity_finalizer: *Session.FinalizerCallback.Identity = @ptrCast(@alignCast(ptr));
|
||||
const identity_finalizer: *FinalizerCallback.Identity = @ptrCast(@alignCast(ptr));
|
||||
|
||||
// Identity is allocated from pool, so it's valid even after frame reset.
|
||||
const session = identity_finalizer.session;
|
||||
const page = identity_finalizer.page;
|
||||
const resolved_ptr_id = identity_finalizer.resolved_ptr_id;
|
||||
defer session.fc_identity_pool.destroy(identity_finalizer);
|
||||
defer page.session.fc_identity_pool.destroy(identity_finalizer);
|
||||
|
||||
// Always clean up the identity map entry
|
||||
if (identity_finalizer.identity.identity_map.fetchRemove(resolved_ptr_id)) |kv| {
|
||||
@@ -1237,28 +1240,29 @@ fn resolveT(comptime T: type, value: *T) Resolved {
|
||||
v8.v8__Global__Reset(&global);
|
||||
}
|
||||
|
||||
// If done, FC was already cleaned up during frame reset. The
|
||||
// finalizer_ptr_id may have been reused for a new object, so
|
||||
// we must not look it up in the map.
|
||||
// If done, FC was already cleaned up during Page teardown.
|
||||
// The finalizer_ptr_id may have been reused for a new object,
|
||||
// so we must not look it up in the map. It's also unsafe to
|
||||
// dereference identity_finalizer.page after done is true.
|
||||
if (identity_finalizer.done) return;
|
||||
|
||||
const finalizer_ptr_id = identity_finalizer.finalizer_ptr_id;
|
||||
const fc = session.finalizer_callbacks.get(finalizer_ptr_id) orelse return;
|
||||
const fc = page.finalizer_callbacks.get(finalizer_ptr_id) orelse return;
|
||||
|
||||
const identity_count = fc.identity_count;
|
||||
if (identity_count == 1) {
|
||||
// Last identity - clean up the FC.
|
||||
// Remove from map before releaseRef to prevent address reuse issues.
|
||||
_ = session.finalizer_callbacks.remove(finalizer_ptr_id);
|
||||
FT.releaseRef(@ptrFromInt(finalizer_ptr_id), session);
|
||||
session.releaseArena(fc.arena);
|
||||
_ = page.finalizer_callbacks.remove(finalizer_ptr_id);
|
||||
FT.releaseRef(@ptrFromInt(finalizer_ptr_id), page);
|
||||
page.releaseArena(fc.arena);
|
||||
} else {
|
||||
fc.identity_count = identity_count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn releaseRefFromZig(ptr_id: usize, session: *Session) void {
|
||||
FT.releaseRef(@ptrFromInt(ptr_id), session);
|
||||
fn releaseRefFromZig(ptr_id: usize, page: *Page) void {
|
||||
FT.releaseRef(@ptrFromInt(ptr_id), page);
|
||||
}
|
||||
};
|
||||
break :blk .{
|
||||
@@ -1524,17 +1528,17 @@ fn createFinalizerCallback(
|
||||
// The most specific value where finalizers are defined
|
||||
// What actually gets acquired / released / deinit
|
||||
finalizer_ptr_id: usize,
|
||||
release_ref: *const fn (ptr_id: usize, session: *Session) void,
|
||||
) !*Session.FinalizerCallback {
|
||||
const session = self.ctx.session;
|
||||
release_ref: *const fn (ptr_id: usize, page: *Page) void,
|
||||
) !*FinalizerCallback {
|
||||
const page = self.ctx.page;
|
||||
|
||||
const arena = try session.getArena(.tiny, "FinalizerCallback");
|
||||
errdefer session.releaseArena(arena);
|
||||
const arena = try page.getArena(.tiny, "FinalizerCallback");
|
||||
errdefer page.releaseArena(arena);
|
||||
|
||||
const fc = try arena.create(Session.FinalizerCallback);
|
||||
const fc = try arena.create(FinalizerCallback);
|
||||
fc.* = .{
|
||||
.page = page,
|
||||
.arena = arena,
|
||||
.session = session,
|
||||
.release_ref = release_ref,
|
||||
.resolved_ptr_id = resolved_ptr_id,
|
||||
.finalizer_ptr_id = finalizer_ptr_id,
|
||||
|
||||
@@ -67,7 +67,7 @@ fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Glo
|
||||
return .{ .handle = global, .temps = {} };
|
||||
}
|
||||
try ctx.trackTemp(global);
|
||||
return .{ .handle = global, .temps = &ctx.session.temps };
|
||||
return .{ .handle = global, .temps = &ctx.page.temps };
|
||||
}
|
||||
|
||||
pub const Temp = G(.temp);
|
||||
|
||||
@@ -57,7 +57,7 @@ fn _toSlice(self: String, comptime null_terminate: bool, allocator: Allocator) !
|
||||
|
||||
pub fn toSSO(self: String, comptime global: bool) !(if (global) lp.String.Global else lp.String) {
|
||||
if (comptime global) {
|
||||
return .{ .str = try self.toSSOWithAlloc(self.local.ctx.session.frame_arena) };
|
||||
return .{ .str = try self.toSSOWithAlloc(self.local.ctx.page.frame_arena) };
|
||||
}
|
||||
return self.toSSOWithAlloc(self.local.call_arena);
|
||||
}
|
||||
|
||||
@@ -398,7 +398,7 @@ fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Globa
|
||||
return .{ .handle = global, .temps = {} };
|
||||
}
|
||||
try ctx.trackTemp(global);
|
||||
return .{ .handle = global, .temps = &ctx.session.temps };
|
||||
return .{ .handle = global, .temps = &ctx.page.temps };
|
||||
}
|
||||
|
||||
pub fn toZig(self: Value, comptime T: type) !T {
|
||||
|
||||
@@ -858,6 +858,7 @@ pub const PageJsApis = flattenTypes(&.{
|
||||
@import("../webapi/EventTarget.zig"),
|
||||
@import("../webapi/Location.zig"),
|
||||
@import("../webapi/Navigator.zig"),
|
||||
@import("../webapi/NavigatorUAData.zig"),
|
||||
@import("../webapi/net/FormData.zig"),
|
||||
@import("../webapi/net/Headers.zig"),
|
||||
@import("../webapi/net/Request.zig"),
|
||||
@@ -933,10 +934,11 @@ pub const WorkerJsApis = flattenTypes(&.{
|
||||
@import("../webapi/streams/WritableStreamDefaultController.zig"),
|
||||
@import("../webapi/encoding/TextEncoderStream.zig"),
|
||||
@import("../webapi/encoding/TextDecoderStream.zig"),
|
||||
// @import("../webapi/URL.zig"),
|
||||
@import("../webapi/AbortSignal.zig"),
|
||||
@import("../webapi/AbortController.zig"),
|
||||
@import("../webapi/URL.zig"),
|
||||
@import("../webapi/canvas/OffscreenCanvas.zig"),
|
||||
// @import("../webapi/Performance.zig"),
|
||||
// @import("../webapi/AbortSignal.zig"),
|
||||
// @import("../webapi/AbortController.zig"),
|
||||
});
|
||||
|
||||
// Master list of ALL JS APIs across all contexts.
|
||||
|
||||
@@ -31,7 +31,7 @@ pub fn collectLinks(arena: Allocator, root: *Node, frame: *Frame) ![]const []con
|
||||
var links: std.ArrayList([]const u8) = .empty;
|
||||
|
||||
if (Selector.querySelectorAll(root, "a[href]", frame)) |list| {
|
||||
defer list.deinit(frame._session);
|
||||
defer list.deinit(frame._page);
|
||||
|
||||
for (list._nodes) |node| {
|
||||
if (node.is(Element.Html.Anchor)) |anchor| {
|
||||
|
||||
@@ -474,8 +474,8 @@ pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, frame: *Frame) !voi
|
||||
|
||||
fn testMarkdownHTML(html: []const u8, expected: []const u8) !void {
|
||||
const testing = @import("../testing.zig");
|
||||
const frame = try testing.test_session.createFrame();
|
||||
defer testing.test_session.removeFrame();
|
||||
const frame = try testing.test_session.createPage();
|
||||
defer testing.test_session.removePage();
|
||||
frame.url = "http://localhost/";
|
||||
|
||||
const doc = frame.window._document;
|
||||
@@ -677,8 +677,8 @@ test "browser.markdown: skip empty links" {
|
||||
|
||||
test "browser.markdown: resolve links" {
|
||||
const testing = @import("../testing.zig");
|
||||
const frame = try testing.test_session.createFrame();
|
||||
defer testing.test_session.removeFrame();
|
||||
const frame = try testing.test_session.createPage();
|
||||
defer testing.test_session.removePage();
|
||||
frame.url = "https://example.com/a/index.html";
|
||||
|
||||
const doc = frame.window._document;
|
||||
|
||||
@@ -318,8 +318,8 @@ fn collectLink(
|
||||
const testing = @import("../testing.zig");
|
||||
|
||||
fn testStructuredData(html: []const u8) !StructuredData {
|
||||
const frame = try testing.test_session.createFrame();
|
||||
defer testing.test_session.removeFrame();
|
||||
const frame = try testing.test_session.createPage();
|
||||
defer testing.test_session.removePage();
|
||||
|
||||
const doc = frame.window._document;
|
||||
const div = try doc.createElement("div", null, frame);
|
||||
|
||||
@@ -498,6 +498,28 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CSSStyleSheet_insertRule_out_of_range_index">
|
||||
{
|
||||
// Our lack of fully processing sources (e.g. @import, @font-face) means that
|
||||
// our indexes are sometimes not what libraries expect. As a workaround we
|
||||
// clamp the index to the end of the rule list. See #2214.
|
||||
const style = document.createElement('style');
|
||||
document.head.appendChild(style);
|
||||
const sheet = style.sheet;
|
||||
|
||||
const returned = sheet.insertRule('.out-of-range { color: red; }', 99);
|
||||
testing.expectEqual(0, returned);
|
||||
testing.expectEqual(1, sheet.cssRules.length);
|
||||
testing.expectEqual('.out-of-range', sheet.cssRules[0].selectorText);
|
||||
|
||||
// And at the tail of a non-empty sheet
|
||||
const tail = sheet.insertRule('.tail { color: blue; }', 99);
|
||||
testing.expectEqual(1, tail);
|
||||
testing.expectEqual(2, sheet.cssRules.length);
|
||||
testing.expectEqual('.tail', sheet.cssRules[1].selectorText);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CSSStyleSheet_replaceSync">
|
||||
{
|
||||
const sheet = new CSSStyleSheet();
|
||||
|
||||
107
src/browser/tests/document/set_body.html
Normal file
107
src/browser/tests/document/set_body.html
Normal file
@@ -0,0 +1,107 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<iframe id=isimple src=set_body/simple.html></iframe>
|
||||
<script id=simple>
|
||||
{
|
||||
testing.onload(() => {
|
||||
testing.expectEqual(
|
||||
'<head></head><body><p>replaced</p></body>',
|
||||
isimple.contentDocument.documentElement.innerHTML,
|
||||
);
|
||||
// The original body element is gone; a fresh one is in place.
|
||||
testing.expectEqual(null, isimple.contentDocument.getElementById('orig'));
|
||||
testing.expectEqual(true, isimple.contentDocument.body instanceof isimple.contentWindow.HTMLBodyElement);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<iframe id=inone src=set_body/no_body.html></iframe>
|
||||
<script id=no_body>
|
||||
{
|
||||
testing.onload(() => {
|
||||
// Document only had a <head>; setting body should append a fresh body.
|
||||
testing.expectEqual(
|
||||
'<head><title>no_body</title></head>\n<body><p>appended</p></body>',
|
||||
inone.contentDocument.documentElement.innerHTML,
|
||||
);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<iframe id=iempty src=set_body/empty.html></iframe>
|
||||
<script id=empty>
|
||||
{
|
||||
testing.onload(() => {
|
||||
// Setting body to the empty string yields an empty <body>.
|
||||
testing.expectEqual(
|
||||
'<head></head><body></body>',
|
||||
iempty.contentDocument.documentElement.innerHTML,
|
||||
);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<iframe id=icomplex src=set_body/complex.html></iframe>
|
||||
<script id=complex>
|
||||
{
|
||||
testing.onload(() => {
|
||||
const doc = icomplex.contentDocument;
|
||||
// Multiple siblings + text node + attributes survive as body children.
|
||||
testing.expectEqual(
|
||||
'<body><h1>title</h1>text node<div id="d" class="x"><span>nested</span></div></body>',
|
||||
doc.documentElement.querySelector('body').outerHTML,
|
||||
);
|
||||
// The replacement body can still be located via document.body.
|
||||
testing.expectEqual('d', doc.body.querySelector('div').id);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<iframe id=iframeset src=set_body/frameset.html></iframe>
|
||||
<script id=html_wrapper_stripped>
|
||||
{
|
||||
testing.onload(() => {
|
||||
// Author wrapped content in <html><body>…</body></html>; the parser
|
||||
// should unwrap those so the new body contains just the <p>.
|
||||
testing.expectEqual(
|
||||
'<body><p>unwrapped</p></body>',
|
||||
iframeset.contentDocument.body.outerHTML,
|
||||
);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=no_document_element>
|
||||
// Build a detached HTMLDocument, strip its documentElement, and verify
|
||||
// setting body throws HierarchyRequestError.
|
||||
{
|
||||
const doc = document.implementation.createHTMLDocument('x');
|
||||
doc.documentElement.remove();
|
||||
testing.expectEqual(null, doc.documentElement);
|
||||
|
||||
let err = null;
|
||||
try {
|
||||
doc.body = '<p>hi</p>';
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
testing.expectEqual(true, err instanceof DOMException);
|
||||
testing.expectEqual('HierarchyRequestError', err.name);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=detached_html_document>
|
||||
// Fresh HTMLDocument with both head and body: replacing body should leave
|
||||
// head untouched and install a new body.
|
||||
{
|
||||
const doc = document.implementation.createHTMLDocument('x');
|
||||
const originalBody = doc.body;
|
||||
doc.body = '<span>new</span>';
|
||||
|
||||
testing.expectEqual(true, doc.body !== originalBody);
|
||||
testing.expectEqual('<span>new</span>', doc.body.innerHTML);
|
||||
// Head (including the auto-created <title>) is preserved.
|
||||
testing.expectEqual('<title>x</title>', doc.head.innerHTML);
|
||||
}
|
||||
</script>
|
||||
5
src/browser/tests/document/set_body/complex.html
Normal file
5
src/browser/tests/document/set_body/complex.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<html>
|
||||
<body><div>original</div></body>
|
||||
<script>
|
||||
document.body = '<h1>title</h1>text node<div id="d" class="x"><span>nested</span></div>';
|
||||
</script>
|
||||
5
src/browser/tests/document/set_body/empty.html
Normal file
5
src/browser/tests/document/set_body/empty.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<html>
|
||||
<body><div>to be wiped</div></body>
|
||||
<script>
|
||||
document.body = '';
|
||||
</script>
|
||||
7
src/browser/tests/document/set_body/frameset.html
Normal file
7
src/browser/tests/document/set_body/frameset.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body><div>original</div></body>
|
||||
<script>
|
||||
// Author wraps contents in their own <html><body>. Fragment parsing
|
||||
// should unwrap these so the resulting body holds just the <p>.
|
||||
document.body = '<html><body><p>unwrapped</p></body></html>';
|
||||
</script>
|
||||
9
src/browser/tests/document/set_body/no_body.html
Normal file
9
src/browser/tests/document/set_body/no_body.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>no_body</title></head>
|
||||
<body><!-- this body is removed before setBody runs --></body>
|
||||
<script>
|
||||
document.body.remove();
|
||||
document.body = '<p>appended</p>';
|
||||
</script>
|
||||
</html>
|
||||
5
src/browser/tests/document/set_body/simple.html
Normal file
5
src/browser/tests/document/set_body/simple.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<html>
|
||||
<body id=orig><div>original</div></body>
|
||||
<script>
|
||||
document.body = '<p>replaced</p>';
|
||||
</script>
|
||||
@@ -65,6 +65,49 @@
|
||||
testing.expectEqual(8, navigator.deviceMemory);
|
||||
</script>
|
||||
|
||||
<script id=userAgentData>
|
||||
testing.expectEqual(true, navigator.userAgentData !== undefined);
|
||||
testing.expectEqual(false, navigator.userAgentData.mobile);
|
||||
testing.expectEqual(true, navigator.userAgentData.platform.length > 0);
|
||||
|
||||
const validUAPlatforms = ['macOS', 'Windows', 'Linux', 'FreeBSD', 'Unknown'];
|
||||
testing.expectEqual(true, validUAPlatforms.includes(navigator.userAgentData.platform));
|
||||
|
||||
testing.expectEqual(1, navigator.userAgentData.brands.length);
|
||||
testing.expectEqual({brand: 'Lightpanda', version: "1"}, navigator.userAgentData.brands[0]);
|
||||
|
||||
// Same instance returned on repeated access
|
||||
testing.expectEqual(true, navigator.userAgentData === navigator.userAgentData);
|
||||
|
||||
const json = navigator.userAgentData.toJSON();
|
||||
testing.expectEqual(false, json.mobile);
|
||||
testing.expectEqual(navigator.userAgentData.platform, json.platform);
|
||||
testing.expectEqual(true, Array.isArray(json.brands));
|
||||
testing.expectEqual('Lightpanda', json.brands[0].brand);
|
||||
</script>
|
||||
|
||||
<script id=userAgentData_highEntropy type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
const p = navigator.userAgentData.getHighEntropyValues(['architecture', 'platformVersion', 'fullVersionList']);
|
||||
testing.expectTrue(p instanceof Promise);
|
||||
const v = await p;
|
||||
state.resolve();
|
||||
|
||||
await state.done(() => {
|
||||
testing.expectEqual(false, v.mobile);
|
||||
testing.expectEqual(navigator.userAgentData.platform, v.platform);
|
||||
testing.expectEqual(true, Array.isArray(v.brands));
|
||||
testing.expectEqual(true, Array.isArray(v.fullVersionList));
|
||||
testing.expectEqual('Lightpanda', v.fullVersionList[0].brand);
|
||||
testing.expectEqual('1.0.0.0', v.uaFullVersion);
|
||||
testing.expectEqual(false, v.wow64);
|
||||
testing.expectEqual(true, Array.isArray(v.formFactor));
|
||||
testing.expectEqual('Desktop', v.formFactor[0]);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=getBattery type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
|
||||
@@ -29,6 +29,27 @@
|
||||
const response_body_text = await response.text();
|
||||
const response_clone_text = await response.clone().text();
|
||||
|
||||
// AbortController + AbortSignal dispatch (exercises the inline-else dispatch path)
|
||||
const controller = new AbortController();
|
||||
const ac_aborted_before = controller.signal.aborted;
|
||||
let ac_listener_fired = false;
|
||||
controller.signal.addEventListener('abort', () => { ac_listener_fired = true; });
|
||||
controller.abort('cancelled');
|
||||
const ac_aborted_after = controller.signal.aborted;
|
||||
const ac_reason = String(controller.signal.reason);
|
||||
|
||||
// Pre-aborted static constructor + throwIfAborted
|
||||
const pre = AbortSignal.abort('already-gone');
|
||||
const pre_aborted = pre.aborted;
|
||||
let pre_threw = false;
|
||||
try { pre.throwIfAborted(); } catch (_) { pre_threw = true; }
|
||||
|
||||
// URL.createObjectURL / revokeObjectURL from a worker
|
||||
const blob = new Blob(['hello worker'], { type: 'text/plain' });
|
||||
const blob_url = URL.createObjectURL(blob);
|
||||
const blob_url_is_blob = blob_url.startsWith('blob:');
|
||||
URL.revokeObjectURL(blob_url);
|
||||
|
||||
postMessage({
|
||||
ok: true,
|
||||
results: {
|
||||
@@ -47,6 +68,13 @@
|
||||
response_headers_content_type: response.headers.get('content-type'),
|
||||
response_body_text,
|
||||
response_clone_text,
|
||||
ac_aborted_before,
|
||||
ac_aborted_after,
|
||||
ac_listener_fired,
|
||||
ac_reason,
|
||||
pre_aborted,
|
||||
pre_threw,
|
||||
blob_url_is_blob,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -209,6 +209,17 @@
|
||||
testing.expectEqual('text/plain', r.response_headers_content_type);
|
||||
testing.expectEqual('response body', r.response_body_text);
|
||||
testing.expectEqual('response body', r.response_clone_text);
|
||||
|
||||
// AbortController / AbortSignal
|
||||
testing.expectEqual(false, r.ac_aborted_before);
|
||||
testing.expectEqual(true, r.ac_aborted_after);
|
||||
testing.expectEqual(true, r.ac_listener_fired);
|
||||
testing.expectEqual('cancelled', r.ac_reason);
|
||||
testing.expectEqual(true, r.pre_aborted);
|
||||
testing.expectEqual(true, r.pre_threw);
|
||||
|
||||
// URL.createObjectURL / revokeObjectURL
|
||||
testing.expectEqual(true, r.blob_url_is_blob);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -553,9 +553,11 @@ fn execClick(session: *lp.Session, registry: *CDPNode.Registry, arena: std.mem.A
|
||||
|
||||
// If the click triggered a navigation (e.g. form submission, link click),
|
||||
// wait for it to complete.
|
||||
if (session.queued_navigation.items.len != 0) {
|
||||
var runner = session.runner(.{}) catch return ToolError.InternalError;
|
||||
runner.wait(.{ .ms = 10000, .until = .done }) catch return ToolError.NavigationFailed;
|
||||
if (session.currentPage()) |page| {
|
||||
if (page.queued_navigation.items.len != 0) {
|
||||
var runner = session.runner(.{}) catch return ToolError.InternalError;
|
||||
runner.wait(.{ .ms = 10000, .until = .done }) catch return ToolError.NavigationFailed;
|
||||
}
|
||||
}
|
||||
|
||||
const page = session.currentFrame() orelse return ToolError.FrameNotLoaded;
|
||||
@@ -711,9 +713,11 @@ fn execPress(session: *lp.Session, registry: *CDPNode.Registry, arena: std.mem.A
|
||||
};
|
||||
|
||||
// Pressing Enter on a form input triggers implicit form submission.
|
||||
if (session.queued_navigation.items.len != 0) {
|
||||
var runner = session.runner(.{}) catch return ToolError.InternalError;
|
||||
runner.wait(.{ .ms = 10000, .until = .done }) catch return ToolError.NavigationFailed;
|
||||
if (session.currentPage()) |p| {
|
||||
if (p.queued_navigation.items.len != 0) {
|
||||
var runner = session.runner(.{}) catch return ToolError.InternalError;
|
||||
runner.wait(.{ .ms = 10000, .until = .done }) catch return ToolError.NavigationFailed;
|
||||
}
|
||||
}
|
||||
|
||||
const current_page = session.currentFrame() orelse return ToolError.FrameNotLoaded;
|
||||
@@ -882,11 +886,11 @@ fn ensurePage(session: *lp.Session, registry: *CDPNode.Registry, url: ?[:0]const
|
||||
}
|
||||
|
||||
fn performGoto(session: *lp.Session, registry: *CDPNode.Registry, url: [:0]const u8, timeout: ?u32, waitUntil: ?lp.Config.WaitUntil) ToolError!void {
|
||||
if (session.frame != null) {
|
||||
if (session.page != null) {
|
||||
registry.reset();
|
||||
session.removeFrame();
|
||||
session.removePage();
|
||||
}
|
||||
const page = session.createFrame() catch return ToolError.NavigationFailed;
|
||||
const page = session.createPage() catch return ToolError.NavigationFailed;
|
||||
_ = page.navigate(url, .{
|
||||
.reason = .address_bar,
|
||||
.kind = .{ .push = null },
|
||||
|
||||
@@ -19,16 +19,17 @@
|
||||
const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const Frame = @import("../Frame.zig");
|
||||
const AbortSignal = @import("AbortSignal.zig");
|
||||
|
||||
const Execution = js.Execution;
|
||||
|
||||
const AbortController = @This();
|
||||
|
||||
_signal: *AbortSignal,
|
||||
|
||||
pub fn init(frame: *Frame) !*AbortController {
|
||||
const signal = try AbortSignal.init(frame);
|
||||
return frame._factory.create(AbortController{
|
||||
pub fn init(exec: *const Execution) !*AbortController {
|
||||
const signal = try AbortSignal.init(exec);
|
||||
return exec._factory.create(AbortController{
|
||||
._signal = signal,
|
||||
});
|
||||
}
|
||||
@@ -37,8 +38,8 @@ pub fn getSignal(self: *const AbortController) *AbortSignal {
|
||||
return self._signal;
|
||||
}
|
||||
|
||||
pub fn abort(self: *AbortController, reason_: ?js.Value.Global, frame: *Frame) !void {
|
||||
try self._signal.abort(if (reason_) |r| .{ .js_val = r } else null, frame);
|
||||
pub fn abort(self: *AbortController, reason_: ?js.Value.Global, exec: *const Execution) !void {
|
||||
try self._signal.abort(if (reason_) |r| .{ .js_val = r } else null, exec);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -20,12 +20,12 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
|
||||
const Event = @import("Event.zig");
|
||||
const EventTarget = @import("EventTarget.zig");
|
||||
|
||||
const log = lp.log;
|
||||
const Execution = js.Execution;
|
||||
|
||||
const AbortSignal = @This();
|
||||
|
||||
@@ -34,8 +34,8 @@ _aborted: bool = false,
|
||||
_reason: Reason = .undefined,
|
||||
_on_abort: ?js.Function.Global = null,
|
||||
|
||||
pub fn init(frame: *Frame) !*AbortSignal {
|
||||
return frame._factory.eventTarget(AbortSignal{
|
||||
pub fn init(exec: *const Execution) !*AbortSignal {
|
||||
return exec._factory.eventTarget(AbortSignal{
|
||||
._proto = undefined,
|
||||
});
|
||||
}
|
||||
@@ -60,7 +60,7 @@ pub fn asEventTarget(self: *AbortSignal) *EventTarget {
|
||||
return self._proto;
|
||||
}
|
||||
|
||||
pub fn abort(self: *AbortSignal, reason_: ?Reason, frame: *Frame) !void {
|
||||
pub fn abort(self: *AbortSignal, reason_: ?Reason, exec: *const Execution) !void {
|
||||
if (self._aborted) {
|
||||
return;
|
||||
}
|
||||
@@ -71,36 +71,40 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, frame: *Frame) !void {
|
||||
if (reason_) |reason| {
|
||||
switch (reason) {
|
||||
.js_val => |js_val| self._reason = .{ .js_val = js_val },
|
||||
.string => |str| self._reason = .{ .string = try frame.dupeString(str) },
|
||||
.string => |str| self._reason = .{ .string = try exec.dupeString(str) },
|
||||
.undefined => self._reason = reason,
|
||||
}
|
||||
} else {
|
||||
self._reason = .{ .string = "AbortError" };
|
||||
}
|
||||
|
||||
// Dispatch abort event
|
||||
const target = self.asEventTarget();
|
||||
if (frame._event_manager.hasDirectListeners(target, "abort", self._on_abort)) {
|
||||
const event = try Event.initTrusted(comptime .wrap("abort"), .{}, frame);
|
||||
try frame._event_manager.dispatchDirect(target, event, self._on_abort, .{ .context = "abort signal" });
|
||||
const on_abort = self._on_abort;
|
||||
switch (exec.context.global) {
|
||||
inline else => |g| {
|
||||
if (g._event_manager.hasDirectListeners(target, "abort", on_abort)) {
|
||||
const event = try Event.initTrusted(comptime .wrap("abort"), .{}, g._page);
|
||||
try g.dispatch(target, event, on_abort, .{ .context = "abort signal" });
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Static method to create an already-aborted signal
|
||||
pub fn createAborted(reason_: ?js.Value.Global, frame: *Frame) !*AbortSignal {
|
||||
const signal = try init(frame);
|
||||
try signal.abort(if (reason_) |r| .{ .js_val = r } else null, frame);
|
||||
pub fn createAborted(reason_: ?js.Value.Global, exec: *const Execution) !*AbortSignal {
|
||||
const signal = try init(exec);
|
||||
try signal.abort(if (reason_) |r| .{ .js_val = r } else null, exec);
|
||||
return signal;
|
||||
}
|
||||
|
||||
pub fn createTimeout(delay: u32, frame: *Frame) !*AbortSignal {
|
||||
const callback = try frame.arena.create(TimeoutCallback);
|
||||
pub fn createTimeout(delay: u32, exec: *const Execution) !*AbortSignal {
|
||||
const callback = try exec.arena.create(TimeoutCallback);
|
||||
callback.* = .{
|
||||
.frame = frame,
|
||||
.signal = try init(frame),
|
||||
.exec = exec,
|
||||
.signal = try init(exec),
|
||||
};
|
||||
|
||||
try frame.js.scheduler.add(callback, TimeoutCallback.run, delay, .{
|
||||
try exec._scheduler.add(callback, TimeoutCallback.run, delay, .{
|
||||
.name = "AbortSignal.timeout",
|
||||
});
|
||||
|
||||
@@ -111,8 +115,8 @@ const ThrowIfAborted = union(enum) {
|
||||
exception: js.Exception,
|
||||
undefined: void,
|
||||
};
|
||||
pub fn throwIfAborted(self: *const AbortSignal, frame: *Frame) !ThrowIfAborted {
|
||||
const local = frame.js.local.?;
|
||||
pub fn throwIfAborted(self: *const AbortSignal, exec: *const Execution) !ThrowIfAborted {
|
||||
const local = exec.context.local.?;
|
||||
|
||||
if (self._aborted) {
|
||||
const exception = switch (self._reason) {
|
||||
@@ -132,12 +136,12 @@ const Reason = union(enum) {
|
||||
};
|
||||
|
||||
const TimeoutCallback = struct {
|
||||
frame: *Frame,
|
||||
exec: *const Execution,
|
||||
signal: *AbortSignal,
|
||||
|
||||
fn run(ctx: *anyopaque) !?u32 {
|
||||
const self: *TimeoutCallback = @ptrCast(@alignCast(ctx));
|
||||
self.signal.abort(.{ .string = "TimeoutError" }, self.frame) catch |err| {
|
||||
self.signal.abort(.{ .string = "TimeoutError" }, self.exec) catch |err| {
|
||||
log.warn(.app, "abort signal timeout", .{ .err = err });
|
||||
};
|
||||
return null;
|
||||
|
||||
@@ -20,8 +20,7 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const Session = @import("../Session.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
|
||||
const Node = @import("Node.zig");
|
||||
const Range = @import("Range.zig");
|
||||
@@ -49,15 +48,15 @@ pub fn acquireRef(self: *AbstractRange) void {
|
||||
self._rc.acquire();
|
||||
}
|
||||
|
||||
pub fn deinit(self: *AbstractRange, session: *Session) void {
|
||||
if (session.findFrameByLoaderId(self._frame_loader_id)) |frame| {
|
||||
pub fn deinit(self: *AbstractRange, page: *Page) void {
|
||||
if (page.findFrameByLoaderId(self._frame_loader_id)) |frame| {
|
||||
frame._live_ranges.remove(&self._range_link);
|
||||
}
|
||||
session.releaseArena(self._arena);
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *AbstractRange, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *AbstractRange, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub const Type = union(enum) {
|
||||
|
||||
@@ -20,7 +20,7 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Session = @import("../Session.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
|
||||
const Mime = @import("../Mime.zig");
|
||||
|
||||
@@ -61,7 +61,8 @@ const InitOptions = struct {
|
||||
|
||||
/// Creates a new Blob from JS values with optional MIME validation.
|
||||
/// This is the JS Constructor
|
||||
pub fn init(parts_: ?[]const js.Value, opts_: ?InitOptions, session: *Session) !*Blob {
|
||||
pub fn init(parts_: ?[]const js.Value, opts_: ?InitOptions, page: *Page) !*Blob {
|
||||
const session = page.session;
|
||||
const arena = try session.getArena(.large, "Blob");
|
||||
errdefer session.releaseArena(arena);
|
||||
|
||||
@@ -94,9 +95,9 @@ pub fn init(parts_: ?[]const js.Value, opts_: ?InitOptions, session: *Session) !
|
||||
}
|
||||
|
||||
/// Creates a new Blob from raw byte slices (for internal Zig use).
|
||||
pub fn initFromBytes(data: []const u8, content_type: []const u8, validate_mime: bool, session: *Session) !*Blob {
|
||||
const arena = try session.getArena(.large, "Blob");
|
||||
errdefer session.releaseArena(arena);
|
||||
pub fn initFromBytes(data: []const u8, content_type: []const u8, validate_mime: bool, page: *Page) !*Blob {
|
||||
const arena = try page.getArena(.large, "Blob");
|
||||
errdefer page.releaseArena(arena);
|
||||
|
||||
const mime = try validateMimeType(arena, content_type, validate_mime);
|
||||
|
||||
@@ -137,12 +138,12 @@ fn validateMimeType(arena: Allocator, mime_type: []const u8, full_validation: bo
|
||||
return buf;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Blob, session: *Session) void {
|
||||
session.releaseArena(self._arena);
|
||||
pub fn deinit(self: *Blob, page: *Page) void {
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *Blob, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *Blob, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *Blob) void {
|
||||
@@ -291,7 +292,7 @@ pub fn slice(
|
||||
start_: ?i32,
|
||||
end_: ?i32,
|
||||
content_type_: ?[]const u8,
|
||||
session: *Session,
|
||||
page: *Page,
|
||||
) !*Blob {
|
||||
const data = self._slice;
|
||||
|
||||
@@ -312,7 +313,7 @@ pub fn slice(
|
||||
break :blk @min(data.len, @max(start, @as(u31, @intCast(requested_end))));
|
||||
};
|
||||
|
||||
return Blob.initFromBytes(data[start..end], content_type_ orelse "", false, session);
|
||||
return Blob.initFromBytes(data[start..end], content_type_ orelse "", false, page);
|
||||
}
|
||||
|
||||
/// Returns the size of the Blob in bytes.
|
||||
|
||||
@@ -353,12 +353,12 @@ pub fn createEvent(_: *const Document, event_type: []const u8, frame: *Frame) !*
|
||||
const normalized = std.ascii.lowerString(&frame.buf, event_type);
|
||||
|
||||
if (std.mem.eql(u8, normalized, "event") or std.mem.eql(u8, normalized, "events") or std.mem.eql(u8, normalized, "htmlevents")) {
|
||||
return Event.init("", null, frame);
|
||||
return Event.init("", null, frame._page);
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, normalized, "customevent") or std.mem.eql(u8, normalized, "customevents")) {
|
||||
const CustomEvent = @import("event/CustomEvent.zig");
|
||||
return (try CustomEvent.init("", null, frame)).asEvent();
|
||||
return (try CustomEvent.init("", null, frame._page)).asEvent();
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, normalized, "keyboardevent")) {
|
||||
@@ -378,7 +378,7 @@ pub fn createEvent(_: *const Document, event_type: []const u8, frame: *Frame) !*
|
||||
|
||||
if (std.mem.eql(u8, normalized, "messageevent")) {
|
||||
const MessageEvent = @import("event/MessageEvent.zig");
|
||||
return (try MessageEvent.init("", null, frame._session)).asEvent();
|
||||
return (try MessageEvent.init("", null, frame._page)).asEvent();
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, normalized, "uievent") or std.mem.eql(u8, normalized, "uievents")) {
|
||||
|
||||
@@ -20,8 +20,8 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Session = @import("../Session.zig");
|
||||
|
||||
const Node = @import("Node.zig");
|
||||
const EventTarget = @import("EventTarget.zig");
|
||||
@@ -89,16 +89,16 @@ pub const Options = struct {
|
||||
composed: bool = false,
|
||||
};
|
||||
|
||||
pub fn init(typ: []const u8, opts_: ?Options, frame: *Frame) !*Event {
|
||||
const arena = try frame.getArena(.tiny, "Event");
|
||||
errdefer frame.releaseArena(arena);
|
||||
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event {
|
||||
const arena = try page.getArena(.tiny, "Event");
|
||||
errdefer page.releaseArena(arena);
|
||||
const str = try String.init(arena, typ, .{});
|
||||
return initWithTrusted(arena, str, opts_, false);
|
||||
}
|
||||
|
||||
pub fn initTrusted(typ: String, opts_: ?Options, frame: *Frame) !*Event {
|
||||
const arena = try frame.getArena(.tiny, "Event.trusted");
|
||||
errdefer frame.releaseArena(arena);
|
||||
pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*Event {
|
||||
const arena = try page.getArena(.tiny, "Event.trusted");
|
||||
errdefer page.releaseArena(arena);
|
||||
return initWithTrusted(arena, typ, opts_, true);
|
||||
}
|
||||
|
||||
@@ -145,13 +145,12 @@ pub fn acquireRef(self: *Event) void {
|
||||
self._rc.acquire();
|
||||
}
|
||||
|
||||
/// Force cleanup on Session shutdown.
|
||||
pub fn deinit(self: *Event, session: *Session) void {
|
||||
session.releaseArena(self._arena);
|
||||
pub fn deinit(self: *Event, page: *Page) void {
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *Event, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *Event, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn as(self: *Event, comptime T: type) *T {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const Session = @import("../Session.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const EventManager = @import("../EventManager.zig");
|
||||
|
||||
const Event = @import("Event.zig");
|
||||
@@ -52,8 +52,8 @@ pub const Type = union(enum) {
|
||||
websocket: *@import("net/WebSocket.zig"),
|
||||
};
|
||||
|
||||
pub fn init(session: *Session) !*EventTarget {
|
||||
return session.factory.create(EventTarget{
|
||||
pub fn init(page: *Page) !*EventTarget {
|
||||
return page.factory.create(EventTarget{
|
||||
._type = .generic,
|
||||
});
|
||||
}
|
||||
@@ -67,10 +67,10 @@ pub fn dispatchEvent(self: *EventTarget, event: *Event, exec: *js.Execution) !bo
|
||||
switch (exec.context.global) {
|
||||
.frame => |frame| {
|
||||
event.acquireRef();
|
||||
defer _ = event.releaseRef(frame._session);
|
||||
defer _ = event.releaseRef(frame._page);
|
||||
try frame._event_manager.dispatch(self, event);
|
||||
},
|
||||
.worker => |wgs| try wgs.dispatch(self, event, null),
|
||||
.worker => |wgs| try wgs.dispatch(self, event, null, .{}),
|
||||
}
|
||||
return !event._cancelable or !event._prevent_default;
|
||||
}
|
||||
@@ -101,8 +101,7 @@ pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventLi
|
||||
};
|
||||
|
||||
switch (exec.context.global) {
|
||||
.frame => |frame| _ = try frame._event_manager.register(self, typ, em_callback, options),
|
||||
.worker => |wgs| _ = try wgs._event_manager.register(self, typ, em_callback, options),
|
||||
inline else => |g| _ = try g._event_manager.register(self, typ, em_callback, options),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,8 +137,7 @@ pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: ?Even
|
||||
};
|
||||
|
||||
switch (exec.context.global) {
|
||||
.frame => |frame| frame._event_manager.remove(self, typ, em_callback, use_capture),
|
||||
.worker => |wgs| wgs._event_manager.remove(self, typ, em_callback, use_capture),
|
||||
inline else => |g| g._event_manager.remove(self, typ, em_callback, use_capture),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Session = @import("../Session.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
|
||||
const Blob = @import("Blob.zig");
|
||||
|
||||
@@ -29,10 +29,11 @@ const File = @This();
|
||||
_proto: *Blob,
|
||||
|
||||
// TODO: Implement File API.
|
||||
pub fn init(session: *Session) !*File {
|
||||
pub fn init(page: *Page) !*File {
|
||||
const session = page.session;
|
||||
const arena = try session.getArena(.tiny, "File");
|
||||
errdefer session.releaseArena(arena);
|
||||
return session.factory.blob(arena, File{ ._proto = undefined });
|
||||
return page.factory.blob(arena, File{ ._proto = undefined });
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -21,8 +21,8 @@ const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Session = @import("../Session.zig");
|
||||
const EventTarget = @import("EventTarget.zig");
|
||||
const ProgressEvent = @import("event/ProgressEvent.zig");
|
||||
const Blob = @import("Blob.zig");
|
||||
@@ -73,7 +73,7 @@ pub fn init(frame: *Frame) !*FileReader {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn deinit(self: *FileReader, session: *Session) void {
|
||||
pub fn deinit(self: *FileReader, page: *Page) void {
|
||||
if (self._on_abort) |func| func.release();
|
||||
if (self._on_error) |func| func.release();
|
||||
if (self._on_load) |func| func.release();
|
||||
@@ -81,11 +81,11 @@ pub fn deinit(self: *FileReader, session: *Session) void {
|
||||
if (self._on_load_start) |func| func.release();
|
||||
if (self._on_progress) |func| func.release();
|
||||
|
||||
session.releaseArena(self._arena);
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *FileReader, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *FileReader, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *FileReader) void {
|
||||
|
||||
@@ -57,8 +57,30 @@ pub fn getHead(self: *HTMLDocument) ?*Element.Html.Head {
|
||||
}
|
||||
|
||||
pub fn getBody(self: *HTMLDocument) ?*Element.Html.Body {
|
||||
const doc_el = self._proto.getDocumentElement() orelse return null;
|
||||
var child = doc_el.asNode().firstChild();
|
||||
const document_element = self._proto.getDocumentElement() orelse return null;
|
||||
return findBodyForDoc(document_element);
|
||||
}
|
||||
|
||||
pub fn setBody(self: *HTMLDocument, html: []const u8, frame: *Frame) !void {
|
||||
const document_element = self._proto.getDocumentElement() orelse return error.HierarchyError;
|
||||
|
||||
// Build a fresh <body> holding the parsed HTML as its children. Fragment
|
||||
// parsing strips any <html>/<body>/<head> wrappers the author included.
|
||||
const new_body_node = try frame.createElementNS(.html, "body", null);
|
||||
if (html.len > 0) {
|
||||
try frame.parseHtmlAsChildren(new_body_node, html);
|
||||
}
|
||||
|
||||
const document_node = document_element.asNode();
|
||||
if (findBodyForDoc(document_element)) |current| {
|
||||
_ = try document_node.replaceChild(new_body_node, current.asNode(), frame);
|
||||
} else {
|
||||
_ = try document_node.appendChild(new_body_node, frame);
|
||||
}
|
||||
}
|
||||
|
||||
fn findBodyForDoc(document_element: *Element) ?*Element.Html.Body {
|
||||
var child = document_element.asNode().firstChild();
|
||||
while (child) |node| {
|
||||
if (node.is(Element.Html.Body)) |body| {
|
||||
return body;
|
||||
@@ -276,7 +298,7 @@ pub const JsApi = struct {
|
||||
|
||||
pub const dir = bridge.accessor(HTMLDocument.getDir, HTMLDocument.setDir, .{});
|
||||
pub const head = bridge.accessor(HTMLDocument.getHead, null, .{});
|
||||
pub const body = bridge.accessor(HTMLDocument.getBody, null, .{});
|
||||
pub const body = bridge.accessor(HTMLDocument.getBody, HTMLDocument.setBody, .{ .dom_exception = true });
|
||||
pub const lang = bridge.accessor(HTMLDocument.getLang, HTMLDocument.setLang, .{});
|
||||
pub const title = bridge.accessor(HTMLDocument.getTitle, HTMLDocument.setTitle, .{});
|
||||
pub const images = bridge.accessor(HTMLDocument.getImages, null, .{});
|
||||
|
||||
@@ -20,8 +20,8 @@ const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Session = @import("../Session.zig");
|
||||
|
||||
const Node = @import("Node.zig");
|
||||
const Element = @import("Element.zig");
|
||||
@@ -110,18 +110,18 @@ pub fn init(callback: js.Function.Temp, options: ?ObserverInit, frame: *Frame) !
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *IntersectionObserver, session: *Session) void {
|
||||
pub fn deinit(self: *IntersectionObserver, page: *Page) void {
|
||||
self._callback.release();
|
||||
for (self._pending_entries.items) |entry| {
|
||||
// These were never handed to v8, they do not have a corresponding
|
||||
// FinalizerCallback. We 100% own them.
|
||||
entry.deinit(session);
|
||||
entry.deinit(page);
|
||||
}
|
||||
session.releaseArena(self._arena);
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *IntersectionObserver, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *IntersectionObserver, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *IntersectionObserver) void {
|
||||
@@ -164,7 +164,7 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, frame: *Frame) v
|
||||
while (j < self._pending_entries.items.len) {
|
||||
if (self._pending_entries.items[j]._target == target) {
|
||||
const entry = self._pending_entries.swapRemove(j);
|
||||
entry.deinit(frame._session);
|
||||
entry.deinit(frame._page);
|
||||
} else {
|
||||
j += 1;
|
||||
}
|
||||
@@ -180,7 +180,7 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, frame: *Frame) v
|
||||
|
||||
pub fn disconnect(self: *IntersectionObserver, frame: *Frame) void {
|
||||
for (self._pending_entries.items) |entry| {
|
||||
entry.deinit(frame._session);
|
||||
entry.deinit(frame._page);
|
||||
}
|
||||
self._pending_entries.clearRetainingCapacity();
|
||||
self._previous_states.clearRetainingCapacity();
|
||||
@@ -330,12 +330,12 @@ pub const IntersectionObserverEntry = struct {
|
||||
_intersection_ratio: f64,
|
||||
_is_intersecting: bool,
|
||||
|
||||
pub fn deinit(self: *IntersectionObserverEntry, session: *Session) void {
|
||||
session.releaseArena(self._arena);
|
||||
pub fn deinit(self: *IntersectionObserverEntry, page: *Page) void {
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *IntersectionObserverEntry, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *IntersectionObserverEntry, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *IntersectionObserverEntry) void {
|
||||
|
||||
@@ -131,7 +131,7 @@ const PostMessageCallback = struct {
|
||||
.data = .{ .value = self.message },
|
||||
.origin = "",
|
||||
.source = null,
|
||||
}, frame._session) catch |err| {
|
||||
}, frame._page) catch |err| {
|
||||
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
|
||||
return null;
|
||||
}).asEvent();
|
||||
|
||||
@@ -20,8 +20,8 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Session = @import("../Session.zig");
|
||||
|
||||
const Node = @import("Node.zig");
|
||||
const Element = @import("Element.zig");
|
||||
@@ -86,18 +86,18 @@ pub fn init(callback: js.Function.Temp, frame: *Frame) !*MutationObserver {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *MutationObserver, session: *Session) void {
|
||||
pub fn deinit(self: *MutationObserver, page: *Page) void {
|
||||
for (self._pending_records.items) |record| {
|
||||
// These were never handed to v8, they do not have a corresponding
|
||||
// FinalizerCallback. We 100% own them.
|
||||
record.deinit(session);
|
||||
record.deinit(page);
|
||||
}
|
||||
self._callback.release();
|
||||
session.releaseArena(self._arena);
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *MutationObserver, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *MutationObserver, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *MutationObserver) void {
|
||||
@@ -178,7 +178,7 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
|
||||
|
||||
pub fn disconnect(self: *MutationObserver, frame: *Frame) void {
|
||||
for (self._pending_records.items) |record| {
|
||||
record.deinit(frame._session);
|
||||
record.deinit(frame._page);
|
||||
}
|
||||
self._pending_records.clearRetainingCapacity();
|
||||
|
||||
@@ -375,11 +375,11 @@ pub const MutationRecord = struct {
|
||||
characterData,
|
||||
};
|
||||
|
||||
pub fn deinit(self: *MutationRecord, session: *Session) void {
|
||||
pub fn deinit(self: *MutationRecord, session: *Page) void {
|
||||
session.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *MutationRecord, session: *Session) void {
|
||||
pub fn releaseRef(self: *MutationRecord, session: *Page) void {
|
||||
self._rc.release(self, session);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ const Frame = @import("../Frame.zig");
|
||||
const PluginArray = @import("PluginArray.zig");
|
||||
const Permissions = @import("Permissions.zig");
|
||||
const StorageManager = @import("StorageManager.zig");
|
||||
const NavigatorUAData = @import("NavigatorUAData.zig");
|
||||
|
||||
const log = lp.log;
|
||||
|
||||
@@ -34,6 +35,7 @@ _pad: bool = false,
|
||||
_plugins: PluginArray = .{},
|
||||
_permissions: Permissions = .{},
|
||||
_storage: StorageManager = .{},
|
||||
_ua_data: NavigatorUAData = .{},
|
||||
|
||||
pub const init: Navigator = .{};
|
||||
|
||||
@@ -72,6 +74,10 @@ pub fn getStorage(self: *Navigator) *StorageManager {
|
||||
return &self._storage;
|
||||
}
|
||||
|
||||
pub fn getUserAgentData(self: *Navigator) *NavigatorUAData {
|
||||
return &self._ua_data;
|
||||
}
|
||||
|
||||
pub fn getBattery(_: *const Navigator, frame: *Frame) !js.Promise {
|
||||
log.info(.not_implemented, "navigator.getBattery", .{});
|
||||
return frame.js.local.?.rejectErrorPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||
@@ -182,6 +188,7 @@ pub const JsApi = struct {
|
||||
pub const getBattery = bridge.function(Navigator.getBattery, .{});
|
||||
pub const permissions = bridge.accessor(Navigator.getPermissions, null, .{});
|
||||
pub const storage = bridge.accessor(Navigator.getStorage, null, .{});
|
||||
pub const userAgentData = bridge.accessor(Navigator.getUserAgentData, null, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
134
src/browser/webapi/NavigatorUAData.zig
Normal file
134
src/browser/webapi/NavigatorUAData.zig
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const Config = @import("../../Config.zig");
|
||||
const js = @import("../js/js.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
|
||||
const NavigatorUAData = @This();
|
||||
|
||||
_pad: bool = false,
|
||||
|
||||
const Brand = struct {
|
||||
brand: []const u8,
|
||||
version: []const u8,
|
||||
};
|
||||
|
||||
pub fn getBrands(_: *const NavigatorUAData) []const Brand {
|
||||
return brandList();
|
||||
}
|
||||
|
||||
pub fn getMobile(_: *const NavigatorUAData) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn getPlatform(_: *const NavigatorUAData) []const u8 {
|
||||
return uaPlatform();
|
||||
}
|
||||
|
||||
pub fn toJSON(_: *const NavigatorUAData) struct {
|
||||
brands: []const Brand,
|
||||
mobile: bool,
|
||||
platform: []const u8,
|
||||
} {
|
||||
return .{
|
||||
.mobile = false,
|
||||
.brands = brandList(),
|
||||
.platform = uaPlatform(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getHighEntropyValues(_: *const NavigatorUAData, hints: []const []const u8, frame: *Frame) !js.Promise {
|
||||
// This should always return `brands` + `mobile` + `platform` and then whatever
|
||||
// "hints" field is requested (assuming the browser has permission), but it's
|
||||
// also valid to just return everything.
|
||||
|
||||
_ = hints;
|
||||
|
||||
return frame.js.local.?.resolvePromise(.{
|
||||
.brands = brandList(),
|
||||
.mobile = false,
|
||||
.platform = uaPlatform(),
|
||||
.architecture = uaArchitecture(),
|
||||
.bitness = uaBitness(),
|
||||
.model = "",
|
||||
.platformVersion = "",
|
||||
.uaFullVersion = "1.0.0.0",
|
||||
.fullVersionList = brandList(),
|
||||
.wow64 = false,
|
||||
.formFactor = [_][]const u8{"Desktop"},
|
||||
});
|
||||
}
|
||||
|
||||
fn brandList() []const Brand {
|
||||
const out = comptime blk: {
|
||||
const src = &Config.HttpHeaders.brands;
|
||||
var arr: [src.len]Brand = undefined;
|
||||
for (src, 0..) |b, i| {
|
||||
arr[i] = .{ .brand = b.brand, .version = b.version };
|
||||
}
|
||||
const final = arr;
|
||||
break :blk final;
|
||||
};
|
||||
return &out;
|
||||
}
|
||||
|
||||
fn uaPlatform() []const u8 {
|
||||
return switch (builtin.os.tag) {
|
||||
.macos => "macOS",
|
||||
.windows => "Windows",
|
||||
.linux => "Linux",
|
||||
.freebsd => "FreeBSD",
|
||||
else => "Unknown",
|
||||
};
|
||||
}
|
||||
|
||||
fn uaArchitecture() []const u8 {
|
||||
return switch (builtin.cpu.arch) {
|
||||
.x86, .x86_64 => "x86",
|
||||
.aarch64, .aarch64_be, .arm, .armeb => "arm",
|
||||
else => "",
|
||||
};
|
||||
}
|
||||
|
||||
fn uaBitness() []const u8 {
|
||||
return switch (builtin.cpu.arch) {
|
||||
.x86_64, .aarch64, .aarch64_be, .powerpc64, .powerpc64le, .riscv64 => "64",
|
||||
else => "32",
|
||||
};
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(NavigatorUAData);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "NavigatorUAData";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const empty_with_no_proto = true;
|
||||
};
|
||||
|
||||
pub const brands = bridge.accessor(NavigatorUAData.getBrands, null, .{});
|
||||
pub const mobile = bridge.accessor(NavigatorUAData.getMobile, null, .{});
|
||||
pub const platform = bridge.accessor(NavigatorUAData.getPlatform, null, .{});
|
||||
pub const toJSON = bridge.function(NavigatorUAData.toJSON, .{});
|
||||
pub const getHighEntropyValues = bridge.function(NavigatorUAData.getHighEntropyValues, .{});
|
||||
};
|
||||
@@ -18,9 +18,10 @@
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Session = @import("../Session.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
@@ -56,12 +57,12 @@ const PermissionStatus = struct {
|
||||
_name: []const u8,
|
||||
_state: []const u8,
|
||||
|
||||
pub fn deinit(self: *PermissionStatus, session: *Session) void {
|
||||
session.releaseArena(self._arena);
|
||||
pub fn deinit(self: *PermissionStatus, page: *Page) void {
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *PermissionStatus, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *PermissionStatus, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *PermissionStatus) void {
|
||||
|
||||
@@ -20,8 +20,8 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Session = @import("../Session.zig");
|
||||
|
||||
const Range = @import("Range.zig");
|
||||
const AbstractRange = @import("AbstractRange.zig");
|
||||
@@ -39,15 +39,15 @@ _direction: SelectionDirection = .none,
|
||||
|
||||
pub const init: Selection = .{};
|
||||
|
||||
pub fn deinit(self: *Selection, session: *Session) void {
|
||||
pub fn deinit(self: *Selection, page: *Page) void {
|
||||
if (self._range) |r| {
|
||||
r.asAbstractRange().releaseRef(session);
|
||||
r.asAbstractRange().releaseRef(page);
|
||||
self._range = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *Selection, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *Selection, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *Selection) void {
|
||||
@@ -55,7 +55,7 @@ pub fn acquireRef(self: *Selection) void {
|
||||
}
|
||||
|
||||
fn dispatchSelectionChangeEvent(frame: *Frame) !void {
|
||||
const event = try Event.init("selectionchange", .{}, frame);
|
||||
const event = try Event.init("selectionchange", .{}, frame._page);
|
||||
try frame._event_manager.dispatch(frame.document.asEventTarget(), event);
|
||||
}
|
||||
|
||||
@@ -703,7 +703,7 @@ pub fn toString(self: *const Selection, frame: *Frame) ![]const u8 {
|
||||
|
||||
fn setRange(self: *Selection, new_range: ?*Range, frame: *Frame) void {
|
||||
if (self._range) |existing| {
|
||||
_ = existing.asAbstractRange().releaseRef(frame._session);
|
||||
_ = existing.asAbstractRange().releaseRef(frame._page);
|
||||
}
|
||||
if (new_range) |nr| {
|
||||
nr.asAbstractRange().acquireRef();
|
||||
|
||||
@@ -160,8 +160,8 @@ pub fn TreeWalker(comptime mode: Mode) type {
|
||||
|
||||
test "TreeWalker: skipChildren" {
|
||||
const testing = @import("../../testing.zig");
|
||||
const frame = try testing.test_session.createFrame();
|
||||
defer testing.test_session.removeFrame();
|
||||
const frame = try testing.test_session.createPage();
|
||||
defer testing.test_session.removePage();
|
||||
const doc = frame.window._document;
|
||||
|
||||
// <div>
|
||||
|
||||
@@ -20,7 +20,6 @@ const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const U = @import("../URL.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const URLSearchParams = @import("net/URLSearchParams.zig");
|
||||
const Blob = @import("Blob.zig");
|
||||
const Execution = js.Execution;
|
||||
@@ -248,28 +247,36 @@ pub fn canParse(url: []const u8, base_: ?[]const u8) bool {
|
||||
return U.isCompleteHTTPUrl(url);
|
||||
}
|
||||
|
||||
pub fn createObjectURL(blob: *Blob, frame: *Frame) ![]const u8 {
|
||||
pub fn createObjectURL(blob: *Blob, exec: *const Execution) ![]const u8 {
|
||||
var uuid_buf: [36]u8 = undefined;
|
||||
@import("../../id.zig").uuidv4(&uuid_buf);
|
||||
|
||||
const blob_url = try std.fmt.allocPrint(
|
||||
frame.arena,
|
||||
"blob:{s}/{s}",
|
||||
.{ frame.origin orelse "null", uuid_buf },
|
||||
);
|
||||
try frame._blob_urls.put(frame.arena, blob_url, blob);
|
||||
blob.acquireRef();
|
||||
return blob_url;
|
||||
switch (exec.context.global) {
|
||||
inline else => |g| {
|
||||
const blob_url = try std.fmt.allocPrint(
|
||||
g.arena,
|
||||
"blob:{s}/{s}",
|
||||
.{ g.origin orelse "null", uuid_buf },
|
||||
);
|
||||
try g._blob_urls.put(g.arena, blob_url, blob);
|
||||
blob.acquireRef();
|
||||
return blob_url;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn revokeObjectURL(url: []const u8, frame: *Frame) void {
|
||||
pub fn revokeObjectURL(url: []const u8, exec: *const Execution) void {
|
||||
// Per spec: silently ignore non-blob URLs
|
||||
if (!std.mem.startsWith(u8, url, "blob:")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame._blob_urls.fetchRemove(url)) |entry| {
|
||||
entry.value.releaseRef(frame._session);
|
||||
switch (exec.context.global) {
|
||||
inline else => |g| {
|
||||
if (g._blob_urls.fetchRemove(url)) |entry| {
|
||||
entry.value.releaseRef(g._page);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -362,7 +362,7 @@ pub fn reportError(self: *Window, err: js.Value, frame: *Frame) !void {
|
||||
.message = err.toStringSlice() catch "Unknown error",
|
||||
.bubbles = false,
|
||||
.cancelable = true,
|
||||
}, frame._session);
|
||||
}, frame._page);
|
||||
|
||||
// Invoke window.onerror callback if set (per WHATWG spec, this is called
|
||||
// with 5 arguments: message, source, lineno, colno, error)
|
||||
@@ -530,17 +530,16 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, frame: *Frame) !void
|
||||
frame,
|
||||
struct {
|
||||
fn dispatch(_frame: *anyopaque) anyerror!?u32 {
|
||||
const p: *Frame = @ptrCast(@alignCast(_frame));
|
||||
const pos = &p.window._scroll_pos;
|
||||
const f: *Frame = @ptrCast(@alignCast(_frame));
|
||||
const pos = &f.window._scroll_pos;
|
||||
// If the state isn't scroll, we can ignore safely to throttle
|
||||
// the events.
|
||||
if (pos.state != .scroll) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, p);
|
||||
try p._event_manager.dispatch(p.document.asEventTarget(), event);
|
||||
|
||||
const event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, f._page);
|
||||
try f._event_manager.dispatch(f.document.asEventTarget(), event);
|
||||
pos.state = .end;
|
||||
|
||||
return null;
|
||||
@@ -554,8 +553,8 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, frame: *Frame) !void
|
||||
frame,
|
||||
struct {
|
||||
fn dispatch(_frame: *anyopaque) anyerror!?u32 {
|
||||
const p: *Frame = @ptrCast(@alignCast(_frame));
|
||||
const pos = &p.window._scroll_pos;
|
||||
const f: *Frame = @ptrCast(@alignCast(_frame));
|
||||
const pos = &f.window._scroll_pos;
|
||||
// Dispatch only if the state is .end.
|
||||
// If a scroll is pending, retry in 10ms.
|
||||
// If the state is .end, the event has been dispatched, so
|
||||
@@ -565,9 +564,8 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, frame: *Frame) !void
|
||||
.end => {},
|
||||
.done => return null,
|
||||
}
|
||||
const event = try Event.initTrusted(comptime .wrap("scrollend"), .{ .bubbles = true }, p);
|
||||
try p._event_manager.dispatch(p.document.asEventTarget(), event);
|
||||
|
||||
const event = try Event.initTrusted(comptime .wrap("scrollend"), .{ .bubbles = true }, f._page);
|
||||
try f._event_manager.dispatch(f.document.asEventTarget(), event);
|
||||
pos.state = .done;
|
||||
|
||||
return null;
|
||||
@@ -622,7 +620,7 @@ pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js.
|
||||
const event = (try @import("event/PromiseRejectionEvent.zig").init(event_name, .{
|
||||
.reason = if (rejection.reason()) |r| try r.temp() else null,
|
||||
.promise = try rejection.promise().temp(),
|
||||
}, frame._session)).asEvent();
|
||||
}, frame._page)).asEvent();
|
||||
try frame._event_manager.dispatchDirect(target, event, attribute_callback, .{ .context = "window.unhandledrejection" });
|
||||
}
|
||||
}
|
||||
@@ -811,7 +809,7 @@ const PostMessageCallback = struct {
|
||||
.source = self.source,
|
||||
.bubbles = false,
|
||||
.cancelable = false,
|
||||
}, frame._session)).asEvent();
|
||||
}, frame._page)).asEvent();
|
||||
try frame._event_manager.dispatchDirect(event_target, event, window._on_message, .{ .context = "window.postMessage" });
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ const http = @import("../../network/http.zig");
|
||||
|
||||
const URL = @import("../URL.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Session = @import("../Session.zig");
|
||||
const HttpClient = @import("../HttpClient.zig");
|
||||
|
||||
const Blob = @import("Blob.zig");
|
||||
@@ -71,7 +70,7 @@ pub fn init(url: []const u8, exec: *Execution) !*Worker {
|
||||
errdefer session.releaseArena(arena);
|
||||
|
||||
const resolved_url = try URL.resolve(arena, exec.url.*, url, .{});
|
||||
const self = try session.factory.eventTargetWithAllocator(arena, Worker{
|
||||
const self = try frame._page.factory.eventTargetWithAllocator(arena, Worker{
|
||||
._arena = arena,
|
||||
._proto = undefined,
|
||||
._frame = frame,
|
||||
@@ -216,7 +215,6 @@ fn fireErrorEvent(self: *Worker, message: []const u8, error_value: ?js.Value.Tem
|
||||
|
||||
fn _fireErrorEvent(self: *Worker, message: []const u8, error_value: ?js.Value.Temp) !void {
|
||||
const frame = self._frame;
|
||||
const session = frame._session;
|
||||
const target = self.asEventTarget();
|
||||
const on_error = self._on_error;
|
||||
|
||||
@@ -232,7 +230,7 @@ fn _fireErrorEvent(self: *Worker, message: []const u8, error_value: ?js.Value.Te
|
||||
.filename = self._url,
|
||||
.bubbles = false,
|
||||
.cancelable = true,
|
||||
}, session);
|
||||
}, frame._page);
|
||||
|
||||
try frame._event_manager.dispatchDirect(target, error_event.asEvent(), on_error, .{
|
||||
.context = "Worker.onerror",
|
||||
@@ -354,7 +352,7 @@ const ReceiveMessageCallback = struct {
|
||||
.data = .{ .string = @errorName(err) },
|
||||
.bubbles = false,
|
||||
.cancelable = false,
|
||||
}, frame._session)).asEvent();
|
||||
}, frame._page)).asEvent();
|
||||
try frame._event_manager.dispatchDirect(target, event, on_messageerror, .{ .context = "Worker.messageerror" });
|
||||
return null;
|
||||
};
|
||||
@@ -371,7 +369,7 @@ const ReceiveMessageCallback = struct {
|
||||
.data = .{ .value = data },
|
||||
.bubbles = false,
|
||||
.cancelable = false,
|
||||
}, frame._session)).asEvent();
|
||||
}, frame._page)).asEvent();
|
||||
|
||||
try frame._event_manager.dispatchDirect(target, event, on_message, .{ .context = "Worker.receiveMessage" });
|
||||
|
||||
|
||||
@@ -24,10 +24,12 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const JS = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const Factory = @import("../Factory.zig");
|
||||
const Session = @import("../Session.zig");
|
||||
const EventManagerBase = @import("../EventManagerBase.zig");
|
||||
|
||||
const Blob = @import("Blob.zig");
|
||||
const Worker = @import("Worker.zig");
|
||||
const Crypto = @import("Crypto.zig");
|
||||
const Console = @import("Console.zig");
|
||||
@@ -47,16 +49,22 @@ const WorkerGlobalScope = @This();
|
||||
// can access these the same for a Page of a WGS.
|
||||
// These fields represent the "Page"-like component of the WGS
|
||||
_session: *Session,
|
||||
_page: *Page,
|
||||
_factory: *Factory,
|
||||
_identity: JS.Identity = .{},
|
||||
arena: Allocator,
|
||||
call_arena: Allocator,
|
||||
url: [:0]const u8,
|
||||
// Same-origin constraint: a worker's origin is inherited from its parent frame.
|
||||
origin: ?[]const u8 = null,
|
||||
buf: [1024]u8 = undefined, // same size as frame.buf
|
||||
// Document charset (matches Page.charset). Workers default to UTF-8.
|
||||
charset: []const u8 = "UTF-8",
|
||||
js: *JS.Context,
|
||||
|
||||
// Blob URL registry for URL.createObjectURL/revokeObjectURL.
|
||||
_blob_urls: std.StringHashMapUnmanaged(*Blob) = .{},
|
||||
|
||||
// Reference back to the Worker object (for postMessage to frame)
|
||||
_worker: *Worker,
|
||||
|
||||
@@ -76,18 +84,21 @@ _on_messageerror: ?JS.Function.Global = null,
|
||||
|
||||
pub fn init(worker: *Worker, url: [:0]const u8) !*WorkerGlobalScope {
|
||||
const arena = worker._arena;
|
||||
const parent = worker._frame;
|
||||
const session = worker._frame._session;
|
||||
const factory = &session.factory;
|
||||
|
||||
const call_arena = try session.getArena(.small, "WorkerGlobalScope.call_arena");
|
||||
errdefer session.releaseArena(call_arena);
|
||||
|
||||
const factory = parent._factory;
|
||||
const self = try factory.eventTargetWithAllocator(arena, WorkerGlobalScope{
|
||||
.url = url,
|
||||
.arena = arena,
|
||||
.origin = parent.origin,
|
||||
.js = undefined,
|
||||
.call_arena = call_arena,
|
||||
._session = session,
|
||||
._page = parent._page,
|
||||
._identity = .{},
|
||||
._proto = undefined,
|
||||
._factory = factory,
|
||||
@@ -107,9 +118,13 @@ pub fn init(worker: *Worker, url: [:0]const u8) !*WorkerGlobalScope {
|
||||
|
||||
pub fn deinit(self: *WorkerGlobalScope) void {
|
||||
self._identity.deinit();
|
||||
const session = self._session;
|
||||
session.browser.env.destroyContext(self.js);
|
||||
session.releaseArena(self.call_arena);
|
||||
const page = self._page;
|
||||
var it = self._blob_urls.valueIterator();
|
||||
while (it.next()) |blob| {
|
||||
blob.*.releaseRef(page);
|
||||
}
|
||||
page.session.browser.env.destroyContext(self.js);
|
||||
page.releaseArena(self.call_arena);
|
||||
}
|
||||
|
||||
pub fn base(self: *const WorkerGlobalScope) [:0]const u8 {
|
||||
@@ -123,15 +138,21 @@ pub fn asEventTarget(self: *WorkerGlobalScope) *EventTarget {
|
||||
const Event = @import("Event.zig");
|
||||
|
||||
// Dispatch an event to listeners on the given target within this worker context.
|
||||
pub fn dispatch(self: *WorkerGlobalScope, target: *EventTarget, event: *Event, handler: anytype) !void {
|
||||
pub fn dispatch(
|
||||
self: *WorkerGlobalScope,
|
||||
target: *EventTarget,
|
||||
event: *Event,
|
||||
handler: anytype,
|
||||
comptime opts: EventManagerBase.DispatchDirectOptions,
|
||||
) !void {
|
||||
try self._event_manager.dispatchDirect(
|
||||
self.call_arena,
|
||||
self.js,
|
||||
target,
|
||||
event,
|
||||
handler,
|
||||
self._session,
|
||||
.{},
|
||||
self._page,
|
||||
opts,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -264,8 +285,8 @@ pub fn unhandledPromiseRejection(self: *WorkerGlobalScope, no_handler: bool, rej
|
||||
const event = (try @import("event/PromiseRejectionEvent.zig").init(event_name, .{
|
||||
.reason = if (rejection.reason()) |r| try r.temp() else null,
|
||||
.promise = try rejection.promise().temp(),
|
||||
}, self._session)).asEvent();
|
||||
try self.dispatch(target, event, attribute_callback);
|
||||
}, self._page)).asEvent();
|
||||
try self.dispatch(target, event, attribute_callback, .{});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,7 +302,7 @@ pub fn reportError(self: *WorkerGlobalScope, err: JS.Value) !void {
|
||||
.message = err.toStringSlice() catch "Unknown error",
|
||||
.bubbles = false,
|
||||
.cancelable = true,
|
||||
}, self._session);
|
||||
}, self._page);
|
||||
|
||||
// Invoke onerror callback if set (per WHATWG spec, this is called
|
||||
// with 5 arguments: message, source, lineno, colno, error)
|
||||
@@ -311,7 +332,7 @@ pub fn reportError(self: *WorkerGlobalScope, err: JS.Value) !void {
|
||||
event._prevent_default = prevent_default;
|
||||
// Pass null as handler: onerror was already called above with 5 args.
|
||||
// We still dispatch so that addEventListener('error', ...) listeners fire.
|
||||
try self.dispatch(self.asEventTarget(), event, null);
|
||||
try self.dispatch(self.asEventTarget(), event, null, .{});
|
||||
|
||||
if (comptime builtin.is_test == false) {
|
||||
if (!event._prevent_default) {
|
||||
@@ -374,8 +395,8 @@ const ReceiveMessageCallback = struct {
|
||||
const event = (try MessageEvent.initTrusted(comptime .wrap("messageerror"), .{
|
||||
.bubbles = false,
|
||||
.cancelable = false,
|
||||
}, worker_scope._session)).asEvent();
|
||||
try worker_scope.dispatch(target, event, on_messageerror);
|
||||
}, worker_scope._page)).asEvent();
|
||||
try worker_scope.dispatch(target, event, on_messageerror, .{});
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -391,8 +412,8 @@ const ReceiveMessageCallback = struct {
|
||||
.data = .{ .value = self.data.? },
|
||||
.bubbles = false,
|
||||
.cancelable = false,
|
||||
}, worker_scope._session)).asEvent();
|
||||
try worker_scope.dispatch(target, event, on_message);
|
||||
}, worker_scope._page)).asEvent();
|
||||
try worker_scope.dispatch(target, event, on_message, .{});
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
const js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Session = @import("../../Session.zig");
|
||||
|
||||
const log = lp.log;
|
||||
const Allocator = std.mem.Allocator;
|
||||
@@ -64,12 +64,12 @@ pub fn init(frame: *Frame) !*Animation {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Animation, session: *Session) void {
|
||||
session.releaseArena(self._arena);
|
||||
pub fn deinit(self: *Animation, page: *Page) void {
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *Animation, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *Animation, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *Animation) void {
|
||||
@@ -211,7 +211,7 @@ fn update(ctx: *anyopaque) !?u32 {
|
||||
}
|
||||
|
||||
// No future change scheduled, set the object weak for garbage collection.
|
||||
self.releaseRef(self._frame._session);
|
||||
self.releaseRef(self._frame._page);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,12 @@
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("../../js/js.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
|
||||
const Blob = @import("../Blob.zig");
|
||||
const OffscreenCanvasRenderingContext2D = @import("OffscreenCanvasRenderingContext2D.zig");
|
||||
|
||||
const Execution = js.Execution;
|
||||
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas
|
||||
const OffscreenCanvas = @This();
|
||||
|
||||
@@ -37,8 +38,8 @@ const DrawingContext = union(enum) {
|
||||
@"2d": *OffscreenCanvasRenderingContext2D,
|
||||
};
|
||||
|
||||
pub fn constructor(width: u32, height: u32, frame: *Frame) !*OffscreenCanvas {
|
||||
return frame._factory.create(OffscreenCanvas{
|
||||
pub fn constructor(width: u32, height: u32, exec: *Execution) !*OffscreenCanvas {
|
||||
return exec._factory.create(OffscreenCanvas{
|
||||
._width = width,
|
||||
._height = height,
|
||||
});
|
||||
@@ -60,9 +61,9 @@ pub fn setHeight(self: *OffscreenCanvas, value: u32) void {
|
||||
self._height = value;
|
||||
}
|
||||
|
||||
pub fn getContext(_: *OffscreenCanvas, context_type: []const u8, frame: *Frame) !?DrawingContext {
|
||||
pub fn getContext(_: *OffscreenCanvas, context_type: []const u8, exec: *Execution) !?DrawingContext {
|
||||
if (std.mem.eql(u8, context_type, "2d")) {
|
||||
const ctx = try frame._factory.create(OffscreenCanvasRenderingContext2D{});
|
||||
const ctx = try exec._factory.create(OffscreenCanvasRenderingContext2D{});
|
||||
return .{ .@"2d" = ctx };
|
||||
}
|
||||
|
||||
@@ -71,9 +72,9 @@ pub fn getContext(_: *OffscreenCanvas, context_type: []const u8, frame: *Frame)
|
||||
|
||||
/// Returns a Promise that resolves to a Blob containing the image.
|
||||
/// Since we have no actual rendering, this returns an empty blob.
|
||||
pub fn convertToBlob(_: *OffscreenCanvas, frame: *Frame) !js.Promise {
|
||||
const blob = try Blob.init(null, null, frame._session);
|
||||
return frame.js.local.?.resolvePromise(blob);
|
||||
pub fn convertToBlob(_: *OffscreenCanvas, exec: *Execution) !js.Promise {
|
||||
const blob = try Blob.init(null, null, exec.context.page);
|
||||
return exec.context.local.?.resolvePromise(blob);
|
||||
}
|
||||
|
||||
/// Returns an ImageBitmap with the rendered content (stub).
|
||||
|
||||
@@ -19,9 +19,10 @@
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Node = @import("../Node.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Session = @import("../../Session.zig");
|
||||
|
||||
const Node = @import("../Node.zig");
|
||||
const GenericIterator = @import("iterator.zig").Entry;
|
||||
|
||||
// Optimized for node.childNodes, which has to be a live list.
|
||||
@@ -55,8 +56,8 @@ pub fn init(node: *Node, frame: *Frame) !*ChildNodes {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const ChildNodes, session: *Session) void {
|
||||
session.releaseArena(self._arena);
|
||||
pub fn deinit(self: *const ChildNodes, page: *Page) void {
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn length(self: *ChildNodes, frame: *Frame) !u32 {
|
||||
|
||||
@@ -20,8 +20,9 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Session = @import("../../Session.zig");
|
||||
|
||||
const Node = @import("../Node.zig");
|
||||
|
||||
const ChildNodes = @import("ChildNodes.zig");
|
||||
@@ -41,16 +42,16 @@ _data: union(enum) {
|
||||
},
|
||||
_rc: lp.RC(u32) = .{},
|
||||
|
||||
pub fn deinit(self: *NodeList, session: *Session) void {
|
||||
pub fn deinit(self: *NodeList, page: *Page) void {
|
||||
switch (self._data) {
|
||||
.child_nodes => |cn| cn.deinit(session),
|
||||
.selector_list => |list| list.deinit(session),
|
||||
.child_nodes => |cn| cn.deinit(page),
|
||||
.selector_list => |list| list.deinit(page),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *NodeList, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *NodeList, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *NodeList) void {
|
||||
@@ -115,12 +116,12 @@ const Iterator = struct {
|
||||
|
||||
const Entry = struct { u32, *Node };
|
||||
|
||||
pub fn deinit(self: *Iterator, session: *Session) void {
|
||||
self.list.deinit(session);
|
||||
pub fn deinit(self: *Iterator, page: *Page) void {
|
||||
self.list.deinit(page);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *Iterator, session: *Session) void {
|
||||
self.list.releaseRef(session);
|
||||
pub fn releaseRef(self: *Iterator, page: *Page) void {
|
||||
self.list.releaseRef(page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *Iterator) void {
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Session = @import("../../Session.zig");
|
||||
|
||||
const Execution = js.Execution;
|
||||
|
||||
pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type {
|
||||
@@ -48,15 +50,15 @@ pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self, session: *Session) void {
|
||||
pub fn deinit(self: *Self, page: *Page) void {
|
||||
if (@hasDecl(Inner, "releaseRef")) {
|
||||
self._inner.releaseRef(session);
|
||||
self._inner.releaseRef(page);
|
||||
}
|
||||
session.factory.destroy(self);
|
||||
page.factory.destroy(self);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *Self, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *Self, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *Self) void {
|
||||
|
||||
@@ -79,7 +79,7 @@ pub fn getOwnerRule(self: *const CSSStyleSheet) ?*CSSRule {
|
||||
}
|
||||
|
||||
pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, maybe_index: ?u32, frame: *Frame) !u32 {
|
||||
const index = maybe_index orelse 0;
|
||||
const requested_index = maybe_index orelse 0;
|
||||
var it = Parser.parseStylesheet(rule);
|
||||
const parsed_rule = it.next() orelse {
|
||||
if (it.has_skipped_at_rule) {
|
||||
@@ -88,7 +88,7 @@ pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, maybe_index: ?u32, fra
|
||||
// CSS parser. To prevent JS apps (like Expo/Reanimated) from crashing
|
||||
// during initialization, we simulate a successful insertion by returning
|
||||
// the requested index.
|
||||
return index;
|
||||
return requested_index;
|
||||
}
|
||||
return error.SyntaxError;
|
||||
};
|
||||
@@ -103,6 +103,16 @@ pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, maybe_index: ?u32, fra
|
||||
try style.setCssText(parsed_rule.block, frame);
|
||||
|
||||
const rules = try self.getCssRules(frame);
|
||||
|
||||
// Per spec, an index > rules.length should throw IndexSizeError. But because
|
||||
// we don't process @import and @font-face, indexes that code hard-codes can
|
||||
// be off. As a workaround, we clamp to the tail.
|
||||
// See #2214 (and the sibling #1970 / #1972 tolerance for at-rules).
|
||||
const length = rules.length();
|
||||
const index = if (requested_index > length) length else requested_index;
|
||||
if (index != requested_index) {
|
||||
log.debug(.not_implemented, "insertRule clamped index", .{});
|
||||
}
|
||||
try rules.insert(index, style_rule._proto, frame);
|
||||
|
||||
// Notify StyleManager that rules have changed
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Session = @import("../../Session.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
@@ -44,12 +45,12 @@ pub fn init(family: []const u8, source: []const u8, frame: *Frame) !*FontFace {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *FontFace, session: *Session) void {
|
||||
session.releaseArena(self._arena);
|
||||
pub fn deinit(self: *FontFace, page: *Page) void {
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *FontFace, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *FontFace, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *FontFace) void {
|
||||
|
||||
@@ -18,12 +18,15 @@
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Session = @import("../../Session.zig");
|
||||
const FontFace = @import("FontFace.zig");
|
||||
const EventTarget = @import("../EventTarget.zig");
|
||||
|
||||
const Event = @import("../Event.zig");
|
||||
const EventTarget = @import("../EventTarget.zig");
|
||||
|
||||
const FontFace = @import("FontFace.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
@@ -43,12 +46,12 @@ pub fn init(frame: *Frame) !*FontFaceSet {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn deinit(self: *FontFaceSet, session: *Session) void {
|
||||
session.releaseArena(self._arena);
|
||||
pub fn deinit(self: *FontFaceSet, page: *Page) void {
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *FontFaceSet, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *FontFaceSet, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *FontFaceSet) void {
|
||||
@@ -80,13 +83,13 @@ pub fn load(self: *FontFaceSet, font: []const u8, frame: *Frame) !js.Promise {
|
||||
// Dispatch loading event
|
||||
const target = self.asEventTarget();
|
||||
if (frame._event_manager.hasDirectListeners(target, "loading", null)) {
|
||||
const event = try Event.initTrusted(comptime .wrap("loading"), .{}, frame);
|
||||
const event = try Event.initTrusted(comptime .wrap("loading"), .{}, frame._page);
|
||||
try frame._event_manager.dispatchDirect(target, event, null, .{ .context = "load font face set" });
|
||||
}
|
||||
|
||||
// Dispatch loadingdone event
|
||||
if (frame._event_manager.hasDirectListeners(target, "loadingdone", null)) {
|
||||
const event = try Event.initTrusted(comptime .wrap("loadingdone"), .{}, frame);
|
||||
const event = try Event.initTrusted(comptime .wrap("loadingdone"), .{}, frame._page);
|
||||
try frame._event_manager.dispatchDirect(target, event, null, .{ .context = "load font face set" });
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ const CanvasRenderingContext2D = @import("../../canvas/CanvasRenderingContext2D.
|
||||
const WebGLRenderingContext = @import("../../canvas/WebGLRenderingContext.zig");
|
||||
const OffscreenCanvas = @import("../../canvas/OffscreenCanvas.zig");
|
||||
|
||||
const Execution = js.Execution;
|
||||
|
||||
const Canvas = @This();
|
||||
_proto: *HtmlElement,
|
||||
_cached: ?DrawingContext = null,
|
||||
@@ -97,10 +99,10 @@ pub fn getContext(self: *Canvas, context_type: []const u8, frame: *Frame) !?Draw
|
||||
|
||||
/// Transfers control of the canvas to an OffscreenCanvas.
|
||||
/// Returns an OffscreenCanvas with the same dimensions.
|
||||
pub fn transferControlToOffscreen(self: *Canvas, frame: *Frame) !*OffscreenCanvas {
|
||||
pub fn transferControlToOffscreen(self: *Canvas, exec: *Execution) !*OffscreenCanvas {
|
||||
const width = self.getWidth();
|
||||
const height = self.getHeight();
|
||||
return OffscreenCanvas.constructor(width, height, frame);
|
||||
return OffscreenCanvas.constructor(width, height, exec);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -102,7 +102,7 @@ pub fn setOnSelectionChange(self: *Input, listener: ?js.Function) !void {
|
||||
}
|
||||
|
||||
fn dispatchSelectionChangeEvent(self: *Input, frame: *Frame) !void {
|
||||
const event = try Event.init("selectionchange", .{ .bubbles = true }, frame);
|
||||
const event = try Event.init("selectionchange", .{ .bubbles = true }, frame._page);
|
||||
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
||||
}
|
||||
|
||||
@@ -372,7 +372,7 @@ pub fn setAutocomplete(self: *Input, autocomplete: []const u8, frame: *Frame) !v
|
||||
pub fn select(self: *Input, frame: *Frame) !void {
|
||||
const len = if (self._value) |v| @as(u32, @intCast(v.len)) else 0;
|
||||
try self.setSelectionRange(0, len, null, frame);
|
||||
const event = try Event.init("select", .{ .bubbles = true }, frame);
|
||||
const event = try Event.init("select", .{ .bubbles = true }, frame._page);
|
||||
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
||||
}
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ pub fn load(self: *Media, frame: *Frame) !void {
|
||||
}
|
||||
|
||||
fn dispatchEvent(self: *Media, name: []const u8, frame: *Frame) !void {
|
||||
const event = try Event.init(name, .{ .bubbles = false, .cancelable = false }, frame);
|
||||
const event = try Event.init(name, .{ .bubbles = false, .cancelable = false }, frame._page);
|
||||
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ pub fn setOnSelectionChange(self: *TextArea, listener: ?js.Function) !void {
|
||||
}
|
||||
|
||||
fn dispatchSelectionChangeEvent(self: *TextArea, frame: *Frame) !void {
|
||||
const event = try Event.init("selectionchange", .{ .bubbles = true }, frame);
|
||||
const event = try Event.init("selectionchange", .{ .bubbles = true }, frame._page);
|
||||
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ pub fn setRequired(self: *TextArea, required: bool, frame: *Frame) !void {
|
||||
pub fn select(self: *TextArea, frame: *Frame) !void {
|
||||
const len = if (self._value) |v| @as(u32, @intCast(v.len)) else 0;
|
||||
try self.setSelectionRange(0, len, null, frame);
|
||||
const event = try Event.init("select", .{ .bubbles = true }, frame);
|
||||
const event = try Event.init("select", .{ .bubbles = true }, frame._page);
|
||||
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,12 @@
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
const html5ever = @import("../../parser/html5ever.zig");
|
||||
|
||||
const Session = @import("../../Session.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const TextDecoder = @This();
|
||||
@@ -41,7 +43,7 @@ const InitOpts = struct {
|
||||
ignoreBOM: bool = false,
|
||||
};
|
||||
|
||||
pub fn init(label_: ?[]const u8, opts_: ?InitOpts, session: *Session) !*TextDecoder {
|
||||
pub fn init(label_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*TextDecoder {
|
||||
const label = label_ orelse "utf-8";
|
||||
|
||||
const info = html5ever.encoding_for_label(label.ptr, label.len);
|
||||
@@ -55,8 +57,8 @@ pub fn init(label_: ?[]const u8, opts_: ?InitOpts, session: *Session) !*TextDeco
|
||||
return error.RangeError;
|
||||
}
|
||||
|
||||
const arena = try session.getArena(.large, "TextDecoder");
|
||||
errdefer session.releaseArena(arena);
|
||||
const arena = try page.getArena(.large, "TextDecoder");
|
||||
errdefer page.releaseArena(arena);
|
||||
|
||||
const opts = opts_ orelse InitOpts{};
|
||||
const self = try arena.create(TextDecoder);
|
||||
@@ -73,15 +75,15 @@ pub fn init(label_: ?[]const u8, opts_: ?InitOpts, session: *Session) !*TextDeco
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TextDecoder, session: *Session) void {
|
||||
pub fn deinit(self: *TextDecoder, page: *Page) void {
|
||||
if (self._decoder) |decoder| {
|
||||
html5ever.encoding_decoder_free(decoder);
|
||||
}
|
||||
session.releaseArena(self._arena);
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *TextDecoder, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *TextDecoder, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *TextDecoder) void {
|
||||
|
||||
@@ -20,8 +20,9 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Session = @import("../../Session.zig");
|
||||
|
||||
const Event = @import("../Event.zig");
|
||||
|
||||
const String = lp.String;
|
||||
@@ -39,13 +40,13 @@ const CustomEventOptions = struct {
|
||||
|
||||
const Options = Event.inheritOptions(CustomEvent, CustomEventOptions);
|
||||
|
||||
pub fn init(typ: []const u8, opts_: ?Options, frame: *Frame) !*CustomEvent {
|
||||
const arena = try frame.getArena(.tiny, "CustomEvent");
|
||||
errdefer frame.releaseArena(arena);
|
||||
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CustomEvent {
|
||||
const arena = try page.getArena(.tiny, "CustomEvent");
|
||||
errdefer page.releaseArena(arena);
|
||||
const type_string = try String.init(arena, typ, .{});
|
||||
|
||||
const opts = opts_ orelse Options{};
|
||||
const event = try frame._factory.event(
|
||||
const event = try page.factory.event(
|
||||
arena,
|
||||
type_string,
|
||||
CustomEvent{
|
||||
@@ -75,21 +76,21 @@ pub fn initCustomEvent(
|
||||
self._detail = detail_;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *CustomEvent, session: *Session) void {
|
||||
pub fn deinit(self: *CustomEvent, page: *Page) void {
|
||||
if (self._detail) |d| {
|
||||
d.release();
|
||||
}
|
||||
self._proto.deinit(session);
|
||||
self._proto.deinit(page);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *CustomEvent, page: *Page) void {
|
||||
self._proto._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *CustomEvent) void {
|
||||
self._proto.acquireRef();
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *CustomEvent, session: *Session) void {
|
||||
self._proto._rc.release(self, session);
|
||||
}
|
||||
|
||||
pub fn asEvent(self: *CustomEvent) *Event {
|
||||
return self._proto;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Session = @import("../../Session.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
const Event = @import("../Event.zig");
|
||||
|
||||
@@ -47,23 +47,23 @@ pub const ErrorEventOptions = struct {
|
||||
|
||||
const Options = Event.inheritOptions(ErrorEvent, ErrorEventOptions);
|
||||
|
||||
pub fn init(typ: []const u8, opts_: ?Options, session: *Session) !*ErrorEvent {
|
||||
const arena = try session.getArena(.small, "ErrorEvent");
|
||||
errdefer session.releaseArena(arena);
|
||||
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*ErrorEvent {
|
||||
const arena = try page.getArena(.small, "ErrorEvent");
|
||||
errdefer page.releaseArena(arena);
|
||||
const type_string = try String.init(arena, typ, .{});
|
||||
return initWithTrusted(arena, type_string, opts_, false, session);
|
||||
return initWithTrusted(arena, type_string, opts_, false, page);
|
||||
}
|
||||
|
||||
pub fn initTrusted(typ: String, opts_: ?Options, session: *Session) !*ErrorEvent {
|
||||
const arena = try session.getArena(.small, "ErrorEvent.trusted");
|
||||
errdefer session.releaseArena(arena);
|
||||
return initWithTrusted(arena, typ, opts_, true, session);
|
||||
pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*ErrorEvent {
|
||||
const arena = try page.getArena(.small, "ErrorEvent.trusted");
|
||||
errdefer page.releaseArena(arena);
|
||||
return initWithTrusted(arena, typ, opts_, true, page);
|
||||
}
|
||||
|
||||
fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, session: *Session) !*ErrorEvent {
|
||||
fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, page: *Page) !*ErrorEvent {
|
||||
const opts = opts_ orelse Options{};
|
||||
|
||||
const event = try session.factory.event(
|
||||
const event = try page.factory.event(
|
||||
arena,
|
||||
typ,
|
||||
ErrorEvent{
|
||||
@@ -81,21 +81,21 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool
|
||||
return event;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ErrorEvent, session: *Session) void {
|
||||
pub fn deinit(self: *ErrorEvent, page: *Page) void {
|
||||
if (self._error) |e| {
|
||||
e.release();
|
||||
}
|
||||
self._proto.deinit(session);
|
||||
self._proto.deinit(page);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *ErrorEvent, page: *Page) void {
|
||||
self._proto._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *ErrorEvent) void {
|
||||
self._proto.acquireRef();
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *ErrorEvent, session: *Session) void {
|
||||
self._proto._rc.release(self, session);
|
||||
}
|
||||
|
||||
pub fn asEvent(self: *ErrorEvent) *Event {
|
||||
return self._proto;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Session = @import("../../Session.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
const Event = @import("../Event.zig");
|
||||
const Window = @import("../Window.zig");
|
||||
@@ -50,23 +50,23 @@ pub const Data = union(enum) {
|
||||
|
||||
const Options = Event.inheritOptions(MessageEvent, MessageEventOptions);
|
||||
|
||||
pub fn init(typ: []const u8, opts_: ?Options, session: *Session) !*MessageEvent {
|
||||
const arena = try session.getArena(.small, "MessageEvent");
|
||||
errdefer session.releaseArena(arena);
|
||||
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*MessageEvent {
|
||||
const arena = try page.getArena(.small, "MessageEvent");
|
||||
errdefer page.releaseArena(arena);
|
||||
const type_string = try String.init(arena, typ, .{});
|
||||
return initWithTrusted(arena, type_string, opts_, false, session);
|
||||
return initWithTrusted(arena, type_string, opts_, false, page);
|
||||
}
|
||||
|
||||
pub fn initTrusted(typ: String, opts_: ?Options, session: *Session) !*MessageEvent {
|
||||
const arena = try session.getArena(.small, "MessageEvent.trusted");
|
||||
errdefer session.releaseArena(arena);
|
||||
return initWithTrusted(arena, typ, opts_, true, session);
|
||||
pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*MessageEvent {
|
||||
const arena = try page.getArena(.small, "MessageEvent.trusted");
|
||||
errdefer page.releaseArena(arena);
|
||||
return initWithTrusted(arena, typ, opts_, true, page);
|
||||
}
|
||||
|
||||
fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, session: *Session) !*MessageEvent {
|
||||
fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, page: *Page) !*MessageEvent {
|
||||
const opts = opts_ orelse Options{};
|
||||
|
||||
const event = try session.factory.event(
|
||||
const event = try page.factory.event(
|
||||
arena,
|
||||
typ,
|
||||
MessageEvent{
|
||||
@@ -81,23 +81,23 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool
|
||||
return event;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *MessageEvent, session: *Session) void {
|
||||
pub fn deinit(self: *MessageEvent, page: *Page) void {
|
||||
if (self._data) |d| {
|
||||
switch (d) {
|
||||
.value => |js_val| js_val.release(),
|
||||
.blob => |blob| blob.releaseRef(session),
|
||||
.blob => |blob| blob.releaseRef(page),
|
||||
.string, .arraybuffer => {},
|
||||
}
|
||||
}
|
||||
self._proto.deinit(session);
|
||||
self._proto.deinit(page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *MessageEvent) void {
|
||||
self._proto.acquireRef();
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *MessageEvent, session: *Session) void {
|
||||
self._proto._rc.release(self, session);
|
||||
pub fn releaseRef(self: *MessageEvent, page: *Page) void {
|
||||
self._proto._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn asEvent(self: *MessageEvent) *Event {
|
||||
|
||||
@@ -19,7 +19,7 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Session = @import("../../Session.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
const Event = @import("../Event.zig");
|
||||
|
||||
@@ -38,13 +38,13 @@ const PromiseRejectionEventOptions = struct {
|
||||
|
||||
const Options = Event.inheritOptions(PromiseRejectionEvent, PromiseRejectionEventOptions);
|
||||
|
||||
pub fn init(typ: []const u8, opts_: ?Options, session: *Session) !*PromiseRejectionEvent {
|
||||
const arena = try session.getArena(.tiny, "PromiseRejectionEvent");
|
||||
errdefer session.releaseArena(arena);
|
||||
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*PromiseRejectionEvent {
|
||||
const arena = try page.getArena(.tiny, "PromiseRejectionEvent");
|
||||
errdefer page.releaseArena(arena);
|
||||
const type_string = try String.init(arena, typ, .{});
|
||||
|
||||
const opts = opts_ orelse Options{};
|
||||
const event = try session.factory.event(
|
||||
const event = try page.factory.event(
|
||||
arena,
|
||||
type_string,
|
||||
PromiseRejectionEvent{
|
||||
@@ -58,24 +58,24 @@ pub fn init(typ: []const u8, opts_: ?Options, session: *Session) !*PromiseReject
|
||||
return event;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *PromiseRejectionEvent, session: *Session) void {
|
||||
pub fn deinit(self: *PromiseRejectionEvent, page: *Page) void {
|
||||
if (self._reason) |r| {
|
||||
r.release();
|
||||
}
|
||||
if (self._promise) |p| {
|
||||
p.release();
|
||||
}
|
||||
self._proto.deinit(session);
|
||||
self._proto.deinit(page);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *PromiseRejectionEvent, page: *Page) void {
|
||||
self._proto._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *PromiseRejectionEvent) void {
|
||||
self._proto.acquireRef();
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *PromiseRejectionEvent, session: *Session) void {
|
||||
self._proto._rc.release(self, session);
|
||||
}
|
||||
|
||||
pub fn asEvent(self: *PromiseRejectionEvent) *Event {
|
||||
return self._proto;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ pub fn init(input: Input, options: ?InitOpts, frame: *Frame) !js.Promise {
|
||||
}
|
||||
|
||||
const response = try Response.init(null, .{ .status = 0 }, &frame.js.execution);
|
||||
errdefer response.deinit(frame._session);
|
||||
errdefer response.deinit(frame._page);
|
||||
|
||||
const fetch = try response._arena.create(Fetch);
|
||||
fetch.* = .{
|
||||
@@ -248,7 +248,7 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
// clear this. (defer since `self is in the response's arena).
|
||||
|
||||
defer if (self._owns_response) {
|
||||
response.deinit(self._frame._session);
|
||||
response.deinit(self._frame._page);
|
||||
};
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
@@ -269,7 +269,7 @@ fn httpShutdownCallback(ctx: *anyopaque) void {
|
||||
if (self._owns_response) {
|
||||
var response = self._response;
|
||||
response._http_response = null;
|
||||
response.deinit(self._frame._session);
|
||||
response.deinit(self._frame._page);
|
||||
// Do not access `self` after this point: the Fetch struct was
|
||||
// allocated from response._arena which has been released.
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ pub fn blob(self: *Request, exec: *const Execution) !js.Promise {
|
||||
const headers = try self.getHeaders(exec);
|
||||
const content_type = try headers.get("content-type", exec) orelse "";
|
||||
|
||||
const b = try Blob.initFromBytes(body, content_type, true, exec.context.session);
|
||||
const b = try Blob.initFromBytes(body, content_type, true, exec.context.page);
|
||||
|
||||
return exec.context.local.?.resolvePromise(b);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Session = @import("../../Session.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const HttpClient = @import("../../HttpClient.zig");
|
||||
|
||||
const Blob = @import("../Blob.zig");
|
||||
@@ -72,7 +72,7 @@ pub const BodyInit = union(enum) {
|
||||
};
|
||||
|
||||
pub fn init(body_: ?BodyInit, opts_: ?InitOpts, exec: *const Execution) !*Response {
|
||||
const session = exec.context.session;
|
||||
const session = exec.context.page.session;
|
||||
const arena = try session.getArena(.large, "Response");
|
||||
errdefer session.releaseArena(arena);
|
||||
|
||||
@@ -109,16 +109,16 @@ pub fn init(body_: ?BodyInit, opts_: ?InitOpts, exec: *const Execution) !*Respon
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Response, session: *Session) void {
|
||||
pub fn deinit(self: *Response, page: *Page) void {
|
||||
if (self._http_response) |resp| {
|
||||
resp.abort(error.Abort);
|
||||
self._http_response = null;
|
||||
}
|
||||
session.releaseArena(self._arena);
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *Response, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *Response, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *Response) void {
|
||||
@@ -321,7 +321,7 @@ pub fn blob(self: *const Response, exec: *const Execution) !js.Promise {
|
||||
.stream => return local.rejectPromise(.{ .type_error = "Cannot read blob from stream body" }),
|
||||
};
|
||||
const content_type = try self._headers.get("content-type", exec) orelse "";
|
||||
const b = try Blob.initFromBytes(body, content_type, true, exec.context.session);
|
||||
const b = try Blob.initFromBytes(body, content_type, true, exec.context.page);
|
||||
return local.resolvePromise(b);
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ pub fn bytes(self: *const Response, exec: *const Execution) !js.Promise {
|
||||
}
|
||||
|
||||
pub fn clone(self: *const Response, exec: *const Execution) !*Response {
|
||||
const session = exec.context.session;
|
||||
const session = exec.context.page.session;
|
||||
const body_len = switch (self._body) {
|
||||
.bytes => |b| b.len,
|
||||
.empty => 0,
|
||||
|
||||
@@ -24,14 +24,15 @@ const http = @import("../../../network/http.zig");
|
||||
const js = @import("../../js/js.zig");
|
||||
const Blob = @import("../Blob.zig");
|
||||
const URL = @import("../../URL.zig");
|
||||
|
||||
const Page = @import("../../Page.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Session = @import("../../Session.zig");
|
||||
const HttpClient = @import("../../HttpClient.zig");
|
||||
|
||||
const Event = @import("../Event.zig");
|
||||
const EventTarget = @import("../EventTarget.zig");
|
||||
const MessageEvent = @import("../event/MessageEvent.zig");
|
||||
const CloseEvent = @import("../event/CloseEvent.zig");
|
||||
const MessageEvent = @import("../event/MessageEvent.zig");
|
||||
|
||||
const log = lp.log;
|
||||
const Allocator = std.mem.Allocator;
|
||||
@@ -160,7 +161,7 @@ pub fn init(url: []const u8, protocols: [][]const u8, frame: *Frame) !*WebSocket
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *WebSocket, session: *Session) void {
|
||||
pub fn deinit(self: *WebSocket, page: *Page) void {
|
||||
self.cleanup();
|
||||
|
||||
if (self._on_open) |func| {
|
||||
@@ -177,14 +178,14 @@ pub fn deinit(self: *WebSocket, session: *Session) void {
|
||||
}
|
||||
|
||||
for (self._send_queue.items) |msg| {
|
||||
msg.deinit(session);
|
||||
msg.deinit(page);
|
||||
}
|
||||
|
||||
session.releaseArena(self._arena);
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *WebSocket, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *WebSocket, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *WebSocket) void {
|
||||
@@ -235,7 +236,7 @@ fn cleanup(self: *WebSocket) void {
|
||||
self._http_client.removeConn(conn);
|
||||
self._req_headers.deinit();
|
||||
self._conn = null;
|
||||
self.releaseRef(self._frame._session);
|
||||
self.releaseRef(self._frame._page);
|
||||
self._send_queue.clearRetainingCapacity();
|
||||
}
|
||||
}
|
||||
@@ -303,8 +304,8 @@ pub fn send(self: *WebSocket, data: SendData) !void {
|
||||
|
||||
switch (data) {
|
||||
.blob => |blob| {
|
||||
const arena = try self._frame._session.getArena(blob._slice.len, "WebSocket.message");
|
||||
errdefer self._frame._session.releaseArena(arena);
|
||||
const arena = try self._frame.getArena(blob._slice.len, "WebSocket.message");
|
||||
errdefer self._frame.releaseArena(arena);
|
||||
try self.queueMessage(.{ .binary = .{
|
||||
.arena = arena,
|
||||
.data = try arena.dupe(u8, blob._slice),
|
||||
@@ -312,8 +313,8 @@ pub fn send(self: *WebSocket, data: SendData) !void {
|
||||
},
|
||||
.js_val => |js_val| {
|
||||
if (js_val.isString()) |str| {
|
||||
const arena = try self._frame._session.getArena(str.len(), "WebSocket.message");
|
||||
errdefer self._frame._session.releaseArena(arena);
|
||||
const arena = try self._frame.getArena(str.len(), "WebSocket.message");
|
||||
errdefer self._frame.releaseArena(arena);
|
||||
try self.queueMessage(.{ .text = .{
|
||||
.arena = arena,
|
||||
.data = try str.toSliceWithAlloc(arena),
|
||||
@@ -322,8 +323,8 @@ pub fn send(self: *WebSocket, data: SendData) !void {
|
||||
const binary = try js_val.toZig(BinaryData);
|
||||
const buffer = binary.asBuffer();
|
||||
|
||||
const arena = try self._frame._session.getArena(buffer.len, "WebSocket.message");
|
||||
errdefer self._frame._session.releaseArena(arena);
|
||||
const arena = try self._frame.getArena(buffer.len, "WebSocket.message");
|
||||
errdefer self._frame.releaseArena(arena);
|
||||
try self.queueMessage(.{ .binary = .{
|
||||
.arena = arena,
|
||||
.data = try arena.dupe(u8, buffer),
|
||||
@@ -452,7 +453,7 @@ fn dispatchOpenEvent(self: *WebSocket) !void {
|
||||
const target = self.asEventTarget();
|
||||
|
||||
if (frame._event_manager.hasDirectListeners(target, "open", self._on_open)) {
|
||||
const event = try Event.initTrusted(comptime .wrap("open"), .{}, frame);
|
||||
const event = try Event.initTrusted(comptime .wrap("open"), .{}, frame._page);
|
||||
try frame._event_manager.dispatchDirect(target, event, self._on_open, .{ .context = "WebSocket open" });
|
||||
}
|
||||
}
|
||||
@@ -466,7 +467,7 @@ fn dispatchMessageEvent(self: *WebSocket, data: []const u8, frame_type: http.WsF
|
||||
switch (self._binary_type) {
|
||||
.arraybuffer => .{ .arraybuffer = .{ .values = data } },
|
||||
.blob => blk: {
|
||||
const blob = try Blob.initFromBytes(data, "", false, frame._session);
|
||||
const blob = try Blob.initFromBytes(data, "", false, frame._page);
|
||||
blob.acquireRef();
|
||||
break :blk .{ .blob = blob };
|
||||
},
|
||||
@@ -477,7 +478,7 @@ fn dispatchMessageEvent(self: *WebSocket, data: []const u8, frame_type: http.WsF
|
||||
const event = try MessageEvent.initTrusted(comptime .wrap("message"), .{
|
||||
.data = msg_data,
|
||||
.origin = "",
|
||||
}, frame._session);
|
||||
}, frame._page);
|
||||
try frame._event_manager.dispatchDirect(target, event.asEvent(), self._on_message, .{ .context = "WebSocket message" });
|
||||
}
|
||||
}
|
||||
@@ -487,7 +488,7 @@ fn dispatchErrorEvent(self: *WebSocket) !void {
|
||||
const target = self.asEventTarget();
|
||||
|
||||
if (frame._event_manager.hasDirectListeners(target, "error", self._on_error)) {
|
||||
const event = try Event.initTrusted(comptime .wrap("error"), .{}, frame);
|
||||
const event = try Event.initTrusted(comptime .wrap("error"), .{}, frame._page);
|
||||
try frame._event_manager.dispatchDirect(target, event, self._on_error, .{ .context = "WebSocket error" });
|
||||
}
|
||||
}
|
||||
@@ -575,7 +576,7 @@ fn writeContent(self: *WebSocket, conn: *http.Connection, buf: []u8, byte_msg: M
|
||||
|
||||
if (self._send_offset >= byte_msg.data.len) {
|
||||
const removed = self._send_queue.orderedRemove(0);
|
||||
removed.deinit(self._frame._session);
|
||||
removed.deinit(self._frame._page);
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.websocket, "send complete", .{ .url = self._url, .len = byte_msg.data.len, .queue = self._send_queue.items.len });
|
||||
}
|
||||
@@ -718,9 +719,9 @@ const Message = union(enum) {
|
||||
arena: Allocator,
|
||||
data: []const u8,
|
||||
};
|
||||
fn deinit(self: Message, session: *Session) void {
|
||||
fn deinit(self: Message, page: *Page) void {
|
||||
switch (self) {
|
||||
.text, .binary => |msg| session.releaseArena(msg.arena),
|
||||
.text, .binary => |msg| page.releaseArena(msg.arena),
|
||||
.close => {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ const http = @import("../../../network/http.zig");
|
||||
|
||||
const URL = @import("../../URL.zig");
|
||||
const Mime = @import("../../Mime.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Session = @import("../../Session.zig");
|
||||
|
||||
const Node = @import("../Node.zig");
|
||||
const Event = @import("../Event.zig");
|
||||
@@ -100,7 +100,7 @@ pub fn init(frame: *Frame) !*XMLHttpRequest {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *XMLHttpRequest, session: *Session) void {
|
||||
pub fn deinit(self: *XMLHttpRequest, page: *Page) void {
|
||||
if (self._http_response) |resp| {
|
||||
resp.abort(error.Abort);
|
||||
self._http_response = null;
|
||||
@@ -135,19 +135,19 @@ pub fn deinit(self: *XMLHttpRequest, session: *Session) void {
|
||||
}
|
||||
}
|
||||
|
||||
session.releaseArena(self._arena);
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
fn releaseSelfRef(self: *XMLHttpRequest) void {
|
||||
if (self._active_request == false) {
|
||||
return;
|
||||
}
|
||||
self.releaseRef(self._frame._session);
|
||||
self.releaseRef(self._frame._page);
|
||||
self._active_request = false;
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *XMLHttpRequest, session: *Session) void {
|
||||
self._rc.release(self, session);
|
||||
pub fn releaseRef(self: *XMLHttpRequest, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *XMLHttpRequest) void {
|
||||
@@ -588,7 +588,7 @@ fn stateChanged(self: *XMLHttpRequest, state: ReadyState, frame: *Frame) !void {
|
||||
|
||||
const target = self.asEventTarget();
|
||||
if (frame._event_manager.hasDirectListeners(target, "readystatechange", self._on_ready_state_change)) {
|
||||
const event = try Event.initTrusted(.wrap("readystatechange"), .{}, frame);
|
||||
const event = try Event.initTrusted(.wrap("readystatechange"), .{}, frame._page);
|
||||
try frame._event_manager.dispatchDirect(target, event, self._on_ready_state_change, .{ .context = "XHR state change" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Page = @import("../../Page.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Session = @import("../../Session.zig");
|
||||
|
||||
const Node = @import("../Node.zig");
|
||||
const Part = @import("Selector.zig").Part;
|
||||
@@ -41,8 +41,8 @@ pub const EntryIterator = GenericIterator(Iterator, null);
|
||||
pub const KeyIterator = GenericIterator(Iterator, "0");
|
||||
pub const ValueIterator = GenericIterator(Iterator, "1");
|
||||
|
||||
pub fn deinit(self: *const List, session: *Session) void {
|
||||
session.releaseArena(self._arena);
|
||||
pub fn deinit(self: *const List, page: *Page) void {
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn collect(
|
||||
|
||||
@@ -1366,7 +1366,7 @@ test "AXNode: writer" {
|
||||
defer registry.deinit();
|
||||
|
||||
var frame = try testing.pageTest("cdp/dom3.html", .{});
|
||||
defer frame._session.removeFrame();
|
||||
defer frame._session.removePage();
|
||||
var doc = frame.window._document;
|
||||
|
||||
const node = try registry.register(doc.asNode());
|
||||
@@ -1440,7 +1440,7 @@ test "AXNode: writer prunes hidden and resolves labels" {
|
||||
defer registry.deinit();
|
||||
|
||||
var frame = try testing.pageTest("cdp/ax_tree.html", .{});
|
||||
defer frame._session.removeFrame();
|
||||
defer frame._session.removePage();
|
||||
var doc = frame.window._document;
|
||||
|
||||
const node = try registry.register(doc.asNode());
|
||||
|
||||
@@ -223,6 +223,7 @@ fn dispatchCommand(command: *Command, method: []const u8) !void {
|
||||
5 => switch (@as(u40, @bitCast(domain[0..5].*))) {
|
||||
asUint(u40, "Fetch") => return @import("domains/fetch.zig").processMessage(command),
|
||||
asUint(u40, "Input") => return @import("domains/input.zig").processMessage(command),
|
||||
asUint(u40, "Audit") => return @import("domains/audit.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
6 => switch (@as(u48, @bitCast(domain[0..6].*))) {
|
||||
@@ -836,7 +837,7 @@ const IsolatedWorld = struct {
|
||||
// The isolate world must share at least some of the state with the related frame, specifically the DocumentHTML
|
||||
// (assuming grantUniversalAccess will be set to True!).
|
||||
// We just created the world and the frame. The frame's state lives in the session, but is update on navigation.
|
||||
// This also means this pointer becomes invalid after removeFrame until a new frame is created.
|
||||
// This also means this pointer becomes invalid after removePage until a new frame is created.
|
||||
// Currently we have only 1 frame and thus also only 1 state in the isolate world.
|
||||
pub fn createContext(self: *IsolatedWorld, frame: *Frame) !*js.Context {
|
||||
if (self.context == null) {
|
||||
|
||||
@@ -346,7 +346,7 @@ test "cdp Node: Registry register" {
|
||||
try testing.expectEqual(0, registry.lookup_by_node.count());
|
||||
|
||||
var frame = try testing.pageTest("cdp/registry1.html", .{});
|
||||
defer frame._session.removeFrame();
|
||||
defer frame._session.removePage();
|
||||
var doc = frame.window._document;
|
||||
|
||||
{
|
||||
@@ -403,12 +403,12 @@ test "cdp Node: search list" {
|
||||
|
||||
{
|
||||
var frame = try testing.pageTest("cdp/registry2.html", .{});
|
||||
defer frame._session.removeFrame();
|
||||
defer frame._session.removePage();
|
||||
var doc = frame.window._document;
|
||||
|
||||
{
|
||||
const l1 = try doc.querySelectorAll(.wrap("a"), frame);
|
||||
defer l1.deinit(frame._session);
|
||||
defer l1.deinit(frame._page);
|
||||
const s1 = try search_list.create(l1._nodes);
|
||||
try testing.expectEqual("1", s1.name);
|
||||
try testing.expectEqualSlices(u32, &.{ 1, 2 }, s1.node_ids);
|
||||
@@ -419,7 +419,7 @@ test "cdp Node: search list" {
|
||||
|
||||
{
|
||||
const l2 = try doc.querySelectorAll(.wrap("#a1"), frame);
|
||||
defer l2.deinit(frame._session);
|
||||
defer l2.deinit(frame._page);
|
||||
const s2 = try search_list.create(l2._nodes);
|
||||
try testing.expectEqual("2", s2.name);
|
||||
try testing.expectEqualSlices(u32, &.{1}, s2.node_ids);
|
||||
@@ -427,7 +427,7 @@ test "cdp Node: search list" {
|
||||
|
||||
{
|
||||
const l3 = try doc.querySelectorAll(.wrap("#a2"), frame);
|
||||
defer l3.deinit(frame._session);
|
||||
defer l3.deinit(frame._page);
|
||||
const s3 = try search_list.create(l3._nodes);
|
||||
try testing.expectEqual("3", s3.name);
|
||||
try testing.expectEqualSlices(u32, &.{2}, s3.node_ids);
|
||||
@@ -443,7 +443,7 @@ test "cdp Node: Writer" {
|
||||
defer registry.deinit();
|
||||
|
||||
var frame = try testing.pageTest("cdp/registry3.html", .{});
|
||||
defer frame._session.removeFrame();
|
||||
defer frame._session.removePage();
|
||||
var doc = frame.window._document;
|
||||
|
||||
{
|
||||
|
||||
39
src/cdp/domains/audit.zig
Normal file
39
src/cdp/domains/audit.zig
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const CDP = @import("../CDP.zig");
|
||||
|
||||
pub fn processMessage(cmd: *CDP.Command) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
disable,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return enable(cmd),
|
||||
.disable => return disable(cmd),
|
||||
}
|
||||
}
|
||||
fn enable(cmd: *CDP.Command) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn disable(cmd: *CDP.Command) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
@@ -101,7 +101,7 @@ fn performSearch(cmd: *CDP.Command) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const frame = bc.session.currentFrame() orelse return error.FrameNotLoaded;
|
||||
const list = try Selector.querySelectorAll(frame.window._document.asNode(), params.query, frame);
|
||||
defer list.deinit(frame._session);
|
||||
defer list.deinit(frame._page);
|
||||
|
||||
const search = try bc.node_search_list.create(list._nodes);
|
||||
|
||||
@@ -252,7 +252,7 @@ fn querySelectorAll(cmd: *CDP.Command) !void {
|
||||
};
|
||||
|
||||
const selected_nodes = try Selector.querySelectorAll(node.dom, params.selector, frame);
|
||||
defer selected_nodes.deinit(frame._session);
|
||||
defer selected_nodes.deinit(frame._page);
|
||||
|
||||
const nodes = selected_nodes._nodes;
|
||||
|
||||
|
||||
@@ -299,7 +299,7 @@ test "cdp.lp: getMarkdown" {
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{});
|
||||
_ = try bc.session.createFrame();
|
||||
_ = try bc.session.createPage();
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 1,
|
||||
@@ -315,7 +315,7 @@ test "cdp.lp: getInteractiveElements" {
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{});
|
||||
_ = try bc.session.createFrame();
|
||||
_ = try bc.session.createPage();
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 1,
|
||||
@@ -331,7 +331,7 @@ test "cdp.lp: getStructuredData" {
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{});
|
||||
_ = try bc.session.createFrame();
|
||||
_ = try bc.session.createPage();
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 1,
|
||||
@@ -347,7 +347,7 @@ test "cdp.lp: action tools" {
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{});
|
||||
const frame = try bc.session.createFrame();
|
||||
const frame = try bc.session.createPage();
|
||||
const url = "http://localhost:9582/src/browser/tests/mcp_actions.html";
|
||||
try frame.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } });
|
||||
var runner = try bc.session.runner(.{});
|
||||
@@ -408,7 +408,7 @@ test "cdp.lp: waitForSelector" {
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{});
|
||||
const frame = try bc.session.createFrame();
|
||||
const frame = try bc.session.createPage();
|
||||
const url = "http://localhost:9582/src/browser/tests/mcp_wait_for_selector.html";
|
||||
try frame.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } });
|
||||
var runner = try bc.session.runner(.{});
|
||||
|
||||
@@ -187,7 +187,7 @@ fn getCookies(cmd: *CDP.Command) !void {
|
||||
const params = (try cmd.params(GetCookiesParam)) orelse GetCookiesParam{};
|
||||
|
||||
// If not specified, use the URLs of the page and all of its subframes. TODO subframes
|
||||
const frame_url = if (bc.session.frame) |frame| frame.url else null;
|
||||
const frame_url = if (bc.session.currentFrame()) |frame| frame.url else null;
|
||||
const param_urls = params.urls orelse &[_][:0]const u8{frame_url orelse return error.InvalidParams};
|
||||
|
||||
var urls = try std.ArrayList(CdpStorage.PreparedUri).initCapacity(cmd.arena, param_urls.len);
|
||||
@@ -239,7 +239,7 @@ pub fn httpRequestFail(bc: *CDP.BrowserContext, msg: *const Notification.Request
|
||||
|
||||
// Isn't possible to do a network request within a Browser (which our
|
||||
// notification is tied to), without a frame.
|
||||
lp.assert(bc.session.frame != null, "CDP.network.httpRequestFail null frame", .{});
|
||||
lp.assert(bc.session.page != null, "CDP.network.httpRequestFail null frame", .{});
|
||||
|
||||
// We're missing a bunch of fields, but, for now, this seems like enough
|
||||
try bc.cdp.sendEvent("Network.loadingFailed", .{
|
||||
|
||||
@@ -212,7 +212,7 @@ fn close(cmd: *CDP.Command) !void {
|
||||
const target_id = bc.target_id orelse return error.TargetNotLoaded;
|
||||
|
||||
// can't be null if we have a target_id
|
||||
lp.assert(bc.session.frame != null, "CDP.frame.close null frame", .{});
|
||||
lp.assert(bc.session.page != null, "CDP.frame.close null frame", .{});
|
||||
|
||||
try cmd.sendResult(.{}, .{});
|
||||
|
||||
@@ -235,7 +235,7 @@ fn close(cmd: *CDP.Command) !void {
|
||||
bc.session_id = null;
|
||||
}
|
||||
|
||||
bc.session.removeFrame();
|
||||
bc.session.removePage();
|
||||
for (bc.isolated_worlds.items) |world| {
|
||||
world.deinit();
|
||||
}
|
||||
@@ -307,7 +307,7 @@ fn navigate(cmd: *CDP.Command) !void {
|
||||
isolated_world.identity.deinit();
|
||||
isolated_world.identity = .{};
|
||||
}
|
||||
frame = try session.replaceFrame();
|
||||
frame = try session.replacePage();
|
||||
}
|
||||
|
||||
const encoded_url = try URL.ensureEncoded(frame.call_arena, params.url, "UTF-8");
|
||||
@@ -333,7 +333,7 @@ fn doReload(cmd: *CDP.Command) !void {
|
||||
const session = bc.session;
|
||||
var frame = session.currentFrame() orelse return error.FrameNotLoaded;
|
||||
|
||||
// Dupe URL before replaceFrame() frees the old frame's arena.
|
||||
// Dupe URL before replacePage() frees the old frame's arena.
|
||||
const reload_url = try cmd.arena.dupeZ(u8, frame.url);
|
||||
|
||||
if (frame._load_state != .waiting) {
|
||||
@@ -343,7 +343,7 @@ fn doReload(cmd: *CDP.Command) !void {
|
||||
isolated_world.identity.deinit();
|
||||
isolated_world.identity = .{};
|
||||
}
|
||||
frame = try session.replaceFrame();
|
||||
frame = try session.replacePage();
|
||||
}
|
||||
|
||||
try frame.navigate(reload_url, .{
|
||||
|
||||
@@ -171,12 +171,12 @@ fn createTarget(cmd: *CDP.Command) !void {
|
||||
}
|
||||
|
||||
// if target_id is null, we should never have a blank frame
|
||||
lp.assert(bc.session.frame == null, "CDP.target.createTarget not null page", .{});
|
||||
lp.assert(bc.session.page == null, "CDP.target.createTarget not null page", .{});
|
||||
|
||||
// if target_id is null, we should never have a session_id
|
||||
lp.assert(bc.session_id == null, "CDP.target.createTarget not null session_id", .{});
|
||||
|
||||
const frame = try bc.session.createFrame();
|
||||
const frame = try bc.session.createPage();
|
||||
|
||||
// the target_id == the frame_id of the "root" frame
|
||||
const frame_id = id.toFrameId(frame._frame_id);
|
||||
@@ -284,7 +284,7 @@ fn closeTarget(cmd: *CDP.Command) !void {
|
||||
}
|
||||
|
||||
// can't be null if we have a target_id
|
||||
lp.assert(bc.session.frame != null, "CDP.target.closeTarget null frame", .{});
|
||||
lp.assert(bc.session.page != null, "CDP.target.closeTarget null frame", .{});
|
||||
|
||||
try cmd.sendResult(.{ .success = true }, .{ .include_session_id = false });
|
||||
|
||||
@@ -305,7 +305,7 @@ fn closeTarget(cmd: *CDP.Command) !void {
|
||||
bc.session_id = null;
|
||||
}
|
||||
|
||||
bc.session.removeFrame();
|
||||
bc.session.removePage();
|
||||
for (bc.isolated_worlds.items) |world| {
|
||||
world.deinit();
|
||||
}
|
||||
@@ -626,7 +626,7 @@ test "cdp.target: closeTarget" {
|
||||
}
|
||||
|
||||
// pretend we createdTarget first
|
||||
_ = try bc.session.createFrame();
|
||||
_ = try bc.session.createPage();
|
||||
bc.target_id = "TID-000000000A".*;
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } });
|
||||
@@ -636,7 +636,7 @@ test "cdp.target: closeTarget" {
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 11, .method = "Target.closeTarget", .params = .{ .targetId = "TID-000000000A" } });
|
||||
try ctx.expectSentResult(.{ .success = true }, .{ .id = 11 });
|
||||
try testing.expectEqual(null, bc.session.frame);
|
||||
try testing.expectEqual(null, bc.session.page);
|
||||
try testing.expectEqual(null, bc.target_id);
|
||||
}
|
||||
}
|
||||
@@ -657,7 +657,7 @@ test "cdp.target: attachToTarget" {
|
||||
}
|
||||
|
||||
// pretend we createdTarget first
|
||||
_ = try bc.session.createFrame();
|
||||
_ = try bc.session.createPage();
|
||||
bc.target_id = "TID-000000000B".*;
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } });
|
||||
@@ -701,7 +701,7 @@ test "cdp.target: getTargetInfo" {
|
||||
}
|
||||
|
||||
// pretend we createdTarget first
|
||||
_ = try bc.session.createFrame();
|
||||
_ = try bc.session.createPage();
|
||||
bc.target_id = "TID-000000000C".*;
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } });
|
||||
|
||||
@@ -93,7 +93,7 @@ const TestContext = struct {
|
||||
if (bc.target_id == null) {
|
||||
bc.target_id = "TID-000000000Z".*;
|
||||
}
|
||||
const frame = try bc.session.createFrame();
|
||||
const frame = try bc.session.createPage();
|
||||
const full_url = try std.fmt.allocPrintSentinel(
|
||||
base.arena_allocator,
|
||||
"http://127.0.0.1:9582/src/browser/tests/{s}",
|
||||
@@ -204,7 +204,7 @@ const TestContext = struct {
|
||||
|
||||
if (self.cdp_) |*cdp__| {
|
||||
if (cdp__.browser_context) |*bc| {
|
||||
if (bc.session.frame != null) {
|
||||
if (bc.session.page != null) {
|
||||
var runner = try bc.session.runner(.{});
|
||||
_ = try runner.tick(.{ .ms = 1000 });
|
||||
}
|
||||
|
||||
592
src/cli.zig
Normal file
592
src/cli.zig
Normal file
@@ -0,0 +1,592 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const lp = @import("lightpanda");
|
||||
const log = lp.log;
|
||||
|
||||
/// Comptime CLI builder that generates a tagged union parser from a
|
||||
/// declarative command recipe. Each command becomes a union variant whose
|
||||
/// payload is a struct with one field per option.
|
||||
///
|
||||
/// ## Command descriptor fields
|
||||
///
|
||||
/// - `name: []const u8` — canonical command name on the command line.
|
||||
/// - `options: tuple` — tuple of option descriptors (see below). Use `.{}`
|
||||
/// for none.
|
||||
/// - `shared_options: tuple` (optional) — extra options merged into this
|
||||
/// command. Useful for common flags shared across commands.
|
||||
/// - `positional: struct` (optional) — a single positional argument with
|
||||
/// `.name` and `.type`. Type must be an optional pointer-to-u8 slice
|
||||
/// (e.g. `?[:0]const u8`). Positionals can appear anywhere in argv and
|
||||
/// must be provided; a missing positional returns `error.MissingArgument`.
|
||||
///
|
||||
/// ## Option descriptor fields
|
||||
///
|
||||
/// - `name: []const u8` — snake_case field name. Both `--snake_case` and
|
||||
/// `--kebab-case` are accepted on the command line.
|
||||
/// - `type` — the Zig type of the parsed value (see supported types below).
|
||||
/// - `default` (optional) — compile-time default when the flag is absent.
|
||||
/// Rules vary by type; see the defaults section below.
|
||||
/// - `multiple: bool` (optional) — when `true`, the field becomes a
|
||||
/// `std.ArrayList(type)` and each occurrence appends. Not supported for
|
||||
/// `bool` or packed-struct options.
|
||||
/// - `validator: fn` (optional) — custom parse function that replaces the
|
||||
/// built-in type switch. See the validator section below.
|
||||
///
|
||||
/// ## Supported types and their defaults
|
||||
///
|
||||
/// - `bool` — presence flips the field to the opposite of its `default`
|
||||
/// (so a flag with `default = true` acts as a disable switch). Defaults
|
||||
/// to `false` when no `default` is given. `?bool` is not allowed.
|
||||
/// - Integers (`u8`, `u16`, `u31`, `usize`, etc.) — parsed with
|
||||
/// `std.fmt.parseInt`. Requires `default` unless wrapped in `?`.
|
||||
/// - `[]const u8`, `[:0]const u8` (and mutable variants) — string slices
|
||||
/// duped from argv. Sentinel is preserved. Requires `default` unless `?`.
|
||||
/// - Enums — parsed via `std.meta.stringToEnum`. Returns
|
||||
/// `error.UnknownArgument` on a bad value. Requires `default` unless `?`.
|
||||
/// - Packed structs of `bool` fields — parsed from a comma-separated list
|
||||
/// (e.g. `--strip-mode js,css`). The literal `"full"` sets every field.
|
||||
/// Unknown names return `error.UnknownArgument`. Requires `default`.
|
||||
/// `multiple` is not supported.
|
||||
/// - Optional types default to `null` when `default` is omitted.
|
||||
///
|
||||
/// ## Validators
|
||||
///
|
||||
/// A `validator` is a custom parse function that takes over argument
|
||||
/// consumption for an option. Its signature depends on whether `multiple`
|
||||
/// is set:
|
||||
///
|
||||
/// - Single: `fn (Allocator, *ArgIterator) !T` — returns the parsed value.
|
||||
/// - Multiple: `fn (Allocator, *ArgIterator, *std.ArrayList(T)) !void` —
|
||||
/// appends directly into the list.
|
||||
///
|
||||
/// When a validator is present, the built-in type switch is skipped entirely.
|
||||
/// The validator owns advancing the iterator and is free to peek ahead.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```zig
|
||||
/// const StripMode = packed struct(u2) {
|
||||
/// js: bool = false,
|
||||
/// css: bool = false,
|
||||
/// };
|
||||
///
|
||||
/// const WaitUntil = enum { load, domcontentloaded, networkidle };
|
||||
///
|
||||
/// const CommonOptions = .{
|
||||
/// .{ .name = "verbose", .type = bool },
|
||||
/// .{ .name = "log_level", .type = ?log.Level },
|
||||
/// .{ .name = "timeout", .type = u31, .default = 30 },
|
||||
/// };
|
||||
///
|
||||
/// const Cli = cli.Builder(.{
|
||||
/// .{
|
||||
/// .name = "serve",
|
||||
/// .options = .{
|
||||
/// .{ .name = "host", .type = []const u8, .default = "127.0.0.1" },
|
||||
/// .{ .name = "port", .type = u16, .default = 9222 },
|
||||
/// },
|
||||
/// .shared_options = CommonOptions,
|
||||
/// },
|
||||
/// .{
|
||||
/// .name = "fetch",
|
||||
/// .positional = .{ .name = "url", .type = ?[:0]const u8 },
|
||||
/// .options = .{
|
||||
/// .{ .name = "dump", .type = ?DumpFormat, .validator = dumpValidator },
|
||||
/// .{ .name = "strip_mode", .type = StripMode, .default = .{} },
|
||||
/// .{ .name = "wait_until", .type = ?WaitUntil },
|
||||
/// .{ .name = "extra_header", .type = []const u8, .multiple = true },
|
||||
/// },
|
||||
/// .shared_options = CommonOptions,
|
||||
/// },
|
||||
/// .{ .name = "version", .options = .{} },
|
||||
/// .{ .name = "help", .options = .{} },
|
||||
/// });
|
||||
///
|
||||
/// const _, const cmd = try Cli.parse(arena);
|
||||
/// switch (cmd) {
|
||||
/// .serve => |opts| listen(opts.host, opts.port),
|
||||
/// .fetch => |opts| fetch(opts.url.?, opts.dump),
|
||||
/// .version => printVersion(),
|
||||
/// .help => printHelp(),
|
||||
/// }
|
||||
/// ```
|
||||
pub fn Builder(comptime commands: anytype) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
/// Enum type for provided commands.
|
||||
pub const Enum = blk: {
|
||||
var enum_fields: [commands.len]std.builtin.Type.EnumField = undefined;
|
||||
for (commands, 0..) |command, i| {
|
||||
enum_fields[i] = .{ .name = command.name, .value = i };
|
||||
}
|
||||
|
||||
break :blk @Type(.{
|
||||
.@"enum" = .{
|
||||
.decls = &.{},
|
||||
.fields = &enum_fields,
|
||||
.is_exhaustive = true,
|
||||
.tag_type = std.math.IntFittingRange(0, commands.len),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/// Creates an array of `StructField` out of given options.
|
||||
fn optionsToStructFields(comptime options: anytype) [options.len]std.builtin.Type.StructField {
|
||||
var fields: [options.len]std.builtin.Type.StructField = undefined;
|
||||
|
||||
inline for (options, 0..) |option, j| {
|
||||
// Whether prefer `ArrayList` for the option.
|
||||
const is_multiple = @hasField(@TypeOf(option), "multiple") and option.multiple;
|
||||
// Whether option has a default value.
|
||||
const has_default = @hasField(@TypeOf(option), "default");
|
||||
|
||||
const T = if (is_multiple) std.ArrayList(option.type) else option.type;
|
||||
|
||||
const default = blk: {
|
||||
if (is_multiple) {
|
||||
// We currently don't allow default values for lists.
|
||||
if (has_default) {
|
||||
@compileError("`default` is not allowed for lists");
|
||||
}
|
||||
// Multiples are always initialized the same.
|
||||
break :blk @as(*const anyopaque, @ptrCast(&@as(T, .{})));
|
||||
}
|
||||
|
||||
switch (@typeInfo(option.type)) {
|
||||
.optional => |optional| {
|
||||
if (optional.child == bool) {
|
||||
@compileError("?bool is not supported, prefer enum");
|
||||
}
|
||||
|
||||
// If type is an optional type without default value, prefer null.
|
||||
if (!has_default) {
|
||||
break :blk @as(*const anyopaque, @ptrCast(&@as(T, null)));
|
||||
}
|
||||
// We have default value for an optional.
|
||||
break :blk @as(*const anyopaque, @ptrCast(&@as(T, option.default)));
|
||||
},
|
||||
.bool => {
|
||||
// Prefer `false` if no default.
|
||||
const default = if (has_default) option.default else false;
|
||||
break :blk @as(*const anyopaque, @ptrCast(&@as(T, default)));
|
||||
},
|
||||
inline else => {
|
||||
if (!has_default) {
|
||||
@compileError("option `" ++ option.name ++ "` is not optional type and has no default value");
|
||||
}
|
||||
break :blk @as(*const anyopaque, @ptrCast(&@as(T, option.default)));
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
fields[j] = .{
|
||||
.name = option.name,
|
||||
.type = T,
|
||||
.default_value_ptr = default,
|
||||
.is_comptime = false,
|
||||
.alignment = @alignOf(T),
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
/// Union type for provided commands.
|
||||
pub const Union = blk: {
|
||||
var union_fields: [commands.len]std.builtin.Type.UnionField = undefined;
|
||||
for (commands, 0..) |command, i| {
|
||||
const Command = @TypeOf(command);
|
||||
const options = command.options;
|
||||
|
||||
const fields = optionsToStructFields(options) ++
|
||||
(if (@hasField(Command, "shared_options"))
|
||||
optionsToStructFields(command.shared_options)
|
||||
else
|
||||
.{}) ++
|
||||
(if (@hasField(Command, "positional"))
|
||||
[1]std.builtin.Type.StructField{
|
||||
.{
|
||||
.name = command.positional.name,
|
||||
.type = command.positional.type,
|
||||
.default_value_ptr = @ptrCast(&@as(command.positional.type, null)),
|
||||
.is_comptime = false,
|
||||
.alignment = @alignOf(command.positional.type),
|
||||
},
|
||||
}
|
||||
else
|
||||
.{});
|
||||
|
||||
const T = @Type(.{
|
||||
.@"struct" = .{
|
||||
.decls = &.{},
|
||||
.fields = &fields,
|
||||
.is_tuple = false,
|
||||
.layout = .auto,
|
||||
},
|
||||
});
|
||||
|
||||
union_fields[i] = .{ .name = command.name, .type = T, .alignment = @alignOf(T) };
|
||||
}
|
||||
|
||||
break :blk @Type(.{
|
||||
.@"union" = .{
|
||||
.decls = &.{},
|
||||
.fields = &union_fields,
|
||||
.layout = .auto,
|
||||
.tag_type = Enum,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/// Parses executable name, command and options via single call.
|
||||
pub fn parse(allocator: Allocator) !struct { []const u8, Union } {
|
||||
var args = try std.process.argsWithAllocator(allocator);
|
||||
defer args.deinit();
|
||||
|
||||
const exec_name = std.fs.path.basename(args.next().?);
|
||||
|
||||
const cmd_str: []const u8 = args.next() orelse return error.MissingCommand;
|
||||
inline for (commands) |command| {
|
||||
// Match a command.
|
||||
if (std.mem.eql(u8, cmd_str, command.name)) {
|
||||
return .{ exec_name, try parseCommand(allocator, command, &args) };
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort, try sniffing.
|
||||
const command_enum = try sniffCommand(cmd_str);
|
||||
|
||||
// `help` takes no arguments; short-circuit so the sniffed flag
|
||||
// isn't re-parsed as an unknown option.
|
||||
if (command_enum == .help) {
|
||||
return .{ exec_name, .{ .help = .{} } };
|
||||
}
|
||||
|
||||
// "cmd_str" wasn't a command but an option. We can't reset args, but
|
||||
// we can create a new one. Not great, but this fallback is temporary
|
||||
// as we transition to this command mode approach.
|
||||
args.deinit();
|
||||
args = try std.process.argsWithAllocator(allocator);
|
||||
// Skip the `exec_name`.
|
||||
_ = args.skip();
|
||||
|
||||
inline for (commands) |command| {
|
||||
if (std.mem.eql(u8, @tagName(command_enum), command.name)) {
|
||||
return .{ exec_name, try parseCommand(allocator, command, &args) };
|
||||
}
|
||||
}
|
||||
|
||||
unreachable;
|
||||
}
|
||||
|
||||
/// Try to sniff the command out of given option.
|
||||
/// Only exists for legacy reasons; hence hardcoded.
|
||||
fn sniffCommand(cmd_str: []const u8) error{UnknownCommand}!Enum {
|
||||
if (std.mem.startsWith(u8, cmd_str, "--") == false) {
|
||||
return .fetch;
|
||||
}
|
||||
|
||||
// Fetch heuristics.
|
||||
inline for (.{
|
||||
"--dump",
|
||||
"--strip-mode",
|
||||
"--strip_mode",
|
||||
"--with-base",
|
||||
"--with_base",
|
||||
"--with-frames",
|
||||
"--with_frames",
|
||||
}) |heuristic| {
|
||||
if (std.mem.eql(u8, cmd_str, heuristic)) {
|
||||
return .fetch;
|
||||
}
|
||||
}
|
||||
|
||||
// Serve heuristics.
|
||||
inline for (.{
|
||||
"--host",
|
||||
"--port",
|
||||
"--timeout",
|
||||
}) |heuristic| {
|
||||
if (std.mem.eql(u8, cmd_str, heuristic)) {
|
||||
return .serve;
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy `--help` flag maps to the `help` command.
|
||||
if (std.mem.eql(u8, cmd_str, "--help")) {
|
||||
return .help;
|
||||
}
|
||||
|
||||
return error.UnknownCommand;
|
||||
}
|
||||
|
||||
/// Parses the command with its options.
|
||||
fn parseCommand(
|
||||
allocator: Allocator,
|
||||
command: anytype,
|
||||
args: *std.process.ArgIterator,
|
||||
) !Union {
|
||||
const Command = @FieldType(Union, command.name);
|
||||
var c = Command{};
|
||||
|
||||
const options = blk: {
|
||||
if (@hasField(@TypeOf(command), "shared_options")) {
|
||||
break :blk command.options ++ command.shared_options;
|
||||
}
|
||||
|
||||
break :blk command.options;
|
||||
};
|
||||
iter_args: while (args.next()) |option_name| {
|
||||
inline for (options) |option| {
|
||||
// We allow both `--my-option` and `--my_option` variants;
|
||||
// assuming given `option` struct prefer snake_case for `name`.
|
||||
const kebab_cased = comptime casing: {
|
||||
var output: [option.name.len]u8 = undefined;
|
||||
@memcpy(&output, option.name);
|
||||
std.mem.replaceScalar(u8, &output, '_', '-');
|
||||
break :casing "--" ++ output;
|
||||
};
|
||||
|
||||
// Match an option.
|
||||
const match =
|
||||
std.mem.eql(u8, option_name, "--" ++ option.name) or
|
||||
std.mem.eql(u8, option_name, kebab_cased);
|
||||
|
||||
if (match) {
|
||||
const T = option.type;
|
||||
const option_info = blk: {
|
||||
const info = @typeInfo(T);
|
||||
// If wrapped in optional, prefer the child type.
|
||||
if (info == .optional) break :blk @typeInfo(info.optional.child);
|
||||
break :blk info;
|
||||
};
|
||||
|
||||
const is_multiple = @hasField(@TypeOf(option), "multiple") and option.multiple;
|
||||
const has_validator = @hasField(@TypeOf(option), "validator");
|
||||
|
||||
// Prefer custom validator logic instead.
|
||||
if (has_validator) {
|
||||
const validator = option.validator;
|
||||
if (is_multiple) {
|
||||
// Pass the list.
|
||||
try @call(.auto, validator, .{ allocator, args, &@field(c, option.name) });
|
||||
} else {
|
||||
// Receive the value from return.
|
||||
const v = try @call(.auto, validator, .{ allocator, args });
|
||||
@field(c, option.name) = v;
|
||||
}
|
||||
} else {
|
||||
switch (option_info) {
|
||||
.int => |int| {
|
||||
const Int = std.meta.Int(int.signedness, int.bits);
|
||||
|
||||
const str = args.next() orelse return error.MissingArgument;
|
||||
const v = std.fmt.parseInt(Int, str, 10) catch |err| {
|
||||
switch (err) {
|
||||
error.Overflow => log.fatal(.app, "range overflow", .{ .arg = kebab_cased, .value = str }),
|
||||
error.InvalidCharacter => log.fatal(.app, "invalid character", .{ .arg = kebab_cased, .value = str }),
|
||||
}
|
||||
continue :iter_args;
|
||||
};
|
||||
|
||||
if (is_multiple) {
|
||||
// Push to ArrayList.
|
||||
try @field(c, option.name).append(allocator, v);
|
||||
} else {
|
||||
@field(c, option.name) = v;
|
||||
}
|
||||
},
|
||||
.pointer => |pointer| {
|
||||
const not_u8_slice = pointer.child != u8 or pointer.size != .slice;
|
||||
if (not_u8_slice) {
|
||||
@compileError("Only []u8, []const u8, [:sentinel]u8 and [:sentinel]const u8 pointers are supported");
|
||||
}
|
||||
|
||||
const v = blk: {
|
||||
const str = args.next() orelse return error.MissingArgument;
|
||||
|
||||
// DupeZ branch.
|
||||
if (comptime pointer.sentinel()) |sentinel| {
|
||||
const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment), str.len + 1);
|
||||
@memcpy(buf[0..str.len], str);
|
||||
buf[str.len] = sentinel;
|
||||
break :blk buf[0..str.len :sentinel];
|
||||
}
|
||||
|
||||
// Dupe branch.
|
||||
const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment), str.len);
|
||||
@memcpy(buf, str);
|
||||
break :blk buf;
|
||||
};
|
||||
|
||||
if (is_multiple) {
|
||||
try @field(c, option.name).append(allocator, v);
|
||||
} else {
|
||||
@field(c, option.name) = v;
|
||||
}
|
||||
},
|
||||
.@"struct" => |_struct| {
|
||||
// Don't support multiple for structs for now.
|
||||
if (is_multiple) {
|
||||
@compileError("multiple option is not supported for structs");
|
||||
}
|
||||
|
||||
const not_packed = _struct.layout != .@"packed";
|
||||
if (not_packed) {
|
||||
@compileError("only packed structs are allowed");
|
||||
}
|
||||
|
||||
const str = args.next() orelse return error.MissingArgument;
|
||||
|
||||
if (std.mem.eql(u8, str, "full")) {
|
||||
// "full" sets all the fields of packed struct.
|
||||
const Int = _struct.backing_integer.?;
|
||||
@field(c, option.name) = @bitCast(@as(Int, std.math.maxInt(Int)));
|
||||
} else {
|
||||
// Parse given args.
|
||||
var it = std.mem.tokenizeScalar(u8, str, ',');
|
||||
outer: while (it.next()) |part| {
|
||||
const trimmed = std.mem.trim(u8, part, &std.ascii.whitespace);
|
||||
|
||||
inline for (_struct.fields) |f| {
|
||||
lp.assert(f.type == bool, "all fields of packed struct must be boolean", .{
|
||||
.option = option.name,
|
||||
.field = f.name,
|
||||
});
|
||||
|
||||
if (std.mem.eql(u8, trimmed, @as([]const u8, f.name))) {
|
||||
@field(@field(c, option.name), f.name) = true;
|
||||
continue :outer;
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid option choice.
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = kebab_cased, .value = trimmed });
|
||||
}
|
||||
}
|
||||
},
|
||||
.@"enum" => {
|
||||
const E = switch (@typeInfo(T)) {
|
||||
.optional => |optional| optional.child,
|
||||
inline else => T,
|
||||
};
|
||||
|
||||
const str = args.next() orelse return error.MissingArgument;
|
||||
const v = std.meta.stringToEnum(E, str) orelse {
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = kebab_cased, .value = str });
|
||||
continue :iter_args;
|
||||
};
|
||||
|
||||
if (is_multiple) {
|
||||
try @field(c, option.name).append(allocator, v);
|
||||
} else {
|
||||
@field(c, option.name) = v;
|
||||
}
|
||||
},
|
||||
.bool => {
|
||||
if (is_multiple) {
|
||||
@compileError("multiple option is not supported for booleans");
|
||||
}
|
||||
|
||||
const default = blk: {
|
||||
if (@hasField(@TypeOf(option), "default")) {
|
||||
break :blk option.default;
|
||||
}
|
||||
break :blk false;
|
||||
};
|
||||
|
||||
// Set opposite of the default.
|
||||
@field(c, option.name) = !default;
|
||||
},
|
||||
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
continue :iter_args;
|
||||
}
|
||||
}
|
||||
|
||||
// Encountered an option we don't know of.
|
||||
if (std.mem.startsWith(u8, option_name, "--")) {
|
||||
log.fatal(.app, "unknown argument", .{ .mode = command.name, .arg = option_name });
|
||||
return error.UnknownOption;
|
||||
}
|
||||
|
||||
// Parse positional arg if provided; can be given out of order:
|
||||
//
|
||||
// lightpanda fetch --wait-ms 2_000 "https://lightpanda.io" --dump "html"
|
||||
// ---------------------------------^
|
||||
if (comptime @hasField(@TypeOf(command), "positional")) {
|
||||
const positional = command.positional;
|
||||
|
||||
// Already given one.
|
||||
if (@field(c, positional.name) != null) {
|
||||
return error.TooManyPositionalArguments;
|
||||
}
|
||||
|
||||
// The positional must be an optional type.
|
||||
const info = @typeInfo(@typeInfo(positional.type).optional.child);
|
||||
|
||||
const str = @as([]const u8, option_name);
|
||||
switch (info) {
|
||||
.pointer => |pointer| {
|
||||
const not_u8_slice = pointer.child != u8 or pointer.size != .slice;
|
||||
if (not_u8_slice) {
|
||||
@compileError("Only []u8, []const u8, [:sentinel]u8 and [:sentinel]const u8 pointers are supported");
|
||||
}
|
||||
|
||||
const v = blk: {
|
||||
// DupeZ branch.
|
||||
if (comptime pointer.sentinel()) |sentinel| {
|
||||
const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment), str.len + 1);
|
||||
@memcpy(buf[0..str.len], str);
|
||||
buf[str.len] = sentinel;
|
||||
break :blk buf[0..str.len :sentinel];
|
||||
}
|
||||
|
||||
// Dupe branch.
|
||||
const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment), str.len);
|
||||
@memcpy(buf, str);
|
||||
break :blk buf;
|
||||
};
|
||||
|
||||
@field(c, positional.name) = v;
|
||||
},
|
||||
inline else => @compileError("not supported"),
|
||||
}
|
||||
} else {
|
||||
log.fatal(.app, "unknown argument", .{ .mode = command.name, .arg = option_name });
|
||||
return error.UnknownOption;
|
||||
}
|
||||
}
|
||||
|
||||
// Parsing is complete and positional is null.
|
||||
const positional_is_null = @hasField(@TypeOf(command), "positional") and @field(c, command.positional.name) == null;
|
||||
if (positional_is_null) {
|
||||
return error.MissingArgument;
|
||||
}
|
||||
|
||||
return @unionInit(Union, command.name, c);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -23,12 +23,14 @@ pub const App = @import("App.zig");
|
||||
pub const Network = @import("network/Network.zig");
|
||||
pub const Server = @import("Server.zig");
|
||||
pub const Config = @import("Config.zig");
|
||||
pub const URL = @import("browser/URL.zig");
|
||||
pub const String = @import("string.zig").String;
|
||||
pub const Notification = @import("Notification.zig");
|
||||
|
||||
pub const URL = @import("browser/URL.zig");
|
||||
pub const Page = @import("browser/Page.zig");
|
||||
pub const Frame = @import("browser/Frame.zig");
|
||||
pub const Browser = @import("browser/Browser.zig");
|
||||
pub const Session = @import("browser/Session.zig");
|
||||
pub const Notification = @import("Notification.zig");
|
||||
|
||||
pub const js = @import("browser/js/js.zig");
|
||||
pub const dump = @import("browser/dump.zig");
|
||||
@@ -41,14 +43,14 @@ pub const forms = @import("browser/forms.zig");
|
||||
pub const actions = @import("browser/actions.zig");
|
||||
pub const structured_data = @import("browser/structured_data.zig");
|
||||
pub const tools = @import("browser/tools.zig");
|
||||
pub const HttpClient = @import("browser/HttpClient.zig");
|
||||
|
||||
pub const mcp = @import("mcp.zig");
|
||||
pub const agent = @import("agent.zig");
|
||||
pub const cookies = @import("cookies.zig");
|
||||
pub const build_config = @import("build_config");
|
||||
pub const crash_handler = @import("crash_handler.zig");
|
||||
|
||||
pub const HttpClient = @import("browser/HttpClient.zig");
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
pub const FetchOpts = struct {
|
||||
@@ -82,7 +84,7 @@ pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void {
|
||||
}
|
||||
}
|
||||
|
||||
const frame = try session.createFrame();
|
||||
const frame = try session.createPage();
|
||||
|
||||
// // Comment this out to get a profile of the JS code in v8/profile.json.
|
||||
// // You can open this in Chrome's profiler.
|
||||
@@ -264,7 +266,7 @@ pub fn RC(comptime T: type) type {
|
||||
self._refs += 1;
|
||||
}
|
||||
|
||||
pub fn release(self: *@This(), value: anytype, session: *Session) void {
|
||||
pub fn release(self: *@This(), value: anytype, page: *Page) void {
|
||||
assert(self._refs > 0, "release overflow", .{ .type = @typeName(@TypeOf(value)) });
|
||||
|
||||
const refs = self._refs - 1;
|
||||
@@ -272,7 +274,7 @@ pub fn RC(comptime T: type) type {
|
||||
if (refs > 0) {
|
||||
return;
|
||||
}
|
||||
value.deinit(session);
|
||||
value.deinit(page);
|
||||
}
|
||||
|
||||
pub fn format(self: @This(), writer: *std.Io.Writer) !void {
|
||||
|
||||
18
src/main.zig
18
src/main.zig
@@ -55,7 +55,7 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
|
||||
|
||||
switch (args.mode) {
|
||||
.help => {
|
||||
args.printUsageAndExit(args.mode.help);
|
||||
args.printUsageAndExit(true);
|
||||
return std.process.cleanExit();
|
||||
},
|
||||
.version => {
|
||||
@@ -72,9 +72,9 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
|
||||
if (args.logFormat()) |lf| {
|
||||
log.opts.format = lf;
|
||||
}
|
||||
if (args.logFilterScopes()) |lfs| {
|
||||
log.opts.filter_scopes = lfs;
|
||||
}
|
||||
|
||||
// Set log filter scopes.
|
||||
log.opts.filter_scopes = args.logFilterScopes().items;
|
||||
|
||||
// must be installed before any other threads
|
||||
const sighandler = try main_arena.create(SigHandler);
|
||||
@@ -118,16 +118,16 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
|
||||
},
|
||||
.fetch => |opts| {
|
||||
const url = opts.url;
|
||||
log.debug(.app, "startup", .{ .mode = "fetch", .dump_mode = opts.dump_mode, .url = url, .snapshot = app.snapshot.fromEmbedded() });
|
||||
log.debug(.app, "startup", .{ .mode = "fetch", .dump_mode = opts.dump, .url = url, .snapshot = app.snapshot.fromEmbedded() });
|
||||
|
||||
var fetch_opts = lp.FetchOpts{
|
||||
.wait_ms = opts.wait_ms,
|
||||
.wait_until = opts.wait_until,
|
||||
.wait_script = opts.wait_script,
|
||||
.wait_selector = opts.wait_selector,
|
||||
.dump_mode = opts.dump_mode,
|
||||
.dump_mode = opts.dump,
|
||||
.dump = .{
|
||||
.strip = opts.strip,
|
||||
.strip = opts.strip_mode,
|
||||
.with_base = opts.with_base,
|
||||
.with_frames = opts.with_frames,
|
||||
},
|
||||
@@ -135,11 +135,11 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
|
||||
|
||||
var stdout = std.fs.File.stdout();
|
||||
var writer = stdout.writer(&.{});
|
||||
if (opts.dump_mode != null) {
|
||||
if (opts.dump != null) {
|
||||
fetch_opts.writer = &writer.interface;
|
||||
}
|
||||
|
||||
var worker_thread = try std.Thread.spawn(.{}, fetchThread, .{ app, url, fetch_opts });
|
||||
var worker_thread = try std.Thread.spawn(.{}, fetchThread, .{ app, url.?, fetch_opts });
|
||||
defer worker_thread.join();
|
||||
|
||||
app.network.run();
|
||||
|
||||
@@ -33,10 +33,8 @@ pub fn main() !void {
|
||||
}
|
||||
lp.log.opts.level = .warn;
|
||||
const config = try lp.Config.init(allocator, "legacy-test", .{ .serve = .{
|
||||
.common = .{
|
||||
.tls_verify_host = false,
|
||||
.user_agent_suffix = "internal-tester",
|
||||
},
|
||||
.insecure_disable_tls_host_verification = true,
|
||||
.user_agent_suffix = "internal-tester",
|
||||
} });
|
||||
defer config.deinit(allocator);
|
||||
|
||||
@@ -94,8 +92,8 @@ pub fn main() !void {
|
||||
pub fn run(allocator: Allocator, file: []const u8, session: *lp.Session) !void {
|
||||
const url = try std.fmt.allocPrintSentinel(allocator, "http://localhost:9589/{s}", .{file}, 0);
|
||||
|
||||
const frame = try session.createFrame();
|
||||
defer session.removeFrame();
|
||||
const frame = try session.createPage();
|
||||
defer session.removePage();
|
||||
|
||||
var ls: lp.js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
|
||||
@@ -106,7 +106,7 @@ test "MCP - Actions: click, fill, scroll, hover, press, selectOption, setChecked
|
||||
const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer);
|
||||
defer server.deinit();
|
||||
|
||||
const frame = &server.session.frame.?;
|
||||
const frame = server.session.currentFrame().?;
|
||||
|
||||
{
|
||||
// Test Click
|
||||
@@ -424,7 +424,7 @@ fn testLoadPage(url: [:0]const u8, writer: *std.Io.Writer) !*Server {
|
||||
var server = try Server.init(testing.allocator, testing.test_app, writer);
|
||||
errdefer server.deinit();
|
||||
|
||||
const frame = try server.session.createFrame();
|
||||
const frame = try server.session.createPage();
|
||||
try frame.navigate(url, .{});
|
||||
|
||||
var runner = try server.session.runner(.{});
|
||||
|
||||
@@ -394,8 +394,8 @@ pub fn htmlRunner(comptime path: []const u8, opts: HtmlRunnerOpts) !void {
|
||||
}
|
||||
|
||||
fn runWebApiTest(test_file: [:0]const u8) !void {
|
||||
const frame = try test_session.createFrame();
|
||||
defer test_session.removeFrame();
|
||||
const frame = try test_session.createPage();
|
||||
defer test_session.removePage();
|
||||
|
||||
const url = try std.fmt.allocPrintSentinel(
|
||||
arena_allocator,
|
||||
@@ -453,8 +453,8 @@ const PageTestOpts = struct {
|
||||
wait_until_done: bool = true,
|
||||
};
|
||||
pub fn pageTest(comptime test_file: []const u8, opts: PageTestOpts) !*Frame {
|
||||
const frame = try test_session.createFrame();
|
||||
errdefer test_session.removeFrame();
|
||||
const frame = try test_session.createPage();
|
||||
errdefer test_session.removePage();
|
||||
|
||||
const url = try std.fmt.allocPrintSentinel(
|
||||
arena_allocator,
|
||||
@@ -491,11 +491,9 @@ test "tests:beforeAll" {
|
||||
const test_allocator = @import("root").tracking_allocator;
|
||||
|
||||
test_config = try Config.init(test_allocator, "test", .{ .serve = .{
|
||||
.common = .{
|
||||
.tls_verify_host = false,
|
||||
.user_agent_suffix = "internal-tester",
|
||||
.ws_max_concurrent = 50,
|
||||
},
|
||||
.insecure_disable_tls_host_verification = true,
|
||||
.user_agent_suffix = "internal-tester",
|
||||
.ws_max_concurrent = 50,
|
||||
} });
|
||||
|
||||
test_app = try App.init(test_allocator, &test_config);
|
||||
|
||||
Reference in New Issue
Block a user