Merge branch 'main' into agent

This commit is contained in:
Adrià Arrufat
2026-04-22 07:13:04 +02:00
223 changed files with 285874 additions and 4958 deletions

View File

@@ -117,12 +117,12 @@ const browser = await puppeteer.connect({
// The rest of your script remains the same.
const context = await browser.createBrowserContext();
const page = await context.newPage();
const frame = await context.newPage();
// Dump all the links from the page.
await page.goto('https://demo-browser.lightpanda.io/amiibo/', {waitUntil: "networkidle0"});
// Dump all the links from the frame.
await frame.goto('https://demo-browser.lightpanda.io/amiibo/', {waitUntil: "networkidle0"});
const links = await page.evaluate(() => {
const links = await frame.evaluate(() => {
return Array.from(document.querySelectorAll('a')).map(row => {
return row.getAttribute('href');
});
@@ -130,7 +130,7 @@ const links = await page.evaluate(() => {
console.log(links);
await page.close();
await frame.close();
await context.close();
await browser.disconnect();
```

View File

@@ -93,6 +93,27 @@ pub fn build(b: *Build) !void {
break :blk mod;
};
lightpanda_module.addCSourceFile(.{
.file = b.path("lib/sqlite3/sqlite3.c"),
.flags = &[_][]const u8{
"-DSQLITE_DQS=0",
"-DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1",
"-DSQLITE_USE_ALLOCA=1",
"-DSQLITE_THREADSAFE=1",
"-DSQLITE_TEMP_STORE=3",
"-DSQLITE_ENABLE_API_ARMOR=1",
"-DSQLITE_ENABLE_UNLOCK_NOTIFY",
"-DSQLITE_DEFAULT_FILE_PERMISSIONS=0600",
"-DSQLITE_OMIT_DECLTYPE=1",
"-DSQLITE_OMIT_DEPRECATED=1",
"-DSQLITE_OMIT_LOAD_EXTENSION=1",
"-DSQLITE_OMIT_PROGRESS_CALLBACK=1",
"-DSQLITE_OMIT_SHARED_CACHE",
"-DSQLITE_OMIT_TRACE=1",
"-DSQLITE_OMIT_UTF16=1",
},
});
// Check compilation
const check = b.step("check", "Check if lightpanda compiles");

265977
lib/sqlite3/sqlite3.c Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,7 @@ const Snapshot = @import("browser/js/Snapshot.zig");
const Platform = @import("browser/js/Platform.zig");
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
const Storage = @import("storage/Storage.zig");
const Network = @import("network/Network.zig");
pub const ArenaPool = @import("ArenaPool.zig");
@@ -34,6 +35,7 @@ const App = @This();
network: Network,
config: *const Config,
storage: Storage,
platform: Platform,
snapshot: Snapshot,
telemetry: Telemetry,
@@ -42,26 +44,29 @@ arena_pool: ArenaPool,
app_dir_path: ?[]const u8,
pub fn init(allocator: Allocator, config: *const Config) !*App {
const platform = try Platform.init();
errdefer platform.deinit();
const snapshot = try Snapshot.load();
errdefer snapshot.deinit();
var storage = try Storage.init(allocator, config);
errdefer storage.deinit(allocator);
const app = try allocator.create(App);
errdefer allocator.destroy(app);
app.* = .{
.config = config,
.allocator = allocator,
.platform = platform,
.snapshot = snapshot,
.storage = storage,
.network = undefined,
.platform = undefined,
.snapshot = undefined,
.app_dir_path = undefined,
.telemetry = undefined,
.arena_pool = undefined,
};
app.platform = try Platform.init();
errdefer app.platform.deinit();
app.snapshot = try Snapshot.load();
errdefer app.snapshot.deinit();
app.network = try Network.init(allocator, app, config);
errdefer app.network.deinit();
@@ -91,6 +96,7 @@ pub fn deinit(self: *App) void {
self.snapshot.deinit();
self.platform.deinit();
self.arena_pool.deinit();
self.storage.deinit(allocator);
allocator.destroy(self);
}

View File

@@ -23,6 +23,7 @@ const builtin = @import("builtin");
const dump = @import("browser/dump.zig");
const mcp = @import("mcp.zig");
const Storage = @import("storage/Storage.zig");
const WebBotAuthConfig = @import("network/WebBotAuth.zig").Config;
const log = lp.log;
@@ -220,6 +221,20 @@ pub fn maxPendingConnections(self: *const Config) u31 {
};
}
pub fn storageEngine(self: *const Config) ?Storage.EngineType {
return switch (self.mode) {
inline .serve, .fetch, .mcp => |opts| opts.common.storage_engine,
else => unreachable,
};
}
pub fn storageSqlitePath(self: *const Config) ?[:0]const u8 {
return switch (self.mode) {
inline .serve, .fetch, .mcp => |opts| opts.common.storage_sqlite_path,
else => unreachable,
};
}
pub const Mode = union(RunMode) {
help: bool, // false when being printed because of an error
fetch: Fetch,
@@ -314,6 +329,8 @@ pub const Common = struct {
http_cache_dir: ?[]const u8 = null,
cookie: ?[]const u8 = null,
cookie_jar: ?[]const u8 = null,
storage_engine: ?Storage.EngineType = null,
storage_sqlite_path: ?[:0]const u8 = null,
web_bot_auth_key_file: ?[]const u8 = null,
web_bot_auth_keyid: ?[]const u8 = null,
@@ -469,6 +486,14 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
\\ Path to a directory to use as a Filesystem Cache for network resources.
\\ Omitting this will result is no caching.
\\ Defaults to no caching.
\\
\\--storage-engine
\\ The storage engine to use. Choices are: sqlite.
\\ Default to sqlite.
\\
\\--storage-sqlite-path
\\ Path to SQLite database file for persistent storage.
\\ Use ":memory:" for in-memory storage.
;
// MAX_HELP_LEN|
@@ -1345,6 +1370,27 @@ fn parseCommonArg(
return true;
}
if (std.mem.eql(u8, "--storage-engine", opt)) {
const str = args.next() orelse {
log.fatal(.app, "missing argument value", .{ .arg = "--storage-engine" });
return error.InvalidArgument;
};
common.storage_engine = std.meta.stringToEnum(Storage.EngineType, str) orelse {
log.fatal(.app, "invalid argument value", .{ .arg = opt, .val = str });
return error.InvalidArgument;
};
return true;
}
if (std.mem.eql(u8, "--storage-sqlite-path", opt)) {
const str = args.next() orelse {
log.fatal(.app, "missing argument value", .{ .arg = "--storage-sqlite-path" });
return error.InvalidArgument;
};
common.storage_sqlite_path = try allocator.dupeZ(u8, str);
return true;
}
if (std.mem.eql(u8, "--block-private-networks", opt)) {
common.block_private_networks = true;
return true;

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const lp = @import("lightpanda");
const Page = @import("browser/Page.zig");
const Frame = @import("browser/Frame.zig");
const Transfer = @import("browser/HttpClient.zig").Transfer;
const log = lp.log;
@@ -66,15 +66,15 @@ allocator: Allocator,
mem_pool: std.heap.MemoryPool(Listener),
const EventListeners = struct {
page_remove: List = .{},
page_created: List = .{},
page_navigate: List = .{},
page_navigated: List = .{},
page_network_idle: List = .{},
page_network_almost_idle: List = .{},
page_frame_created: List = .{},
page_dom_content_loaded: List = .{},
page_loaded: List = .{},
frame_remove: List = .{},
frame_created: List = .{},
frame_navigate: List = .{},
frame_navigated: List = .{},
frame_network_idle: List = .{},
frame_network_almost_idle: List = .{},
frame_child_frame_created: List = .{},
frame_dom_content_loaded: List = .{},
frame_loaded: List = .{},
http_request_fail: List = .{},
http_request_start: List = .{},
http_request_intercept: List = .{},
@@ -86,15 +86,15 @@ const EventListeners = struct {
};
const Events = union(enum) {
page_remove: PageRemove,
page_created: *Page,
page_navigate: *const PageNavigate,
page_navigated: *const PageNavigated,
page_network_idle: *const PageNetworkIdle,
page_network_almost_idle: *const PageNetworkAlmostIdle,
page_frame_created: *const PageFrameCreated,
page_dom_content_loaded: *const PageDOMContentLoaded,
page_loaded: *const PageLoaded,
frame_remove: FrameRemove,
frame_created: *Frame,
frame_navigate: *const FrameNavigate,
frame_navigated: *const FrameNavigated,
frame_network_idle: *const FrameNetworkIdle,
frame_network_almost_idle: *const FrameNetworkAlmostIdle,
frame_child_frame_created: *const FrameChildFrameCreated,
frame_dom_content_loaded: *const FrameDOMContentLoaded,
frame_loaded: *const FrameLoaded,
http_request_fail: *const RequestFail,
http_request_start: *const RequestStart,
http_request_intercept: *const RequestIntercept,
@@ -106,58 +106,58 @@ const Events = union(enum) {
};
const EventType = std.meta.FieldEnum(Events);
pub const PageRemove = struct {};
pub const FrameRemove = struct {};
pub const PageNavigate = struct {
pub const FrameNavigate = struct {
req_id: u32,
page_id: u32,
frame_id: u32,
loader_id: u32,
timestamp: u64,
url: [:0]const u8,
opts: Page.NavigateOpts,
opts: Frame.NavigateOpts,
};
pub const PageNavigated = struct {
pub const FrameNavigated = struct {
req_id: u32,
page_id: u32,
frame_id: u32,
loader_id: u32,
timestamp: u64,
url: [:0]const u8,
opts: Page.NavigatedOpts,
opts: Frame.NavigatedOpts,
};
pub const PageNetworkIdle = struct {
pub const FrameNetworkIdle = struct {
req_id: u32,
page_id: u32,
frame_id: u32,
loader_id: u32,
timestamp: u64,
};
pub const PageNetworkAlmostIdle = struct {
pub const FrameNetworkAlmostIdle = struct {
req_id: u32,
page_id: u32,
frame_id: u32,
loader_id: u32,
timestamp: u64,
};
pub const PageFrameCreated = struct {
page_id: u32,
pub const FrameChildFrameCreated = struct {
frame_id: u32,
loader_id: u32,
parent_id: u32,
timestamp: u64,
};
pub const PageDOMContentLoaded = struct {
pub const FrameDOMContentLoaded = struct {
req_id: u32,
page_id: u32,
frame_id: u32,
loader_id: u32,
timestamp: u64,
};
pub const PageLoaded = struct {
pub const FrameLoaded = struct {
req_id: u32,
page_id: u32,
frame_id: u32,
loader_id: u32,
timestamp: u64,
};
@@ -348,8 +348,8 @@ test "Notification" {
defer notifier.deinit();
// noop
notifier.dispatch(.page_navigate, &.{
.page_id = 39,
notifier.dispatch(.frame_navigate, &.{
.loader_id = 39,
.frame_id = 0,
.req_id = 1,
.timestamp = 4,
@@ -359,96 +359,96 @@ test "Notification" {
var tc = TestClient{};
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
notifier.dispatch(.page_navigate, &.{
.page_id = 39,
try notifier.register(.frame_navigate, &tc, TestClient.frameNavigate);
notifier.dispatch(.frame_navigate, &.{
.loader_id = 39,
.frame_id = 0,
.req_id = 1,
.timestamp = 4,
.url = undefined,
.opts = .{},
});
try testing.expectEqual(4, tc.page_navigate);
try testing.expectEqual(4, tc.frame_navigate);
notifier.unregisterAll(&tc);
notifier.dispatch(.page_navigate, &.{
.page_id = 39,
notifier.dispatch(.frame_navigate, &.{
.loader_id = 39,
.frame_id = 0,
.req_id = 1,
.timestamp = 10,
.url = undefined,
.opts = .{},
});
try testing.expectEqual(4, tc.page_navigate);
try testing.expectEqual(4, tc.frame_navigate);
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
notifier.dispatch(.page_navigate, &.{
.page_id = 39,
try notifier.register(.frame_navigate, &tc, TestClient.frameNavigate);
try notifier.register(.frame_navigated, &tc, TestClient.frameNavigated);
notifier.dispatch(.frame_navigate, &.{
.loader_id = 39,
.frame_id = 0,
.req_id = 1,
.timestamp = 10,
.url = undefined,
.opts = .{},
});
notifier.dispatch(.page_navigated, &.{ .page_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 6, .url = undefined, .opts = .{} });
try testing.expectEqual(14, tc.page_navigate);
try testing.expectEqual(6, tc.page_navigated);
notifier.dispatch(.frame_navigated, &.{ .loader_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 6, .url = undefined, .opts = .{} });
try testing.expectEqual(14, tc.frame_navigate);
try testing.expectEqual(6, tc.frame_navigated);
notifier.unregisterAll(&tc);
notifier.dispatch(.page_navigate, &.{
.page_id = 39,
notifier.dispatch(.frame_navigate, &.{
.loader_id = 39,
.frame_id = 0,
.req_id = 1,
.timestamp = 100,
.url = undefined,
.opts = .{},
});
notifier.dispatch(.page_navigated, &.{ .page_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
try testing.expectEqual(14, tc.page_navigate);
try testing.expectEqual(6, tc.page_navigated);
notifier.dispatch(.frame_navigated, &.{ .loader_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
try testing.expectEqual(14, tc.frame_navigate);
try testing.expectEqual(6, tc.frame_navigated);
{
// unregister
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
notifier.dispatch(.page_navigate, &.{ .page_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .page_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(1006, tc.page_navigated);
try notifier.register(.frame_navigate, &tc, TestClient.frameNavigate);
try notifier.register(.frame_navigated, &tc, TestClient.frameNavigated);
notifier.dispatch(.frame_navigate, &.{ .loader_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.frame_navigated, &.{ .loader_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
try testing.expectEqual(114, tc.frame_navigate);
try testing.expectEqual(1006, tc.frame_navigated);
notifier.unregister(.page_navigate, &tc);
notifier.dispatch(.page_navigate, &.{ .page_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .page_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated);
notifier.unregister(.frame_navigate, &tc);
notifier.dispatch(.frame_navigate, &.{ .loader_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.frame_navigated, &.{ .loader_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
try testing.expectEqual(114, tc.frame_navigate);
try testing.expectEqual(2006, tc.frame_navigated);
notifier.unregister(.page_navigated, &tc);
notifier.dispatch(.page_navigate, &.{ .page_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .page_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated);
notifier.unregister(.frame_navigated, &tc);
notifier.dispatch(.frame_navigate, &.{ .loader_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.frame_navigated, &.{ .loader_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
try testing.expectEqual(114, tc.frame_navigate);
try testing.expectEqual(2006, tc.frame_navigated);
// already unregistered, try anyways
notifier.unregister(.page_navigated, &tc);
notifier.dispatch(.page_navigate, &.{ .page_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .page_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated);
notifier.unregister(.frame_navigated, &tc);
notifier.dispatch(.frame_navigate, &.{ .loader_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.frame_navigated, &.{ .loader_id = 39, .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
try testing.expectEqual(114, tc.frame_navigate);
try testing.expectEqual(2006, tc.frame_navigated);
}
}
const TestClient = struct {
page_navigate: u64 = 0,
page_navigated: u64 = 0,
frame_navigate: u64 = 0,
frame_navigated: u64 = 0,
fn pageNavigate(ptr: *anyopaque, data: *const Notification.PageNavigate) !void {
fn frameNavigate(ptr: *anyopaque, data: *const Notification.FrameNavigate) !void {
const self: *TestClient = @ptrCast(@alignCast(ptr));
self.page_navigate += data.timestamp;
self.frame_navigate += data.timestamp;
}
fn pageNavigated(ptr: *anyopaque, data: *const Notification.PageNavigated) !void {
fn frameNavigated(ptr: *anyopaque, data: *const Notification.FrameNavigated) !void {
const self: *TestClient = @ptrCast(@alignCast(ptr));
self.page_navigated += data.timestamp;
self.frame_navigated += data.timestamp;
}
};

View File

@@ -29,13 +29,13 @@ const AXNode = @import("cdp/AXNode.zig");
const CDPNode = @import("cdp/Node.zig");
const log = lp.log;
const Page = lp.Page;
const Frame = lp.Frame;
const Self = @This();
dom_node: *Node,
registry: *CDPNode.Registry,
page: *Page,
frame: *Frame,
arena: std.mem.Allocator,
prune: bool = true,
interactive_only: bool = false,
@@ -44,7 +44,7 @@ max_depth: u32 = std.math.maxInt(u32) - 1,
pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!void {
var visitor = JsonVisitor{ .jw = jw, .tree = self };
var xpath_buffer: std.ArrayList(u8) = .{};
const listener_targets = interactive.buildListenerTargetMap(self.page, self.arena) catch |err| {
const listener_targets = interactive.buildListenerTargetMap(self.frame, self.arena) catch |err| {
log.err(.app, "listener map failed", .{ .err = err });
return error.WriteFailed;
};
@@ -65,7 +65,7 @@ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!
pub fn textStringify(self: @This(), writer: *std.Io.Writer) error{WriteFailed}!void {
var visitor = TextVisitor{ .writer = writer, .tree = self, .depth = 0 };
var xpath_buffer: std.ArrayList(u8) = .empty;
const listener_targets = interactive.buildListenerTargetMap(self.page, self.arena) catch |err| {
const listener_targets = interactive.buildListenerTargetMap(self.frame, self.arena) catch |err| {
log.err(.app, "listener map failed", .{ .err = err });
return error.WriteFailed;
};
@@ -130,7 +130,7 @@ fn walk(
if (tag == .datalist or tag == .option or tag == .optgroup) return;
// Check visibility using the engine's checkVisibility which handles CSS display: none
if (!el.checkVisibilityCached(ctx.visibility_cache, self.page)) {
if (!el.checkVisibilityCached(ctx.visibility_cache, self.frame)) {
return;
}
@@ -166,17 +166,17 @@ fn walk(
checked = input.getChecked();
}
if (el.getAttributeSafe(comptime .wrap("list"))) |list_id| {
options = try extractDataListOptions(list_id, self.page, self.arena);
options = try extractDataListOptions(list_id, self.frame, self.arena);
}
} else if (el.is(Element.Html.TextArea)) |textarea| {
value = textarea.getValue();
} else if (el.is(Element.Html.Select)) |select| {
value = select.getValue(self.page);
options = try extractSelectOptions(el.asNode(), self.page, self.arena);
value = select.getValue(self.frame);
options = try extractSelectOptions(el.asNode(), self.frame, self.arena);
}
if (el.is(Element.Html)) |html_el| {
if (interactive.classifyInteractivity(self.page, el, html_el, ctx.listener_targets, ctx.pointer_events_cache) != null) {
if (interactive.classifyInteractivity(self.frame, el, html_el, ctx.listener_targets, ctx.pointer_events_cache) != null) {
is_interactive = true;
}
}
@@ -190,7 +190,7 @@ fn walk(
try appendXPathSegment(node, ctx.xpath_buffer.writer(self.arena), index);
const xpath = ctx.xpath_buffer.items;
var name = try axn.getName(self.page, self.arena);
var name = try axn.getName(self.frame, self.arena);
const has_explicit_label = if (node.is(Element)) |el|
el.getAttributeSafe(.wrap("aria-label")) != null or el.getAttributeSafe(.wrap("title")) != null
@@ -287,15 +287,15 @@ fn walk(
ctx.xpath_buffer.shrinkRetainingCapacity(initial_xpath_len);
}
fn extractSelectOptions(node: *Node, page: *Page, arena: std.mem.Allocator) ![]OptionData {
fn extractSelectOptions(node: *Node, frame: *Frame, arena: std.mem.Allocator) ![]OptionData {
var options: std.ArrayList(OptionData) = .empty;
var it = node.childrenIterator();
while (it.next()) |child| {
if (child.is(Element)) |el| {
if (el.getTag() == .option) {
if (el.is(Element.Html.Option)) |opt| {
const text = opt.getText(page);
const value = opt.getValue(page);
const text = opt.getText(frame);
const value = opt.getValue(frame);
const selected = opt.getSelected();
try options.append(arena, .{ .text = text, .value = value, .selected = selected });
}
@@ -303,8 +303,8 @@ fn extractSelectOptions(node: *Node, page: *Page, arena: std.mem.Allocator) ![]O
var group_it = child.childrenIterator();
while (group_it.next()) |group_child| {
if (group_child.is(Element.Html.Option)) |opt| {
const text = opt.getText(page);
const value = opt.getValue(page);
const text = opt.getText(frame);
const value = opt.getValue(frame);
const selected = opt.getSelected();
try options.append(arena, .{ .text = text, .value = value, .selected = selected });
}
@@ -315,10 +315,10 @@ fn extractSelectOptions(node: *Node, page: *Page, arena: std.mem.Allocator) ![]O
return options.toOwnedSlice(arena);
}
fn extractDataListOptions(list_id: []const u8, page: *Page, arena: std.mem.Allocator) !?[]OptionData {
if (page.document.getElementById(list_id, page)) |referenced_el| {
fn extractDataListOptions(list_id: []const u8, frame: *Frame, arena: std.mem.Allocator) !?[]OptionData {
if (frame.document.getElementById(list_id, frame)) |referenced_el| {
if (referenced_el.getTag() == .datalist) {
return try extractSelectOptions(referenced_el.asNode(), page, arena);
return try extractSelectOptions(referenced_el.asNode(), frame, arena);
}
}
return null;
@@ -644,12 +644,12 @@ pub fn getNodeDetails(
arena: std.mem.Allocator,
node: *Node,
registry: *CDPNode.Registry,
page: *Page,
frame: *Frame,
) !NodeDetails {
const cdp_node = try registry.register(node);
const axn = AXNode.fromNode(node);
const role = try axn.getRole();
const name = try axn.getName(page, arena);
const name = try axn.getName(frame, arena);
var is_interactive = false;
var is_disabled = false;
@@ -672,7 +672,7 @@ pub fn getNodeDetails(
if (el.getAttributeSafe(comptime .wrap("href"))) |h| {
const URL = lp.URL;
href = URL.resolve(arena, page.base(), h, .{ .encoding = page.charset }) catch h;
href = URL.resolve(arena, frame.base(), h, .{ .encoding = frame.charset }) catch h;
}
if (el.is(Element.Html.Input)) |input| {
@@ -682,19 +682,19 @@ pub fn getNodeDetails(
checked = input.getChecked();
}
if (el.getAttributeSafe(comptime .wrap("list"))) |list_id| {
options = try extractDataListOptions(list_id, page, arena);
options = try extractDataListOptions(list_id, frame, arena);
}
} else if (el.is(Element.Html.TextArea)) |textarea| {
value = textarea.getValue();
} else if (el.is(Element.Html.Select)) |select| {
value = select.getValue(page);
options = try extractSelectOptions(el.asNode(), page, arena);
value = select.getValue(frame);
options = try extractSelectOptions(el.asNode(), frame, arena);
}
if (el.is(Element.Html)) |html_el| {
const listener_targets = try interactive.buildListenerTargetMap(page, arena);
const listener_targets = try interactive.buildListenerTargetMap(frame, arena);
var pointer_events_cache: Element.PointerEventsCache = .empty;
if (interactive.classifyInteractivity(page, el, html_el, listener_targets, &pointer_events_cache) != null) {
if (interactive.classifyInteractivity(frame, el, html_el, listener_targets, &pointer_events_cache) != null) {
is_interactive = true;
}
}
@@ -724,14 +724,14 @@ test "SemanticTree backendDOMNodeId" {
var registry: CDPNode.Registry = .init(testing.allocator);
defer registry.deinit();
var page = try testing.pageTest("cdp/registry1.html", .{});
var frame = try testing.pageTest("cdp/registry1.html", .{});
defer testing.reset();
defer page._session.removePage();
defer frame._session.removeFrame();
const st: Self = .{
.dom_node = page.window._document.asNode(),
.dom_node = frame.window._document.asNode(),
.registry = &registry,
.page = page,
.frame = frame,
.arena = testing.arena_allocator,
.prune = false,
.interactive_only = false,
@@ -748,14 +748,14 @@ test "SemanticTree max_depth" {
var registry: CDPNode.Registry = .init(testing.allocator);
defer registry.deinit();
var page = try testing.pageTest("cdp/registry1.html", .{});
var frame = try testing.pageTest("cdp/registry1.html", .{});
defer testing.reset();
defer page._session.removePage();
defer frame._session.removeFrame();
const st: Self = .{
.dom_node = page.window._document.asNode(),
.dom_node = frame.window._document.asNode(),
.registry = &registry,
.page = page,
.frame = frame,
.arena = testing.arena_allocator,
.prune = false,
.interactive_only = false,

View File

@@ -77,7 +77,7 @@ pub fn getTools(self: *Self) ![]const zenai.provider.Tool {
}
pub fn getCurrentUrl(self: *Self) []const u8 {
const page = self.session.currentPage() orelse return "(no page loaded)";
const page = self.session.currentFrame() orelse return "(no page loaded)";
return page.url;
}

View File

@@ -21,7 +21,7 @@ const lp = @import("lightpanda");
const builtin = @import("builtin");
const js = @import("js/js.zig");
const Page = @import("Page.zig");
const Frame = @import("Frame.zig");
const EventManagerBase = @import("EventManagerBase.zig");
const Node = @import("webapi/Node.zig");
@@ -42,7 +42,7 @@ const IS_DEBUG = builtin.mode == .Debug;
pub const EventManager = @This();
page: *Page,
frame: *Frame,
base: EventManagerBase,
// Used as an optimization in Page._documentIsComplete. If we know there are no
@@ -52,9 +52,9 @@ has_dom_load_listener: bool,
ignore_list: std.ArrayList(*Listener),
pub fn init(arena: Allocator, page: *Page) EventManager {
pub fn init(arena: Allocator, frame: *Frame) EventManager {
return .{
.page = page,
.frame = frame,
.ignore_list = .{},
.has_dom_load_listener = false,
.base = EventManagerBase.init(arena),
@@ -104,10 +104,10 @@ 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.page._session);
defer _ = event.releaseRef(self.frame._session);
// Increment event count for Event Timing API
self.page.window._performance._event_counts.increment(event._type_string.str());
self.frame.window._performance._event_counts.increment(event._type_string.str());
if (comptime IS_DEBUG) {
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
@@ -130,15 +130,15 @@ const DispatchDirectOptions = EventManagerBase.DispatchDirectOptions;
// property handlers. No propagation - just calls the handler and registered listeners.
// Handler can be: null, ?js.Function.Global, ?js.Function.Temp, or js.Function
pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event, handler: anytype, comptime opts: DispatchDirectOptions) !void {
const page = self.page;
const frame = self.frame;
// Set window.event to the currently dispatching event (WHATWG spec)
const window = page.window;
const window = frame.window;
const prev_event = window._current_event;
window._current_event = event;
defer window._current_event = prev_event;
try self.base.dispatchDirect(page.call_arena, page.js, target, event, handler, page._session, opts);
try self.base.dispatchDirect(frame.call_arena, frame.js, target, event, handler, frame._session, opts);
}
/// Check if there are any listeners for a direct dispatch (non-DOM target).
@@ -156,10 +156,10 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
event._dispatch_target = et; // Store original target for composedPath()
}
const page = self.page;
const frame = self.frame;
// Set window.event to the currently dispatching event (WHATWG spec)
const window = page.window;
const window = frame.window;
const prev_event = window._current_event;
window._current_event = event;
defer window._current_event = prev_event;
@@ -170,7 +170,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
// This ensures function handles passed to queueMicrotask remain valid
// throughout the entire dispatch, preventing crashes when microtasks run.
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
frame.js.localScope(&ls);
defer {
if (was_handled) {
ls.local.runMicrotasks();
@@ -178,7 +178,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
ls.deinit();
}
const activation_state = try ActivationState.create(event, target, page);
const activation_state = try ActivationState.create(event, target, frame);
// Defer runs even on early return - ensures event phase is reset
// and default actions execute (unless prevented)
@@ -188,19 +188,19 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
event._stop_immediate_propagation = false;
// Handle checkbox/radio activation rollback or commit
if (activation_state) |state| {
state.restore(event, page);
state.restore(event, frame);
}
// Execute default action if not prevented
if (event._prevent_default) {
// can't return in a defer (╯°□°)╯︵ ┻━┻
} else if (event._type_string.eql(comptime .wrap("click"))) {
page.handleClick(target) catch |err| {
log.warn(.event, "page.click", .{ .err = err });
frame.handleClick(target) catch |err| {
log.warn(.event, "frame.click", .{ .err = err });
};
} else if (event._type_string.eql(comptime .wrap("keydown"))) {
page.handleKeydown(target, event) catch |err| {
log.warn(.event, "page.keydown", .{ .err = err });
frame.handleKeydown(target, event) catch |err| {
log.warn(.event, "frame.keydown", .{ .err = err });
};
}
}
@@ -236,7 +236,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
// The only explicit exception is "load"
if (event._type_string.eql(comptime .wrap("load")) == false) {
if (path_len < path_buffer.len) {
path_buffer[path_len] = page.window.asEventTarget();
path_buffer[path_len] = frame.window.asEventTarget();
path_len += 1;
}
}
@@ -312,7 +312,7 @@ const DispatchPhaseOpts = struct {
};
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, local: *const js.Local, comptime opts: DispatchPhaseOpts) !void {
const page = self.page;
const frame = self.frame;
const base = &self.base;
// Track dispatch depth for deferred removal
@@ -392,7 +392,7 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
switch (listener.function) {
.value => |value| try local.toLocal(value).callWithThis(void, current_target, .{event}),
.string => |string| {
const str = try page.call_arena.dupeZ(u8, string.str());
const str = try frame.call_arena.dupeZ(u8, string.str());
try local.eval(str, null);
},
.object => |obj_global| {
@@ -424,7 +424,7 @@ fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?j
else => return null,
};
return html_element.getAttributeFunction(handler_type, self.page) catch |err| {
return html_element.getAttributeFunction(handler_type, self.frame) catch |err| {
log.warn(.event, "inline html callback", .{ .type = handler_type, .err = err });
return null;
};
@@ -502,7 +502,7 @@ const ActivationState = struct {
const Input = Element.Html.Input;
fn create(event: *const Event, target: *Node, page: *Page) !?ActivationState {
fn create(event: *const Event, target: *Node, frame: *Frame) !?ActivationState {
if (event._type_string.eql(comptime .wrap("click")) == false) {
return null;
}
@@ -517,12 +517,12 @@ const ActivationState = struct {
// For radio buttons, find the currently checked radio in the group
if (input._input_type == .radio and !old_checked) {
previously_checked_radio = try findCheckedRadioInGroup(input, page);
previously_checked_radio = try findCheckedRadioInGroup(input, frame);
}
// Toggle checkbox or check radio (which unchecks others in group)
const new_checked = if (input._input_type == .checkbox) !old_checked else true;
try input.setChecked(new_checked, page);
try input.setChecked(new_checked, frame);
return .{
.input = input,
@@ -531,7 +531,7 @@ const ActivationState = struct {
};
}
fn restore(self: *const ActivationState, event: *const Event, page: *Page) void {
fn restore(self: *const ActivationState, event: *const Event, frame: *Frame) void {
const input = self.input;
if (event._prevent_default) {
// Rollback: restore previous state
@@ -549,16 +549,16 @@ const ActivationState = struct {
// For checkboxes, state always changes. For radios, only if was unchecked.
const state_changed = (input._input_type == .checkbox) or !self.old_checked;
if (state_changed and input.asElement().asNode().isConnected()) {
fireEvent(page, input, "input") catch |err| {
fireEvent(frame, input, "input") catch |err| {
log.warn(.event, "input event", .{ .err = err });
};
fireEvent(page, input, "change") catch |err| {
fireEvent(frame, input, "change") catch |err| {
log.warn(.event, "change event", .{ .err = err });
};
}
}
fn findCheckedRadioInGroup(input: *Input, page: *Page) !?*Input {
fn findCheckedRadioInGroup(input: *Input, frame: *Frame) !?*Input {
const elem = input.asElement();
const name = elem.getAttributeSafe(comptime .wrap("name")) orelse return null;
@@ -566,7 +566,7 @@ const ActivationState = struct {
return null;
}
const form = input.getForm(page);
const form = input.getForm(frame);
// Walk from the root of the tree containing this element
// This handles both document-attached and orphaned elements
@@ -594,7 +594,7 @@ const ActivationState = struct {
}
// Check if same form context
const other_form = other_input.getForm(page);
const other_form = other_input.getForm(frame);
if (form) |f| {
const of = other_form orelse continue;
if (f != of) {
@@ -613,13 +613,13 @@ const ActivationState = struct {
}
// Fire input or change event
fn fireEvent(page: *Page, input: *Input, comptime typ: []const u8) !void {
fn fireEvent(frame: *Frame, input: *Input, comptime typ: []const u8) !void {
const event = try Event.initTrusted(comptime .wrap(typ), .{
.bubbles = true,
.cancelable = false,
}, page);
}, frame);
const target = input.asElement().asEventTarget();
try page._event_manager.dispatch(target, event);
try frame._event_manager.dispatch(target, event);
}
};

View File

@@ -24,7 +24,7 @@ const reflect = @import("reflect.zig");
const SlabAllocator = @import("../slab.zig").SlabAllocator;
const Page = @import("Page.zig");
const Frame = @import("Frame.zig");
const Node = @import("webapi/Node.zig");
const Event = @import("webapi/Event.zig");
const UIEvent = @import("webapi/event/UIEvent.zig");
@@ -266,23 +266,23 @@ pub fn blob(_: *const Factory, arena: Allocator, child: anytype) !*@TypeOf(child
return chain.get(1);
}
pub fn abstractRange(_: *const Factory, arena: Allocator, child: anytype, page: *Page) !*@TypeOf(child) {
pub fn abstractRange(_: *const Factory, arena: Allocator, child: anytype, frame: *Frame) !*@TypeOf(child) {
const chain = try PrototypeChain(&.{ AbstractRange, @TypeOf(child) }).allocate(arena);
const doc = page.document.asNode();
const doc = frame.document.asNode();
const abstract_range = chain.get(0);
abstract_range.* = AbstractRange{
._rc = .{},
._arena = arena,
._page_id = page.id,
._type = unionInit(AbstractRange.Type, chain.get(1)),
._end_offset = 0,
._start_offset = 0,
._end_container = doc,
._start_container = doc,
._frame_loader_id = frame._loader_id,
._type = unionInit(AbstractRange.Type, chain.get(1)),
};
chain.setLeaf(1, child);
page._live_ranges.append(&abstract_range._range_link);
frame._live_ranges.append(&abstract_range._range_link);
return chain.get(1);
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -17,13 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const builtin = @import("builtin");
const posix = std.posix;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const lp = @import("lightpanda");
const log = lp.log;
const builtin = @import("builtin");
const URL = @import("URL.zig");
const Config = @import("../Config.zig");
const Notification = @import("../Notification.zig");
@@ -34,26 +30,31 @@ const http = @import("../network/http.zig");
const Network = @import("../network/Network.zig");
const Robots = @import("../network/Robots.zig");
const Cache = @import("../network/cache/Cache.zig");
const CacheMetadata = Cache.CachedMetadata;
const CachedResponse = Cache.CachedResponse;
const timestamp = @import("../datetime.zig").timestamp;
const log = lp.log;
const posix = std.posix;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const IS_DEBUG = builtin.mode == .Debug;
pub const Method = http.Method;
pub const Headers = http.Headers;
pub const ResponseHead = http.ResponseHead;
pub const HeaderIterator = http.HeaderIterator;
const CacheMetadata = Cache.CachedMetadata;
const CachedResponse = Cache.CachedResponse;
// This is loosely tied to a browser Page. Loading all the <scripts>, doing
// XHR requests, and loading imports all happens through here. Sine the app
// currently supports 1 browser and 1 page at-a-time, we only have 1 Client and
// re-use it from page to page. This allows us better re-use of the various
// currently supports 1 browser and 1 frame at-a-time, we only have 1 Client and
// re-use it from frame to frame. This allows us better re-use of the various
// buffers/caches (including keepalive connections) that libcurl has.
//
// The app has other secondary http needs, like telemetry. While we want to
// share some things (namely the ca blob, and maybe some configuration
// (TODO: ??? should proxy settings be global ???)), we're able to do call
// client.abort() to abort the transfers being made by a page, without impacting
// client.abort() to abort the transfers being made by a frame, without impacting
// those other http requests.
pub const Client = @This();
@@ -328,7 +329,7 @@ fn abortConnections(list: std.DoublyLinkedList, comptime abort_all: bool, frame_
}
},
.websocket => |ws| {
if ((comptime abort_all) or ws._page._frame_id == frame_id) {
if ((comptime abort_all) or ws._frame._frame_id == frame_id) {
ws.kill();
}
},
@@ -516,8 +517,8 @@ fn fetchRobotsThenProcessRequest(self: *Client, robots_url: [:0]const u8, req: R
.method = .GET,
.headers = headers,
.blocking = false,
.page_id = req.page_id,
.frame_id = req.frame_id,
.loader_id = req.loader_id,
.cookie_jar = req.cookie_jar,
.cookie_origin = req.cookie_origin,
.notification = req.notification,
@@ -769,6 +770,7 @@ fn makeTransfer(self: *Client, req: Request) !*Transfer {
const id = self.incrReqId();
transfer.* = .{
.start_time = timestamp(.monotonic),
.arena = ArenaAllocator.init(self.allocator),
.id = id,
.url = req.url,
@@ -1155,8 +1157,8 @@ fn ensureNoActiveConnection(self: *const Client) !void {
}
pub const Request = struct {
page_id: u32,
frame_id: u32,
loader_id: u32,
method: Method,
url: [:0]const u8,
headers: http.Headers,
@@ -1292,6 +1294,7 @@ pub const Transfer = struct {
bytes_received: usize = 0,
_pending_cache_metadata: ?*CacheMetadata = null,
start_time: u64,
aborted: bool = false,
// We'll store the response header here
@@ -1373,7 +1376,7 @@ pub const Transfer = struct {
self.deinit();
}
// internal, when the page is shutting down. Doesn't have the same ceremony
// internal, when the frame is shutting down. Doesn't have the same ceremony
// as abort (doesn't send a notification, doesn't invoke an error callback)
fn kill(self: *Transfer) void {
if (self.req.shutdown_callback) |cb| {

View File

@@ -21,7 +21,7 @@ const lp = @import("lightpanda");
const builtin = @import("builtin");
const js = @import("js/js.zig");
const Page = @import("Page.zig");
const Frame = @import("Frame.zig");
const Session = @import("Session.zig");
const HttpClient = @import("HttpClient.zig");
@@ -33,17 +33,17 @@ const IS_DEBUG = builtin.mode == .Debug;
const Runner = @This();
page: *Page,
frame: *Frame,
session: *Session,
http_client: *HttpClient,
pub const Opts = struct {};
pub fn init(session: *Session, _: Opts) !Runner {
const page = &(session.page orelse return error.NoPage);
const frame = &(session.frame orelse return error.NoPage);
return .{
.page = page,
.frame = frame,
.session = session,
.http_client = session.browser.http_client,
};
@@ -78,7 +78,7 @@ fn _wait(self: *Runner, comptime is_cdp: bool, opts: WaitOpts) !CDPWaitResult {
error.JsError => {}, // already logged (with hopefully more context)
else => log.err(.browser, "session wait", .{
.err = err,
.url = self.page.url,
.url = self.frame.url,
}),
}
return err;
@@ -127,12 +127,12 @@ pub fn tickCDP(self: *Runner, opts: TickOpts) !CDPTickResult {
}
fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !CDPTickResult {
const page = self.page;
const frame = self.frame;
const http_client = self.http_client;
switch (page._parse_state) {
switch (frame._parse_state) {
.pre, .raw, .text, .image => {
// The main page hasn't started/finished navigating.
// The main frame hasn't started/finished navigating.
// There's no JS to run, and no reason to run the scheduler.
if (http_client.http_active == 0 and (comptime is_cdp) == false) {
// haven't started navigating, I guess.
@@ -152,7 +152,7 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !CDPTickResult {
const session = self.session;
if (session.queued_navigation.items.len != 0) {
try session.processQueuedNavigation();
self.page = &session.page.?; // might have changed
self.frame = &session.frame.?; // might have changed
return .{ .ok = 0 };
}
const browser = session.browser;
@@ -166,26 +166,26 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !CDPTickResult {
try browser.runMacrotasks();
// Each call to this runs scheduled load events.
try page.dispatchLoad();
try frame.dispatchLoad();
const http_active = http_client.http_active;
const total_network_activity = http_active + http_client.intercepted;
if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
page.notifyNetworkAlmostIdle();
if (frame._notified_network_almost_idle.check(total_network_activity <= 2)) {
frame.notifyNetworkAlmostIdle();
}
if (page._notified_network_idle.check(total_network_activity == 0)) {
page.notifyNetworkIdle();
if (frame._notified_network_idle.check(total_network_activity == 0)) {
frame.notifyNetworkIdle();
}
switch (opts.until) {
.done => {},
.domcontentloaded => if (page._load_state == .load or page._load_state == .complete) {
.domcontentloaded => if (frame._load_state == .load or frame._load_state == .complete) {
return .done;
},
.load => if (page._load_state == .complete) {
.load => if (frame._load_state == .complete) {
return .done;
},
.networkidle => if (page._notified_network_idle == .done) {
.networkidle => if (frame._notified_network_idle == .done) {
return .done;
},
}
@@ -231,7 +231,7 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !CDPTickResult {
return .{ .ok = 0 };
},
.err => |err| {
page._parse_state = .{ .raw_done = @errorName(err) };
frame._parse_state = .{ .raw_done = @errorName(err) };
return err;
},
.raw_done => {
@@ -255,9 +255,9 @@ pub fn waitForSelector(self: *Runner, selector: [:0]const u8, timeout_ms: u32) !
const parsed_selector = try Selector.parseLeaky(arena, selector);
while (true) {
// self.page can change between ticks
const page = self.page;
if (try parsed_selector.query(page.document.asNode(), page)) |el| {
// self.frame can change between ticks
const frame = self.frame;
if (try parsed_selector.query(frame.document.asNode(), frame)) |el| {
return el;
}
@@ -280,11 +280,11 @@ pub fn waitForScript(runner: *Runner, script: [:0]const u8, timeout_ms: u32) !vo
var timer = try std.time.Timer.start();
while (true) {
const page = runner.page;
const frame = runner.frame;
// Execute the script and check if it returns truthy
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
frame.js.localScope(&ls);
defer ls.deinit();
var try_catch: js.TryCatch = undefined;
@@ -292,7 +292,7 @@ pub fn waitForScript(runner: *Runner, script: [:0]const u8, timeout_ms: u32) !vo
defer try_catch.deinit();
const value = ls.local.exec(script, "wait_script") catch |err| {
const caught = try_catch.caughtOrError(page.call_arena, err);
const caught = try_catch.caughtOrError(frame.call_arena, err);
log.err(.app, "wait script error", .{ .err = caught });
return error.ScriptError;
};
@@ -322,35 +322,35 @@ test "Runner: no page" {
}
test "Runner: waitForSelector timeout" {
const page = try testing.pageTest("runner/runner1.html", .{});
defer page._session.removePage();
const frame = try testing.pageTest("runner/runner1.html", .{});
defer frame._session.removeFrame();
var runner = try page._session.runner(.{});
var runner = try frame._session.runner(.{});
try testing.expectError(error.Timeout, runner.waitForSelector("#nope", 10));
}
test "Runner: waitForSelector" {
defer testing.reset();
const page = try testing.pageTest("runner/runner1.html", .{});
defer page._session.removePage();
const frame = try testing.pageTest("runner/runner1.html", .{});
defer frame._session.removeFrame();
var runner = try page._session.runner(.{});
var runner = try frame._session.runner(.{});
const el = try runner.waitForSelector("#sel1", 10);
try testing.expectEqual("selector-1-content", try el.asNode().getTextContentAlloc(testing.arena_allocator));
}
test "Runner: waitForScript timeout" {
const page = try testing.pageTest("runner/runner1.html", .{});
defer page._session.removePage();
const frame = try testing.pageTest("runner/runner1.html", .{});
defer frame._session.removeFrame();
var runner = try page._session.runner(.{});
var runner = try frame._session.runner(.{});
try testing.expectError(error.Timeout, runner.waitForScript("document.querySelector('#nope')", 10));
}
test "Runner: waitForScript" {
const page = try testing.pageTest("runner/runner1.html", .{});
defer page._session.removePage();
const frame = try testing.pageTest("runner/runner1.html", .{});
defer frame._session.removeFrame();
var runner = try page._session.runner(.{});
var runner = try frame._session.runner(.{});
try runner.waitForScript("document.querySelector('#sel1')", 10);
}

View File

@@ -25,7 +25,7 @@ const http = @import("../network/http.zig");
const js = @import("js/js.zig");
const URL = @import("URL.zig");
const Page = @import("Page.zig");
const Frame = @import("Frame.zig");
const Element = @import("webapi/Element.zig");
@@ -36,7 +36,7 @@ const IS_DEBUG = builtin.mode == .Debug;
const ScriptManager = @This();
page: *Page,
frame: *Frame,
// used to prevent recursive evaluation
is_evaluating: bool,
@@ -79,13 +79,13 @@ imported_modules: std.StringHashMapUnmanaged(ImportedModule),
// importmap contains resolved urls.
importmap: std.StringHashMapUnmanaged([:0]const u8),
// have we notified the page that all scripts are loaded (used to fire the "load"
// have we notified the frame that all scripts are loaded (used to fire the "load"
// event).
page_notified_of_completion: bool,
frame_notified_of_completion: bool,
pub fn init(allocator: Allocator, http_client: *HttpClient, page: *Page) ScriptManager {
pub fn init(allocator: Allocator, http_client: *HttpClient, frame: *Frame) ScriptManager {
return .{
.page = page,
.frame = frame,
.async_scripts = .{},
.defer_scripts = .{},
.ready_scripts = .{},
@@ -95,7 +95,7 @@ pub fn init(allocator: Allocator, http_client: *HttpClient, page: *Page) ScriptM
.imported_modules = .empty,
.client = http_client,
.static_scripts_done = false,
.page_notified_of_completion = false,
.frame_notified_of_completion = false,
};
}
@@ -104,7 +104,7 @@ pub fn deinit(self: *ScriptManager) void {
self.reset();
self.imported_modules.deinit(self.allocator);
// we don't deinit self.importmap b/c we use the page's arena for its
// we don't deinit self.importmap b/c we use the frame's arena for its
// allocations.
}
@@ -118,7 +118,7 @@ pub fn reset(self: *ScriptManager) void {
}
self.imported_modules.clearRetainingCapacity();
// Our allocator is the page arena, it's been reset. We cannot use
// Our allocator is the frame arena, it's been reset. We cannot use
// clearAndRetainCapacity, since that space is no longer ours
self.importmap = .empty;
@@ -137,7 +137,7 @@ fn clearList(list: *std.DoublyLinkedList) void {
fn getHeaders(self: *ScriptManager) !http.Headers {
var headers = try self.client.newHeaders();
try self.page.headersForRequest(&headers);
try self.frame.headersForRequest(&headers);
return headers;
}
@@ -185,16 +185,16 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
};
var handover = false;
const page = self.page;
const frame = self.frame;
const arena = try page.getArena(.large, "SM.addFromElement");
const arena = try frame.getArena(.large, "SM.addFromElement");
errdefer if (!handover) {
page.releaseArena(arena);
frame.releaseArena(arena);
};
var source: Script.Source = undefined;
var remote_url: ?[:0]const u8 = null;
const base_url = page.base();
const base_url = frame.base();
if (element.getAttributeSafe(comptime .wrap("src"))) |src| {
if (try parseDataURI(arena, src)) |data_uri| {
source = .{ .@"inline" = data_uri };
@@ -211,7 +211,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
if (inline_source.len == 0) {
// we haven't set script_element._executed = true yet, which is good.
// If content is appended to the script, we will execute it then.
page.releaseArena(arena);
frame.releaseArena(arena);
return;
}
source = .{ .@"inline" = inline_source };
@@ -274,7 +274,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
if (comptime IS_DEBUG) {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
frame.js.localScope(&ls);
defer ls.deinit();
log.debug(.http, "script queue", .{
@@ -294,14 +294,14 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
.url = url,
.ctx = script,
.method = .GET,
.page_id = page.id,
.frame_id = page._frame_id,
.frame_id = frame._frame_id,
.loader_id = frame._loader_id,
.headers = try self.getHeaders(),
.blocking = is_blocking,
.cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.cookie_jar = &frame._session.cookie_jar,
.cookie_origin = frame.url,
.resource_type = .script,
.notification = page._session.notification,
.notification = frame._session.notification,
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
.header_callback = Script.headerCallback,
.data_callback = Script.dataCallback,
@@ -338,7 +338,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
self.is_evaluating = was_evaluating;
script.deinit();
}
return script.eval(page);
return script.eval(frame);
}
}
@@ -368,9 +368,9 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
}
errdefer _ = self.imported_modules.remove(url);
const page = self.page;
const arena = try page.getArena(.large, "SM.preloadImport");
errdefer page.releaseArena(arena);
const frame = self.frame;
const arena = try frame.getArena(.large, "SM.preloadImport");
errdefer frame.releaseArena(arena);
const script = try arena.create(Script);
script.* = .{
@@ -389,7 +389,7 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
if (comptime IS_DEBUG) {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
frame.js.localScope(&ls);
defer ls.deinit();
log.debug(.http, "script queue", .{
@@ -410,13 +410,13 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
.url = url,
.ctx = script,
.method = .GET,
.page_id = page.id,
.frame_id = page._frame_id,
.frame_id = frame._frame_id,
.loader_id = frame._loader_id,
.headers = try self.getHeaders(),
.cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.cookie_jar = &frame._session.cookie_jar,
.cookie_origin = frame.url,
.resource_type = .script,
.notification = page._session.notification,
.notification = frame._session.notification,
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
.header_callback = Script.headerCallback,
.data_callback = Script.dataCallback,
@@ -469,9 +469,9 @@ pub fn waitForImport(self: *ScriptManager, url: [:0]const u8) !ModuleSource {
}
pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.Callback, cb_data: *anyopaque, referrer: []const u8) !void {
const page = self.page;
const arena = try page.getArena(.large, "SM.getAsyncImport");
errdefer page.releaseArena(arena);
const frame = self.frame;
const arena = try frame.getArena(.large, "SM.getAsyncImport");
errdefer frame.releaseArena(arena);
const script = try arena.create(Script);
script.* = .{
@@ -491,7 +491,7 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
if (comptime IS_DEBUG) {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
frame.js.localScope(&ls);
defer ls.deinit();
log.debug(.http, "script queue", .{
@@ -515,14 +515,14 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
self.client.request(.{
.url = url,
.method = .GET,
.page_id = page.id,
.frame_id = page._frame_id,
.frame_id = frame._frame_id,
.loader_id = frame._loader_id,
.headers = try self.getHeaders(),
.ctx = script,
.resource_type = .script,
.cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.notification = page._session.notification,
.cookie_jar = &frame._session.cookie_jar,
.cookie_origin = frame.url,
.notification = frame._session.notification,
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
.header_callback = Script.headerCallback,
.data_callback = Script.dataCallback,
@@ -548,7 +548,7 @@ fn evaluate(self: *ScriptManager) void {
return;
}
const page = self.page;
const frame = self.frame;
self.is_evaluating = true;
defer self.is_evaluating = false;
@@ -557,7 +557,7 @@ fn evaluate(self: *ScriptManager) void {
switch (script.mode) {
.async => {
defer script.deinit();
script.eval(page);
script.eval(frame);
},
.import_async => |ia| {
if (script.status < 200 or script.status > 299) {
@@ -595,21 +595,21 @@ fn evaluate(self: *ScriptManager) void {
_ = self.defer_scripts.popFirst();
script.deinit();
}
script.eval(page);
script.eval(frame);
}
// At this point all normal scripts and deferred scripts are done, PLUS
// the page has signaled that it's done parsing HTML (static_scripts_done == true).
// the frame has signaled that it's done parsing HTML (static_scripts_done == true).
//
// When all scripts (normal and deferred) are done loading, the document
// state changes (this ultimately triggers the DOMContentLoaded event).
// Page makes this safe to call multiple times.
page.documentIsLoaded();
frame.documentIsLoaded();
if (self.async_scripts.first == null and self.page_notified_of_completion == false) {
self.page_notified_of_completion = true;
page.scriptsCompletedLoading();
if (self.async_scripts.first == null and self.frame_notified_of_completion == false) {
self.frame_notified_of_completion = true;
frame.scriptsCompletedLoading();
}
}
@@ -622,7 +622,7 @@ fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
const imports = try std.json.parseFromSliceLeaky(
Imports,
self.page.arena,
self.frame.arena,
content,
.{ .allocate = .alloc_always },
);
@@ -633,13 +633,13 @@ fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
// > base URL of the document containing the import map.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_modules_using_import_maps
const resolved_url = try URL.resolve(
self.page.arena,
self.page.base(),
self.frame.arena,
self.frame.base(),
entry.value_ptr.*,
.{},
);
try self.importmap.put(self.page.arena, entry.key_ptr.*, resolved_url);
try self.importmap.put(self.frame.arena, entry.key_ptr.*, resolved_url);
}
}
@@ -700,7 +700,7 @@ pub const Script = struct {
};
fn deinit(self: *Script) void {
self.manager.page.releaseArena(self.arena);
self.manager.frame.releaseArena(self.arena);
}
fn startCallback(response: HttpClient.Response) !void {
@@ -843,7 +843,7 @@ pub const Script = struct {
manager.evaluate();
}
fn eval(self: *Script, page: *Page) void {
fn eval(self: *Script, frame: *Frame) void {
// never evaluated, source is passed back to v8, via callbacks.
if (comptime IS_DEBUG) {
std.debug.assert(self.mode != .import_async);
@@ -852,21 +852,21 @@ pub const Script = struct {
std.debug.assert(self.mode != .import);
}
if (page.isGoingAway()) {
// don't evaluate scripts for a dying page.
if (frame.isGoingAway()) {
// don't evaluate scripts for a dying frame.
return;
}
const script_element = self.script_element.?;
const previous_script = page.document._current_script;
page.document._current_script = script_element;
defer page.document._current_script = previous_script;
const previous_script = frame.document._current_script;
frame.document._current_script = script_element;
defer frame.document._current_script = previous_script;
// Clear the document.write insertion point for this script
const previous_write_insertion_point = page.document._write_insertion_point;
page.document._write_insertion_point = null;
defer page.document._write_insertion_point = previous_write_insertion_point;
const previous_write_insertion_point = frame.document._write_insertion_point;
frame.document._write_insertion_point = null;
defer frame.document._write_insertion_point = previous_write_insertion_point;
// inline scripts aren't cached. remote ones are.
const cacheable = self.source == .remote;
@@ -880,7 +880,7 @@ pub const Script = struct {
});
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
frame.js.localScope(&ls);
defer ls.deinit();
const local = &ls.local;
@@ -888,21 +888,21 @@ pub const Script = struct {
// Handle importmap special case here: the content is a JSON containing
// imports.
if (self.kind == .importmap) {
page._script_manager.parseImportmap(self) catch |err| {
frame._script_manager.parseImportmap(self) catch |err| {
log.err(.browser, "parse importmap script", .{
.err = err,
.src = url,
.kind = self.kind,
.cacheable = cacheable,
});
self.executeCallback(comptime .wrap("error"), page);
self.executeCallback(comptime .wrap("error"), frame);
return;
};
self.executeCallback(comptime .wrap("load"), page);
self.executeCallback(comptime .wrap("load"), frame);
return;
}
defer page._event_manager.clearIgnoreList();
defer frame._event_manager.clearIgnoreList();
var try_catch: js.TryCatch = undefined;
try_catch.init(local);
@@ -914,7 +914,7 @@ pub const Script = struct {
.javascript => _ = local.eval(content, url) catch break :blk false,
.module => {
// We don't care about waiting for the evaluation here.
page.js.module(false, local, content, url, cacheable) catch break :blk false;
frame.js.module(false, local, content, url, cacheable) catch break :blk false;
},
.importmap => unreachable, // handled before the try/catch.
}
@@ -927,29 +927,29 @@ pub const Script = struct {
defer {
local.runMacrotasks(); // also runs microtasks
_ = page.js.scheduler.run() catch |err| {
log.err(.page, "scheduler", .{ .err = err });
_ = frame.js.scheduler.run() catch |err| {
log.err(.frame, "scheduler", .{ .err = err });
};
}
if (success) {
self.executeCallback(comptime .wrap("load"), page);
self.executeCallback(comptime .wrap("load"), frame);
return;
}
const caught = try_catch.caughtOrError(page.call_arena, error.Unknown);
const caught = try_catch.caughtOrError(frame.call_arena, error.Unknown);
log.warn(.js, "eval script", .{
.url = url,
.caught = caught,
.cacheable = cacheable,
});
self.executeCallback(comptime .wrap("error"), page);
self.executeCallback(comptime .wrap("error"), frame);
}
fn executeCallback(self: *const Script, typ: String, page: *Page) void {
fn executeCallback(self: *const Script, typ: String, frame: *Frame) void {
const Event = @import("webapi/Event.zig");
const event = Event.initTrusted(typ, .{}, page) catch |err| {
const event = Event.initTrusted(typ, .{}, frame) catch |err| {
log.warn(.js, "script internal callback", .{
.url = self.url,
.type = typ,
@@ -957,7 +957,7 @@ pub const Script = struct {
});
return;
};
page._event_manager.dispatchOpts(self.script_element.?.asNode().asEventTarget(), event, .{ .apply_ignore = true }) catch |err| {
frame._event_manager.dispatchOpts(self.script_element.?.asNode().asEventTarget(), event, .{ .apply_ignore = true }) catch |err| {
log.warn(.js, "script callback", .{
.url = self.url,
.type = typ,

View File

@@ -28,26 +28,26 @@ const storage = @import("webapi/storage/storage.zig");
const Navigation = @import("webapi/navigation/Navigation.zig");
const History = @import("webapi/History.zig");
const Page = @import("Page.zig");
const Frame = @import("Frame.zig");
pub const Runner = @import("Runner.zig");
const Browser = @import("Browser.zig");
const Factory = @import("Factory.zig");
const Notification = @import("../Notification.zig");
const QueuedNavigation = Page.QueuedNavigation;
const QueuedNavigation = Frame.QueuedNavigation;
const log = lp.log;
const ArenaPool = App.ArenaPool;
const Allocator = std.mem.Allocator;
const IS_DEBUG = builtin.mode == .Debug;
// You can create successively multiple pages for a session, but you must
// deinit a page before running another one. It manages two distinct lifetimes.
// You can create successively multiple frames for a session, but you must
// deinit a frame before running another one. It manages two distinct lifetimes.
//
// The first is the lifetime of the Session itself, where pages are created and
// 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 page hierarchy, i.e. \
// the root page and all of its frames (and all of their frames.)
// 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.)
const Session = @This();
// These are the fields that remain intact for the duration of the Session
@@ -59,12 +59,12 @@ storage_shed: storage.Shed,
notification: *Notification,
cookie_jar: storage.Cookie.Jar,
// These are the fields that get reset whenever the Session's page (the root) is reset.
// These are the fields that get reset whenever the Session's frame (the root) is reset.
factory: Factory,
page_arena: Allocator,
frame_arena: Allocator,
// Origin map for same-origin context sharing. Scoped to the root page lifetime.
// 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,
@@ -75,7 +75,7 @@ identity: js.Identity = .{},
// 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 page resets
// 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),
@@ -87,28 +87,28 @@ globals: std.ArrayList(v8.Global) = .empty,
// Lives at Session level so objects holding Temps can outlive individual Identities.
temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Shared resources for all pages in this session.
// These live for the duration of the page tree (root + frames).
// Shared resources for all frames in this session.
// These live for the duration of the frame tree (root + frames).
arena_pool: *ArenaPool,
page: ?Page,
frame: ?Frame,
// 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(*Page),
queued_navigation_2: std.ArrayList(*Page),
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(*Page),
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(*Page),
queued_queued_navigation: std.ArrayList(*Frame),
page_id_gen: u32 = 0,
frame_id_gen: u32 = 0,
loader_id_gen: u32 = 0,
pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void {
const allocator = browser.app.allocator;
@@ -117,17 +117,17 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
const arena = try arena_pool.acquire(.small, "Session");
errdefer arena_pool.release(arena);
const page_arena = try arena_pool.acquire(.large, "Session.page_arena");
errdefer arena_pool.release(page_arena);
const frame_arena = try arena_pool.acquire(.large, "Session.frame_arena");
errdefer arena_pool.release(frame_arena);
self.* = .{
.page = null,
.frame = null,
.arena = arena,
.arena_pool = arena_pool,
.page_arena = page_arena,
.factory = Factory.init(page_arena),
.frame_arena = frame_arena,
.factory = Factory.init(frame_arena),
.history = .{},
// The prototype (EventTarget) for Navigation is created when a Page is created.
// The prototype (EventTarget) for Navigation is created when a Frame is created.
.navigation = .{ ._proto = undefined },
.storage_shed = .{},
.browser = browser,
@@ -143,52 +143,52 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
}
pub fn deinit(self: *Session) void {
if (self.page != null) {
self.removePage();
if (self.frame != null) {
self.removeFrame();
}
self.cookie_jar.deinit();
self.fc_identity_pool.deinit();
self.storage_shed.deinit(self.browser.app.allocator);
self.arena_pool.release(self.page_arena);
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 Page is just returned as a convenience
pub fn createPage(self: *Session) !*Page {
lp.assert(self.page == null, "Session.createPage - page not null", .{});
// 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", .{});
self.page = @as(Page, undefined);
const page = &self.page.?;
try Page.init(page, self.nextFrameId(), self, null);
self.frame = @as(Frame, undefined);
const frame = &self.frame.?;
try Frame.init(frame, self.nextFrameId(), self, null);
// Creates a new NavigationEventTarget for this page.
try self.navigation.onNewPage(page);
// Creates a new NavigationEventTarget for this frame.
try self.navigation.onNewFrame(frame);
if (comptime IS_DEBUG) {
log.debug(.browser, "create page", .{});
log.debug(.browser, "create frame", .{});
}
// start JS env
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
self.notification.dispatch(.page_created, page);
// Inform CDP the main frame has been created such that additional context for other Worlds can be created as well
self.notification.dispatch(.frame_created, frame);
return page;
return frame;
}
pub fn removePage(self: *Session) void {
// Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one
self.notification.dispatch(.page_remove, .{});
lp.assert(self.page != null, "Session.removePage - page is null", .{});
pub fn removeFrame(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", .{});
self.page.?.deinit(false);
self.page = null;
self.frame.?.deinit(false);
self.frame = null;
self.navigation.onRemovePage();
self.resetPageResources();
self.navigation.onRemoveFrame();
self.resetFrameResources();
if (comptime IS_DEBUG) {
log.debug(.browser, "remove page", .{});
log.debug(.browser, "remove frame", .{});
}
}
@@ -235,9 +235,9 @@ pub fn releaseOrigin(self: *Session, origin: *js.Origin) void {
}
}
/// Reset page_arena and factory for a clean slate.
/// Called when root page is removed.
fn resetPageResources(self: *Session) void {
/// 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();
@@ -281,48 +281,48 @@ fn resetPageResources(self: *Session) void {
}
self.frame_id_gen = 0;
self.arena_pool.reset(self.page_arena, 64 * 1024);
self.factory = Factory.init(self.page_arena);
self.arena_pool.reset(self.frame_arena, 64 * 1024);
self.factory = Factory.init(self.frame_arena);
}
pub fn replacePage(self: *Session) !*Page {
pub fn replaceFrame(self: *Session) !*Frame {
if (comptime IS_DEBUG) {
log.debug(.browser, "replace page", .{});
log.debug(.browser, "replace frame", .{});
}
lp.assert(self.page != null, "Session.replacePage null page", .{});
lp.assert(self.page.?.parent == null, "Session.replacePage with parent", .{});
lp.assert(self.frame != null, "Session.replaceFrame null frame", .{});
lp.assert(self.frame.?.parent == null, "Session.replaceFrame with parent", .{});
var current = self.page.?;
var current = self.frame.?;
const frame_id = current._frame_id;
current.deinit(true);
self.resetPageResources();
self.resetFrameResources();
self.page = @as(Page, undefined);
const page = &self.page.?;
try Page.init(page, frame_id, self, null);
return page;
self.frame = @as(Frame, undefined);
const frame = &self.frame.?;
try Frame.init(frame, frame_id, self, null);
return 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);
}
pub fn findPageByFrameId(self: *Session, frame_id: u32) ?*Page {
const page = self.currentPage() orelse return null;
return findPageBy(page, "_frame_id", frame_id);
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 findPageById(self: *Session, id: u32) ?*Page {
const page = self.currentPage() orelse return null;
return findPageBy(page, "id", 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 findPageBy(page: *Page, comptime field: []const u8, id: u32) ?*Page {
if (@field(page, field) == id) return page;
for (page.frames.items) |f| {
if (findPageBy(f, field, id)) |found| {
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;
}
}
@@ -333,18 +333,18 @@ pub fn runner(self: *Session, opts: Runner.Opts) !Runner {
return Runner.init(self, opts);
}
pub fn scheduleNavigation(self: *Session, page: *Page) !void {
pub fn scheduleNavigation(self: *Session, frame: *Frame) !void {
const list = self.queued_navigation;
// Check if page is already queued
// Check if frame is already queued
for (list.items) |existing| {
if (existing == page) {
if (existing == frame) {
// Already queued
return;
}
}
return list.append(self.arena, page);
return list.append(self.arena, frame);
}
pub fn processQueuedNavigation(self: *Session) !void {
@@ -355,10 +355,10 @@ pub fn processQueuedNavigation(self: *Session) !void {
self.queued_navigation = &self.queued_navigation_1;
}
if (self.page.?._queued_navigation != null) {
if (self.frame.?._queued_navigation != null) {
// This is both an optimization and a simplification of sorts. If the
// root page is navigating, then we don't need to process any other
// navigation. Also, the navigation for the root page and for a frame
// 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
// is different enough that have two distinct code blocks is, imo,
// better. Yes, there will be duplication.
navigations.clearRetainingCapacity();
@@ -369,20 +369,20 @@ pub fn processQueuedNavigation(self: *Session) !void {
defer about_blank_queue.clearRetainingCapacity();
// First pass: process async navigations (non-about:blank)
for (navigations.items) |page| {
const qn = page._queued_navigation orelse {
log.debug(.page, "skipped null queued nav", .{});
for (navigations.items) |frame| {
const qn = frame._queued_navigation orelse {
log.debug(.frame, "skipped null queued nav", .{});
continue;
};
if (qn.is_about_blank) {
// Defer about:blank to second pass
try about_blank_queue.append(self.arena, page);
try about_blank_queue.append(self.arena, frame);
continue;
}
self.processFrameNavigation(page, qn) catch |err| {
log.warn(.page, "frame navigation", .{ .url = qn.url, .err = err });
self.processFrameNavigation(frame, qn) catch |err| {
log.warn(.frame, "frame navigation", .{ .url = qn.url, .err = err });
};
}
@@ -390,12 +390,12 @@ pub fn processQueuedNavigation(self: *Session) !void {
// Second pass: process synchronous navigations (about:blank)
// These may trigger new navigations which go into queued_navigation
for (about_blank_queue.items) |page| {
const qn = page._queued_navigation orelse {
log.debug(.page, "skipped null queued nav", .{});
for (about_blank_queue.items) |frame| {
const qn = frame._queued_navigation orelse {
log.debug(.frame, "skipped null queued nav", .{});
continue;
};
try self.processFrameNavigation(page, qn);
try self.processFrameNavigation(frame, qn);
}
// Safety: Remove any about:blank navigations that were queued during
@@ -404,10 +404,10 @@ pub fn processQueuedNavigation(self: *Session) !void {
const new_navigations = self.queued_navigation;
var i: usize = 0;
while (i < new_navigations.items.len) {
const page = new_navigations.items[i];
if (page._queued_navigation) |qn| {
const frame = new_navigations.items[i];
if (frame._queued_navigation) |qn| {
if (qn.is_about_blank) {
log.warn(.page, "recursive about blank", .{});
log.warn(.frame, "recursive about blank", .{});
_ = self.queued_navigation.swapRemove(i);
continue;
}
@@ -416,75 +416,75 @@ pub fn processQueuedNavigation(self: *Session) !void {
}
}
fn processFrameNavigation(self: *Session, page: *Page, qn: *QueuedNavigation) !void {
lp.assert(page.parent != null, "root queued navigation", .{});
fn processFrameNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation) !void {
lp.assert(frame.parent != null, "root queued navigation", .{});
const iframe = page.iframe.?;
const parent = page.parent.?;
const iframe = frame.iframe.?;
const parent = frame.parent.?;
page._queued_navigation = null;
frame._queued_navigation = null;
defer self.releaseArena(qn.arena);
errdefer iframe._window = null;
const parent_notified = page._parent_notified;
const parent_notified = frame._parent_notified;
if (parent_notified) {
// we already notified the parent that we had loaded
parent._pending_loads += 1;
}
const frame_id = page._frame_id;
page.deinit(true);
page.* = undefined;
const frame_id = frame._frame_id;
frame.deinit(true);
frame.* = undefined;
try Page.init(page, frame_id, self, parent);
try Frame.init(frame, frame_id, self, parent);
errdefer {
for (parent.frames.items, 0..) |frame, i| {
if (frame == page) {
parent.frames_sorted = false;
_ = parent.frames.swapRemove(i);
for (parent.child_frames.items, 0..) |f, i| {
if (f == frame) {
parent.child_frames_sorted = false;
_ = parent.child_frames.swapRemove(i);
break;
}
}
if (parent_notified) {
parent._pending_loads -= 1;
}
page.deinit(true);
frame.deinit(true);
}
page.iframe = iframe;
iframe._window = page.window;
frame.iframe = iframe;
iframe._window = frame.window;
page.navigate(qn.url, qn.opts) catch |err| {
frame.navigate(qn.url, qn.opts) catch |err| {
log.err(.browser, "queued frame navigation error", .{ .err = err });
return err;
};
}
fn processRootQueuedNavigation(self: *Session) !void {
const current_page = &self.page.?;
const frame_id = current_page._frame_id;
const current_frame = &self.frame.?;
const frame_id = current_frame._frame_id;
// create a copy before the page is cleared
const qn = current_page._queued_navigation.?;
current_page._queued_navigation = null;
// create a copy before the frame is cleared
const qn = current_frame._queued_navigation.?;
current_frame._queued_navigation = null;
defer self.arena_pool.release(qn.arena);
self.removePage();
self.removeFrame();
self.page = @as(Page, undefined);
const new_page = &self.page.?;
try Page.init(new_page, frame_id, self, null);
self.frame = @as(Frame, undefined);
const new_frame = &self.frame.?;
try Frame.init(new_frame, frame_id, self, null);
// Creates a new NavigationEventTarget for this page.
try self.navigation.onNewPage(new_page);
// Creates a new NavigationEventTarget for this frame.
try self.navigation.onNewFrame(new_frame);
// start JS env
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
self.notification.dispatch(.page_created, new_page);
// Inform CDP the main frame has been created such that additional context for other Worlds can be created as well
self.notification.dispatch(.frame_created, new_frame);
new_page.navigate(qn.url, qn.opts) catch |err| {
new_frame.navigate(qn.url, qn.opts) catch |err| {
log.err(.browser, "queued navigation error", .{ .err = err });
return err;
};
@@ -496,15 +496,15 @@ pub fn nextFrameId(self: *Session) u32 {
return id;
}
pub fn nextPageId(self: *Session) u32 {
const id = self.page_id_gen +% 1;
self.page_id_gen = id;
pub fn nextLoaderId(self: *Session) u32 {
const id = self.loader_id_gen +% 1;
self.loader_id_gen = id;
return id;
}
// 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 page reset.
// release on frame reset.
pub const FinalizerCallback = struct {
arena: Allocator,
session: *Session,
@@ -519,7 +519,7 @@ 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 page resets and
// Allocated from Session.fc_identity_pool so it survives frame resets and
// allows the weak callback to safely check the done flag.
pub const Identity = struct {
session: *Session,
@@ -530,7 +530,7 @@ pub const FinalizerCallback = struct {
done: bool = false,
};
// Called during page reset to force cleanup regardless of identities.
// Called during frame reset to force cleanup regardless of identities.
fn deinit(self: *FinalizerCallback, session: *Session) void {
// Mark all identities as done so stale V8 weak callbacks
// won't find the wrong FC if resolved_ptr_id is reused.

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const lp = @import("lightpanda");
const Page = @import("Page.zig");
const Frame = @import("Frame.zig");
const CssParser = @import("css/Parser.zig");
const Element = @import("webapi/Element.zig");
@@ -47,7 +47,7 @@ const StyleManager = @This();
const Tag = Element.Tag;
const RuleList = std.MultiArrayList(VisibilityRule);
page: *Page,
frame: *Frame,
arena: Allocator,
@@ -63,15 +63,15 @@ next_doc_order: u32 = 0,
// When true, rules need to be rebuilt
dirty: bool = false,
pub fn init(page: *Page) !StyleManager {
pub fn init(frame: *Frame) !StyleManager {
return .{
.page = page,
.arena = try page.getArena(.medium, "StyleManager"),
.frame = frame,
.arena = try frame.getArena(.medium, "StyleManager"),
};
}
pub fn deinit(self: *StyleManager) void {
self.page.releaseArena(self.arena);
self.frame.releaseArena(self.arena);
}
fn parseSheet(self: *StyleManager, sheet: *CSSStyleSheet) !void {
@@ -170,7 +170,7 @@ fn rebuildIfDirty(self: *StyleManager) !void {
const tag_rules_count = self.tag_rules.count();
const other_rules_count = self.other_rules.len;
self.page._session.arena_pool.resetRetain(self.arena);
self.frame._session.arena_pool.resetRetain(self.arena);
self.next_doc_order = 0;
@@ -186,7 +186,7 @@ fn rebuildIfDirty(self: *StyleManager) !void {
self.other_rules = .{};
try self.other_rules.ensureTotalCapacity(self.arena, other_rules_count);
const sheets = self.page.document._style_sheets orelse return;
const sheets = self.frame.document._style_sheets orelse return;
for (sheets._sheets.items) |sheet| {
self.parseSheet(sheet) catch |err| {
log.err(.browser, "StyleManager parseSheet", .{ .err = err });
@@ -219,7 +219,7 @@ pub fn isHidden(self: *StyleManager, el: *Element, cache: ?*VisibilityCache, opt
// Store in cache
if (cache) |c| {
c.put(self.page.call_arena, elem, hidden) catch {};
c.put(self.frame.call_arena, elem, hidden) catch {};
}
if (hidden) {
@@ -272,7 +272,7 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp
// Check inline styles FIRST - they use INLINE_PRIORITY so no stylesheet can beat them
if (options.check_display) {
if (getInlineStyleProperty(el, comptime .wrap("display"), self.page)) |property| {
if (getInlineStyleProperty(el, comptime .wrap("display"), self.frame)) |property| {
if (property._value.eql(comptime .wrap("none"))) {
return true; // Early exit for hiding value
}
@@ -285,7 +285,7 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp
}
if (options.check_visibility) {
if (getInlineStyleProperty(el, comptime .wrap("visibility"), self.page)) |property| {
if (getInlineStyleProperty(el, comptime .wrap("visibility"), self.frame)) |property| {
if (property._value.eql(comptime .wrap("hidden")) or property._value.eql(comptime .wrap("collapse"))) {
return true;
}
@@ -300,7 +300,7 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp
}
if (options.check_opacity) {
if (getInlineStyleProperty(el, comptime .wrap("opacity"), self.page)) |property| {
if (getInlineStyleProperty(el, comptime .wrap("opacity"), self.frame)) |property| {
if (property._value.eql(comptime .wrap("0"))) {
return true;
}
@@ -324,7 +324,7 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp
opacity_zero: *?bool,
opacity_priority: *u64,
el: *Element,
page: *Page,
frame: *Frame,
fn checkRules(ctx: @This(), rules: *const RuleList) void {
if (ctx.display_priority.* == INLINE_PRIORITY and
@@ -351,7 +351,7 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp
if (dominated) continue;
if (matchesSelector(ctx.el, selector, ctx.page)) {
if (matchesSelector(ctx.el, selector, ctx.frame)) {
// Update best priorities
if (props.display_none != null and p > ctx.display_priority.*) {
ctx.display_none.* = props.display_none;
@@ -377,7 +377,7 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp
.opacity_zero = &opacity_zero,
.opacity_priority = &opacity_priority,
.el = el,
.page = self.page,
.frame = self.frame,
};
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
@@ -425,7 +425,7 @@ pub fn hasPointerEventsNone(self: *StyleManager, el: *Element, cache: ?*PointerE
const pe_none = self.elementHasPointerEventsNone(elem);
if (cache) |c| {
c.put(self.page.call_arena, elem, pe_none) catch {};
c.put(self.frame.call_arena, elem, pe_none) catch {};
}
if (pe_none) {
@@ -439,10 +439,10 @@ pub fn hasPointerEventsNone(self: *StyleManager, el: *Element, cache: ?*PointerE
/// Check if a single element (not ancestors) has pointer-events:none.
fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool {
const page = self.page;
const frame = self.frame;
// Check inline style first
if (getInlineStyleProperty(el, .wrap("pointer-events"), page)) |property| {
if (getInlineStyleProperty(el, .wrap("pointer-events"), frame)) |property| {
if (property._value.eql(comptime .wrap("none"))) {
return true;
}
@@ -454,7 +454,7 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool {
// Helper to check a single rule
const checkRules = struct {
fn check(rules: *const RuleList, res: *?bool, current_priority: *u64, elem: *Element, p: *Page) void {
fn check(rules: *const RuleList, res: *?bool, current_priority: *u64, elem: *Element, p: *Frame) void {
if (current_priority.* == INLINE_PRIORITY) return;
const priorities = rules.items(.priority);
@@ -475,7 +475,7 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool {
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
if (self.id_rules.get(id)) |rules| {
checkRules(&rules, &result, &best_priority, el, page);
checkRules(&rules, &result, &best_priority, el, frame);
}
}
@@ -483,16 +483,16 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool {
var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace);
while (it.next()) |class| {
if (self.class_rules.get(class)) |rules| {
checkRules(&rules, &result, &best_priority, el, page);
checkRules(&rules, &result, &best_priority, el, frame);
}
}
}
if (self.tag_rules.get(el.getTag())) |rules| {
checkRules(&rules, &result, &best_priority, el, page);
checkRules(&rules, &result, &best_priority, el, frame);
}
checkRules(&self.other_rules, &result, &best_priority, el, page);
checkRules(&self.other_rules, &result, &best_priority, el, frame);
return result orelse false;
}
@@ -686,9 +686,9 @@ fn countCompoundSpecificity(compound: Selector.Compound, ids: *u32, classes: *u3
}
}
fn matchesSelector(el: *Element, selector: Selector.Selector, page: *Page) bool {
fn matchesSelector(el: *Element, selector: Selector.Selector, frame: *Frame) bool {
const node = el.asNode();
return SelectorList.matches(node, selector, node, page);
return SelectorList.matches(node, selector, node, frame);
}
const VisibilityProperties = struct {
@@ -723,8 +723,8 @@ const CheckVisibilityOptions = struct {
// Inline styles always win over stylesheets - use max u64 as sentinel
const INLINE_PRIORITY: u64 = std.math.maxInt(u64);
fn getInlineStyleProperty(el: *Element, property_name: String, page: *Page) ?*CSSStyleProperty {
const style = el.getOrCreateStyle(page) catch |err| {
fn getInlineStyleProperty(el: *Element, property_name: String, frame: *Frame) ?*CSSStyleProperty {
const style = el.getOrCreateStyle(frame) catch |err| {
log.err(.browser, "StyleManager getOrCreateStyle", .{ .err = err });
return null;
};

View File

@@ -1309,7 +1309,7 @@ test "URL: resolve with encoding" {
},
// Relative paths with encoding
.{
.base = "https://example.com/dir/page.html",
.base = "https://example.com/dir/frame.html",
.path = "../other dir/file.html",
.expected = "https://example.com/other%20dir/file.html",
},

View File

@@ -23,23 +23,23 @@ const Element = @import("webapi/Element.zig");
const Event = @import("webapi/Event.zig");
const MouseEvent = @import("webapi/event/MouseEvent.zig");
const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
const Page = @import("Page.zig");
const Frame = @import("Frame.zig");
const Session = @import("Session.zig");
const Selector = @import("webapi/selector/Selector.zig");
fn dispatchInputAndChangeEvents(el: *Element, page: *Page) !void {
const input_evt: *Event = try .initTrusted(comptime .wrap("input"), .{ .bubbles = true }, page);
page._event_manager.dispatch(el.asEventTarget(), input_evt) catch |err| {
fn dispatchInputAndChangeEvents(el: *Element, frame: *Frame) !void {
const input_evt: *Event = try .initTrusted(comptime .wrap("input"), .{ .bubbles = true }, frame);
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 }, page);
page._event_manager.dispatch(el.asEventTarget(), change_evt) catch |err| {
const change_evt: *Event = try .initTrusted(comptime .wrap("change"), .{ .bubbles = true }, frame);
frame._event_manager.dispatch(el.asEventTarget(), change_evt) catch |err| {
lp.log.err(.app, "dispatch change event failed", .{ .err = err });
};
}
pub fn click(node: *DOMNode, page: *Page) !void {
pub fn click(node: *DOMNode, frame: *Frame) !void {
const el = node.is(Element) orelse return error.InvalidNodeType;
const mouse_event: *MouseEvent = try .initTrusted(comptime .wrap("click"), .{
@@ -48,52 +48,52 @@ pub fn click(node: *DOMNode, page: *Page) !void {
.composed = true,
.clientX = 0,
.clientY = 0,
}, page);
}, frame);
page._event_manager.dispatch(el.asEventTarget(), mouse_event.asEvent()) catch |err| {
frame._event_manager.dispatch(el.asEventTarget(), mouse_event.asEvent()) catch |err| {
lp.log.err(.app, "click failed", .{ .err = err });
return error.ActionFailed;
};
}
pub fn hover(node: *DOMNode, page: *Page) !void {
pub fn hover(node: *DOMNode, frame: *Frame) !void {
const el = node.is(Element) orelse return error.InvalidNodeType;
const mouseover_event: *MouseEvent = try .initTrusted(comptime .wrap("mouseover"), .{
.bubbles = true,
.cancelable = true,
.composed = true,
}, page);
}, frame);
page._event_manager.dispatch(el.asEventTarget(), mouseover_event.asEvent()) catch |err| {
frame._event_manager.dispatch(el.asEventTarget(), mouseover_event.asEvent()) catch |err| {
lp.log.err(.app, "hover mouseover failed", .{ .err = err });
return error.ActionFailed;
};
const mouseenter_event: *MouseEvent = try .initTrusted(comptime .wrap("mouseenter"), .{
.composed = true,
}, page);
}, frame);
page._event_manager.dispatch(el.asEventTarget(), mouseenter_event.asEvent()) catch |err| {
frame._event_manager.dispatch(el.asEventTarget(), mouseenter_event.asEvent()) catch |err| {
lp.log.err(.app, "hover mouseenter failed", .{ .err = err });
return error.ActionFailed;
};
}
pub fn press(node: ?*DOMNode, key: []const u8, page: *Page) !void {
pub fn press(node: ?*DOMNode, key: []const u8, frame: *Frame) !void {
const target = if (node) |n|
(n.is(Element) orelse return error.InvalidNodeType).asEventTarget()
else
page.document.asNode().asEventTarget();
frame.document.asNode().asEventTarget();
const keydown_event: *KeyboardEvent = try .initTrusted(comptime .wrap("keydown"), .{
.bubbles = true,
.cancelable = true,
.composed = true,
.key = key,
}, page);
}, frame);
page._event_manager.dispatch(target, keydown_event.asEvent()) catch |err| {
frame._event_manager.dispatch(target, keydown_event.asEvent()) catch |err| {
lp.log.err(.app, "press keydown failed", .{ .err = err });
return error.ActionFailed;
};
@@ -103,27 +103,27 @@ pub fn press(node: ?*DOMNode, key: []const u8, page: *Page) !void {
.cancelable = true,
.composed = true,
.key = key,
}, page);
}, frame);
page._event_manager.dispatch(target, keyup_event.asEvent()) catch |err| {
frame._event_manager.dispatch(target, keyup_event.asEvent()) catch |err| {
lp.log.err(.app, "press keyup failed", .{ .err = err });
return error.ActionFailed;
};
}
pub fn selectOption(node: *DOMNode, value: []const u8, page: *Page) !void {
pub fn selectOption(node: *DOMNode, value: []const u8, frame: *Frame) !void {
const el = node.is(Element) orelse return error.InvalidNodeType;
const select = el.is(Element.Html.Select) orelse return error.InvalidNodeType;
select.setValue(value, page) catch |err| {
select.setValue(value, frame) catch |err| {
lp.log.err(.app, "select setValue failed", .{ .err = err });
return error.ActionFailed;
};
try dispatchInputAndChangeEvents(el, page);
try dispatchInputAndChangeEvents(el, frame);
}
pub fn setChecked(node: *DOMNode, checked: bool, page: *Page) !void {
pub fn setChecked(node: *DOMNode, checked: bool, frame: *Frame) !void {
const el = node.is(Element) orelse return error.InvalidNodeType;
const input = el.is(Element.Html.Input) orelse return error.InvalidNodeType;
@@ -131,7 +131,7 @@ pub fn setChecked(node: *DOMNode, checked: bool, page: *Page) !void {
return error.InvalidNodeType;
}
input.setChecked(checked, page) catch |err| {
input.setChecked(checked, frame) catch |err| {
lp.log.err(.app, "setChecked failed", .{ .err = err });
return error.ActionFailed;
};
@@ -141,34 +141,34 @@ pub fn setChecked(node: *DOMNode, checked: bool, page: *Page) !void {
.bubbles = true,
.cancelable = true,
.composed = true,
}, page);
}, frame);
page._event_manager.dispatch(el.asEventTarget(), click_event.asEvent()) catch |err| {
frame._event_manager.dispatch(el.asEventTarget(), click_event.asEvent()) catch |err| {
lp.log.err(.app, "dispatch click event failed", .{ .err = err });
};
try dispatchInputAndChangeEvents(el, page);
try dispatchInputAndChangeEvents(el, frame);
}
pub fn fill(node: *DOMNode, text: []const u8, page: *Page) !void {
pub fn fill(node: *DOMNode, text: []const u8, frame: *Frame) !void {
const el = node.is(Element) orelse return error.InvalidNodeType;
el.focus(page) catch |err| {
el.focus(frame) catch |err| {
lp.log.err(.app, "fill focus failed", .{ .err = err });
};
if (el.is(Element.Html.Input)) |input| {
input.setValue(text, page) catch |err| {
input.setValue(text, frame) catch |err| {
lp.log.err(.app, "fill input failed", .{ .err = err });
return error.ActionFailed;
};
} else if (el.is(Element.Html.TextArea)) |textarea| {
textarea.setValue(text, page) catch |err| {
textarea.setValue(text, frame) catch |err| {
lp.log.err(.app, "fill textarea failed", .{ .err = err });
return error.ActionFailed;
};
} else if (el.is(Element.Html.Select)) |select| {
select.setValue(text, page) catch |err| {
select.setValue(text, frame) catch |err| {
lp.log.err(.app, "fill select failed", .{ .err = err });
return error.ActionFailed;
};
@@ -176,32 +176,32 @@ pub fn fill(node: *DOMNode, text: []const u8, page: *Page) !void {
return error.InvalidNodeType;
}
try dispatchInputAndChangeEvents(el, page);
try dispatchInputAndChangeEvents(el, frame);
}
pub fn scroll(node: ?*DOMNode, x: ?i32, y: ?i32, page: *Page) !void {
pub fn scroll(node: ?*DOMNode, x: ?i32, y: ?i32, frame: *Frame) !void {
if (node) |n| {
const el = n.is(Element) orelse return error.InvalidNodeType;
if (x) |val| {
el.setScrollLeft(val, page) catch |err| {
el.setScrollLeft(val, frame) catch |err| {
lp.log.err(.app, "setScrollLeft failed", .{ .err = err });
return error.ActionFailed;
};
}
if (y) |val| {
el.setScrollTop(val, page) catch |err| {
el.setScrollTop(val, frame) catch |err| {
lp.log.err(.app, "setScrollTop failed", .{ .err = err });
return error.ActionFailed;
};
}
const scroll_evt: *Event = try .initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, page);
page._event_manager.dispatch(el.asEventTarget(), scroll_evt) catch |err| {
const scroll_evt: *Event = try .initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, frame);
frame._event_manager.dispatch(el.asEventTarget(), scroll_evt) catch |err| {
lp.log.err(.app, "dispatch scroll event failed", .{ .err = err });
};
} else {
page.window.scrollTo(.{ .x = x orelse 0 }, y, page) catch |err| {
frame.window.scrollTo(.{ .x = x orelse 0 }, y, frame) catch |err| {
lp.log.err(.app, "scroll failed", .{ .err = err });
return error.ActionFailed;
};

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Page = @import("Page.zig");
const Frame = @import("Frame.zig");
const Node = @import("webapi/Node.zig");
const Slot = @import("webapi/element/html/Slot.zig");
const IFrame = @import("webapi/element/html/IFrame.zig");
@@ -48,7 +48,7 @@ pub const Opts = struct {
};
};
pub fn root(doc: *Node.Document, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
pub fn root(doc: *Node.Document, opts: Opts, writer: *std.Io.Writer, frame: *Frame) !void {
if (doc.is(Node.Document.HTMLDocument)) |html_doc| {
blk: {
// Ideally we just render the doctype which is part of the document
@@ -64,20 +64,20 @@ pub fn root(doc: *Node.Document, opts: Opts, writer: *std.Io.Writer, page: *Page
if (opts.with_base) {
const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode();
const base = try doc.createElement("base", null, page);
try base.setAttributeSafe(comptime .wrap("base"), .wrap(page.base()), page);
_ = try parent.insertBefore(base.asNode(), parent.firstChild(), page);
const base = try doc.createElement("base", null, frame);
try base.setAttributeSafe(comptime .wrap("base"), .wrap(frame.base()), frame);
_ = try parent.insertBefore(base.asNode(), parent.firstChild(), frame);
}
}
return deep(doc.asNode(), opts, writer, page);
return deep(doc.asNode(), opts, writer, frame);
}
pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {
return _deep(node, opts, false, writer, page);
pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, frame: *Frame) error{WriteFailed}!void {
return _deep(node, opts, false, writer, frame);
}
fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {
fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Writer, frame: *Frame) error{WriteFailed}!void {
switch (node._type) {
.cdata => |cd| {
if (node.is(Node.CData.Comment)) |_| {
@@ -119,13 +119,13 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
if (opts.shadow == .rendered) {
if (el.is(Slot)) |slot| {
try dumpSlotContent(slot, opts, writer, page);
try dumpSlotContent(slot, opts, writer, frame);
return writer.writeAll("</slot>");
}
}
if (opts.shadow != .skip) {
if (page._element_shadow_roots.get(el)) |shadow| {
try children(shadow.asNode(), opts, writer, page);
if (frame._element_shadow_roots.get(el)) |shadow| {
try children(shadow.asNode(), opts, writer, frame);
// In rendered mode, light DOM is only shown through slots, not directly
if (opts.shadow == .rendered) {
// Skip rendering light DOM children
@@ -140,21 +140,21 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
}
if (opts.with_frames and el.is(IFrame) != null) {
const frame = el.as(IFrame);
if (frame.getContentDocument()) |doc| {
// A frame's document should always ahave a page, but
const iframe = el.as(IFrame);
if (iframe.getContentDocument()) |doc| {
// A frame's document should always ahave a frame, but
// I'm not willing to crash a release build on that assertion.
if (comptime IS_DEBUG) {
std.debug.assert(doc._page != null);
std.debug.assert(doc._frame != null);
}
if (doc._page) |frame_page| {
if (doc._frame) |f| {
try writer.writeByte('\n');
root(doc, opts, writer, frame_page) catch return error.WriteFailed;
root(doc, opts, writer, f) catch return error.WriteFailed;
try writer.writeByte('\n');
}
}
} else {
try children(node, opts, writer, page);
try children(node, opts, writer, frame);
}
if (!isVoidElement(el)) {
@@ -163,7 +163,7 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
try writer.writeByte('>');
}
},
.document => try children(node, opts, writer, page),
.document => try children(node, opts, writer, frame),
.document_type => |dt| {
try writer.writeAll("<!DOCTYPE ");
try writer.writeAll(dt.getName());
@@ -187,7 +187,7 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
}
try writer.writeAll(">\n");
},
.document_fragment => try children(node, opts, writer, page),
.document_fragment => try children(node, opts, writer, frame),
.attribute => {
// Not called normally, but can be called via XMLSerializer.serializeToString
// in which case it should return an empty string
@@ -196,10 +196,10 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
}
}
pub fn children(parent: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
pub fn children(parent: *Node, opts: Opts, writer: *std.Io.Writer, frame: *Frame) !void {
var it = parent.childrenIterator();
while (it.next()) |child| {
try deep(child, opts, writer, page);
try deep(child, opts, writer, frame);
}
}
@@ -243,15 +243,15 @@ pub fn toJSON(node: *Node, writer: *std.json.Stringify) !void {
try writer.endObject();
}
fn dumpSlotContent(slot: *Slot, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
const assigned = slot.assignedNodes(null, page) catch return;
fn dumpSlotContent(slot: *Slot, opts: Opts, writer: *std.Io.Writer, frame: *Frame) !void {
const assigned = slot.assignedNodes(null, frame) catch return;
if (assigned.len > 0) {
for (assigned) |assigned_node| {
try _deep(assigned_node, opts, true, writer, page);
try _deep(assigned_node, opts, true, writer, frame);
}
} else {
try children(slot.asNode(), opts, writer, page);
try children(slot.asNode(), opts, writer, frame);
}
}

View File

@@ -18,7 +18,7 @@
const std = @import("std");
const Page = @import("Page.zig");
const Frame = @import("Frame.zig");
const TreeWalker = @import("webapi/TreeWalker.zig");
const Element = @import("webapi/Element.zig");
const Node = @import("webapi/Node.zig");
@@ -157,7 +157,7 @@ pub fn registerNodes(forms_data: []FormInfo, registry: anytype) !void {
pub fn collectForms(
arena: Allocator,
root: *Node,
page: *Page,
frame: *Frame,
) ![]FormInfo {
var forms: std.ArrayList(FormInfo) = .empty;
@@ -166,7 +166,7 @@ pub fn collectForms(
const form = node.is(Element.Html.Form) orelse continue;
const el = form.asElement();
const fields = try collectFormFields(arena, form, page);
const fields = try collectFormFields(arena, form, frame);
if (fields.len == 0) continue;
const action_attr = el.getAttributeSafe(comptime .wrap("action"));
@@ -186,11 +186,11 @@ pub fn collectForms(
fn collectFormFields(
arena: Allocator,
form: *Element.Html.Form,
page: *Page,
frame: *Frame,
) ![]FormField {
var fields: std.ArrayList(FormField) = .empty;
var elements = try form.getElements(page);
var elements = try form.getElements(frame);
var it = try elements.iterator();
while (it.next()) |el| {
const node = el.asNode();
@@ -231,7 +231,7 @@ fn collectFormFields(
}
if (el.is(Element.Html.Select)) |select| {
const options = try collectSelectOptions(arena, node, page);
const options = try collectSelectOptions(arena, node, frame);
try fields.append(arena, .{
.node = node,
@@ -240,7 +240,7 @@ fn collectFormFields(
.input_type = null,
.required = el.getAttributeSafe(comptime .wrap("required")) != null,
.disabled = is_disabled,
.value = select.getValue(page),
.value = select.getValue(frame),
.placeholder = null,
.options = options,
});
@@ -256,7 +256,7 @@ fn collectFormFields(
fn collectSelectOptions(
arena: Allocator,
select_node: *Node,
page: *Page,
frame: *Frame,
) ![]SelectOption {
var options: std.ArrayList(SelectOption) = .empty;
const Option = Element.Html.Option;
@@ -267,8 +267,8 @@ fn collectSelectOptions(
const option = el.is(Option) orelse continue;
try options.append(arena, .{
.value = option.getValue(page),
.text = option.getText(page),
.value = option.getValue(frame),
.text = option.getText(frame),
});
}
@@ -278,18 +278,18 @@ fn collectSelectOptions(
const testing = @import("../testing.zig");
fn testForms(html: []const u8) ![]FormInfo {
const page = try testing.test_session.createPage();
const frame = try testing.test_session.createFrame();
const doc = page.window._document;
const div = try doc.createElement("div", null, page);
try page.parseHtmlAsChildren(div.asNode(), html);
const doc = frame.window._document;
const div = try doc.createElement("div", null, frame);
try frame.parseHtmlAsChildren(div.asNode(), html);
return collectForms(page.call_arena, div.asNode(), page);
return collectForms(frame.call_arena, div.asNode(), frame);
}
test "browser.forms: login form" {
defer testing.reset();
defer testing.test_session.removePage();
defer testing.test_session.removeFrame();
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.removePage();
defer testing.test_session.removeFrame();
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.removePage();
defer testing.test_session.removeFrame();
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.removePage();
defer testing.test_session.removeFrame();
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.removePage();
defer testing.test_session.removeFrame();
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.removePage();
defer testing.test_session.removeFrame();
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.removePage();
defer testing.test_session.removeFrame();
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.removePage();
defer testing.test_session.removeFrame();
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.removePage();
defer testing.test_session.removeFrame();
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.removePage();
defer testing.test_session.removeFrame();
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.removePage();
defer testing.test_session.removeFrame();
const forms = try testForms(
\\<form>
\\ <input type="text" name="q">

View File

@@ -18,7 +18,7 @@
const std = @import("std");
const Page = @import("Page.zig");
const Frame = @import("Frame.zig");
const URL = @import("URL.zig");
const TreeWalker = @import("webapi/TreeWalker.zig");
const Element = @import("webapi/Element.zig");
@@ -143,11 +143,11 @@ pub fn registerNodes(elements: []InteractiveElement, registry: anytype) !void {
pub fn collectInteractiveElements(
root: *Node,
arena: Allocator,
page: *Page,
frame: *Frame,
) ![]InteractiveElement {
// Pre-build a map of event_target pointer → event type names,
// so classify and getListenerTypes are both O(1) per element.
const listener_targets = try buildListenerTargetMap(page, arena);
const listener_targets = try buildListenerTargetMap(frame, arena);
var css_cache: Element.PointerEventsCache = .empty;
@@ -164,7 +164,7 @@ pub fn collectInteractiveElements(
else => {},
}
const itype = classifyInteractivity(page, el, html_el, listener_targets, &css_cache) orelse continue;
const itype = classifyInteractivity(frame, el, html_el, listener_targets, &css_cache) orelse continue;
const listener_types = getListenerTypes(
el.asEventTarget(),
@@ -183,7 +183,7 @@ pub fn collectInteractiveElements(
.id = el.getAttributeSafe(comptime .wrap("id")),
.class = el.getAttributeSafe(comptime .wrap("class")),
.href = if (el.getAttributeSafe(comptime .wrap("href"))) |href|
URL.resolve(arena, page.base(), href, .{ .encoding = page.charset }) catch href
URL.resolve(arena, frame.base(), href, .{ .encoding = frame.charset }) catch href
else
null,
.input_type = getInputType(el),
@@ -201,11 +201,11 @@ pub const ListenerTargetMap = std.AutoHashMapUnmanaged(usize, std.ArrayList([]co
/// Pre-build a map from event_target pointer → list of event type names.
/// This lets both classifyInteractivity (O(1) "has any?") and
/// getListenerTypes (O(1) "which ones?") avoid re-iterating per element.
pub fn buildListenerTargetMap(page: *Page, arena: Allocator) !ListenerTargetMap {
pub fn buildListenerTargetMap(frame: *Frame, arena: Allocator) !ListenerTargetMap {
var map = ListenerTargetMap{};
// addEventListener registrations
var it = page._event_manager.base.lookup.iterator();
var it = frame._event_manager.base.lookup.iterator();
while (it.next()) |entry| {
const list = entry.value_ptr.*;
if (list.first != null) {
@@ -216,7 +216,7 @@ pub fn buildListenerTargetMap(page: *Page, arena: Allocator) !ListenerTargetMap
}
// Inline handlers (onclick, onmousedown, etc.)
var attr_it = page._event_target_attr_listeners.iterator();
var attr_it = frame._event_target_attr_listeners.iterator();
while (attr_it.next()) |entry| {
const gop = try map.getOrPut(arena, @intFromPtr(entry.key_ptr.target));
if (!gop.found_existing) gop.value_ptr.* = .empty;
@@ -228,13 +228,13 @@ pub fn buildListenerTargetMap(page: *Page, arena: Allocator) !ListenerTargetMap
}
pub fn classifyInteractivity(
page: *Page,
frame: *Frame,
el: *Element,
html_el: *Element.Html,
listener_targets: ListenerTargetMap,
cache: ?*Element.PointerEventsCache,
) ?InteractivityType {
if (el.hasPointerEventsNone(cache, page)) return null;
if (el.hasPointerEventsNone(cache, frame)) return null;
// 1. Native interactive by tag
switch (el.getTag()) {
@@ -452,14 +452,14 @@ fn getListenerTypes(target: *EventTarget, listener_targets: ListenerTargetMap) [
const testing = @import("../testing.zig");
fn testInteractive(html: []const u8) ![]InteractiveElement {
const page = try testing.test_session.createPage();
defer testing.test_session.removePage();
const frame = try testing.test_session.createFrame();
defer testing.test_session.removeFrame();
const doc = page.window._document;
const div = try doc.createElement("div", null, page);
try page.parseHtmlAsChildren(div.asNode(), html);
const doc = frame.window._document;
const div = try doc.createElement("div", null, frame);
try frame.parseHtmlAsChildren(div.asNode(), html);
return collectInteractiveElements(div.asNode(), page.call_arena, page);
return collectInteractiveElements(div.asNode(), frame.call_arena, frame);
}
test "browser.interactive: button" {

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const lp = @import("lightpanda");
const string = @import("../../string.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Session = @import("../Session.zig");
const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig");
@@ -448,7 +448,7 @@ fn tupleFieldName(comptime i: usize) [:0]const u8 {
}
fn isPage(comptime T: type) bool {
return T == *Page or T == *const Page;
return T == *Frame or T == *const Frame;
}
fn isSession(comptime T: type) bool {
@@ -462,7 +462,7 @@ fn isExecution(comptime T: type) bool {
fn getGlobalArg(comptime T: type, ctx: *Context) T {
if (comptime isPage(T)) {
return switch (ctx.global) {
.page => |page| page,
.frame => |frame| frame,
.worker => unreachable,
};
}

View File

@@ -26,7 +26,7 @@ const Origin = @import("Origin.zig");
const Scheduler = @import("Scheduler.zig");
const Execution = @import("Execution.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Session = @import("../Session.zig");
const ScriptManager = @import("../ScriptManager.zig");
const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig");
@@ -41,26 +41,26 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
const Context = @This();
pub const GlobalScope = union(enum) {
page: *Page,
frame: *Frame,
worker: *WorkerGlobalScope,
pub fn base(self: GlobalScope) [:0]const u8 {
return switch (self) {
.page => |page| page.base(),
.frame => |frame| frame.base(),
.worker => |worker| worker.base(),
};
}
pub fn getJs(self: GlobalScope) *Context {
return switch (self) {
.page => |page| page.js,
.frame => |frame| frame.js,
.worker => |worker| worker.js,
};
}
pub fn setJs(self: GlobalScope, ctx: *Context) void {
switch (self) {
.page => |page| page.js = ctx,
.frame => |frame| frame.js = ctx,
.worker => |worker| worker.js = ctx,
}
}
@@ -90,7 +90,7 @@ templates: []*const v8.FunctionTemplate,
arena: Allocator,
// The call_arena for this context. For main world contexts this is
// page.call_arena. For isolated world contexts this is a separate arena
// frame.call_arena. For isolated world contexts this is a separate arena
// owned by the IsolatedWorld.
call_arena: Allocator,
@@ -114,7 +114,7 @@ origin: *Origin,
identity: *js.Identity,
// Allocator to use for identity map operations. For main world contexts this is
// session.page_arena, for isolated worlds it's the isolated world's arena.
// session.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
@@ -131,7 +131,7 @@ module_cache: std.StringHashMapUnmanaged(ModuleEntry) = .empty,
// necessary to lookup/store the dependent module in the module_cache.
module_identifier: std.AutoHashMapUnmanaged(u32, [:0]const u8) = .empty,
// the page's script manager
// the frame's script manager
script_manager: ?*ScriptManager,
// Our macrotasks
@@ -227,9 +227,9 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
const isolate = env.isolate;
if (comptime IS_DEBUG) {
// A page starts off with an opaque origin. After navigation, setOrigin
// A frame starts off with an opaque origin. After navigation, setOrigin
// is called. This is the only time setOrigin should be called for that
// page. Therefore, when setOrigin is called, the previous origin should
// frame. Therefore, when setOrigin is called, the previous origin should
// have been opaque and its rc should have been 1.
lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc });
}
@@ -252,11 +252,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.page_arena, global);
return self.session.globals.append(self.session.frame_arena, global);
}
pub fn trackTemp(self: *Context, global: v8.Global) !void {
return self.session.temps.put(self.session.page_arena, global.data_ptr, global);
return self.session.temps.put(self.session.frame_arena, global.data_ptr, global);
}
pub const IdentityResult = struct {
@@ -294,10 +294,10 @@ pub fn toLocal(self: *Context, global: anytype) js.Local.ToLocalReturnType(@Type
return l.toLocal(global);
}
pub fn getIncumbent(self: *Context) *Page {
pub fn getIncumbent(self: *Context) *Frame {
const ctx = fromC(v8.v8__Isolate__GetIncumbentContext(self.env.isolate.handle).?).?;
return switch (ctx.global) {
.page => |page| page,
.frame => |frame| frame,
.worker => unreachable,
};
}
@@ -860,7 +860,7 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul
// The microtask is tied to the isolate, not the context
// it can be resolved while another context is active
// (Which seems crazy to me). If that happens, then
// another page was loaded and we MUST ignore this
// another frame was loaded and we MUST ignore this
// (most of the fields in state are not valid)
return;
}
@@ -903,7 +903,7 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul
}
// Used to make temporarily enter and exit a context, updating and restoring
// page.js:
// frame.js:
// var hs: js.HandleScope = undefined;
// const entered = ctx.enter(&hs);
// defer entered.exit();
@@ -920,7 +920,7 @@ pub fn enter(self: *Context, hs: *js.HandleScope) Entered {
}
const Entered = struct {
// the context we should restore on the page/worker
// the context we should restore on the frame/worker
original: *Context,
// the handle of the entered context
@@ -941,7 +941,7 @@ pub fn queueMutationDelivery(self: *Context) !void {
self.enqueueMicrotask(struct {
fn run(ctx: *Context) void {
switch (ctx.global) {
.page => |page| page.deliverMutations(),
.frame => |frame| frame.deliverMutations(),
.worker => unreachable,
}
}
@@ -952,7 +952,7 @@ pub fn queueIntersectionChecks(self: *Context) !void {
self.enqueueMicrotask(struct {
fn run(ctx: *Context) void {
switch (ctx.global) {
.page => |page| page.performScheduledIntersectionChecks(),
.frame => |frame| frame.performScheduledIntersectionChecks(),
.worker => unreachable,
}
}
@@ -963,7 +963,7 @@ pub fn queueIntersectionDelivery(self: *Context) !void {
self.enqueueMicrotask(struct {
fn run(ctx: *Context) void {
switch (ctx.global) {
.page => |page| page.deliverIntersections(),
.frame => |frame| frame.deliverIntersections(),
.worker => unreachable,
}
}
@@ -974,7 +974,7 @@ pub fn queueSlotchangeDelivery(self: *Context) !void {
self.enqueueMicrotask(struct {
fn run(ctx: *Context) void {
switch (ctx.global) {
.page => |page| page.deliverSlotchangeEvents(),
.frame => |frame| frame.deliverSlotchangeEvents(),
.worker => unreachable,
}
}

View File

@@ -29,7 +29,7 @@ const Snapshot = @import("Snapshot.zig");
const Inspector = @import("Inspector.zig");
const App = @import("../../App.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Window = @import("../webapi/Window.zig");
const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig");
@@ -74,8 +74,8 @@ context_id: usize,
// Maps origin -> shared Origin contains, for v8 values shared across
// same-origin Contexts. There's a mismatch here between our JS model and our
// Browser model. Origins only live as long as the root page of a session exists.
// It would be wrong/dangerous to re-use an Origin across root page navigations.
// Browser model. Origins only live as long as the root frame of a session exists.
// It would be wrong/dangerous to re-use an Origin across root frame navigations.
// Global handles that need to be freed on deinit
eternal_function_templates: []v8.Eternal,
@@ -217,8 +217,8 @@ pub const ContextParams = struct {
debug_name: []const u8 = "Context",
};
pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context {
return self._createContext(page, params);
pub fn createContext(self: *Env, frame: *Frame, params: ContextParams) !*Context {
return self._createContext(frame, params);
}
pub fn createWorkerContext(self: *Env, worker: *WorkerGlobalScope, params: ContextParams) !*Context {
@@ -227,7 +227,7 @@ pub fn createWorkerContext(self: *Env, worker: *WorkerGlobalScope, params: Conte
fn _createContext(self: *Env, global: anytype, params: ContextParams) !*Context {
const T = @TypeOf(global);
const is_page = T == *Page;
const is_frame = T == *Frame;
const context_arena = try self.app.arena_pool.acquire(.medium, params.debug_name);
errdefer self.app.arena_pool.release(context_arena);
@@ -242,7 +242,7 @@ fn _createContext(self: *Env, global: anytype, params: ContextParams) !*Context
errdefer v8.v8__MicrotaskQueue__DELETE(microtask_queue);
// Restore the context from the snapshot (0 = Page, 1 = Worker)
const snapshot_index: u32 = if (comptime is_page) 0 else 1;
const snapshot_index: u32 = if (comptime is_frame) 0 else 1;
const v8_context = v8.v8__Context__FromSnapshot__Config(isolate.handle, snapshot_index, &.{
.global_template = null,
.global_object = null,
@@ -259,7 +259,7 @@ fn _createContext(self: *Env, global: anytype, params: ContextParams) !*Context
// Store our TAO inside the internal field of the global object. This
// maps the v8::Object -> Zig instance.
const tao = try params.identity_arena.create(@import("TaggedOpaque.zig"));
tao.* = if (comptime is_page) .{
tao.* = if (comptime is_frame) .{
.value = @ptrCast(global.window),
.prototype_chain = (&Window.JsApi.Meta.prototype_chain).ptr,
.prototype_len = @intCast(Window.JsApi.Meta.prototype_chain.len),
@@ -282,7 +282,7 @@ fn _createContext(self: *Env, global: anytype, params: ContextParams) !*Context
const context = try context_arena.create(Context);
context.* = .{
.env = self,
.global = if (comptime is_page) .{ .page = global } else .{ .worker = global },
.global = if (comptime is_frame) .{ .frame = global } else .{ .worker = global },
.origin = origin,
.id = context_id,
.session = session,
@@ -292,7 +292,7 @@ fn _createContext(self: *Env, global: anytype, params: ContextParams) !*Context
.templates = self.templates,
.call_arena = params.call_arena,
.microtask_queue = microtask_queue,
.script_manager = if (comptime is_page) &global._script_manager else null,
.script_manager = if (comptime is_frame) &global._script_manager else null,
.scheduler = .init(context_arena),
.identity = params.identity,
.identity_arena = params.identity_arena,
@@ -312,7 +312,7 @@ fn _createContext(self: *Env, global: anytype, params: ContextParams) !*Context
// Register in the identity map. Multiple contexts can be created for the
// same global (via CDP), so we only register the first one.
const identity_ptr = if (comptime is_page) @intFromPtr(global.window) else @intFromPtr(global);
const identity_ptr = if (comptime is_frame) @intFromPtr(global.window) else @intFromPtr(global);
const gop = try params.identity.identity_map.getOrPut(params.identity_arena, identity_ptr);
if (gop.found_existing == false) {
var global_global: v8.Global = undefined;
@@ -380,7 +380,7 @@ pub fn runMacrotasks(self: *Env) !void {
// I hate this comptime check as much as you do. But we have tests
// which rely on short execution before shutdown. In real world, it's
// underterministic whether a timer will or won't run before the
// page shutsdown. But for tests, we need to run them to their end.
// frame shutsdown. But for tests, we need to run them to their end.
if (ctx.scheduler.hasReadyTasks() == false) {
continue;
}
@@ -506,11 +506,11 @@ fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) v
const no_handler = promise_event == v8.kPromiseRejectWithNoHandler;
switch (ctx.global) {
.page => |page| {
page.window.unhandledPromiseRejection(no_handler, .{
.frame => |frame| {
frame.window.unhandledPromiseRejection(no_handler, .{
.local = &local,
.handle = &message_handle,
}, page) catch |err| {
}, frame) catch |err| {
log.warn(.browser, "unhandled rejection handler", .{ .err = err, .target = "window" });
};
},
@@ -558,10 +558,10 @@ const PrivateSymbols = struct {
const testing = @import("../../testing.zig");
test "Env: Worker context " {
const session = testing.test_session;
const page = try session.createPage();
defer session.removePage();
const frame = try session.createFrame();
defer session.removeFrame();
const worker = try @import("../webapi/Worker.zig").init("http://localhost:9582/src/browser/tests/testing.js", &page.js.execution);
const worker = try @import("../webapi/Worker.zig").init("http://localhost:9582/src/browser/tests/testing.js", &frame.js.execution);
var ls: js.Local.Scope = undefined;
worker._worker_scope.js.localScope(&ls);
@@ -571,13 +571,13 @@ test "Env: Worker context " {
try testing.expectEqual(true, (try ls.local.exec("typeof WorkerGlobalScope !== 'undefined'", null)).isTrue());
}
test "Env: Page context" {
test "Env: Frame context" {
const session = testing.test_session;
const page = try session.createPage();
defer session.removePage();
const frame = try session.createFrame();
defer session.removeFrame();
// Page already has a context created, use it directly
const ctx = page.js;
// Frame already has a context created, use it directly
const ctx = frame.js;
var ls: js.Local.Scope = undefined;
ctx.localScope(&ls);

View File

@@ -19,7 +19,7 @@
//! Execution context for worker-compatible APIs.
//!
//! This provides a common interface for APIs that work in both Window and Worker
//! contexts. Instead of taking `*Page` (which is DOM-specific), these APIs take
//! contexts. Instead of taking `*Frame` (which is DOM-specific), these APIs take
//! `*Execution` which abstracts the common infrastructure.
//!
//! The bridge constructs an Execution on-the-fly from the current context,

View File

@@ -34,7 +34,7 @@ const log = lp.log;
const CallOpts = Caller.CallOpts;
const IS_DEBUG = @import("builtin").mode == .Debug;
// Where js.Context has a lifetime tied to the page, and holds the
// Where js.Context has a lifetime tied to the frame, and holds the
// v8::Global<v8::Context>, this has a much shorter lifetime and holds a
// v8::Local<v8::Context>. In V8, you need a Local<v8::Context> or get anything
// done, but the local only exists for the lifetime of the HandleScope it was
@@ -271,11 +271,11 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
const finalizer_ptr_id = finalizer.ptr_id;
const session = ctx.session;
const finalizer_gop = try session.finalizer_callbacks.getOrPut(session.page_arena, finalizer_ptr_id);
const finalizer_gop = try session.finalizer_callbacks.getOrPut(session.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 page reset if v8 doesn't finalize.
// so that we can cleanup on frame reset if v8 doesn't finalize.
errdefer _ = session.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);
@@ -336,15 +336,15 @@ pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts)
}
if (@typeInfo(ptr.child) == .@"struct" and @hasDecl(ptr.child, "runtimeGenericWrap")) {
const page = switch (self.ctx.global) {
.page => |p| p,
const frame = switch (self.ctx.global) {
.frame => |f| f,
.worker => {
// No Worker-related API currently uses this, so haven't
// added support for it
unreachable;
},
};
const wrap = try value.runtimeGenericWrap(page);
const wrap = try value.runtimeGenericWrap(frame);
return self.zigValueToJs(wrap, opts);
}
@@ -424,15 +424,15 @@ pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts)
// zig fmt: on
if (@hasDecl(T, "runtimeGenericWrap")) {
const page = switch (self.ctx.global) {
.page => |p| p,
const frame = switch (self.ctx.global) {
.frame => |f| f,
.worker => {
// No Worker-related API currently uses this, so haven't
// added support for it
unreachable;
},
};
const wrap = try value.runtimeGenericWrap(page);
const wrap = try value.runtimeGenericWrap(frame);
return self.zigValueToJs(wrap, opts);
}
@@ -1226,7 +1226,7 @@ fn resolveT(comptime T: type, value: *T) Resolved {
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
const identity_finalizer: *Session.FinalizerCallback.Identity = @ptrCast(@alignCast(ptr));
// Identity is allocated from pool, so it's valid even after page reset.
// Identity is allocated from pool, so it's valid even after frame reset.
const session = identity_finalizer.session;
const resolved_ptr_id = identity_finalizer.resolved_ptr_id;
defer session.fc_identity_pool.destroy(identity_finalizer);
@@ -1237,7 +1237,7 @@ fn resolveT(comptime T: type, value: *T) Resolved {
v8.v8__Global__Reset(&global);
}
// If done, FC was already cleaned up during page reset. The
// 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 (identity_finalizer.done) return;
@@ -1474,7 +1474,7 @@ pub fn throw(self: *const Local, err: []const u8) js.Exception {
}
// Convert a Global (or optional Global) to a Local (or optional Local).
// Meant to be used from either page.js.toLocal, where the context must have an
// Meant to be used from either frame.js.toLocal, where the context must have an
// non-null local (orelse panic), or from a LocalScope
pub fn toLocal(self: *const Local, global: anytype) ToLocalReturnType(@TypeOf(global)) {
const T = @TypeOf(global);
@@ -1561,12 +1561,12 @@ fn createFinalizerCallback(
// initiated or not), we need to create a Local.Scope:
//
// var ls: js.Local.Scope = udnefined;
// page.js.localScope(&ls);
// frame.js.localScope(&ls);
// defer ls.deinit();
// // can use ls.local as needed.
//
// Note: Zig code that is 100% guaranteed to be v8-initiated can get a local via:
// page.js.local.?
// frame.js.local.?
pub const Scope = struct {
local: Local,
handle_scope: js.HandleScope,

View File

@@ -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.page_arena) };
return .{ .str = try self.toSSOWithAlloc(self.local.ctx.session.frame_arena) };
}
return self.toSSOWithAlloc(self.local.call_arena);
}

View File

@@ -328,12 +328,23 @@ pub fn structuredCloneTo(self: Value, target: *const js.Local) !Value {
// Called by V8 to report serialization errors. The exception should already be thrown.
fn throwDataCloneError(_: ?*anyopaque, _: ?*const v8.String) callconv(.c) void {}
// Called when V8 encounters a SharedArrayBuffer. We don't support sharing them across
// contexts, so throw a DataCloneError and return false. V8's WriteJSArrayBuffer calls
// RETURN_VALUE_IF_EXCEPTION after this, so throwing prevents the fatal FromJust call.
fn getSharedArrayBufferId(_: ?*anyopaque, isolate: ?*v8.Isolate, _: ?*const v8.SharedArrayBuffer, _: ?*u32) callconv(.c) bool {
const iso = isolate orelse return false;
const message = v8.v8__String__NewFromUtf8(iso, "SharedArrayBuffer cannot be cloned.", v8.kNormal, -1);
const error_value = v8.v8__Exception__Error(message) orelse return false;
_ = v8.v8__Isolate__ThrowException(iso, error_value);
return false;
}
};
const size, const data = blk: {
const serializer = v8.v8__ValueSerializer__New(v8_isolate, &.{
.data = null,
.get_shared_array_buffer_id = null,
.get_shared_array_buffer_id = SerializerDelegate.getSharedArrayBufferId,
.write_host_object = SerializerDelegate.writeHostObject,
.throw_data_clone_error = SerializerDelegate.throwDataCloneError,
}) orelse return error.JsException;

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const lp = @import("lightpanda");
const js = @import("js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Session = @import("../Session.zig");
const v8 = js.v8;
@@ -161,7 +161,7 @@ pub const Function = struct {
var params = @typeInfo(T).@"fn".params;
for (params[1..]) |p| { // start at 1, skip self
const PT = p.type.?;
if (PT == *Page or PT == *const Page) {
if (PT == *Frame or PT == *const Frame) {
break;
}
if (@typeInfo(PT) == .optional) {
@@ -428,9 +428,9 @@ pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8
// Only Page contexts have document.getElementById lookup
switch (local.ctx.global) {
.page => |page| {
const document = page.document;
if (document.getElementById(property, page)) |el| {
.frame => |frame| {
const document = frame.document;
if (document.getElementById(property, frame)) |el| {
const js_val = local.zigValueToJs(el, .{}) catch return 0;
var pc = Caller.PropertyCallbackInfo{ .handle = handle.? };
pc.getReturnValue().set(js_val);
@@ -916,6 +916,7 @@ pub const WorkerJsApis = flattenTypes(&.{
@import("../webapi/net/URLSearchParams.zig"),
@import("../webapi/encoding/TextEncoder.zig"),
@import("../webapi/encoding/TextDecoder.zig"),
@import("../webapi/Blob.zig"),
@import("../webapi/File.zig"),
@import("../webapi/Console.zig"),
@import("../webapi/Crypto.zig"),
@@ -923,17 +924,19 @@ pub const WorkerJsApis = flattenTypes(&.{
@import("../webapi/net/Headers.zig"),
@import("../webapi/net/Request.zig"),
@import("../webapi/net/Response.zig"),
@import("../webapi/streams/TransformStream.zig"),
@import("../webapi/streams/ReadableStream.zig"),
@import("../webapi/streams/ReadableStreamDefaultReader.zig"),
@import("../webapi/streams/ReadableStreamDefaultController.zig"),
@import("../webapi/streams/WritableStream.zig"),
@import("../webapi/streams/WritableStreamDefaultWriter.zig"),
@import("../webapi/streams/WritableStreamDefaultController.zig"),
@import("../webapi/encoding/TextEncoderStream.zig"),
@import("../webapi/encoding/TextDecoderStream.zig"),
// @import("../webapi/URL.zig"),
// @import("../webapi/Blob.zig"),
// @import("../webapi/Performance.zig"),
// @import("../webapi/AbortSignal.zig"),
// @import("../webapi/AbortController.zig"),
// @import("../webapi/streams/ReadableStream.zig"),
// @import("../webapi/streams/ReadableStreamDefaultReader.zig"),
// @import("../webapi/streams/ReadableStreamDefaultController.zig"),
// @import("../webapi/streams/WritableStream.zig"),
// @import("../webapi/streams/WritableStreamDefaultWriter.zig"),
// @import("../webapi/streams/WritableStreamDefaultController.zig"),
});
// Master list of ALL JS APIs across all contexts.

View File

@@ -20,22 +20,22 @@ const std = @import("std");
const Element = @import("webapi/Element.zig");
const Node = @import("webapi/Node.zig");
const Page = @import("Page.zig");
const Frame = @import("Frame.zig");
const Selector = @import("webapi/selector/Selector.zig");
const Allocator = std.mem.Allocator;
/// Collect all links (href attributes from anchor tags) under `root`.
/// Returns a slice of strings allocated with `arena`.
pub fn collectLinks(arena: Allocator, root: *Node, page: *Page) ![]const []const u8 {
pub fn collectLinks(arena: Allocator, root: *Node, frame: *Frame) ![]const []const u8 {
var links: std.ArrayList([]const u8) = .empty;
if (Selector.querySelectorAll(root, "a[href]", page)) |list| {
defer list.deinit(page._session);
if (Selector.querySelectorAll(root, "a[href]", frame)) |list| {
defer list.deinit(frame._session);
for (list._nodes) |node| {
if (node.is(Element.Html.Anchor)) |anchor| {
const href = anchor.getHref(page) catch |err| {
const href = anchor.getHref(frame) catch |err| {
@import("../lightpanda.zig").log.err(.app, "resolve href failed", .{ .err = err });
continue;
};

View File

@@ -18,7 +18,7 @@
const std = @import("std");
const Page = @import("Page.zig");
const Frame = @import("Frame.zig");
const URL = @import("URL.zig");
const TreeWalker = @import("webapi/TreeWalker.zig");
const Element = @import("webapi/Element.zig");
@@ -131,7 +131,7 @@ fn analyzeContent(root: *Node) ContentInfo {
const Context = struct {
state: State,
writer: *std.Io.Writer,
page: *Page,
frame: *Frame,
fn ensureNewline(self: *Context) !void {
if (!self.state.last_char_was_newline) {
@@ -278,8 +278,8 @@ const Context = struct {
}
try self.writer.writeAll("](");
if (el.getAttributeSafe(comptime .wrap("src"))) |src| {
const page = self.page;
const absolute_src = URL.resolve(page.call_arena, page.base(), src, .{ .encoding = page.charset }) catch src;
const frame = self.frame;
const absolute_src = URL.resolve(frame.call_arena, frame.base(), src, .{ .encoding = frame.charset }) catch src;
try self.writer.writeAll(absolute_src);
}
try self.writer.writeAll(")");
@@ -287,14 +287,14 @@ const Context = struct {
return;
},
.anchor => {
const page = self.page;
const frame = self.frame;
const info = analyzeContent(el.asNode());
const label = getAnchorLabel(el);
const href_raw = el.getAttributeSafe(comptime .wrap("href"));
if (!info.has_visible and label == null and href_raw == null) return;
const href = if (href_raw) |h| URL.resolve(page.call_arena, page.base(), h, .{ .encoding = page.charset }) catch h else null;
const href = if (href_raw) |h| URL.resolve(frame.call_arena, frame.base(), h, .{ .encoding = frame.charset }) catch h else null;
if (info.has_block) {
try self.renderChildren(el.asNode());
@@ -459,12 +459,12 @@ const Context = struct {
}
};
pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, frame: *Frame) !void {
_ = opts;
var ctx: Context = .{
.state = .{},
.writer = writer,
.page = page,
.frame = frame,
};
try ctx.render(node);
if (!ctx.state.last_char_was_newline) {
@@ -474,18 +474,18 @@ pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void
fn testMarkdownHTML(html: []const u8, expected: []const u8) !void {
const testing = @import("../testing.zig");
const page = try testing.test_session.createPage();
defer testing.test_session.removePage();
page.url = "http://localhost/";
const frame = try testing.test_session.createFrame();
defer testing.test_session.removeFrame();
frame.url = "http://localhost/";
const doc = page.window._document;
const doc = frame.window._document;
const div = try doc.createElement("div", null, page);
try page.parseHtmlAsChildren(div.asNode(), html);
const div = try doc.createElement("div", null, frame);
try frame.parseHtmlAsChildren(div.asNode(), html);
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
defer aw.deinit();
try dump(div.asNode(), .{}, &aw.writer, page);
try dump(div.asNode(), .{}, &aw.writer, frame);
try testing.expectString(expected, aw.written());
}
@@ -677,13 +677,13 @@ test "browser.markdown: skip empty links" {
test "browser.markdown: resolve links" {
const testing = @import("../testing.zig");
const page = try testing.test_session.createPage();
defer testing.test_session.removePage();
page.url = "https://example.com/a/index.html";
const frame = try testing.test_session.createFrame();
defer testing.test_session.removeFrame();
frame.url = "https://example.com/a/index.html";
const doc = page.window._document;
const div = try doc.createElement("div", null, page);
try page.parseHtmlAsChildren(div.asNode(),
const doc = frame.window._document;
const div = try doc.createElement("div", null, frame);
try frame.parseHtmlAsChildren(div.asNode(),
\\<a href="b">Link</a>
\\<img src="../c.png" alt="Img">
\\<a href="/my page">Space</a>
@@ -691,7 +691,7 @@ test "browser.markdown: resolve links" {
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
defer aw.deinit();
try dump(div.asNode(), .{}, &aw.writer, page);
try dump(div.asNode(), .{}, &aw.writer, frame);
try testing.expectString(
\\[Link](https://example.com/a/b)

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const lp = @import("lightpanda");
const h5e = @import("html5ever.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Node = @import("../webapi/Node.zig");
const Element = @import("../webapi/Element.zig");
@@ -41,16 +41,16 @@ pub const ParsedNode = struct {
const Parser = @This();
page: *Page,
frame: *Frame,
err: ?Error,
container: ParsedNode,
arena: Allocator,
strings: std.StringHashMapUnmanaged(void),
pub fn init(arena: Allocator, node: *Node, page: *Page) Parser {
pub fn init(arena: Allocator, node: *Node, frame: *Frame) Parser {
return .{
.err = null,
.page = page,
.frame = frame,
.strings = .empty,
.arena = arena,
.container = ParsedNode{
@@ -179,10 +179,10 @@ pub const Streaming = struct {
parser: Parser,
handle: ?*anyopaque,
pub fn init(arena: Allocator, node: *Node, page: *Page) Streaming {
pub fn init(arena: Allocator, node: *Node, frame: *Frame) Streaming {
return .{
.handle = null,
.parser = Parser.init(arena, node, page),
.parser = Parser.init(arena, node, frame),
};
}
@@ -252,7 +252,7 @@ fn popCallback(ctx: *anyopaque, node_ref: *anyopaque) callconv(.c) void {
}
fn _popCallback(self: *Parser, node: *Node) !void {
try self.page.nodeComplete(node);
try self.frame.nodeComplete(node);
}
fn createElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {
@@ -271,11 +271,11 @@ fn _createElementCallbackWithDefaultnamespace(ctx: *anyopaque, data: *anyopaque,
};
}
fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator, default_namespace: Element.Namespace) !*anyopaque {
const page = self.page;
const frame = self.frame;
const name = qname.local.slice();
const namespace_string = qname.ns.slice();
const namespace = if (namespace_string.len == 0) default_namespace else Element.Namespace.parse(namespace_string);
const node = try page.createElementNS(namespace, name, attributes);
const node = try frame.createElementNS(namespace, name, attributes);
const pn = try self.arena.create(ParsedNode);
pn.* = .{
@@ -293,8 +293,8 @@ fn createCommentCallback(ctx: *anyopaque, str: h5e.StringSlice) callconv(.c) ?*a
};
}
fn _createCommentCallback(self: *Parser, str: []const u8) !*anyopaque {
const page = self.page;
const node = try page.createComment(str);
const frame = self.frame;
const node = try frame.createComment(str);
const pn = try self.arena.create(ParsedNode);
pn.* = .{
.data = null,
@@ -311,8 +311,8 @@ fn createProcessingInstruction(ctx: *anyopaque, target: h5e.StringSlice, data: h
};
}
fn _createProcessingInstruction(self: *Parser, target: []const u8, data: []const u8) !*anyopaque {
const page = self.page;
const node = try page.createProcessingInstruction(target, data);
const frame = self.frame;
const node = try frame.createProcessingInstruction(target, data);
const pn = try self.arena.create(ParsedNode);
pn.* = .{
.data = null,
@@ -328,19 +328,19 @@ fn appendDoctypeToDocument(ctx: *anyopaque, name: h5e.StringSlice, public_id: h5
};
}
fn _appendDoctypeToDocument(self: *Parser, name: []const u8, public_id: []const u8, system_id: []const u8) !void {
const page = self.page;
const frame = self.frame;
// Create the DocumentType node
const DocumentType = @import("../webapi/DocumentType.zig");
const doctype = try page._factory.node(DocumentType{
const doctype = try frame._factory.node(DocumentType{
._proto = undefined,
._name = try page.dupeString(name),
._public_id = try page.dupeString(public_id),
._system_id = try page.dupeString(system_id),
._name = try frame.dupeString(name),
._public_id = try frame.dupeString(public_id),
._system_id = try frame.dupeString(system_id),
});
// Append it to the document
try page.appendNew(self.container.node, .{ .node = doctype.asNode() });
try frame.appendNew(self.container.node, .{ .node = doctype.asNode() });
}
fn addAttrsIfMissingCallback(ctx: *anyopaque, target_ref: *anyopaque, attributes: h5e.AttributeIterator) callconv(.c) void {
@@ -351,14 +351,14 @@ fn addAttrsIfMissingCallback(ctx: *anyopaque, target_ref: *anyopaque, attributes
}
fn _addAttrsIfMissingCallback(self: *Parser, node: *Node, attributes: h5e.AttributeIterator) !void {
const element = node.as(Element);
const page = self.page;
const frame = self.frame;
const attr_list = try element.getOrCreateAttributeList(page);
const attr_list = try element.getOrCreateAttributeList(frame);
while (attributes.next()) |attr| {
const name = attr.name.local.slice();
const value = attr.value.slice();
// putNew only adds if the attribute doesn't already exist
try attr_list.putNew(name, value, page);
try attr_list.putNew(name, value, frame);
}
}
@@ -412,11 +412,11 @@ fn _appendCallback(self: *Parser, parent: *Node, node_or_text: h5e.NodeOrText) !
if (comptime IS_DEBUG) {
unreachable;
}
self.page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() });
self.frame.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() });
}
try self.page.appendNew(parent, .{ .node = child });
try self.frame.appendNew(parent, .{ .node = child });
},
.text => |txt| try self.page.appendNew(parent, .{ .text = txt }),
.text => |txt| try self.frame.appendNew(parent, .{ .text = txt }),
}
}
@@ -428,7 +428,7 @@ fn removeFromParentCallback(ctx: *anyopaque, target_ref: *anyopaque) callconv(.c
}
fn _removeFromParentCallback(self: *Parser, node: *Node) !void {
const parent = node.parentNode() orelse return;
_ = try parent.removeChild(node, self.page);
_ = try parent.removeChild(node, self.frame);
}
fn reparentChildrenCallback(ctx: *anyopaque, node_ref: *anyopaque, new_parent_ref: *anyopaque) callconv(.c) void {
@@ -438,7 +438,7 @@ fn reparentChildrenCallback(ctx: *anyopaque, node_ref: *anyopaque, new_parent_re
};
}
fn _reparentChildrenCallback(self: *Parser, node: *Node, new_parent: *Node) !void {
try self.page.appendAllChildren(node, new_parent);
try self.frame.appendAllChildren(node, new_parent);
}
fn appendBeforeSiblingCallback(ctx: *anyopaque, sibling_ref: *anyopaque, node_or_text: h5e.NodeOrText) callconv(.c) void {
@@ -456,13 +456,13 @@ fn _appendBeforeSiblingCallback(self: *Parser, sibling: *Node, node_or_text: h5e
// A custom element constructor may have inserted the node into the
// DOM before the parser officially places it (e.g. via foster
// parenting). Detach it first so insertNodeRelative's assertion holds.
self.page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() });
self.frame.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() });
}
break :blk child;
},
.text => |txt| try self.page.createTextNode(txt),
.text => |txt| try self.frame.createTextNode(txt),
};
try self.page.insertNodeRelative(parent, node, .{ .before = sibling }, .{});
try self.frame.insertNodeRelative(parent, node, .{ .before = sibling }, .{});
}
fn appendBasedOnParentNodeCallback(ctx: *anyopaque, element_ref: *anyopaque, prev_element_ref: *anyopaque, node_or_text: h5e.NodeOrText) callconv(.c) void {

View File

@@ -18,7 +18,7 @@
const std = @import("std");
const Page = @import("Page.zig");
const Frame = @import("Frame.zig");
const URL = @import("URL.zig");
const TreeWalker = @import("webapi/TreeWalker.zig");
const Element = @import("webapi/Element.zig");
@@ -132,11 +132,11 @@ fn writeProperties(jw: anytype, properties: []const Property) !void {
try jw.endObject();
}
/// Extract all structured data from the page.
/// Extract all structured data from the frame.
pub fn collectStructuredData(
root: *Node,
arena: Allocator,
page: *Page,
frame: *Frame,
) !StructuredData {
var json_ld: std.ArrayList([]const u8) = .empty;
var open_graph: std.ArrayList(Property) = .empty;
@@ -175,7 +175,7 @@ pub fn collectStructuredData(
},
.meta => collectMeta(el, &open_graph, &twitter_card, &meta, arena) catch {},
.title => try collectTitle(node, arena, &meta),
.link => try collectLink(el, arena, page, &links, &alternate),
.link => try collectLink(el, arena, frame, &links, &alternate),
// Skip body subtree for non-JSON-LD — all other metadata is in <head>.
// JSON-LD can appear in <body> so we don't skip the whole body.
else => {},
@@ -282,13 +282,13 @@ fn collectTitle(
fn collectLink(
el: *Element,
arena: Allocator,
page: *Page,
frame: *Frame,
links: *std.ArrayList(Property),
alternate: *std.ArrayList(AlternateLink),
) !void {
const rel = el.getAttributeSafe(comptime .wrap("rel")) orelse return;
const raw_href = el.getAttributeSafe(comptime .wrap("href")) orelse return;
const href = URL.resolve(arena, page.base(), raw_href, .{ .encoding = page.charset }) catch raw_href;
const href = URL.resolve(arena, frame.base(), raw_href, .{ .encoding = frame.charset }) catch raw_href;
if (std.ascii.eqlIgnoreCase(rel, "alternate")) {
try alternate.append(arena, .{
@@ -318,14 +318,14 @@ fn collectLink(
const testing = @import("../testing.zig");
fn testStructuredData(html: []const u8) !StructuredData {
const page = try testing.test_session.createPage();
defer testing.test_session.removePage();
const frame = try testing.test_session.createFrame();
defer testing.test_session.removeFrame();
const doc = page.window._document;
const div = try doc.createElement("div", null, page);
try page.parseHtmlAsChildren(div.asNode(), html);
const doc = frame.window._document;
const div = try doc.createElement("div", null, frame);
try frame.parseHtmlAsChildren(div.asNode(), html);
return collectStructuredData(div.asNode(), page.call_arena, page);
return collectStructuredData(div.asNode(), frame.call_arena, frame);
}
fn findProperty(props: []const Property, key: []const u8) ?[]const u8 {

View File

@@ -59,7 +59,7 @@
let byTagNameAll = document.getElementsByTagName('*');
// If you add a script block (or change the HTML in any other way on this
// page), this test will break. Adjust it accordingly.
// frame), this test will break. Adjust it accordingly.
testing.expectEqual(12, byTagNameAll.length);
testing.expectEqual('html', byTagNameAll.item(0).localName);
testing.expectEqual('SCRIPT', byTagNameAll.item(11).tagName);

View File

@@ -7,7 +7,7 @@
//
// This is pretty straightforward, with the only complexity coming from "eventually"
// assertions, which are assertions we lazily check in `getStatus()`. We
// do this because, by the time `getStatus()`, `page.wait()` will have been called
// do this because, by the time `getStatus()`, `frame.wait()` will have been called
// and any timer (setTimeout, requestAnimation, MutationObserver, etc...) will
// have been evaluated. Test which use/test these behavior will use `eventually`.
(() => {
@@ -87,7 +87,7 @@
}
// Set expectations to happen at some point in the future. Necessary for
// testing callbacks which will only be executed after page.wait is called.
// testing callbacks which will only be executed after frame.wait is called.
function eventually(fn) {
// capture the current state (script id, stack) so that, when we do run this
// we can display more meaningful details on failure.

View File

@@ -302,7 +302,7 @@ pub const tool_defs = [_]ToolDef{
};
pub const ToolError = error{
PageNotLoaded,
FrameNotLoaded,
InvalidParams,
NodeNotFound,
NavigationFailed,
@@ -327,7 +327,7 @@ pub const UrlParams = struct {
waitUntil: ?lp.Config.WaitUntil = null,
};
const NodeAndPage = struct { node: *DOMNode, page: *lp.Page };
const NodeAndPage = struct { node: *DOMNode, page: *lp.Frame };
pub const Action = enum {
goto,
@@ -438,7 +438,7 @@ fn execTree(session: *lp.Session, registry: *CDPNode.Registry, arena: std.mem.Al
const st = lp.SemanticTree{
.dom_node = root_node,
.registry = registry,
.page = page,
.frame = page,
.arena = arena,
.prune = true,
.max_depth = args.maxDepth orelse std.math.maxInt(u32) - 1,
@@ -453,7 +453,7 @@ fn execNodeDetails(session: *lp.Session, registry: *CDPNode.Registry, arena: std
const Params = struct { backendNodeId: CDPNode.Id };
const args = try parseArgsOrErr(Params, arena, arguments) orelse return ToolError.InvalidParams;
const page = session.currentPage() orelse return ToolError.PageNotLoaded;
const page = session.currentFrame() orelse return ToolError.FrameNotLoaded;
const node = registry.lookup_by_id.get(args.backendNodeId) orelse
return ToolError.NodeNotFound;
@@ -558,7 +558,7 @@ fn execClick(session: *lp.Session, registry: *CDPNode.Registry, arena: std.mem.A
runner.wait(.{ .ms = 10000, .until = .done }) catch return ToolError.NavigationFailed;
}
const page = session.currentPage() orelse return ToolError.PageNotLoaded;
const page = session.currentFrame() orelse return ToolError.FrameNotLoaded;
const page_title = page.getTitle() catch null;
if (args.selector) |sel| {
return std.fmt.allocPrint(arena, "Clicked element (selector: {s}). Page url: {s}, title: {s}", .{
@@ -617,7 +617,7 @@ fn execScroll(session: *lp.Session, registry: *CDPNode.Registry, arena: std.mem.
y: ?i32 = null,
};
const args = try parseArgsOrDefault(Params, arena, arguments);
const page = session.currentPage() orelse return ToolError.PageNotLoaded;
const page = session.currentFrame() orelse return ToolError.FrameNotLoaded;
var target_node: ?*DOMNode = null;
if (args.backendNodeId) |node_id| {
@@ -646,7 +646,7 @@ fn execWaitForSelector(session: *lp.Session, registry: *CDPNode.Registry, arena:
};
const args = try parseArgsOrErr(Params, arena, arguments) orelse return ToolError.InvalidParams;
_ = session.currentPage() orelse return ToolError.PageNotLoaded;
_ = session.currentFrame() orelse return ToolError.FrameNotLoaded;
const timeout_ms = args.timeout orelse 5000;
@@ -697,7 +697,7 @@ fn execPress(session: *lp.Session, registry: *CDPNode.Registry, arena: std.mem.A
};
const args = try parseArgsOrErr(Params, arena, arguments) orelse return ToolError.InvalidParams;
const page = session.currentPage() orelse return ToolError.PageNotLoaded;
const page = session.currentFrame() orelse return ToolError.FrameNotLoaded;
var target_node: ?*DOMNode = null;
if (args.backendNodeId) |node_id| {
@@ -716,7 +716,7 @@ fn execPress(session: *lp.Session, registry: *CDPNode.Registry, arena: std.mem.A
runner.wait(.{ .ms = 10000, .until = .done }) catch return ToolError.NavigationFailed;
}
const current_page = session.currentPage() orelse return ToolError.PageNotLoaded;
const current_page = session.currentFrame() orelse return ToolError.FrameNotLoaded;
const page_title = current_page.getTitle() catch null;
return std.fmt.allocPrint(arena, "Pressed key '{s}'. Page url: {s}, title: {s}", .{
args.key,
@@ -801,7 +801,7 @@ fn execFindElement(session: *lp.Session, registry: *CDPNode.Registry, arena: std
if (args.role == null and args.name == null) return ToolError.InvalidParams;
const page = session.currentPage() orelse return ToolError.PageNotLoaded;
const page = session.currentFrame() orelse return ToolError.FrameNotLoaded;
const elements = lp.interactive.collectInteractiveElements(page.document.asNode(), arena, page) catch
return ToolError.InternalError;
@@ -841,7 +841,7 @@ fn execConsoleLogs(
session: *lp.Session,
arena: std.mem.Allocator,
) ToolError![]const u8 {
const page = session.currentPage() orelse return ToolError.PageNotLoaded;
const page = session.currentFrame() orelse return ToolError.FrameNotLoaded;
const messages = page.drainConsoleMessages();
if (messages.len == 0) return "No console messages.";
@@ -854,7 +854,7 @@ fn execConsoleLogs(
}
fn execGetUrl(session: *lp.Session) ToolError![]const u8 {
const page = session.currentPage() orelse return ToolError.PageNotLoaded;
const page = session.currentFrame() orelse return ToolError.FrameNotLoaded;
return page.url;
}
@@ -874,19 +874,19 @@ fn execGetCookies(session: *lp.Session, arena: std.mem.Allocator) ToolError![]co
return aw.written();
}
fn ensurePage(session: *lp.Session, registry: *CDPNode.Registry, url: ?[:0]const u8, timeout: ?u32, waitUntil: ?lp.Config.WaitUntil) ToolError!*lp.Page {
fn ensurePage(session: *lp.Session, registry: *CDPNode.Registry, url: ?[:0]const u8, timeout: ?u32, waitUntil: ?lp.Config.WaitUntil) ToolError!*lp.Frame {
if (url) |u| {
try performGoto(session, registry, u, timeout, waitUntil);
}
return session.currentPage() orelse ToolError.PageNotLoaded;
return session.currentFrame() orelse ToolError.FrameNotLoaded;
}
fn performGoto(session: *lp.Session, registry: *CDPNode.Registry, url: [:0]const u8, timeout: ?u32, waitUntil: ?lp.Config.WaitUntil) ToolError!void {
if (session.page != null) {
if (session.frame != null) {
registry.reset();
session.removePage();
session.removeFrame();
}
const page = session.createPage() catch return ToolError.NavigationFailed;
const page = session.createFrame() catch return ToolError.NavigationFailed;
_ = page.navigate(url, .{
.reason = .address_bar,
.kind = .{ .push = null },
@@ -900,13 +900,13 @@ fn performGoto(session: *lp.Session, registry: *CDPNode.Registry, url: [:0]const
}
fn resolveNodeAndPage(session: *lp.Session, registry: *CDPNode.Registry, node_id: CDPNode.Id) ToolError!NodeAndPage {
const page = session.currentPage() orelse return ToolError.PageNotLoaded;
const page = session.currentFrame() orelse return ToolError.FrameNotLoaded;
const node = registry.lookup_by_id.get(node_id) orelse return ToolError.NodeNotFound;
return .{ .node = node.dom, .page = page };
}
fn resolveBySelector(session: *lp.Session, selector: []const u8) ToolError!NodeAndPage {
const page = session.currentPage() orelse return ToolError.PageNotLoaded;
const page = session.currentFrame() orelse return ToolError.FrameNotLoaded;
const element = Selector.querySelector(page.document.asNode(), selector, page) catch return ToolError.InvalidParams;
const node = (element orelse return ToolError.NodeNotFound).asNode();
return .{ .node = node, .page = page };

View File

@@ -19,16 +19,16 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const AbortSignal = @import("AbortSignal.zig");
const AbortController = @This();
_signal: *AbortSignal,
pub fn init(page: *Page) !*AbortController {
const signal = try AbortSignal.init(page);
return page._factory.create(AbortController{
pub fn init(frame: *Frame) !*AbortController {
const signal = try AbortSignal.init(frame);
return frame._factory.create(AbortController{
._signal = signal,
});
}
@@ -37,8 +37,8 @@ pub fn getSignal(self: *const AbortController) *AbortSignal {
return self._signal;
}
pub fn abort(self: *AbortController, reason_: ?js.Value.Global, page: *Page) !void {
try self._signal.abort(if (reason_) |r| .{ .js_val = r } else null, page);
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 const JsApi = struct {

View File

@@ -20,7 +20,7 @@ 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 Event = @import("Event.zig");
const EventTarget = @import("EventTarget.zig");
@@ -34,8 +34,8 @@ _aborted: bool = false,
_reason: Reason = .undefined,
_on_abort: ?js.Function.Global = null,
pub fn init(page: *Page) !*AbortSignal {
return page._factory.eventTarget(AbortSignal{
pub fn init(frame: *Frame) !*AbortSignal {
return frame._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, page: *Page) !void {
pub fn abort(self: *AbortSignal, reason_: ?Reason, frame: *Frame) !void {
if (self._aborted) {
return;
}
@@ -71,7 +71,7 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void {
if (reason_) |reason| {
switch (reason) {
.js_val => |js_val| self._reason = .{ .js_val = js_val },
.string => |str| self._reason = .{ .string = try page.dupeString(str) },
.string => |str| self._reason = .{ .string = try frame.dupeString(str) },
.undefined => self._reason = reason,
}
} else {
@@ -80,27 +80,27 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void {
// Dispatch abort event
const target = self.asEventTarget();
if (page._event_manager.hasDirectListeners(target, "abort", self._on_abort)) {
const event = try Event.initTrusted(comptime .wrap("abort"), .{}, page);
try page._event_manager.dispatchDirect(target, event, self._on_abort, .{ .context = "abort signal" });
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" });
}
}
// Static method to create an already-aborted signal
pub fn createAborted(reason_: ?js.Value.Global, page: *Page) !*AbortSignal {
const signal = try init(page);
try signal.abort(if (reason_) |r| .{ .js_val = r } else null, page);
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);
return signal;
}
pub fn createTimeout(delay: u32, page: *Page) !*AbortSignal {
const callback = try page.arena.create(TimeoutCallback);
pub fn createTimeout(delay: u32, frame: *Frame) !*AbortSignal {
const callback = try frame.arena.create(TimeoutCallback);
callback.* = .{
.page = page,
.signal = try init(page),
.frame = frame,
.signal = try init(frame),
};
try page.js.scheduler.add(callback, TimeoutCallback.run, delay, .{
try frame.js.scheduler.add(callback, TimeoutCallback.run, delay, .{
.name = "AbortSignal.timeout",
});
@@ -111,8 +111,8 @@ const ThrowIfAborted = union(enum) {
exception: js.Exception,
undefined: void,
};
pub fn throwIfAborted(self: *const AbortSignal, page: *Page) !ThrowIfAborted {
const local = page.js.local.?;
pub fn throwIfAborted(self: *const AbortSignal, frame: *Frame) !ThrowIfAborted {
const local = frame.js.local.?;
if (self._aborted) {
const exception = switch (self._reason) {
@@ -132,12 +132,12 @@ const Reason = union(enum) {
};
const TimeoutCallback = struct {
page: *Page,
frame: *Frame,
signal: *AbortSignal,
fn run(ctx: *anyopaque) !?u32 {
const self: *TimeoutCallback = @ptrCast(@alignCast(ctx));
self.signal.abort(.{ .string = "TimeoutError" }, self.page) catch |err| {
self.signal.abort(.{ .string = "TimeoutError" }, self.frame) catch |err| {
log.warn(.app, "abort signal timeout", .{ .err = err });
};
return null;

View File

@@ -35,10 +35,10 @@ pub const _prototype_root = true;
_rc: lp.RC(u8) = .{},
_type: Type,
_page_id: u32,
_arena: Allocator,
_end_offset: u32,
_start_offset: u32,
_frame_loader_id: u32,
_end_container: *Node,
_start_container: *Node,
@@ -50,8 +50,8 @@ pub fn acquireRef(self: *AbstractRange) void {
}
pub fn deinit(self: *AbstractRange, session: *Session) void {
if (session.findPageById(self._page_id)) |page| {
page._live_ranges.remove(&self._range_link);
if (session.findFrameByLoaderId(self._frame_loader_id)) |frame| {
frame._live_ranges.remove(&self._range_link);
}
session.releaseArena(self._arena);
}

View File

@@ -20,12 +20,12 @@ const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Session = @import("../Session.zig");
const Mime = @import("../Mime.zig");
const Writer = std.Io.Writer;
const Execution = js.Execution;
const Allocator = std.mem.Allocator;
/// https://w3c.github.io/FileAPI/#blob-section
@@ -61,9 +61,9 @@ 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, page: *Page) !*Blob {
const arena = try page.getArena(.large, "Blob");
errdefer page.releaseArena(arena);
pub fn init(parts_: ?[]const js.Value, opts_: ?InitOptions, session: *Session) !*Blob {
const arena = try session.getArena(.large, "Blob");
errdefer session.releaseArena(arena);
const opts: InitOptions = opts_ orelse .{};
const mime = try validateMimeType(arena, opts.type, false);
@@ -259,29 +259,29 @@ fn writePartWithEndings(part: []const u8, use_native_endings: bool, writer: *Wri
/// Returns a Promise that resolves with the contents of the blob
/// as binary data contained in an ArrayBuffer.
pub fn arrayBuffer(self: *const Blob, page: *Page) !js.Promise {
return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = self._slice });
pub fn arrayBuffer(self: *const Blob, exec: *Execution) !js.Promise {
return exec.context.local.?.resolvePromise(js.ArrayBuffer{ .values = self._slice });
}
const ReadableStream = @import("streams/ReadableStream.zig");
/// Returns a ReadableStream which upon reading returns the data
/// contained within the Blob.
pub fn stream(self: *const Blob, page: *Page) !*ReadableStream {
return ReadableStream.initWithData(self._slice, page);
pub fn stream(self: *const Blob, exec: *Execution) !*ReadableStream {
return ReadableStream.initWithData(self._slice, exec);
}
/// Returns a Promise that resolves with a string containing
/// the contents of the blob, interpreted as UTF-8.
pub fn text(self: *const Blob, page: *Page) !js.Promise {
return page.js.local.?.resolvePromise(self._slice);
pub fn text(self: *const Blob, exec: *Execution) !js.Promise {
return exec.context.local.?.resolvePromise(self._slice);
}
/// Extension to Blob; works on Firefox and Safari.
/// https://developer.mozilla.org/en-US/docs/Web/API/Blob/bytes
/// Returns a Promise that resolves with a Uint8Array containing
/// the contents of the blob as an array of bytes.
pub fn bytes(self: *const Blob, page: *Page) !js.Promise {
return page.js.local.?.resolvePromise(js.TypedArray(u8){ .values = self._slice });
pub fn bytes(self: *const Blob, exec: *Execution) !js.Promise {
return exec.context.local.?.resolvePromise(js.TypedArray(u8){ .values = self._slice });
}
/// Returns a new Blob object which contains data

View File

@@ -20,7 +20,7 @@ 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 Node = @import("Node.zig");
pub const Text = @import("cdata/Text.zig");
@@ -226,26 +226,26 @@ pub fn render(self: *const CData, writer: *std.io.Writer, opts: RenderOpts) !boo
return false;
}
pub fn setData(self: *CData, value: ?[]const u8, page: *Page) !void {
pub fn setData(self: *CData, value: ?[]const u8, frame: *Frame) !void {
const old_value = self._data;
if (value) |v| {
self._data = try page.dupeSSO(v);
self._data = try frame.dupeSSO(v);
} else {
self._data = .empty;
}
page.characterDataChange(self.asNode(), old_value);
frame.characterDataChange(self.asNode(), old_value);
}
/// JS bridge wrapper for `data` setter.
/// Per spec, setting .data runs replaceData(0, this.length, value),
/// which includes live range updates.
/// Handles [LegacyNullToEmptyString]: null → "" per spec.
pub fn _setData(self: *CData, value: js.Value, page: *Page) !void {
pub fn _setData(self: *CData, value: js.Value, frame: *Frame) !void {
const new_value: []const u8 = if (value.isNull()) "" else try value.toZig([]const u8);
const length = self.getLength();
try self.replaceData(0, length, new_value, page);
try self.replaceData(0, length, new_value, frame);
}
pub fn format(self: *const CData, writer: *std.io.Writer) !void {
@@ -277,70 +277,70 @@ pub fn isEqualNode(self: *const CData, other: *const CData) bool {
return self._data.eql(other._data);
}
pub fn appendData(self: *CData, data: []const u8, page: *Page) !void {
pub fn appendData(self: *CData, data: []const u8, frame: *Frame) !void {
// Per DOM spec, appendData(data) is replaceData(length, 0, data).
const length = self.getLength();
try self.replaceData(length, 0, data, page);
try self.replaceData(length, 0, data, frame);
}
pub fn deleteData(self: *CData, offset: usize, count: usize, page: *Page) !void {
pub fn deleteData(self: *CData, offset: usize, count: usize, frame: *Frame) !void {
const end_utf16 = std.math.add(usize, offset, count) catch std.math.maxInt(usize);
const range = try utf16RangeToUtf8(self._data.str(), offset, end_utf16);
// Update live ranges per DOM spec replaceData steps (deleteData = replaceData with data="")
const length = self.getLength();
const effective_count: u32 = @intCast(@min(count, length - offset));
page.updateRangesForCharacterDataReplace(self.asNode(), @intCast(offset), effective_count, 0);
frame.updateRangesForCharacterDataReplace(self.asNode(), @intCast(offset), effective_count, 0);
const old_data = self._data;
const old_value = old_data.str();
if (range.start == 0) {
self._data = try page.dupeSSO(old_value[range.end..]);
self._data = try frame.dupeSSO(old_value[range.end..]);
} else if (range.end >= old_value.len) {
self._data = try page.dupeSSO(old_value[0..range.start]);
self._data = try frame.dupeSSO(old_value[0..range.start]);
} else {
// Deleting from middle - concat prefix and suffix
self._data = try String.concat(page.arena, &.{
self._data = try String.concat(frame.arena, &.{
old_value[0..range.start],
old_value[range.end..],
});
}
page.characterDataChange(self.asNode(), old_data);
frame.characterDataChange(self.asNode(), old_data);
}
pub fn insertData(self: *CData, offset: usize, data: []const u8, page: *Page) !void {
pub fn insertData(self: *CData, offset: usize, data: []const u8, frame: *Frame) !void {
const byte_offset = try utf16OffsetToUtf8(self._data.str(), offset);
// Update live ranges per DOM spec replaceData steps (insertData = replaceData with count=0)
page.updateRangesForCharacterDataReplace(self.asNode(), @intCast(offset), 0, @intCast(utf16Len(data)));
frame.updateRangesForCharacterDataReplace(self.asNode(), @intCast(offset), 0, @intCast(utf16Len(data)));
const old_value = self._data;
const existing = old_value.str();
self._data = try String.concat(page.arena, &.{
self._data = try String.concat(frame.arena, &.{
existing[0..byte_offset],
data,
existing[byte_offset..],
});
page.characterDataChange(self.asNode(), old_value);
frame.characterDataChange(self.asNode(), old_value);
}
pub fn replaceData(self: *CData, offset: usize, count: usize, data: []const u8, page: *Page) !void {
pub fn replaceData(self: *CData, offset: usize, count: usize, data: []const u8, frame: *Frame) !void {
const end_utf16 = std.math.add(usize, offset, count) catch std.math.maxInt(usize);
const range = try utf16RangeToUtf8(self._data.str(), offset, end_utf16);
// Update live ranges per DOM spec replaceData steps
const length = self.getLength();
const effective_count: u32 = @intCast(@min(count, length - offset));
page.updateRangesForCharacterDataReplace(self.asNode(), @intCast(offset), effective_count, @intCast(utf16Len(data)));
frame.updateRangesForCharacterDataReplace(self.asNode(), @intCast(offset), effective_count, @intCast(utf16Len(data)));
const old_value = self._data;
const existing = old_value.str();
self._data = try String.concat(page.arena, &.{
self._data = try String.concat(frame.arena, &.{
existing[0..range.start],
data,
existing[range.end..],
});
page.characterDataChange(self.asNode(), old_value);
frame.characterDataChange(self.asNode(), old_value);
}
pub fn substringData(self: *const CData, offset: usize, count: usize) ![]const u8 {
@@ -349,49 +349,49 @@ pub fn substringData(self: *const CData, offset: usize, count: usize) ![]const u
return self._data.str()[range.start..range.end];
}
pub fn remove(self: *CData, page: *Page) !void {
pub fn remove(self: *CData, frame: *Frame) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
_ = try parent.removeChild(node, page);
_ = try parent.removeChild(node, frame);
}
pub fn before(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void {
pub fn before(self: *CData, nodes: []const Node.NodeOrText, frame: *Frame) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
_ = try parent.insertBefore(child, node, page);
const child = try node_or_text.toNode(frame);
_ = try parent.insertBefore(child, node, frame);
}
}
pub fn after(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void {
pub fn after(self: *CData, nodes: []const Node.NodeOrText, frame: *Frame) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
const viable_next = Node.NodeOrText.viableNextSibling(node, nodes);
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
_ = try parent.insertBefore(child, viable_next, page);
const child = try node_or_text.toNode(frame);
_ = try parent.insertBefore(child, viable_next, frame);
}
}
pub fn replaceWith(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void {
pub fn replaceWith(self: *CData, nodes: []const Node.NodeOrText, frame: *Frame) !void {
const ref_node = self.asNode();
const parent = ref_node.parentNode() orelse return;
var rm_ref_node = true;
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
const child = try node_or_text.toNode(frame);
if (child == ref_node) {
rm_ref_node = false;
continue;
}
_ = try parent.insertBefore(child, ref_node, page);
_ = try parent.insertBefore(child, ref_node, frame);
}
if (rm_ref_node) {
_ = try parent.removeChild(ref_node, page);
_ = try parent.removeChild(ref_node, frame);
}
}

View File

@@ -18,7 +18,7 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const CSS = @This();
_pad: bool = false,
@@ -40,7 +40,7 @@ pub fn parseDimension(value: []const u8) ?f64 {
/// Escapes a CSS identifier string
/// https://drafts.csswg.org/cssom/#the-css.escape()-method
pub fn escape(_: *const CSS, value: []const u8, page: *Page) ![]const u8 {
pub fn escape(_: *const CSS, value: []const u8, frame: *Frame) ![]const u8 {
if (value.len == 0) {
return "";
}
@@ -65,7 +65,7 @@ pub fn escape(_: *const CSS, value: []const u8, page: *Page) ![]const u8 {
return value;
}
const result = try page.call_arena.alloc(u8, out_len);
const result = try frame.call_arena.alloc(u8, out_len);
var pos: usize = 0;
if (needsEscape(true, first)) {

View File

@@ -20,7 +20,7 @@ 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 logger = lp.log;
const Console = @This();
@@ -149,12 +149,12 @@ fn timestamp() u64 {
return @import("../../datetime.zig").timestamp(.monotonic);
}
// Forwards page-context console output to the Page's message buffer (read by
// Forwards frame-context console output to the Frame's message buffer (read by
// the `consoleLogs` tool / CDP Runtime.consoleAPICalled). Worker contexts are
// dropped — no buffer is attached there.
fn appendMessage(exec: *js.Execution, level: Page.ConsoleMessage.Level, values: []js.Value) void {
fn appendMessage(exec: *js.Execution, level: Frame.ConsoleMessage.Level, values: []js.Value) void {
switch (exec.context.global) {
.page => |p| p.appendConsoleMessage(level, values),
.frame => |f| f.appendConsoleMessage(level, values),
.worker => {},
}
}

View File

@@ -20,7 +20,7 @@ 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 Node = @import("Node.zig");
const Element = @import("Element.zig");
@@ -38,7 +38,7 @@ const DefineOptions = struct {
extends: ?[]const u8 = null,
};
pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Function, options_: ?DefineOptions, page: *Page) !void {
pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Function, options_: ?DefineOptions, frame: *Frame) !void {
const options = options_ orelse DefineOptions{};
try validateName(name);
@@ -55,15 +55,15 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
break :blk tag;
} else null;
const gop = try self._definitions.getOrPut(page.arena, name);
const gop = try self._definitions.getOrPut(frame.arena, name);
if (gop.found_existing) {
// Yes, this is the correct error to return when trying to redefine a name
return error.NotSupported;
}
const owned_name = try page.dupeString(name);
const owned_name = try frame.dupeString(name);
const definition = try page._factory.create(CustomElementDefinition{
const definition = try frame._factory.create(CustomElementDefinition{
.name = owned_name,
.constructor = try constructor.persist(),
.extends = extends_tag,
@@ -75,8 +75,8 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
var js_arr = observed_attrs.toArray();
for (0..js_arr.len()) |i| {
const attr_val = js_arr.get(@intCast(i)) catch continue;
const attr_name = attr_val.toStringSliceWithAlloc(page.arena) catch continue;
definition.observed_attributes.put(page.arena, attr_name, {}) catch continue;
const attr_name = attr_val.toStringSliceWithAlloc(frame.arena) catch continue;
definition.observed_attributes.put(frame.arena, attr_name, {}) catch continue;
}
}
}
@@ -86,8 +86,8 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
// Upgrade any undefined custom elements with this name
var idx: usize = 0;
while (idx < page._undefined_custom_elements.items.len) {
const custom = page._undefined_custom_elements.items[idx];
while (idx < frame._undefined_custom_elements.items.len) {
const custom = frame._undefined_custom_elements.items[idx];
if (!custom._tag_name.eqlSlice(name)) {
idx += 1;
continue;
@@ -98,16 +98,16 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
continue;
}
upgradeCustomElement(custom, definition, page) catch {
_ = page._undefined_custom_elements.swapRemove(idx);
upgradeCustomElement(custom, definition, frame) catch {
_ = frame._undefined_custom_elements.swapRemove(idx);
continue;
};
_ = page._undefined_custom_elements.swapRemove(idx);
_ = frame._undefined_custom_elements.swapRemove(idx);
}
if (self._when_defined.fetchRemove(name)) |entry| {
page.js.toLocal(entry.value).resolve("whenDefined", constructor);
frame.js.toLocal(entry.value).resolve("whenDefined", constructor);
}
}
@@ -116,12 +116,12 @@ pub fn get(self: *CustomElementRegistry, name: []const u8) ?js.Function.Global {
return definition.constructor;
}
pub fn upgrade(self: *CustomElementRegistry, root: *Node, page: *Page) !void {
try upgradeNode(self, root, page);
pub fn upgrade(self: *CustomElementRegistry, root: *Node, frame: *Frame) !void {
try upgradeNode(self, root, frame);
}
pub fn whenDefined(self: *CustomElementRegistry, name: []const u8, page: *Page) !js.Promise {
const local = page.js.local.?;
pub fn whenDefined(self: *CustomElementRegistry, name: []const u8, frame: *Frame) !js.Promise {
const local = frame.js.local.?;
if (self._definitions.get(name)) |definition| {
return local.resolvePromise(definition.constructor);
}
@@ -130,12 +130,12 @@ pub fn whenDefined(self: *CustomElementRegistry, name: []const u8, page: *Page)
error.SyntaxError => return local.rejectPromise(.{ .dom_exception = .{ .err = error.SyntaxError } }),
};
const gop = try self._when_defined.getOrPut(page.arena, name);
const gop = try self._when_defined.getOrPut(frame.arena, name);
if (gop.found_existing) {
return local.toLocal(gop.value_ptr.*).promise();
}
errdefer _ = self._when_defined.remove(name);
const owned_name = try page.dupeString(name);
const owned_name = try frame.dupeString(name);
const resolver = local.createPromiseResolver();
gop.key_ptr.* = owned_name;
@@ -144,20 +144,20 @@ pub fn whenDefined(self: *CustomElementRegistry, name: []const u8, page: *Page)
return resolver.promise();
}
fn upgradeNode(self: *CustomElementRegistry, node: *Node, page: *Page) !void {
fn upgradeNode(self: *CustomElementRegistry, node: *Node, frame: *Frame) !void {
if (node.is(Element)) |element| {
try upgradeElement(self, element, page);
try upgradeElement(self, element, frame);
}
var it = node.childrenIterator();
while (it.next()) |child| {
try upgradeNode(self, child, page);
try upgradeNode(self, child, frame);
}
}
fn upgradeElement(self: *CustomElementRegistry, element: *Element, page: *Page) !void {
fn upgradeElement(self: *CustomElementRegistry, element: *Element, frame: *Frame) !void {
const custom = element.is(Custom) orelse {
return Custom.checkAndAttachBuiltIn(element, page);
return Custom.checkAndAttachBuiltIn(element, frame);
};
if (custom._definition != null) return;
@@ -165,10 +165,10 @@ fn upgradeElement(self: *CustomElementRegistry, element: *Element, page: *Page)
const name = custom._tag_name.str();
const definition = self._definitions.get(name) orelse return;
try upgradeCustomElement(custom, definition, page);
try upgradeCustomElement(custom, definition, frame);
}
pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinition, page: *Page) !void {
pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinition, frame: *Frame) !void {
custom._definition = definition;
// Reset callback flags since this is a fresh upgrade
@@ -176,12 +176,12 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio
custom._disconnected_callback_invoked = false;
const node = custom.asNode();
const prev_upgrading = page._upgrading_element;
page._upgrading_element = node;
defer page._upgrading_element = prev_upgrading;
const prev_upgrading = frame._upgrading_element;
frame._upgrading_element = node;
defer frame._upgrading_element = prev_upgrading;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
frame.js.localScope(&ls);
defer ls.deinit();
var caught: js.TryCatch.Caught = undefined;
@@ -195,12 +195,12 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio
while (attr_it.next()) |attr| {
const name = attr._name;
if (definition.isAttributeObserved(name)) {
custom.invokeAttributeChangedCallback(name, null, attr._value, page);
custom.invokeAttributeChangedCallback(name, null, attr._value, frame);
}
}
if (node.isConnected()) {
custom.invokeConnectedCallback(page);
custom.invokeConnectedCallback(frame);
}
}

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Node = @import("Node.zig");
const Document = @import("Document.zig");
const DocumentType = @import("DocumentType.zig");
@@ -27,60 +27,60 @@ const DocumentType = @import("DocumentType.zig");
const DOMImplementation = @This();
_pad: bool = false,
pub fn createDocumentType(_: *const DOMImplementation, qualified_name: []const u8, public_id: ?[]const u8, system_id: ?[]const u8, page: *Page) !*DocumentType {
return DocumentType.init(qualified_name, public_id, system_id, page);
pub fn createDocumentType(_: *const DOMImplementation, qualified_name: []const u8, public_id: ?[]const u8, system_id: ?[]const u8, frame: *Frame) !*DocumentType {
return DocumentType.init(qualified_name, public_id, system_id, frame);
}
pub fn createHTMLDocument(_: *const DOMImplementation, title: ?js.NullableString, page: *Page) !*Document {
const document = (try page._factory.document(Node.Document.HTMLDocument{ ._proto = undefined })).asDocument();
pub fn createHTMLDocument(_: *const DOMImplementation, title: ?js.NullableString, frame: *Frame) !*Document {
const document = (try frame._factory.document(Node.Document.HTMLDocument{ ._proto = undefined })).asDocument();
document._ready_state = .complete;
document._url = "about:blank";
{
const doctype = try page._factory.node(DocumentType{
const doctype = try frame._factory.node(DocumentType{
._proto = undefined,
._name = "html",
._public_id = "",
._system_id = "",
});
_ = try document.asNode().appendChild(doctype.asNode(), page);
_ = try document.asNode().appendChild(doctype.asNode(), frame);
}
const html_node = try page.createElementNS(.html, "html", null);
_ = try document.asNode().appendChild(html_node, page);
const html_node = try frame.createElementNS(.html, "html", null);
_ = try document.asNode().appendChild(html_node, frame);
const head_node = try page.createElementNS(.html, "head", null);
_ = try html_node.appendChild(head_node, page);
const head_node = try frame.createElementNS(.html, "head", null);
_ = try html_node.appendChild(head_node, frame);
if (title) |t| {
const title_node = try page.createElementNS(.html, "title", null);
_ = try head_node.appendChild(title_node, page);
const text_node = try page.createTextNode(t.value);
_ = try title_node.appendChild(text_node, page);
const title_node = try frame.createElementNS(.html, "title", null);
_ = try head_node.appendChild(title_node, frame);
const text_node = try frame.createTextNode(t.value);
_ = try title_node.appendChild(text_node, frame);
}
const body_node = try page.createElementNS(.html, "body", null);
_ = try html_node.appendChild(body_node, page);
const body_node = try frame.createElementNS(.html, "body", null);
_ = try html_node.appendChild(body_node, frame);
return document;
}
pub fn createDocument(_: *const DOMImplementation, namespace_: ?[]const u8, qualified_name: ?[]const u8, doctype: ?*DocumentType, page: *Page) !*Document {
pub fn createDocument(_: *const DOMImplementation, namespace_: ?[]const u8, qualified_name: ?[]const u8, doctype: ?*DocumentType, frame: *Frame) !*Document {
// Create XML Document
const document = (try page._factory.document(Node.Document.XMLDocument{ ._proto = undefined })).asDocument();
const document = (try frame._factory.document(Node.Document.XMLDocument{ ._proto = undefined })).asDocument();
document._url = "about:blank";
// Append doctype if provided
if (doctype) |dt| {
_ = try document.asNode().appendChild(dt.asNode(), page);
_ = try document.asNode().appendChild(dt.asNode(), frame);
}
// Create and append root element if qualified_name provided
if (qualified_name) |qname| {
if (qname.len > 0) {
const namespace = if (namespace_) |ns| Node.Element.Namespace.parse(ns) else .xml;
const root = try page.createElementNS(namespace, qname, null);
_ = try document.asNode().appendChild(root, page);
const root = try frame.createElementNS(namespace, qname, null);
_ = try document.asNode().appendChild(root, frame);
}
}

View File

@@ -18,7 +18,7 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Node = @import("Node.zig");
const NodeFilter = @import("NodeFilter.zig");
@@ -33,9 +33,9 @@ _reference_node: *Node,
_pointer_before_reference_node: bool,
_active: bool = false,
pub fn init(root: *Node, what_to_show: u32, filter: ?FilterOpts, page: *Page) !*DOMNodeIterator {
pub fn init(root: *Node, what_to_show: u32, filter: ?FilterOpts, frame: *Frame) !*DOMNodeIterator {
const node_filter = try NodeFilter.init(filter);
return page._factory.create(DOMNodeIterator{
return frame._factory.create(DOMNodeIterator{
._root = root,
._filter = node_filter,
._reference_node = root,
@@ -64,7 +64,7 @@ pub fn getFilter(self: *const DOMNodeIterator) ?FilterOpts {
return self._filter._original_filter;
}
pub fn nextNode(self: *DOMNodeIterator, page: *Page) !?*Node {
pub fn nextNode(self: *DOMNodeIterator, frame: *Frame) !?*Node {
if (self._active) {
return error.InvalidStateError;
}
@@ -78,7 +78,7 @@ pub fn nextNode(self: *DOMNodeIterator, page: *Page) !?*Node {
while (true) {
if (before_node) {
before_node = false;
const result = try self.filterNode(node, page);
const result = try self.filterNode(node, frame);
if (result == NodeFilter.FILTER_ACCEPT) {
self._reference_node = node;
self._pointer_before_reference_node = false;
@@ -92,7 +92,7 @@ pub fn nextNode(self: *DOMNodeIterator, page: *Page) !?*Node {
}
node = next.?;
const result = try self.filterNode(node, page);
const result = try self.filterNode(node, frame);
if (result == NodeFilter.FILTER_ACCEPT) {
self._reference_node = node;
self._pointer_before_reference_node = false;
@@ -102,7 +102,7 @@ pub fn nextNode(self: *DOMNodeIterator, page: *Page) !?*Node {
}
}
pub fn previousNode(self: *DOMNodeIterator, page: *Page) !?*Node {
pub fn previousNode(self: *DOMNodeIterator, frame: *Frame) !?*Node {
if (self._active) {
return error.InvalidStateError;
}
@@ -115,7 +115,7 @@ pub fn previousNode(self: *DOMNodeIterator, page: *Page) !?*Node {
while (true) {
if (!before_node) {
const result = try self.filterNode(node, page);
const result = try self.filterNode(node, frame);
if (result == NodeFilter.FILTER_ACCEPT) {
self._reference_node = node;
self._pointer_before_reference_node = true;
@@ -138,7 +138,7 @@ pub fn detach(_: *const DOMNodeIterator) void {
// no-op legacy
}
fn filterNode(self: *const DOMNodeIterator, node: *Node, page: *Page) !i32 {
fn filterNode(self: *const DOMNodeIterator, node: *Node, frame: *Frame) !i32 {
// First check whatToShow
if (!NodeFilter.shouldShow(node, self._what_to_show)) {
return NodeFilter.FILTER_SKIP;
@@ -147,7 +147,7 @@ fn filterNode(self: *const DOMNodeIterator, node: *Node, page: *Page) !i32 {
// Then check the filter callback
// For NodeIterator, REJECT and SKIP are equivalent - both skip the node
// but continue with its descendants
const result = try self._filter.acceptNode(node, page.js.local.?);
const result = try self._filter.acceptNode(node, frame.js.local.?);
return result;
}

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Parser = @import("../parser/Parser.zig");
const HTMLDocument = @import("HTMLDocument.zig");
@@ -40,7 +40,7 @@ pub fn parseFromString(
_: *const DOMParser,
html: []const u8,
mime_type: []const u8,
page: *Page,
frame: *Frame,
) !*Document {
const target_mime = std.meta.stringToEnum(enum {
@"text/html",
@@ -50,13 +50,13 @@ pub fn parseFromString(
@"image/svg+xml",
}, mime_type) orelse return error.NotSupported;
const arena = try page.getArena(.medium, "DOMParser.parseFromString");
defer page.releaseArena(arena);
const arena = try frame.getArena(.medium, "DOMParser.parseFromString");
defer frame.releaseArena(arena);
return switch (target_mime) {
.@"text/html" => {
// Create a new HTMLDocument
const doc = try page._factory.document(HTMLDocument{
const doc = try frame._factory.document(HTMLDocument{
._proto = undefined,
});
@@ -66,7 +66,7 @@ pub fn parseFromString(
}
// Parse HTML into the document
var parser = Parser.init(arena, doc.asNode(), page);
var parser = Parser.init(arena, doc.asNode(), frame);
parser.parse(normalized);
if (parser.err) |pe| {
@@ -77,19 +77,19 @@ pub fn parseFromString(
},
else => {
// Create a new XMLDocument.
const doc = try page._factory.document(XMLDocument{
const doc = try frame._factory.document(XMLDocument{
._proto = undefined,
});
// Parse XML into XMLDocument.
const doc_node = doc.asNode();
var parser = Parser.init(arena, doc_node, page);
var parser = Parser.init(arena, doc_node, frame);
parser.parseXML(html);
if (parser.err != null or doc_node.firstChild() == null) {
// Return a document with a <parsererror> element per spec.
const err_doc = try page._factory.document(XMLDocument{ ._proto = undefined });
var err_parser = Parser.init(arena, err_doc.asNode(), page);
const err_doc = try frame._factory.document(XMLDocument{ ._proto = undefined });
var err_parser = Parser.init(arena, err_doc.asNode(), frame);
err_parser.parseXML("<parsererror xmlns=\"http://www.mozilla.org/newlayout/xml/parsererror.xml\">error</parsererror>");
return err_doc.asDocument();
}
@@ -99,7 +99,7 @@ pub fn parseFromString(
// If first node is a `ProcessingInstruction`, skip it.
if (first_child.getNodeType() == 7) {
// We're sure that firstChild exist, this cannot fail.
_ = try doc_node.removeChild(first_child, page);
_ = try doc_node.removeChild(first_child, frame);
}
return doc.asDocument();

View File

@@ -20,15 +20,15 @@ const DOMRect = @This();
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
_x: f64,
_y: f64,
_width: f64,
_height: f64,
pub fn init(x: f64, y: f64, width: f64, height: f64, page: *Page) !*DOMRect {
return page._factory.create(DOMRect{
pub fn init(x: f64, y: f64, width: f64, height: f64, frame: *Frame) !*DOMRect {
return frame._factory.create(DOMRect{
._x = x,
._y = y,
._width = width,

View File

@@ -18,7 +18,7 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Node = @import("Node.zig");
const NodeFilter = @import("NodeFilter.zig");
@@ -31,9 +31,9 @@ _what_to_show: u32,
_filter: NodeFilter,
_current: *Node,
pub fn init(root: *Node, what_to_show: u32, filter: ?FilterOpts, page: *Page) !*DOMTreeWalker {
pub fn init(root: *Node, what_to_show: u32, filter: ?FilterOpts, frame: *Frame) !*DOMTreeWalker {
const node_filter = try NodeFilter.init(filter);
return page._factory.create(DOMTreeWalker{
return frame._factory.create(DOMTreeWalker{
._root = root,
._current = root,
._filter = node_filter,
@@ -62,13 +62,13 @@ pub fn setCurrentNode(self: *DOMTreeWalker, node: *Node) void {
}
// Navigation methods
pub fn parentNode(self: *DOMTreeWalker, page: *Page) !?*Node {
pub fn parentNode(self: *DOMTreeWalker, frame: *Frame) !?*Node {
var node = self._current._parent;
while (node) |n| {
if (n == self._root._parent) {
return null;
}
if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) {
if (try self.acceptNode(n, frame) == NodeFilter.FILTER_ACCEPT) {
self._current = n;
return n;
}
@@ -77,11 +77,11 @@ pub fn parentNode(self: *DOMTreeWalker, page: *Page) !?*Node {
return null;
}
pub fn firstChild(self: *DOMTreeWalker, page: *Page) !?*Node {
pub fn firstChild(self: *DOMTreeWalker, frame: *Frame) !?*Node {
var node = self._current.firstChild();
while (node) |n| {
const filter_result = try self.acceptNode(n, page);
const filter_result = try self.acceptNode(n, frame);
if (filter_result == NodeFilter.FILTER_ACCEPT) {
self._current = n;
@@ -117,11 +117,11 @@ pub fn firstChild(self: *DOMTreeWalker, page: *Page) !?*Node {
return null;
}
pub fn lastChild(self: *DOMTreeWalker, page: *Page) !?*Node {
pub fn lastChild(self: *DOMTreeWalker, frame: *Frame) !?*Node {
var node = self._current.lastChild();
while (node) |n| {
const filter_result = try self.acceptNode(n, page);
const filter_result = try self.acceptNode(n, frame);
if (filter_result == NodeFilter.FILTER_ACCEPT) {
self._current = n;
@@ -157,10 +157,10 @@ pub fn lastChild(self: *DOMTreeWalker, page: *Page) !?*Node {
return null;
}
pub fn previousSibling(self: *DOMTreeWalker, page: *Page) !?*Node {
pub fn previousSibling(self: *DOMTreeWalker, frame: *Frame) !?*Node {
var node = self.previousSiblingOrNull(self._current);
while (node) |n| {
if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) {
if (try self.acceptNode(n, frame) == NodeFilter.FILTER_ACCEPT) {
self._current = n;
return n;
}
@@ -169,10 +169,10 @@ pub fn previousSibling(self: *DOMTreeWalker, page: *Page) !?*Node {
return null;
}
pub fn nextSibling(self: *DOMTreeWalker, page: *Page) !?*Node {
pub fn nextSibling(self: *DOMTreeWalker, frame: *Frame) !?*Node {
var node = self.nextSiblingOrNull(self._current);
while (node) |n| {
if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) {
if (try self.acceptNode(n, frame) == NodeFilter.FILTER_ACCEPT) {
self._current = n;
return n;
}
@@ -181,7 +181,7 @@ pub fn nextSibling(self: *DOMTreeWalker, page: *Page) !?*Node {
return null;
}
pub fn previousNode(self: *DOMTreeWalker, page: *Page) !?*Node {
pub fn previousNode(self: *DOMTreeWalker, frame: *Frame) !?*Node {
var node = self._current;
while (node != self._root) {
var sibling = self.previousSiblingOrNull(node);
@@ -189,7 +189,7 @@ pub fn previousNode(self: *DOMTreeWalker, page: *Page) !?*Node {
node = sib;
// Check if this sibling is rejected before descending into it
const sib_result = try self.acceptNode(node, page);
const sib_result = try self.acceptNode(node, frame);
if (sib_result == NodeFilter.FILTER_REJECT) {
// Skip this sibling and its descendants entirely
sibling = self.previousSiblingOrNull(node);
@@ -204,7 +204,7 @@ pub fn previousNode(self: *DOMTreeWalker, page: *Page) !?*Node {
while (child) |c| {
if (!self.isInSubtree(c)) break;
const filter_result = try self.acceptNode(c, page);
const filter_result = try self.acceptNode(c, frame);
if (filter_result == NodeFilter.FILTER_REJECT) {
// Skip this child and try its previous sibling
child = self.previousSiblingOrNull(c);
@@ -220,7 +220,7 @@ pub fn previousNode(self: *DOMTreeWalker, page: *Page) !?*Node {
node = child.?;
}
if (try self.acceptNode(node, page) == NodeFilter.FILTER_ACCEPT) {
if (try self.acceptNode(node, frame) == NodeFilter.FILTER_ACCEPT) {
self._current = node;
return node;
}
@@ -232,7 +232,7 @@ pub fn previousNode(self: *DOMTreeWalker, page: *Page) !?*Node {
}
const parent = node._parent orelse return null;
if (try self.acceptNode(parent, page) == NodeFilter.FILTER_ACCEPT) {
if (try self.acceptNode(parent, frame) == NodeFilter.FILTER_ACCEPT) {
self._current = parent;
return parent;
}
@@ -241,14 +241,14 @@ pub fn previousNode(self: *DOMTreeWalker, page: *Page) !?*Node {
return null;
}
pub fn nextNode(self: *DOMTreeWalker, page: *Page) !?*Node {
pub fn nextNode(self: *DOMTreeWalker, frame: *Frame) !?*Node {
var node = self._current;
while (true) {
// Try children first (depth-first)
if (node.firstChild()) |child| {
node = child;
const filter_result = try self.acceptNode(node, page);
const filter_result = try self.acceptNode(node, frame);
if (filter_result == NodeFilter.FILTER_ACCEPT) {
self._current = node;
return node;
@@ -271,7 +271,7 @@ pub fn nextNode(self: *DOMTreeWalker, page: *Page) !?*Node {
if (node.nextSibling()) |sibling| {
node = sibling;
const filter_result = try self.acceptNode(node, page);
const filter_result = try self.acceptNode(node, frame);
if (filter_result == NodeFilter.FILTER_ACCEPT) {
self._current = node;
return node;
@@ -293,7 +293,7 @@ pub fn nextNode(self: *DOMTreeWalker, page: *Page) !?*Node {
}
// Helper methods
fn acceptNode(self: *const DOMTreeWalker, node: *Node, page: *Page) !i32 {
fn acceptNode(self: *const DOMTreeWalker, node: *Node, frame: *Frame) !i32 {
// First check whatToShow
if (!NodeFilter.shouldShow(node, self._what_to_show)) {
return NodeFilter.FILTER_SKIP;
@@ -303,7 +303,7 @@ fn acceptNode(self: *const DOMTreeWalker, node: *Node, page: *Page) !i32 {
// For TreeWalker, REJECT means reject node and its descendants
// SKIP means skip node but check its descendants
// ACCEPT means accept the node
return try self._filter.acceptNode(node, page.js.local.?);
return try self._filter.acceptNode(node, frame.js.local.?);
}
fn isInSubtree(self: *const DOMTreeWalker, node: *Node) bool {

View File

@@ -20,7 +20,7 @@ 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 URL = @import("../URL.zig");
const Node = @import("Node.zig");
@@ -47,7 +47,7 @@ const Document = @This();
_type: Type,
_proto: *Node,
_page: ?*Page = null,
_frame: ?*Frame = null,
_location: ?*Location = null,
_url: ?[:0]const u8 = null, // URL for documents created via DOMImplementation (about:blank)
_ready_state: ReadyState = .loading,
@@ -118,8 +118,8 @@ pub fn asEventTarget(self: *Document) *@import("EventTarget.zig") {
return self._proto.asEventTarget();
}
pub fn getURL(self: *const Document, page: *const Page) [:0]const u8 {
return self._url orelse page.url;
pub fn getURL(self: *const Document, frame: *const Frame) [:0]const u8 {
return self._url orelse frame.url;
}
pub fn getContentType(self: *const Document) []const u8 {
@@ -130,66 +130,66 @@ pub fn getContentType(self: *const Document) []const u8 {
};
}
pub fn getDomain(_: *const Document, page: *const Page) []const u8 {
return URL.getHostname(page.url);
pub fn getDomain(_: *const Document, frame: *const Frame) []const u8 {
return URL.getHostname(frame.url);
}
const CreateElementOptions = struct {
is: ?[]const u8 = null,
};
pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element {
pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElementOptions, frame: *Frame) !*Element {
try validateElementName(name);
const ns: Element.Namespace, const normalized_name = blk: {
if (self._type == .html) {
break :blk .{ .html, std.ascii.lowerString(&page.buf, name) };
break :blk .{ .html, std.ascii.lowerString(&frame.buf, name) };
}
// Generic and XML documents create elements with null namespace
break :blk .{ .null, name };
};
// HTML documents are case-insensitive - lowercase the tag name
const node = try page.createElementNS(ns, normalized_name, null);
const node = try frame.createElementNS(ns, normalized_name, null);
const element = node.as(Element);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
if (self != frame.document) {
try frame.setNodeOwnerDocument(node, self);
}
const options = options_ orelse return element;
if (options.is) |is_value| {
try element.setAttribute(comptime .wrap("is"), .wrap(is_value), page);
try Element.Html.Custom.checkAndAttachBuiltIn(element, page);
try element.setAttribute(comptime .wrap("is"), .wrap(is_value), frame);
try Element.Html.Custom.checkAndAttachBuiltIn(element, frame);
}
return element;
}
pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element {
pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8, frame: *Frame) !*Element {
try validateElementName(name);
const ns = Element.Namespace.parse(namespace);
// Per spec, createElementNS does NOT lowercase (unlike createElement).
const node = try page.createElementNS(ns, name, null);
const node = try frame.createElementNS(ns, name, null);
// Store original URI for unknown namespaces so lookupNamespaceURI can return it
if (ns == .unknown) {
if (namespace) |uri| {
const duped = try page.dupeString(uri);
try page._element_namespace_uris.put(page.arena, node.as(Element), duped);
const duped = try frame.dupeString(uri);
try frame._element_namespace_uris.put(frame.arena, node.as(Element), duped);
}
}
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
if (self != frame.document) {
try frame.setNodeOwnerDocument(node, self);
}
return node.as(Element);
}
pub fn createAttribute(_: *const Document, name: String.Global, page: *Page) !?*Element.Attribute {
pub fn createAttribute(_: *const Document, name: String.Global, frame: *Frame) !?*Element.Attribute {
try Element.Attribute.validateAttributeName(name.str);
return page._factory.node(Element.Attribute{
return frame._factory.node(Element.Attribute{
._proto = undefined,
._name = name.str,
._value = String.empty,
@@ -197,13 +197,13 @@ pub fn createAttribute(_: *const Document, name: String.Global, page: *Page) !?*
});
}
pub fn createAttributeNS(_: *const Document, namespace: []const u8, name: String.Global, page: *Page) !?*Element.Attribute {
pub fn createAttributeNS(_: *const Document, namespace: []const u8, name: String.Global, frame: *Frame) !?*Element.Attribute {
if (std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml") == false) {
log.warn(.not_implemented, "document.createAttributeNS", .{ .namespace = namespace });
}
try Element.Attribute.validateAttributeName(name.str);
return page._factory.node(Element.Attribute{
return frame._factory.node(Element.Attribute{
._proto = undefined,
._name = name.str,
._value = String.empty,
@@ -211,7 +211,7 @@ pub fn createAttributeNS(_: *const Document, namespace: []const u8, name: String
});
}
pub fn getElementById(self: *Document, id: []const u8, page: *Page) ?*Element {
pub fn getElementById(self: *Document, id: []const u8, frame: *Frame) ?*Element {
if (id.len == 0) {
return null;
}
@@ -229,8 +229,8 @@ pub fn getElementById(self: *Document, id: []const u8, page: *Page) ?*Element {
// we ignore this error to keep getElementById easy to call
// if it really failed, then we're out of memory and nothing's
// going to work like it should anyways.
const owned_id = page.dupeString(id) catch return null;
self._elements_by_id.put(page.arena, owned_id, el) catch return null;
const owned_id = frame.dupeString(id) catch return null;
self._elements_by_id.put(frame.arena, owned_id, el) catch return null;
return el;
}
}
@@ -239,26 +239,26 @@ pub fn getElementById(self: *Document, id: []const u8, page: *Page) ?*Element {
return null;
}
pub fn getElementsByTagName(self: *Document, tag_name: []const u8, page: *Page) !Node.GetElementsByTagNameResult {
return self.asNode().getElementsByTagName(tag_name, page);
pub fn getElementsByTagName(self: *Document, tag_name: []const u8, frame: *Frame) !Node.GetElementsByTagNameResult {
return self.asNode().getElementsByTagName(tag_name, frame);
}
pub fn getElementsByTagNameNS(self: *Document, namespace: ?[]const u8, local_name: []const u8, page: *Page) !collections.NodeLive(.tag_name_ns) {
return self.asNode().getElementsByTagNameNS(namespace, local_name, page);
pub fn getElementsByTagNameNS(self: *Document, namespace: ?[]const u8, local_name: []const u8, frame: *Frame) !collections.NodeLive(.tag_name_ns) {
return self.asNode().getElementsByTagNameNS(namespace, local_name, frame);
}
pub fn getElementsByClassName(self: *Document, class_name: []const u8, page: *Page) !collections.NodeLive(.class_name) {
return self.asNode().getElementsByClassName(class_name, page);
pub fn getElementsByClassName(self: *Document, class_name: []const u8, frame: *Frame) !collections.NodeLive(.class_name) {
return self.asNode().getElementsByClassName(class_name, frame);
}
pub fn getElementsByName(self: *Document, name: []const u8, page: *Page) !collections.NodeLive(.name) {
const arena = page.arena;
pub fn getElementsByName(self: *Document, name: []const u8, frame: *Frame) !collections.NodeLive(.name) {
const arena = frame.arena;
const filter = try arena.dupe(u8, name);
return collections.NodeLive(.name).init(self.asNode(), filter, page);
return collections.NodeLive(.name).init(self.asNode(), filter, frame);
}
pub fn getChildren(self: *Document, page: *Page) !collections.NodeLive(.child_elements) {
return collections.NodeLive(.child_elements).init(self.asNode(), {}, page);
pub fn getChildren(self: *Document, frame: *Frame) !collections.NodeLive(.child_elements) {
return collections.NodeLive(.child_elements).init(self.asNode(), {}, frame);
}
pub fn getDocumentElement(self: *Document) ?*Element {
@@ -276,135 +276,135 @@ pub fn getSelection(self: *Document) *Selection {
return &self._selection;
}
pub fn querySelector(self: *Document, input: String, page: *Page) !?*Element {
return Selector.querySelector(self.asNode(), input.str(), page);
pub fn querySelector(self: *Document, input: String, frame: *Frame) !?*Element {
return Selector.querySelector(self.asNode(), input.str(), frame);
}
pub fn querySelectorAll(self: *Document, input: String, page: *Page) !*Selector.List {
return Selector.querySelectorAll(self.asNode(), input.str(), page);
pub fn querySelectorAll(self: *Document, input: String, frame: *Frame) !*Selector.List {
return Selector.querySelectorAll(self.asNode(), input.str(), frame);
}
pub fn getImplementation(self: *Document, page: *Page) !*DOMImplementation {
pub fn getImplementation(self: *Document, frame: *Frame) !*DOMImplementation {
if (self._implementation) |impl| return impl;
const impl = try page._factory.create(DOMImplementation{});
const impl = try frame._factory.create(DOMImplementation{});
self._implementation = impl;
return impl;
}
pub fn createDocumentFragment(self: *Document, page: *Page) !*Node.DocumentFragment {
const frag = try Node.DocumentFragment.init(page);
pub fn createDocumentFragment(self: *Document, frame: *Frame) !*Node.DocumentFragment {
const frag = try Node.DocumentFragment.init(frame);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(frag.asNode(), self);
if (self != frame.document) {
try frame.setNodeOwnerDocument(frag.asNode(), self);
}
return frag;
}
pub fn createComment(self: *Document, data: []const u8, page: *Page) !*Node {
const node = try page.createComment(data);
pub fn createComment(self: *Document, data: []const u8, frame: *Frame) !*Node {
const node = try frame.createComment(data);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
if (self != frame.document) {
try frame.setNodeOwnerDocument(node, self);
}
return node;
}
pub fn createTextNode(self: *Document, data: []const u8, page: *Page) !*Node {
const node = try page.createTextNode(data);
pub fn createTextNode(self: *Document, data: []const u8, frame: *Frame) !*Node {
const node = try frame.createTextNode(data);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
if (self != frame.document) {
try frame.setNodeOwnerDocument(node, self);
}
return node;
}
pub fn createCDATASection(self: *Document, data: []const u8, page: *Page) !*Node {
pub fn createCDATASection(self: *Document, data: []const u8, frame: *Frame) !*Node {
const node = switch (self._type) {
.html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument
.xml => try page.createCDATASection(data),
.generic => try page.createCDATASection(data),
.xml => try frame.createCDATASection(data),
.generic => try frame.createCDATASection(data),
};
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
if (self != frame.document) {
try frame.setNodeOwnerDocument(node, self);
}
return node;
}
pub fn createProcessingInstruction(self: *Document, target: []const u8, data: []const u8, page: *Page) !*Node {
const node = try page.createProcessingInstruction(target, data);
pub fn createProcessingInstruction(self: *Document, target: []const u8, data: []const u8, frame: *Frame) !*Node {
const node = try frame.createProcessingInstruction(target, data);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
if (self != frame.document) {
try frame.setNodeOwnerDocument(node, self);
}
return node;
}
const Range = @import("Range.zig");
pub fn createRange(_: *const Document, page: *Page) !*Range {
return Range.init(page);
pub fn createRange(_: *const Document, frame: *Frame) !*Range {
return Range.init(frame);
}
pub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@import("Event.zig") {
pub fn createEvent(_: *const Document, event_type: []const u8, frame: *Frame) !*@import("Event.zig") {
const Event = @import("Event.zig");
if (event_type.len > 100) {
return error.NotSupported;
}
const normalized = std.ascii.lowerString(&page.buf, event_type);
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, page);
return Event.init("", null, frame);
}
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, page)).asEvent();
return (try CustomEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "keyboardevent")) {
const KeyboardEvent = @import("event/KeyboardEvent.zig");
return (try KeyboardEvent.init("", null, page)).asEvent();
return (try KeyboardEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "inputevent")) {
const InputEvent = @import("event/InputEvent.zig");
return (try InputEvent.init("", null, page)).asEvent();
return (try InputEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "mouseevent") or std.mem.eql(u8, normalized, "mouseevents")) {
const MouseEvent = @import("event/MouseEvent.zig");
return (try MouseEvent.init("", null, page)).asEvent();
return (try MouseEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "messageevent")) {
const MessageEvent = @import("event/MessageEvent.zig");
return (try MessageEvent.init("", null, page._session)).asEvent();
return (try MessageEvent.init("", null, frame._session)).asEvent();
}
if (std.mem.eql(u8, normalized, "uievent") or std.mem.eql(u8, normalized, "uievents")) {
const UIEvent = @import("event/UIEvent.zig");
return (try UIEvent.init("", null, page)).asEvent();
return (try UIEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "focusevent") or std.mem.eql(u8, normalized, "focusevents")) {
const FocusEvent = @import("event/FocusEvent.zig");
return (try FocusEvent.init("", null, page)).asEvent();
return (try FocusEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "textevent") or std.mem.eql(u8, normalized, "textevents")) {
const TextEvent = @import("event/TextEvent.zig");
return (try TextEvent.init("", null, page)).asEvent();
return (try TextEvent.init("", null, frame)).asEvent();
}
return error.NotSupported;
}
pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker {
return DOMTreeWalker.init(root, try whatToShow(what_to_show), filter, page);
pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMTreeWalker.FilterOpts, frame: *Frame) !*DOMTreeWalker {
return DOMTreeWalker.init(root, try whatToShow(what_to_show), filter, frame);
}
pub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator {
return DOMNodeIterator.init(root, try whatToShow(what_to_show), filter, page);
pub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMNodeIterator.FilterOpts, frame: *Frame) !*DOMNodeIterator {
return DOMNodeIterator.init(root, try whatToShow(what_to_show), filter, frame);
}
fn whatToShow(value_: ?js.Value) !u32 {
@@ -441,81 +441,81 @@ pub fn getActiveElement(self: *Document) ?*Element {
return self.getDocumentElement();
}
pub fn getStyleSheets(self: *Document, page: *Page) !*StyleSheetList {
pub fn getStyleSheets(self: *Document, frame: *Frame) !*StyleSheetList {
if (self._style_sheets) |sheets| {
return sheets;
}
const sheets = try StyleSheetList.init(page);
const sheets = try StyleSheetList.init(frame);
self._style_sheets = sheets;
return sheets;
}
pub fn getFonts(self: *Document, page: *Page) !*FontFaceSet {
pub fn getFonts(self: *Document, frame: *Frame) !*FontFaceSet {
if (self._fonts) |fonts| {
return fonts;
}
const fonts = try FontFaceSet.init(page);
const fonts = try FontFaceSet.init(frame);
fonts.acquireRef();
self._fonts = fonts;
return fonts;
}
pub fn adoptNode(_: *const Document, node: *Node, page: *Page) !*Node {
pub fn adoptNode(_: *const Document, node: *Node, frame: *Frame) !*Node {
if (node._type == .document) {
return error.NotSupported;
}
if (node._parent) |parent| {
page.removeNode(parent, node, .{ .will_be_reconnected = false });
frame.removeNode(parent, node, .{ .will_be_reconnected = false });
}
return node;
}
pub fn importNode(_: *const Document, node: *Node, deep_: ?bool, page: *Page) !*Node {
pub fn importNode(_: *const Document, node: *Node, deep_: ?bool, frame: *Frame) !*Node {
if (node._type == .document) {
return error.NotSupported;
}
return node.cloneNode(deep_, page);
return node.cloneNode(deep_, frame);
}
pub fn append(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {
pub fn append(self: *Document, nodes: []const Node.NodeOrText, frame: *Frame) !void {
try validateDocumentNodes(self, nodes, false);
page.domChanged();
frame.domChanged();
const parent = self.asNode();
const parent_is_connected = parent.isConnected();
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
const child = try node_or_text.toNode(frame);
// DocumentFragments are special - append all their children
if (child.is(Node.DocumentFragment)) |_| {
try page.appendAllChildren(child, parent);
try frame.appendAllChildren(child, parent);
continue;
}
var child_connected = false;
if (child._parent) |previous_parent| {
child_connected = child.isConnected();
page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
frame.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
}
try page.appendNode(parent, child, .{ .child_already_connected = child_connected });
try frame.appendNode(parent, child, .{ .child_already_connected = child_connected });
}
}
pub fn prepend(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {
pub fn prepend(self: *Document, nodes: []const Node.NodeOrText, frame: *Frame) !void {
try validateDocumentNodes(self, nodes, false);
page.domChanged();
frame.domChanged();
const parent = self.asNode();
const parent_is_connected = parent.isConnected();
var i = nodes.len;
while (i > 0) {
i -= 1;
const child = try nodes[i].toNode(page);
const child = try nodes[i].toNode(frame);
// DocumentFragments are special - need to insert all their children
if (child.is(Node.DocumentFragment)) |frag| {
@@ -523,11 +523,11 @@ pub fn prepend(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !vo
var frag_child = frag.asNode().lastChild();
while (frag_child) |fc| {
const prev = fc.previousSibling();
page.removeNode(frag.asNode(), fc, .{ .will_be_reconnected = parent_is_connected });
frame.removeNode(frag.asNode(), fc, .{ .will_be_reconnected = parent_is_connected });
if (first_child) |before| {
try page.insertNodeRelative(parent, fc, .{ .before = before }, .{});
try frame.insertNodeRelative(parent, fc, .{ .before = before }, .{});
} else {
try page.appendNode(parent, fc, .{});
try frame.appendNode(parent, fc, .{});
}
frag_child = prev;
}
@@ -537,37 +537,37 @@ pub fn prepend(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !vo
var child_connected = false;
if (child._parent) |previous_parent| {
child_connected = child.isConnected();
page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
frame.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
}
const first_child = parent.firstChild();
if (first_child) |before| {
try page.insertNodeRelative(parent, child, .{ .before = before }, .{ .child_already_connected = child_connected });
try frame.insertNodeRelative(parent, child, .{ .before = before }, .{ .child_already_connected = child_connected });
} else {
try page.appendNode(parent, child, .{ .child_already_connected = child_connected });
try frame.appendNode(parent, child, .{ .child_already_connected = child_connected });
}
}
}
pub fn replaceChildren(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {
pub fn replaceChildren(self: *Document, nodes: []const Node.NodeOrText, frame: *Frame) !void {
try validateDocumentNodes(self, nodes, false);
return self.asNode().replaceChildren(nodes, page);
return self.asNode().replaceChildren(nodes, frame);
}
pub fn elementFromPoint(self: *Document, x: f64, y: f64, page: *Page) !?*Element {
pub fn elementFromPoint(self: *Document, x: f64, y: f64, frame: *Frame) !?*Element {
// Traverse document in depth-first order to find the topmost (last in document order)
// element that contains the point (x, y)
var topmost: ?*Element = null;
const root = self.asNode();
var stack: std.ArrayList(*Node) = .empty;
try stack.append(page.call_arena, root);
try stack.append(frame.call_arena, root);
while (stack.items.len > 0) {
const node = stack.pop() orelse break;
if (node.is(Element)) |element| {
if (element.checkVisibilityCached(null, page)) {
const rect = element.getBoundingClientRectForVisible(page);
if (element.checkVisibilityCached(null, frame)) {
const rect = element.getBoundingClientRectForVisible(frame);
if (x >= rect.getLeft() and x <= rect.getRight() and y >= rect.getTop() and y <= rect.getBottom()) {
topmost = element;
}
@@ -577,7 +577,7 @@ pub fn elementFromPoint(self: *Document, x: f64, y: f64, page: *Page) !?*Element
// Add children to stack in reverse order so we process them in document order
var child = node.lastChild();
while (child) |c| {
try stack.append(page.call_arena, c);
try stack.append(frame.call_arena, c);
child = c.previousSibling();
}
}
@@ -585,12 +585,12 @@ pub fn elementFromPoint(self: *Document, x: f64, y: f64, page: *Page) !?*Element
return topmost;
}
pub fn elementsFromPoint(self: *Document, x: f64, y: f64, page: *Page) ![]const *Element {
pub fn elementsFromPoint(self: *Document, x: f64, y: f64, frame: *Frame) ![]const *Element {
// Get topmost element
var current: ?*Element = (try self.elementFromPoint(x, y, page)) orelse return &.{};
var current: ?*Element = (try self.elementFromPoint(x, y, frame)) orelse return &.{};
var result: std.ArrayList(*Element) = .empty;
while (current) |el| {
try result.append(page.call_arena, el);
try result.append(frame.call_arena, el);
current = el.parentElement();
}
return result.items;
@@ -621,18 +621,18 @@ fn looksLikeNewDocument(html: []const u8) bool {
std.ascii.startsWithIgnoreCase(trimmed, "<html");
}
pub fn write(self: *Document, text: []const []const u8, page: *Page) !void {
return self.writeInternal(text, false, page);
pub fn write(self: *Document, text: []const []const u8, frame: *Frame) !void {
return self.writeInternal(text, false, frame);
}
// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-writeln
// `writeln(...text)` runs the document write steps with `text` followed by a
// U+000A LINE FEED character.
pub fn writeln(self: *Document, text: []const []const u8, page: *Page) !void {
return self.writeInternal(text, true, page);
pub fn writeln(self: *Document, text: []const []const u8, frame: *Frame) !void {
return self.writeInternal(text, true, frame);
}
fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool, page: *Page) !void {
fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool, frame: *Frame) !void {
if (self._type == .xml) {
return error.InvalidStateError;
}
@@ -644,17 +644,17 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool
const html = blk: {
var joined: std.ArrayList(u8) = .empty;
for (text) |str| {
try joined.appendSlice(page.call_arena, str);
try joined.appendSlice(frame.call_arena, str);
}
if (append_newline) {
try joined.append(page.call_arena, '\n');
try joined.append(frame.call_arena, '\n');
}
break :blk joined.items;
};
if (self._current_script == null or page._load_state != .parsing) {
if (self._current_script == null or frame._load_state != .parsing) {
if (self._script_created_parser == null or looksLikeNewDocument(html)) {
_ = try self.open(page);
_ = try self.open(frame);
}
if (html.len > 0) {
@@ -675,17 +675,17 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool
// Our implementation is hacky. We'll write to a DocumentFragment, then
// append its children.
const fragment = try Node.DocumentFragment.init(page);
const fragment = try Node.DocumentFragment.init(frame);
const fragment_node = fragment.asNode();
const previous_parse_mode = page._parse_mode;
page._parse_mode = .document_write;
defer page._parse_mode = previous_parse_mode;
const previous_parse_mode = frame._parse_mode;
frame._parse_mode = .document_write;
defer frame._parse_mode = previous_parse_mode;
const arena = try page.getArena(.medium, "Document.write");
defer page.releaseArena(arena);
const arena = try frame.getArena(.medium, "Document.write");
defer frame.releaseArena(arena);
var parser = Parser.init(arena, fragment_node, page);
var parser = Parser.init(arena, fragment_node, frame);
parser.parseFragment(html);
// Extract children from wrapper HTML element (html5ever wraps fragments)
@@ -720,15 +720,15 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool
for (children_to_insert.items) |child| {
// Clear parent pointer (child is currently parented to fragment/HTML wrapper)
child._parent = null;
try page.insertNodeRelative(parent, child, .{ .after = insert_after.? }, .{});
try frame.insertNodeRelative(parent, child, .{ .after = insert_after.? }, .{});
insert_after = child;
}
page.domChanged();
frame.domChanged();
self._write_insertion_point = children_to_insert.getLast();
}
pub fn open(self: *Document, page: *Page) !*Document {
pub fn open(self: *Document, frame: *Frame) !*Document {
if (self._type == .xml) {
return error.InvalidStateError;
}
@@ -737,7 +737,7 @@ pub fn open(self: *Document, page: *Page) !*Document {
return error.InvalidStateError;
}
if (page._load_state == .parsing) {
if (frame._load_state == .parsing) {
return self;
}
@@ -752,25 +752,25 @@ pub fn open(self: *Document, page: *Page) !*Document {
// Remove all children from document
var it = doc_node.childrenIterator();
while (it.next()) |child| {
page.removeNode(doc_node, child, .{ .will_be_reconnected = false });
frame.removeNode(doc_node, child, .{ .will_be_reconnected = false });
}
}
// reset the document
self._elements_by_id.clearAndFree(page.arena);
self._elements_by_id.clearAndFree(frame.arena);
self._active_element = null;
self._style_sheets = null;
self._implementation = null;
self._ready_state = .loading;
self._script_created_parser = Parser.Streaming.init(page.arena, doc_node, page);
self._script_created_parser = Parser.Streaming.init(frame.arena, doc_node, frame);
try self._script_created_parser.?.start();
page._parse_mode = .document;
frame._parse_mode = .document;
return self;
}
pub fn close(self: *Document, page: *Page) !void {
pub fn close(self: *Document, frame: *Frame) !void {
if (self._type == .xml) {
return error.InvalidStateError;
}
@@ -790,7 +790,7 @@ pub fn close(self: *Document, page: *Page) !void {
self._script_created_parser.?.handle = null;
self._script_created_parser = null;
page.documentIsComplete();
frame.documentIsComplete();
}
pub fn getFirstElementChild(self: *Document) ?*Element {
@@ -825,11 +825,11 @@ pub fn getChildElementCount(self: *Document) u32 {
return i;
}
pub fn getAdoptedStyleSheets(self: *Document, page: *Page) !js.Object.Global {
pub fn getAdoptedStyleSheets(self: *Document, frame: *Frame) !js.Object.Global {
if (self._adopted_style_sheets) |ass| {
return ass;
}
const js_arr = page.js.local.?.newArray(0);
const js_arr = frame.js.local.?.newArray(0);
const js_obj = js_arr.toObject();
self._adopted_style_sheets = try js_obj.persist();
return self._adopted_style_sheets.?;
@@ -963,10 +963,10 @@ fn validateElementName(name: []const u8) !void {
}
}
// When a page or frame's URL is about:blank, or as soon as a frame is
// When a frame's URL is about:blank, or as soon as a frame is
// programmatically created, it has this default "blank" content
pub fn injectBlank(self: *Document, page: *Page) error{InjectBlankError}!void {
self._injectBlank(page) catch |err| {
pub fn injectBlank(self: *Document, frame: *Frame) error{InjectBlankError}!void {
self._injectBlank(frame) catch |err| {
// we wrap _injectBlank like this so that injectBlank can only return an
// InjectBlankError. injectBlank is used in when nodes are inserted
// as since it inserts node itself, Zig can't infer the error set.
@@ -975,18 +975,18 @@ pub fn injectBlank(self: *Document, page: *Page) error{InjectBlankError}!void {
};
}
fn _injectBlank(self: *Document, page: *Page) !void {
fn _injectBlank(self: *Document, frame: *Frame) !void {
if (comptime IS_DEBUG) {
// should only be called on an empty document
std.debug.assert(self.asNode()._children == null);
}
const html = try page.createElementNS(.html, "html", null);
const head = try page.createElementNS(.html, "head", null);
const body = try page.createElementNS(.html, "body", null);
try page.appendNode(html, head, .{});
try page.appendNode(html, body, .{});
try page.appendNode(self.asNode(), html, .{});
const html = try frame.createElementNS(.html, "html", null);
const head = try frame.createElementNS(.html, "head", null);
const body = try frame.createElementNS(.html, "body", null);
try frame.appendNode(html, head, .{});
try frame.appendNode(html, body, .{});
try frame.appendNode(self.asNode(), html, .{});
}
const ReadyState = enum {
@@ -1006,8 +1006,8 @@ pub const JsApi = struct {
};
pub const constructor = bridge.constructor(_constructor, .{});
fn _constructor(page: *Page) !*Document {
return page._factory.node(Document{
fn _constructor(frame: *Frame) !*Document {
return frame._factory.node(Document{
._proto = undefined,
._type = .generic,
});
@@ -1040,15 +1040,15 @@ pub const JsApi = struct {
pub const createTreeWalker = bridge.function(Document.createTreeWalker, .{});
pub const createNodeIterator = bridge.function(Document.createNodeIterator, .{});
pub const getElementById = bridge.function(_getElementById, .{});
fn _getElementById(self: *Document, value_: ?js.Value, page: *Page) !?*Element {
fn _getElementById(self: *Document, value_: ?js.Value, frame: *Frame) !?*Element {
const value = value_ orelse return null;
if (value.isNull()) {
return self.getElementById("null", page);
return self.getElementById("null", frame);
}
if (value.isUndefined()) {
return self.getElementById("undefined", page);
return self.getElementById("undefined", frame);
}
return self.getElementById(try value.toZig([]const u8), page);
return self.getElementById(try value.toZig([]const u8), frame);
}
pub const querySelector = bridge.function(Document.querySelector, .{ .dom_exception = true });
pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true });
@@ -1076,8 +1076,8 @@ pub const JsApi = struct {
pub const hidden = bridge.property(false, .{ .template = false, .readonly = true });
pub const visibilityState = bridge.property("visible", .{ .template = false, .readonly = true });
pub const defaultView = bridge.accessor(struct {
fn defaultView(_: *const Document, page: *Page) *@import("Window.zig") {
return page.window;
fn defaultView(_: *const Document, frame: *Frame) *@import("Window.zig") {
return frame.window;
}
}.defaultView, null, .{});
pub const hasFocus = bridge.function(Document.hasFocus, .{});
@@ -1089,8 +1089,8 @@ pub const JsApi = struct {
pub const compatMode = bridge.property("CSS1Compat", .{ .template = false });
fn getCharacterSet(self: *const Document) []const u8 {
const doc_page = self._page orelse return "UTF-8";
return doc_page.charset;
const doc_frame = self._frame orelse return "UTF-8";
return doc_frame.charset;
}
pub const referrer = bridge.property("", .{ .template = false });
};

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Node = @import("Node.zig");
const Element = @import("Element.zig");
const ShadowRoot = @import("ShadowRoot.zig");
@@ -52,8 +52,8 @@ pub fn as(self: *DocumentFragment, comptime T: type) *T {
return self.is(T).?;
}
pub fn init(page: *Page) !*DocumentFragment {
return page._factory.node(DocumentFragment{
pub fn init(frame: *Frame) !*DocumentFragment {
return frame._factory.node(DocumentFragment{
._type = .generic,
._proto = undefined,
});
@@ -83,16 +83,16 @@ pub fn getElementById(self: *DocumentFragment, id: []const u8) ?*Element {
return null;
}
pub fn querySelector(self: *DocumentFragment, selector: []const u8, page: *Page) !?*Element {
return Selector.querySelector(self.asNode(), selector, page);
pub fn querySelector(self: *DocumentFragment, selector: []const u8, frame: *Frame) !?*Element {
return Selector.querySelector(self.asNode(), selector, frame);
}
pub fn querySelectorAll(self: *DocumentFragment, input: []const u8, page: *Page) !*Selector.List {
return Selector.querySelectorAll(self.asNode(), input, page);
pub fn querySelectorAll(self: *DocumentFragment, input: []const u8, frame: *Frame) !*Selector.List {
return Selector.querySelectorAll(self.asNode(), input, frame);
}
pub fn getChildren(self: *DocumentFragment, page: *Page) !collections.NodeLive(.child_elements) {
return collections.NodeLive(.child_elements).init(self.asNode(), {}, page);
pub fn getChildren(self: *DocumentFragment, frame: *Frame) !collections.NodeLive(.child_elements) {
return collections.NodeLive(.child_elements).init(self.asNode(), {}, frame);
}
pub fn firstElementChild(self: *DocumentFragment) ?*Element {
@@ -124,51 +124,51 @@ pub fn getChildElementCount(self: *DocumentFragment) usize {
return count;
}
pub fn append(self: *DocumentFragment, nodes: []const Node.NodeOrText, page: *Page) !void {
pub fn append(self: *DocumentFragment, nodes: []const Node.NodeOrText, frame: *Frame) !void {
const parent = self.asNode();
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
_ = try parent.appendChild(child, page);
const child = try node_or_text.toNode(frame);
_ = try parent.appendChild(child, frame);
}
}
pub fn prepend(self: *DocumentFragment, nodes: []const Node.NodeOrText, page: *Page) !void {
pub fn prepend(self: *DocumentFragment, nodes: []const Node.NodeOrText, frame: *Frame) !void {
const parent = self.asNode();
var i = nodes.len;
while (i > 0) {
i -= 1;
const child = try nodes[i].toNode(page);
_ = try parent.insertBefore(child, parent.firstChild(), page);
const child = try nodes[i].toNode(frame);
_ = try parent.insertBefore(child, parent.firstChild(), frame);
}
}
pub fn replaceChildren(self: *DocumentFragment, nodes: []const Node.NodeOrText, page: *Page) !void {
return self.asNode().replaceChildren(nodes, page);
pub fn replaceChildren(self: *DocumentFragment, nodes: []const Node.NodeOrText, frame: *Frame) !void {
return self.asNode().replaceChildren(nodes, frame);
}
pub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer, page: *Page) !void {
pub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer, frame: *Frame) !void {
const dump = @import("../dump.zig");
return dump.children(self.asNode(), .{ .shadow = .complete }, writer, page);
return dump.children(self.asNode(), .{ .shadow = .complete }, writer, frame);
}
pub fn setInnerHTML(self: *DocumentFragment, html: []const u8, page: *Page) !void {
pub fn setInnerHTML(self: *DocumentFragment, html: []const u8, frame: *Frame) !void {
const parent = self.asNode();
page.domChanged();
frame.domChanged();
var it = parent.childrenIterator();
while (it.next()) |child| {
page.removeNode(parent, child, .{ .will_be_reconnected = false });
frame.removeNode(parent, child, .{ .will_be_reconnected = false });
}
if (html.len == 0) {
return;
}
try page.parseHtmlAsChildren(parent, html);
try frame.parseHtmlAsChildren(parent, html);
}
pub fn cloneFragment(self: *DocumentFragment, deep: bool, page: *Page) !*Node {
const fragment = try DocumentFragment.init(page);
pub fn cloneFragment(self: *DocumentFragment, deep: bool, frame: *Frame) !*Node {
const fragment = try DocumentFragment.init(frame);
const fragment_node = fragment.asNode();
if (deep) {
@@ -177,8 +177,8 @@ pub fn cloneFragment(self: *DocumentFragment, deep: bool, page: *Page) !*Node {
var child_it = node.childrenIterator();
while (child_it.next()) |child| {
if (try child.cloneNodeForAppending(true, page)) |cloned_child| {
try page.appendNode(fragment_node, cloned_child, .{ .child_already_connected = self_is_connected });
if (try child.cloneNodeForAppending(true, frame)) |cloned_child| {
try frame.appendNode(fragment_node, cloned_child, .{ .child_already_connected = self_is_connected });
}
}
}
@@ -244,9 +244,9 @@ pub const JsApi = struct {
pub const replaceChildren = bridge.function(DocumentFragment.replaceChildren, .{ .dom_exception = true });
pub const innerHTML = bridge.accessor(_innerHTML, DocumentFragment.setInnerHTML, .{});
fn _innerHTML(self: *DocumentFragment, page: *Page) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try self.getInnerHTML(&buf.writer, page);
fn _innerHTML(self: *DocumentFragment, frame: *Frame) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
try self.getInnerHTML(&buf.writer, frame);
return buf.written();
}
};

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Node = @import("Node.zig");
@@ -30,13 +30,13 @@ _name: []const u8,
_public_id: []const u8,
_system_id: []const u8,
pub fn init(qualified_name: []const u8, public_id: ?[]const u8, system_id: ?[]const u8, page: *Page) !*DocumentType {
const name = try page.dupeString(qualified_name);
pub fn init(qualified_name: []const u8, public_id: ?[]const u8, system_id: ?[]const u8, frame: *Frame) !*DocumentType {
const name = try frame.dupeString(qualified_name);
// Firefox converts null to the string "null", not empty string
const pub_id = if (public_id) |p| try page.dupeString(p) else "null";
const sys_id = if (system_id) |s| try page.dupeString(s) else "null";
const pub_id = if (public_id) |p| try frame.dupeString(p) else "null";
const sys_id = if (system_id) |s| try frame.dupeString(s) else "null";
return page._factory.node(DocumentType{
return frame._factory.node(DocumentType{
._proto = undefined,
._name = name,
._public_id = pub_id,
@@ -70,14 +70,14 @@ pub fn isEqualNode(self: *const DocumentType, other: *const DocumentType) bool {
std.mem.eql(u8, self._system_id, other._system_id);
}
pub fn clone(self: *const DocumentType, page: *Page) !*DocumentType {
return .init(self._name, self._public_id, self._system_id, page);
pub fn clone(self: *const DocumentType, frame: *Frame) !*DocumentType {
return .init(self._name, self._public_id, self._system_id, frame);
}
pub fn remove(self: *DocumentType, page: *Page) !void {
pub fn remove(self: *DocumentType, frame: *Frame) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
_ = try parent.removeChild(node, page);
_ = try parent.removeChild(node, frame);
}
pub const JsApi = struct {

View File

File diff suppressed because it is too large Load Diff

View File

@@ -20,7 +20,7 @@ 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");
@@ -89,16 +89,16 @@ pub const Options = struct {
composed: bool = false,
};
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event {
const arena = try page.getArena(.tiny, "Event");
errdefer page.releaseArena(arena);
pub fn init(typ: []const u8, opts_: ?Options, frame: *Frame) !*Event {
const arena = try frame.getArena(.tiny, "Event");
errdefer frame.releaseArena(arena);
const str = try String.init(arena, typ, .{});
return initWithTrusted(arena, str, opts_, false);
}
pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*Event {
const arena = try page.getArena(.tiny, "Event.trusted");
errdefer page.releaseArena(arena);
pub fn initTrusted(typ: String, opts_: ?Options, frame: *Frame) !*Event {
const arena = try frame.getArena(.tiny, "Event.trusted");
errdefer frame.releaseArena(arena);
return initWithTrusted(arena, typ, opts_, true);
}
@@ -267,7 +267,7 @@ pub fn getIsTrusted(self: *const Event) bool {
return self._is_trusted;
}
pub fn composedPath(self: *Event, page: *Page) ![]const *EventTarget {
pub fn composedPath(self: *Event, frame: *Frame) ![]const *EventTarget {
// Return empty array if event is not being dispatched
if (self._event_phase == .none) {
return &.{};
@@ -332,7 +332,7 @@ pub fn composedPath(self: *Event, page: *Page) ![]const *EventTarget {
// Add window at the end (unless we stopped at shadow boundary)
if (!stopped_at_shadow_boundary) {
if (path_len < path_buffer.len) {
path_buffer[path_len] = page.window.asEventTarget();
path_buffer[path_len] = frame.window.asEventTarget();
path_len += 1;
}
}
@@ -369,7 +369,7 @@ pub fn composedPath(self: *Event, page: *Page) ![]const *EventTarget {
const visible_path_len = if (path_len > visible_start_index) path_len - visible_start_index else 0;
// Allocate and return the visible path using call_arena (short-lived)
const path = try page.call_arena.alloc(*EventTarget, visible_path_len);
const path = try frame.call_arena.alloc(*EventTarget, visible_path_len);
@memcpy(path, path_buffer[visible_start_index..path_len]);
return path;
}

View File

@@ -20,7 +20,7 @@ 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 log = lp.log;
@@ -100,16 +100,16 @@ pub fn getSize(_: *const EventCounts) u32 {
return tracked_event_types.len;
}
pub fn keys(self: *EventCounts, page: *Page) !*KeyIterator {
return .init(.{ .event_counts = self }, page);
pub fn keys(self: *EventCounts, frame: *Frame) !*KeyIterator {
return .init(.{ .event_counts = self }, frame);
}
pub fn values(self: *EventCounts, page: *Page) !*ValueIterator {
return .init(.{ .event_counts = self }, page);
pub fn values(self: *EventCounts, frame: *Frame) !*ValueIterator {
return .init(.{ .event_counts = self }, frame);
}
pub fn entries(self: *EventCounts, page: *Page) !*EntryIterator {
return .init(.{ .event_counts = self }, page);
pub fn entries(self: *EventCounts, frame: *Frame) !*EntryIterator {
return .init(.{ .event_counts = self }, frame);
}
pub fn forEach(self: *EventCounts, cb_: js.Function, js_this_: ?js.Object) !void {
@@ -158,7 +158,7 @@ pub const Iterator = struct {
pub const Entry = struct { []const u8, u32 };
pub fn next(self: *Iterator, _: *const Page) ?Iterator.Entry {
pub fn next(self: *Iterator, _: *const Frame) ?Iterator.Entry {
const index = self.index;
if (index >= tracked_event_types.len) {
return null;

View File

@@ -65,10 +65,10 @@ pub fn dispatchEvent(self: *EventTarget, event: *Event, exec: *js.Execution) !bo
event._is_trusted = false;
switch (exec.context.global) {
.page => |page| {
.frame => |frame| {
event.acquireRef();
defer _ = event.releaseRef(page._session);
try page._event_manager.dispatch(self, event);
defer _ = event.releaseRef(frame._session);
try frame._event_manager.dispatch(self, event);
},
.worker => |wgs| try wgs.dispatch(self, event, null),
}
@@ -101,7 +101,7 @@ pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventLi
};
switch (exec.context.global) {
.page => |page| _ = try page._event_manager.register(self, typ, em_callback, options),
.frame => |frame| _ = try frame._event_manager.register(self, typ, em_callback, options),
.worker => |wgs| _ = try wgs._event_manager.register(self, typ, em_callback, options),
}
}
@@ -138,7 +138,7 @@ pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: ?Even
};
switch (exec.context.global) {
.page => |page| page._event_manager.remove(self, typ, em_callback, use_capture),
.frame => |frame| frame._event_manager.remove(self, typ, em_callback, use_capture),
.worker => |wgs| wgs._event_manager.remove(self, typ, em_callback, use_capture),
}
}
@@ -206,7 +206,7 @@ pub const JsApi = struct {
const testing = @import("../../testing.zig");
test "WebApi: EventTarget" {
// we create thousands of these per page. Nothing should bloat it.
// we create thousands of these per frame. Nothing should bloat it.
try testing.expectEqual(16, @sizeOf(EventTarget));
try testing.htmlRunner("events.html", .{});
}

View File

@@ -21,7 +21,7 @@ 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");
@@ -34,7 +34,7 @@ const Allocator = std.mem.Allocator;
const FileReader = @This();
_rc: lp.RC(u8) = .{},
_page: *Page,
_frame: *Frame,
_proto: *EventTarget,
_arena: Allocator,
@@ -62,12 +62,12 @@ const Result = union(enum) {
arraybuffer: js.ArrayBuffer,
};
pub fn init(page: *Page) !*FileReader {
const arena = try page.getArena(.tiny, "FileReader");
errdefer page.releaseArena(arena);
pub fn init(frame: *Frame) !*FileReader {
const arena = try frame.getArena(.tiny, "FileReader");
errdefer frame.releaseArena(arena);
return page._factory.eventTargetWithAllocator(arena, FileReader{
._page = page,
return frame._factory.eventTargetWithAllocator(arena, FileReader{
._frame = frame,
._arena = arena,
._proto = undefined,
});
@@ -191,9 +191,9 @@ fn readInternal(self: *FileReader, blob: *Blob, read_type: ReadType) !void {
self._error = null;
self._aborted = false;
const page = self._page;
const frame = self._frame;
try self.dispatch(.load_start, .{ .loaded = 0, .total = blob.getSize() }, page);
try self.dispatch(.load_start, .{ .loaded = 0, .total = blob.getSize() }, frame);
if (self._aborted) {
return;
}
@@ -201,7 +201,7 @@ fn readInternal(self: *FileReader, blob: *Blob, read_type: ReadType) !void {
// Perform the read (synchronous since data is in memory)
const data = blob._slice;
const size = data.len;
try self.dispatch(.progress, .{ .loaded = size, .total = size }, page);
try self.dispatch(.progress, .{ .loaded = size, .total = size }, frame);
if (self._aborted) {
return;
}
@@ -221,8 +221,8 @@ fn readInternal(self: *FileReader, blob: *Blob, read_type: ReadType) !void {
self._ready_state = .done;
try self.dispatch(.load, .{ .loaded = size, .total = size }, page);
try self.dispatch(.load_end, .{ .loaded = size, .total = size }, page);
try self.dispatch(.load, .{ .loaded = size, .total = size }, frame);
try self.dispatch(.load_end, .{ .loaded = size, .total = size }, frame);
}
pub fn abort(self: *FileReader) !void {
@@ -234,14 +234,14 @@ pub fn abort(self: *FileReader) !void {
self._ready_state = .done;
self._result = null;
const page = self._page;
const frame = self._frame;
try self.dispatch(.abort, null, page);
try self.dispatch(.abort, null, frame);
try self.dispatch(.load_end, null, page);
try self.dispatch(.load_end, null, frame);
}
fn dispatch(self: *FileReader, comptime event_type: DispatchType, progress_: ?Progress, page: *Page) !void {
fn dispatch(self: *FileReader, comptime event_type: DispatchType, progress_: ?Progress, frame: *Frame) !void {
const field, const typ = comptime blk: {
break :blk switch (event_type) {
.abort => .{ "_on_abort", "abort" },
@@ -257,10 +257,10 @@ fn dispatch(self: *FileReader, comptime event_type: DispatchType, progress_: ?Pr
const event = (try ProgressEvent.initTrusted(
comptime .wrap(typ),
.{ .total = progress.total, .loaded = progress.loaded },
page,
frame,
)).asEvent();
return page._event_manager.dispatchDirect(
return frame._event_manager.dispatchDirect(
self.asEventTarget(),
event,
@field(self, field),

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Node = @import("Node.zig");
const Document = @import("Document.zig");
const Element = @import("Element.zig");
@@ -68,7 +68,7 @@ pub fn getBody(self: *HTMLDocument) ?*Element.Html.Body {
return null;
}
pub fn getTitle(self: *HTMLDocument, page: *Page) ![]const u8 {
pub fn getTitle(self: *HTMLDocument, frame: *Frame) ![]const u8 {
// Search the entire document for the first <title> element
const root = self._proto.getDocumentElement() orelse return "";
const title_element = blk: {
@@ -81,7 +81,7 @@ pub fn getTitle(self: *HTMLDocument, page: *Page) ![]const u8 {
return "";
};
var buf = std.Io.Writer.Allocating.init(page.call_arena);
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
try title_element.asNode().getTextContent(&buf.writer);
const text = buf.written();
@@ -92,7 +92,7 @@ pub fn getTitle(self: *HTMLDocument, page: *Page) ![]const u8 {
var started = false;
var in_whitespace = false;
var result: std.ArrayList(u8) = .empty;
try result.ensureTotalCapacity(page.call_arena, text.len);
try result.ensureTotalCapacity(frame.call_arena, text.len);
for (text) |c| {
const is_ascii_ws = c == ' ' or c == '\t' or c == '\n' or c == '\r' or c == '\x0C';
@@ -114,7 +114,7 @@ pub fn getTitle(self: *HTMLDocument, page: *Page) ![]const u8 {
return result.items;
}
pub fn setTitle(self: *HTMLDocument, title: []const u8, page: *Page) !void {
pub fn setTitle(self: *HTMLDocument, title: []const u8, frame: *Frame) !void {
const head = self.getHead() orelse return;
// Find existing title element in head
@@ -123,47 +123,47 @@ pub fn setTitle(self: *HTMLDocument, title: []const u8, page: *Page) !void {
if (node.is(Element.Html.Title)) |title_element| {
// Replace children, but don't create text node for empty string
if (title.len == 0) {
return title_element.asElement().replaceChildren(&.{}, page);
return title_element.asElement().replaceChildren(&.{}, frame);
} else {
return title_element.asElement().replaceChildren(&.{.{ .text = title }}, page);
return title_element.asElement().replaceChildren(&.{.{ .text = title }}, frame);
}
}
}
// No title element found, create one
const title_node = try page.createElementNS(.html, "title", null);
const title_node = try frame.createElementNS(.html, "title", null);
const title_element = title_node.as(Element);
// Only add text if non-empty
if (title.len > 0) {
try title_element.replaceChildren(&.{.{ .text = title }}, page);
try title_element.replaceChildren(&.{.{ .text = title }}, frame);
}
_ = try head.asNode().appendChild(title_node, page);
_ = try head.asNode().appendChild(title_node, frame);
}
pub fn getImages(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) {
return collections.NodeLive(.tag).init(self.asNode(), .img, page);
pub fn getImages(self: *HTMLDocument, frame: *Frame) !collections.NodeLive(.tag) {
return collections.NodeLive(.tag).init(self.asNode(), .img, frame);
}
pub fn getScripts(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) {
return collections.NodeLive(.tag).init(self.asNode(), .script, page);
pub fn getScripts(self: *HTMLDocument, frame: *Frame) !collections.NodeLive(.tag) {
return collections.NodeLive(.tag).init(self.asNode(), .script, frame);
}
pub fn getLinks(self: *HTMLDocument, page: *Page) !collections.NodeLive(.links) {
return collections.NodeLive(.links).init(self.asNode(), {}, page);
pub fn getLinks(self: *HTMLDocument, frame: *Frame) !collections.NodeLive(.links) {
return collections.NodeLive(.links).init(self.asNode(), {}, frame);
}
pub fn getAnchors(self: *HTMLDocument, page: *Page) !collections.NodeLive(.anchors) {
return collections.NodeLive(.anchors).init(self.asNode(), {}, page);
pub fn getAnchors(self: *HTMLDocument, frame: *Frame) !collections.NodeLive(.anchors) {
return collections.NodeLive(.anchors).init(self.asNode(), {}, frame);
}
pub fn getForms(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) {
return collections.NodeLive(.tag).init(self.asNode(), .form, page);
pub fn getForms(self: *HTMLDocument, frame: *Frame) !collections.NodeLive(.tag) {
return collections.NodeLive(.tag).init(self.asNode(), .form, frame);
}
pub fn getEmbeds(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) {
return collections.NodeLive(.tag).init(self.asNode(), .embed, page);
pub fn getEmbeds(self: *HTMLDocument, frame: *Frame) !collections.NodeLive(.tag) {
return collections.NodeLive(.tag).init(self.asNode(), .embed, frame);
}
pub fn getApplets(_: *const HTMLDocument) collections.HTMLCollection {
@@ -178,8 +178,8 @@ pub fn getLocation(self: *const HTMLDocument) ?*@import("Location.zig") {
return self._proto._location;
}
pub fn setLocation(self: *HTMLDocument, url: [:0]const u8, page: *Page) !void {
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = self._proto._page });
pub fn setLocation(self: *HTMLDocument, url: [:0]const u8, frame: *Frame) !void {
return frame.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = self._proto._frame });
}
pub fn getDir(self: *HTMLDocument) []const u8 {
@@ -188,10 +188,10 @@ pub fn getDir(self: *HTMLDocument) []const u8 {
return html.getDir();
}
pub fn setDir(self: *HTMLDocument, value: []const u8, page: *Page) !void {
pub fn setDir(self: *HTMLDocument, value: []const u8, frame: *Frame) !void {
const el = self._proto.getDocumentElement() orelse return;
const html = el.is(Element.Html) orelse return;
try html.setDir(value, page);
try html.setDir(value, frame);
}
pub fn getLang(self: *HTMLDocument) []const u8 {
@@ -200,30 +200,30 @@ pub fn getLang(self: *HTMLDocument) []const u8 {
return html.getLang();
}
pub fn setLang(self: *HTMLDocument, value: []const u8, page: *Page) !void {
pub fn setLang(self: *HTMLDocument, value: []const u8, frame: *Frame) !void {
const el = self._proto.getDocumentElement() orelse return;
const html = el.is(Element.Html) orelse return;
try html.setLang(value, page);
try html.setLang(value, frame);
}
pub fn getAll(self: *HTMLDocument, page: *Page) !*collections.HTMLAllCollection {
return page._factory.create(collections.HTMLAllCollection.init(self.asNode(), page));
pub fn getAll(self: *HTMLDocument, frame: *Frame) !*collections.HTMLAllCollection {
return frame._factory.create(collections.HTMLAllCollection.init(self.asNode(), frame));
}
pub fn getCookie(_: *HTMLDocument, page: *Page) ![]const u8 {
pub fn getCookie(_: *HTMLDocument, frame: *Frame) ![]const u8 {
var buf: std.ArrayList(u8) = .empty;
try page._session.cookie_jar.forRequest(page.url, buf.writer(page.call_arena), .{
try frame._session.cookie_jar.forRequest(frame.url, buf.writer(frame.call_arena), .{
.is_http = false,
.is_navigation = true,
});
return buf.items;
}
pub fn setCookie(_: *HTMLDocument, cookie_str: []const u8, page: *Page) ![]const u8 {
pub fn setCookie(_: *HTMLDocument, cookie_str: []const u8, frame: *Frame) ![]const u8 {
// we use the cookie jar's allocator to parse the cookie because it
// outlives the page's arena.
// outlives the frame's arena.
const Cookie = @import("storage/Cookie.zig");
const c = Cookie.parse(page._session.cookie_jar.allocator, page.url, cookie_str) catch {
const c = Cookie.parse(frame._session.cookie_jar.allocator, frame.url, cookie_str) catch {
// Invalid cookies should be silently ignored, not throw errors
return "";
};
@@ -232,11 +232,11 @@ pub fn setCookie(_: *HTMLDocument, cookie_str: []const u8, page: *Page) ![]const
c.deinit();
return ""; // HttpOnly cookies cannot be set from JS
}
try page._session.cookie_jar.add(c, std.time.timestamp());
try frame._session.cookie_jar.add(c, std.time.timestamp());
return cookie_str;
}
pub fn getDocType(self: *HTMLDocument, page: *Page) !*DocumentType {
pub fn getDocType(self: *HTMLDocument, frame: *Frame) !*DocumentType {
if (self._document_type) |dt| {
return dt;
}
@@ -249,7 +249,7 @@ pub fn getDocType(self: *HTMLDocument, page: *Page) !*DocumentType {
}
}
self._document_type = try page._factory.node(DocumentType{
self._document_type = try frame._factory.node(DocumentType{
._proto = undefined,
._name = "html",
._public_id = "",
@@ -268,8 +268,8 @@ pub const JsApi = struct {
};
pub const constructor = bridge.constructor(_constructor, .{});
fn _constructor(page: *Page) !*HTMLDocument {
return page._factory.document(HTMLDocument{
fn _constructor(frame: *Frame) !*HTMLDocument {
return frame._factory.document(HTMLDocument{
._proto = undefined,
});
}

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Location = @import("Location.zig");
const PopStateEvent = @import("event/PopStateEvent.zig");
const URL = @import("URL.zig");
@@ -30,13 +30,13 @@ const ScrollRestoration = enum { auto, manual };
_scroll_restoration: ScrollRestoration = .auto,
pub fn getLength(_: *const History, page: *Page) u32 {
return @intCast(page._session.navigation._entries.items.len);
pub fn getLength(_: *const History, frame: *Frame) u32 {
return @intCast(frame._session.navigation._entries.items.len);
}
pub fn getState(_: *const History, page: *Page) !?js.Value {
if (page._session.navigation.getCurrentEntry()._state.value) |state| {
const value = try page.js.local.?.parseJSON(state);
pub fn getState(_: *const History, frame: *Frame) !?js.Value {
if (frame._session.navigation.getCurrentEntry()._state.value) |state| {
const value = try frame.js.local.?.parseJSON(state);
return value;
} else return null;
}
@@ -51,69 +51,69 @@ pub fn setScrollRestoration(self: *History, str: []const u8) void {
}
}
pub fn pushState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page._session.arena;
pub fn pushState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8, frame: *Frame) !void {
const arena = frame._session.arena;
const url = if (_url) |u|
try @import("../URL.zig").resolve(arena, page.url, u, .{ .always_dupe = true })
try @import("../URL.zig").resolve(arena, frame.url, u, .{ .always_dupe = true })
else
try arena.dupeZ(u8, page.url);
try arena.dupeZ(u8, frame.url);
const json = state.toJson(arena) catch return error.DataClone;
_ = try page._session.navigation.pushEntry(url, .{ .source = .history, .value = json }, page, true);
_ = try frame._session.navigation.pushEntry(url, .{ .source = .history, .value = json }, frame, true);
page.url = url;
page.window._location._url = try URL.init(url, null, &page.js.execution);
frame.url = url;
frame.window._location._url = try URL.init(url, null, &frame.js.execution);
}
pub fn replaceState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page._session.arena;
pub fn replaceState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8, frame: *Frame) !void {
const arena = frame._session.arena;
const url = if (_url) |u|
try @import("../URL.zig").resolve(arena, page.url, u, .{ .always_dupe = true })
try @import("../URL.zig").resolve(arena, frame.url, u, .{ .always_dupe = true })
else
try arena.dupeZ(u8, page.url);
try arena.dupeZ(u8, frame.url);
const json = state.toJson(arena) catch return error.DataClone;
_ = try page._session.navigation.replaceEntry(url, .{ .source = .history, .value = json }, page, true);
_ = try frame._session.navigation.replaceEntry(url, .{ .source = .history, .value = json }, frame, true);
page.url = url;
page.window._location = try Location.init(url, page);
frame.url = url;
frame.window._location = try Location.init(url, frame);
}
fn goInner(delta: i32, page: *Page) !void {
// 0 behaves the same as no argument, both reloading the page.
fn goInner(delta: i32, frame: *Frame) !void {
// 0 behaves the same as no argument, both reloading the frame.
const current = page._session.navigation._index;
const current = frame._session.navigation._index;
const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta)));
if (index_s < 0 or index_s > page._session.navigation._entries.items.len - 1) {
if (index_s < 0 or index_s > frame._session.navigation._entries.items.len - 1) {
return;
}
const index = @as(usize, @intCast(index_s));
const entry = page._session.navigation._entries.items[index];
const entry = frame._session.navigation._entries.items[index];
if (entry._url) |url| {
if (page.isSameOrigin(url)) {
const target = page.window.asEventTarget();
if (page._event_manager.hasDirectListeners(target, "popstate", page.window._on_popstate)) {
const event = (try PopStateEvent.initTrusted(comptime .wrap("popstate"), .{ .state = entry._state.value }, page)).asEvent();
try page._event_manager.dispatchDirect(target, event, page.window._on_popstate, .{ .context = "Pop State" });
if (frame.isSameOrigin(url)) {
const target = frame.window.asEventTarget();
if (frame._event_manager.hasDirectListeners(target, "popstate", frame.window._on_popstate)) {
const event = (try PopStateEvent.initTrusted(comptime .wrap("popstate"), .{ .state = entry._state.value }, frame)).asEvent();
try frame._event_manager.dispatchDirect(target, event, frame.window._on_popstate, .{ .context = "Pop State" });
}
}
}
_ = try page._session.navigation.navigateInner(entry._url, .{ .traverse = index }, page);
_ = try frame._session.navigation.navigateInner(entry._url, .{ .traverse = index }, frame);
}
pub fn back(_: *History, page: *Page) !void {
try goInner(-1, page);
pub fn back(_: *History, frame: *Frame) !void {
try goInner(-1, frame);
}
pub fn forward(_: *History, page: *Page) !void {
try goInner(1, page);
pub fn forward(_: *History, frame: *Frame) !void {
try goInner(1, frame);
}
pub fn go(_: *History, delta: ?i32, page: *Page) !void {
try goInner(delta orelse 0, page);
pub fn go(_: *History, delta: ?i32, frame: *Frame) !void {
try goInner(delta orelse 0, frame);
}
pub const JsApi = struct {

View File

@@ -20,7 +20,7 @@ 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 String = lp.String;
@@ -55,7 +55,7 @@ pub fn init(
width: u32,
height: u32,
maybe_settings: ?ConstructorSettings,
page: *Page,
frame: *Frame,
) !*ImageData {
// Though arguments are unsigned long, these are capped to max. i32 on Chrome.
// https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/core/html/canvas/image_data.cc#L61
@@ -77,10 +77,10 @@ pub fn init(
size, overflown = @mulWithOverflow(size, 4);
if (overflown == 1) return error.IndexSizeError;
return page._factory.create(ImageData{
return frame._factory.create(ImageData{
._width = width,
._height = height,
._data = try page.js.local.?.createTypedArray(.uint8_clamped, size).persist(),
._data = try frame.js.local.?.createTypedArray(.uint8_clamped, size).persist(),
});
}

View File

@@ -20,7 +20,7 @@ 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");
@@ -69,9 +69,9 @@ pub const ObserverInit = struct {
};
};
pub fn init(callback: js.Function.Temp, options: ?ObserverInit, page: *Page) !*IntersectionObserver {
const arena = try page.getArena(.small, "IntersectionObserver");
errdefer page.releaseArena(arena);
pub fn init(callback: js.Function.Temp, options: ?ObserverInit, frame: *Frame) !*IntersectionObserver {
const arena = try frame.getArena(.small, "IntersectionObserver");
errdefer frame.releaseArena(arena);
const opts = options orelse ObserverInit{};
const root_margin = if (opts.rootMargin) |rm| try arena.dupe(u8, rm) else "0px";
@@ -128,7 +128,7 @@ pub fn acquireRef(self: *IntersectionObserver) void {
self._rc.acquire();
}
pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void {
pub fn observe(self: *IntersectionObserver, target: *Element, frame: *Frame) !void {
// Check if already observing this target
for (self._observing.items) |elem| {
if (elem == target) {
@@ -138,20 +138,20 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void
try self._observing.append(self._arena, target);
if (self._observing.items.len == 1) {
try page.registerIntersectionObserver(self);
try frame.registerIntersectionObserver(self);
}
// Don't initialize previous state yet - let checkIntersection do it
// This ensures we get an entry on first observation
// Check intersection for this new target and schedule delivery
try self.checkIntersection(target, page);
try self.checkIntersection(target, frame);
if (self._pending_entries.items.len > 0) {
try page.scheduleIntersectionDelivery();
try frame.scheduleIntersectionDelivery();
}
}
pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) void {
pub fn unobserve(self: *IntersectionObserver, target: *Element, frame: *Frame) void {
const original_length = self._observing.items.len;
for (self._observing.items, 0..) |elem, i| {
if (elem == target) {
@@ -164,7 +164,7 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) voi
while (j < self._pending_entries.items.len) {
if (self._pending_entries.items[j]._target == target) {
const entry = self._pending_entries.swapRemove(j);
entry.deinit(page._session);
entry.deinit(frame._session);
} else {
j += 1;
}
@@ -174,25 +174,25 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) voi
}
if (original_length > 0 and self._observing.items.len == 0) {
page.unregisterIntersectionObserver(self);
frame.unregisterIntersectionObserver(self);
}
}
pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
pub fn disconnect(self: *IntersectionObserver, frame: *Frame) void {
for (self._pending_entries.items) |entry| {
entry.deinit(page._session);
entry.deinit(frame._session);
}
self._pending_entries.clearRetainingCapacity();
self._previous_states.clearRetainingCapacity();
if (self._observing.items.len > 0) {
page.unregisterIntersectionObserver(self);
frame.unregisterIntersectionObserver(self);
}
self._observing.clearRetainingCapacity();
}
pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {
const entries = try page.call_arena.dupe(*IntersectionObserverEntry, self._pending_entries.items);
pub fn takeRecords(self: *IntersectionObserver, frame: *Frame) ![]*IntersectionObserverEntry {
const entries = try frame.call_arena.dupe(*IntersectionObserverEntry, self._pending_entries.items);
self._pending_entries.clearRetainingCapacity();
return entries;
}
@@ -200,13 +200,13 @@ pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObs
fn calculateIntersection(
self: *IntersectionObserver,
target: *Element,
page: *Page,
frame: *Frame,
) !IntersectionData {
const target_rect = target.getBoundingClientRect(page);
const target_rect = target.getBoundingClientRect(frame);
// Use root element's rect or viewport (simplified: assume 1920x1080)
const root_rect = if (self._root) |root|
root.getBoundingClientRect(page)
root.getBoundingClientRect(frame)
else
// Simplified viewport - assume 1920x1080 for now
DOMRect{
@@ -253,8 +253,8 @@ fn meetsThreshold(self: *IntersectionObserver, ratio: f64) bool {
return false;
}
fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page) !void {
const data = try self.calculateIntersection(target, page);
fn checkIntersection(self: *IntersectionObserver, target: *Element, frame: *Frame) !void {
const data = try self.calculateIntersection(target, frame);
const was_intersecting_opt = self._previous_states.get(target);
const is_now_intersecting = data.is_intersecting and self.meetsThreshold(data.intersection_ratio);
@@ -265,18 +265,18 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page)
(was_intersecting_opt != null and was_intersecting_opt.? != is_now_intersecting);
if (should_report) {
const arena = try page.getArena(.tiny, "IntersectionObserverEntry");
errdefer page.releaseArena(arena);
const arena = try frame.getArena(.tiny, "IntersectionObserverEntry");
errdefer frame.releaseArena(arena);
const entry = try arena.create(IntersectionObserverEntry);
entry.* = .{
._arena = arena,
._target = target,
._time = page.window._performance.now(),
._time = frame.window._performance.now(),
._is_intersecting = is_now_intersecting,
._root_bounds = try page._factory.create(data.root_bounds),
._intersection_rect = try page._factory.create(data.intersection_rect),
._bounding_client_rect = try page._factory.create(data.bounding_client_rect),
._root_bounds = try frame._factory.create(data.root_bounds),
._intersection_rect = try frame._factory.create(data.intersection_rect),
._bounding_client_rect = try frame._factory.create(data.bounding_client_rect),
._intersection_ratio = data.intersection_ratio,
};
try self._pending_entries.append(self._arena, entry);
@@ -287,34 +287,34 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page)
try self._previous_states.put(self._arena, target, is_now_intersecting);
}
pub fn checkIntersections(self: *IntersectionObserver, page: *Page) !void {
pub fn checkIntersections(self: *IntersectionObserver, frame: *Frame) !void {
if (self._observing.items.len == 0) {
return;
}
for (self._observing.items) |target| {
try self.checkIntersection(target, page);
try self.checkIntersection(target, frame);
}
if (self._pending_entries.items.len > 0) {
try page.scheduleIntersectionDelivery();
try frame.scheduleIntersectionDelivery();
}
}
pub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void {
pub fn deliverEntries(self: *IntersectionObserver, frame: *Frame) !void {
if (self._pending_entries.items.len == 0) {
return;
}
const entries = try self.takeRecords(page);
const entries = try self.takeRecords(frame);
var caught: js.TryCatch.Caught = undefined;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
frame.js.localScope(&ls);
defer ls.deinit();
ls.toLocal(self._callback).tryCall(void, .{ entries, self }, &caught) catch |err| {
log.err(.page, "IntsctObserver.deliverEntries", .{ .err = err, .caught = caught });
log.err(.frame, "IntsctObserver.deliverEntries", .{ .err = err, .caught = caught });
return err;
};
}

View File

@@ -20,7 +20,7 @@ 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 h5e = @import("../parser/html5ever.zig");
const String = lp.String;

View File

@@ -20,15 +20,15 @@ const std = @import("std");
const js = @import("../js/js.zig");
const URL = @import("URL.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Location = @This();
_url: *URL,
pub fn init(raw_url: [:0]const u8, page: *Page) !*Location {
const url = try URL.init(raw_url, null, &page.js.execution);
return page._factory.create(Location{
pub fn init(raw_url: [:0]const u8, frame: *Frame) !*Location {
const url = try URL.init(raw_url, null, &frame.js.execution);
return frame._factory.create(Location{
._url = url,
});
}
@@ -65,10 +65,10 @@ pub fn getHash(self: *const Location) []const u8 {
return self._url.getHash();
}
pub fn setHash(_: *const Location, hash: []const u8, page: *Page) !void {
pub fn setHash(_: *const Location, hash: []const u8, frame: *Frame) !void {
const normalized_hash = blk: {
if (hash.len == 0) {
const old_url = page.url;
const old_url = frame.url;
break :blk if (std.mem.indexOfScalar(u8, old_url, '#')) |index|
old_url[0..index]
@@ -77,25 +77,25 @@ pub fn setHash(_: *const Location, hash: []const u8, page: *Page) !void {
} else if (hash[0] == '#')
break :blk hash
else
break :blk try std.fmt.allocPrint(page.call_arena, "#{s}", .{hash});
break :blk try std.fmt.allocPrint(frame.call_arena, "#{s}", .{hash});
};
return page.scheduleNavigation(normalized_hash, .{
return frame.scheduleNavigation(normalized_hash, .{
.reason = .script,
.kind = .{ .replace = null },
}, .{ .script = page });
}, .{ .script = frame });
}
pub fn assign(_: *const Location, url: [:0]const u8, page: *Page) !void {
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = page });
pub fn assign(_: *const Location, url: [:0]const u8, frame: *Frame) !void {
return frame.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = frame });
}
pub fn replace(_: *const Location, url: [:0]const u8, page: *Page) !void {
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .replace = null } }, .{ .script = page });
pub fn replace(_: *const Location, url: [:0]const u8, frame: *Frame) !void {
return frame.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .replace = null } }, .{ .script = frame });
}
pub fn reload(_: *const Location, page: *Page) !void {
return page.scheduleNavigation(page.url, .{ .reason = .script, .kind = .reload }, .{ .script = page });
pub fn reload(_: *const Location, frame: *Frame) !void {
return frame.scheduleNavigation(frame.url, .{ .reason = .script, .kind = .reload }, .{ .script = frame });
}
pub fn toString(self: *const Location, exec: *const js.Execution) ![:0]const u8 {
@@ -113,8 +113,8 @@ pub const JsApi = struct {
pub const toString = bridge.function(Location.toString, .{});
pub const href = bridge.accessor(Location.toString, setHref, .{});
fn setHref(self: *const Location, url: [:0]const u8, page: *Page) !void {
return self.assign(url, page);
fn setHref(self: *const Location, url: [:0]const u8, frame: *Frame) !void {
return self.assign(url, frame);
}
pub const search = bridge.accessor(Location.getSearch, null, .{});

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const MessagePort = @import("MessagePort.zig");
const MessageChannel = @This();
@@ -25,13 +25,13 @@ const MessageChannel = @This();
_port1: *MessagePort,
_port2: *MessagePort,
pub fn init(page: *Page) !*MessageChannel {
const port1 = try MessagePort.init(page);
const port2 = try MessagePort.init(page);
pub fn init(frame: *Frame) !*MessageChannel {
const port1 = try MessagePort.init(frame);
const port2 = try MessagePort.init(frame);
MessagePort.entangle(port1, port2);
return page._factory.create(MessageChannel{
return frame._factory.create(MessageChannel{
._port1 = port1,
._port2 = port2,
});

View File

@@ -20,7 +20,7 @@ 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 EventTarget = @import("EventTarget.zig");
const MessageEvent = @import("event/MessageEvent.zig");
@@ -36,8 +36,8 @@ _on_message: ?js.Function.Global = null,
_on_message_error: ?js.Function.Global = null,
_entangled_port: ?*MessagePort = null,
pub fn init(page: *Page) !*MessagePort {
return page._factory.eventTarget(MessagePort{
pub fn init(frame: *Frame) !*MessagePort {
return frame._factory.eventTarget(MessagePort{
._proto = undefined,
});
}
@@ -51,7 +51,7 @@ pub fn entangle(port1: *MessagePort, port2: *MessagePort) void {
port2._entangled_port = port1;
}
pub fn postMessage(self: *MessagePort, message: js.Value.Temp, page: *Page) !void {
pub fn postMessage(self: *MessagePort, message: js.Value.Temp, frame: *Frame) !void {
if (self._closed) {
return;
}
@@ -62,13 +62,13 @@ pub fn postMessage(self: *MessagePort, message: js.Value.Temp, page: *Page) !voi
}
// Create callback to deliver message
const callback = try page._factory.create(PostMessageCallback{
.page = page,
const callback = try frame._factory.create(PostMessageCallback{
.frame = frame,
.port = other,
.message = message,
});
try page.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
try frame.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
.name = "MessagePort.postMessage",
.low_priority = false,
});
@@ -110,33 +110,33 @@ pub fn setOnMessageError(self: *MessagePort, cb: ?js.Function.Global) !void {
const PostMessageCallback = struct {
port: *MessagePort,
message: js.Value.Temp,
page: *Page,
frame: *Frame,
fn deinit(self: *PostMessageCallback) void {
self.page._factory.destroy(self);
self.frame._factory.destroy(self);
}
fn run(ctx: *anyopaque) !?u32 {
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
defer self.deinit();
const page = self.page;
const frame = self.frame;
if (self.port._closed) {
return null;
}
const target = self.port.asEventTarget();
if (page._event_manager.hasDirectListeners(target, "message", self.port._on_message)) {
if (frame._event_manager.hasDirectListeners(target, "message", self.port._on_message)) {
const event = (MessageEvent.initTrusted(comptime .wrap("message"), .{
.data = .{ .value = self.message },
.origin = "",
.source = null,
}, page._session) catch |err| {
}, frame._session) catch |err| {
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
return null;
}).asEvent();
page._event_manager.dispatchDirect(target, event, self.port._on_message, .{ .context = "MessagePort message" }) catch |err| {
frame._event_manager.dispatchDirect(target, event, self.port._on_message, .{ .context = "MessagePort message" }) catch |err| {
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
};
}

View File

@@ -20,7 +20,7 @@ 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");
@@ -46,7 +46,7 @@ _callback: js.Function.Temp,
_observing: std.ArrayList(Observing) = .{},
_pending_records: std.ArrayList(*MutationRecord) = .{},
/// Intrusively linked to next element (see Page.zig).
/// Intrusively linked to next element (see Frame.zig).
node: std.DoublyLinkedList.Node = .{},
const Observing = struct {
@@ -75,9 +75,9 @@ pub const ObserveOptions = struct {
attributeFilter: ?[]const []const u8 = null,
};
pub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver {
const arena = try page.getArena(.small, "MutationObserver");
errdefer page.releaseArena(arena);
pub fn init(callback: js.Function.Temp, frame: *Frame) !*MutationObserver {
const arena = try frame.getArena(.small, "MutationObserver");
errdefer frame.releaseArena(arena);
const self = try arena.create(MutationObserver);
self.* = .{
._arena = arena,
@@ -104,7 +104,7 @@ pub fn acquireRef(self: *MutationObserver) void {
self._rc.acquire();
}
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, frame: *Frame) !void {
const arena = self._arena;
// Per spec: if attributeOldValue/attributeFilter present and attributes
@@ -172,24 +172,24 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
});
if (self._observing.items.len == 1) {
try page.registerMutationObserver(self);
try frame.registerMutationObserver(self);
}
}
pub fn disconnect(self: *MutationObserver, page: *Page) void {
pub fn disconnect(self: *MutationObserver, frame: *Frame) void {
for (self._pending_records.items) |record| {
record.deinit(page._session);
record.deinit(frame._session);
}
self._pending_records.clearRetainingCapacity();
if (self._observing.items.len > 0) {
page.unregisterMutationObserver(self);
frame.unregisterMutationObserver(self);
}
self._observing.clearRetainingCapacity();
}
pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord {
const records = try page.call_arena.dupe(*MutationRecord, self._pending_records.items);
pub fn takeRecords(self: *MutationObserver, frame: *Frame) ![]*MutationRecord {
const records = try frame.call_arena.dupe(*MutationRecord, self._pending_records.items);
self._pending_records.clearRetainingCapacity();
return records;
}
@@ -200,7 +200,7 @@ pub fn notifyAttributeChange(
target: *Element,
attribute_name: String,
old_value: ?String,
page: *Page,
frame: *Frame,
) !void {
const target_node = target.asNode();
@@ -226,7 +226,7 @@ pub fn notifyAttributeChange(
}
}
const arena = try page.getArena(.tiny, "MutationRecord");
const arena = try frame.getArena(.tiny, "MutationRecord");
const record = try arena.create(MutationRecord);
record.* = .{
._arena = arena,
@@ -245,7 +245,7 @@ pub fn notifyAttributeChange(
try self._pending_records.append(self._arena, record);
try page.scheduleMutationDelivery();
try frame.scheduleMutationDelivery();
break;
}
}
@@ -255,7 +255,7 @@ pub fn notifyCharacterDataChange(
self: *MutationObserver,
target: *Node,
old_value: ?String,
page: *Page,
frame: *Frame,
) !void {
for (self._observing.items) |obs| {
if (obs.target != target) {
@@ -270,7 +270,7 @@ pub fn notifyCharacterDataChange(
continue;
}
const arena = try page.getArena(.tiny, "MutationRecord");
const arena = try frame.getArena(.tiny, "MutationRecord");
const record = try arena.create(MutationRecord);
record.* = .{
._arena = arena,
@@ -289,7 +289,7 @@ pub fn notifyCharacterDataChange(
try self._pending_records.append(self._arena, record);
try page.scheduleMutationDelivery();
try frame.scheduleMutationDelivery();
break;
}
}
@@ -302,7 +302,7 @@ pub fn notifyChildListChange(
removed_nodes: []const *Node,
previous_sibling: ?*Node,
next_sibling: ?*Node,
page: *Page,
frame: *Frame,
) !void {
for (self._observing.items) |obs| {
if (obs.target != target) {
@@ -317,7 +317,7 @@ pub fn notifyChildListChange(
continue;
}
const arena = try page.getArena(.tiny, "MutationRecord");
const arena = try frame.getArena(.tiny, "MutationRecord");
const record = try arena.create(MutationRecord);
record.* = .{
._arena = arena,
@@ -333,26 +333,26 @@ pub fn notifyChildListChange(
try self._pending_records.append(self._arena, record);
try page.scheduleMutationDelivery();
try frame.scheduleMutationDelivery();
break;
}
}
pub fn deliverRecords(self: *MutationObserver, page: *Page) !void {
pub fn deliverRecords(self: *MutationObserver, frame: *Frame) !void {
if (self._pending_records.items.len == 0) {
return;
}
// Take a copy of the records and clear the list before calling callback
// This ensures mutations triggered during the callback go into a fresh list
const records = try self.takeRecords(page);
const records = try self.takeRecords(frame);
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
frame.js.localScope(&ls);
defer ls.deinit();
var caught: js.TryCatch.Caught = undefined;
ls.toLocal(self._callback).tryCall(void, .{ records, self }, &caught) catch |err| {
log.err(.page, "MutObserver.deliverRecords", .{ .err = err, .caught = caught });
log.err(.frame, "MutObserver.deliverRecords", .{ .err = err, .caught = caught });
return err;
};
}

View File

@@ -21,7 +21,7 @@ const lp = @import("lightpanda");
const builtin = @import("builtin");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const PluginArray = @import("PluginArray.zig");
const Permissions = @import("Permissions.zig");
@@ -37,8 +37,8 @@ _storage: StorageManager = .{},
pub const init: Navigator = .{};
pub fn getUserAgent(_: *const Navigator, page: *Page) []const u8 {
return page._session.browser.http_client.getUserAgent();
pub fn getUserAgent(_: *const Navigator, frame: *Frame) []const u8 {
return frame._session.browser.http_client.getUserAgent();
}
pub fn getLanguages(_: *const Navigator) [2][]const u8 {
@@ -72,18 +72,18 @@ pub fn getStorage(self: *Navigator) *StorageManager {
return &self._storage;
}
pub fn getBattery(_: *const Navigator, page: *Page) !js.Promise {
pub fn getBattery(_: *const Navigator, frame: *Frame) !js.Promise {
log.info(.not_implemented, "navigator.getBattery", .{});
return page.js.local.?.rejectErrorPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
return frame.js.local.?.rejectErrorPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
}
pub fn registerProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void {
pub fn registerProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, frame: *const Frame) !void {
try validateProtocolHandlerScheme(scheme);
try validateProtocolHandlerURL(url, page);
try validateProtocolHandlerURL(url, frame);
}
pub fn unregisterProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void {
pub fn unregisterProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, frame: *const Frame) !void {
try validateProtocolHandlerScheme(scheme);
try validateProtocolHandlerURL(url, page);
try validateProtocolHandlerURL(url, frame);
}
fn validateProtocolHandlerScheme(scheme: []const u8) !void {
@@ -136,11 +136,11 @@ fn validateProtocolHandlerScheme(scheme: []const u8) !void {
}
}
fn validateProtocolHandlerURL(url: [:0]const u8, page: *const Page) !void {
fn validateProtocolHandlerURL(url: [:0]const u8, frame: *const Frame) !void {
if (std.mem.indexOf(u8, url, "%s") == null) {
return error.SyntaxError;
}
if (page.isSameOrigin(url) == false) {
if (frame.isSameOrigin(url) == false) {
return error.SyntaxError;
}
}

View File

@@ -20,7 +20,7 @@ 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 URL = @import("../URL.zig");
const reflect = @import("../reflect.zig");
@@ -49,7 +49,7 @@ _parent: ?*Node = null,
_children: ?*Children = null,
_child_link: LinkedList.Node = .{},
// Lookup for nodes that have a different owner document than page.document
// Lookup for nodes that have a different owner document than frame.document
pub const OwnerDocumentLookup = std.AutoHashMapUnmanaged(*Node, *Document);
pub const Type = union(enum) {
@@ -218,15 +218,15 @@ fn validateNodeInsertion(parent: *Node, node: *Node) !void {
}
}
pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node {
pub fn appendChild(self: *Node, child: *Node, frame: *Frame) !*Node {
if (child.is(DocumentFragment)) |_| {
try page.appendAllChildren(child, self);
try frame.appendAllChildren(child, self);
return child;
}
try validateNodeInsertion(self, child);
page.domChanged();
frame.domChanged();
// If the child is currently connected, and if its new parent is connected,
// then we can remove + add a bit more efficiently (we don't have to fully
@@ -234,30 +234,30 @@ pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node {
const child_connected = child.isConnected();
// Check if we're adopting the node to a different document
const child_owner = child.ownerDocument(page);
const parent_owner = self.ownerDocument(page) orelse self.as(Document);
const child_owner = child.ownerDocument(frame);
const parent_owner = self.ownerDocument(frame) orelse self.as(Document);
const adopting_to_new_document = child_owner != null and child_owner.? != parent_owner;
if (child._parent) |parent| {
// we can signal removeNode that the child will remain connected
// (when it's appended to self) so that it can be a bit more efficient.
page.removeNode(parent, child, .{ .will_be_reconnected = self.isConnected() });
frame.removeNode(parent, child, .{ .will_be_reconnected = self.isConnected() });
}
// Adopt the node tree if moving between documents
if (adopting_to_new_document) {
try page.adoptNodeTree(child, parent_owner);
try frame.adoptNodeTree(child, parent_owner);
}
try page.appendNode(self, child, .{
try frame.appendNode(self, child, .{
.child_already_connected = child_connected,
.adopting_to_new_document = adopting_to_new_document,
});
return child;
}
pub fn childNodes(self: *Node, page: *Page) !*collections.ChildNodes {
return collections.ChildNodes.init(self, page);
pub fn childNodes(self: *Node, frame: *Frame) !*collections.ChildNodes {
return collections.ChildNodes.init(self, frame);
}
pub fn getTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFailed}!void {
@@ -300,25 +300,25 @@ pub fn getChildTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFaile
}
}
pub fn setTextContent(self: *Node, data: []const u8, page: *Page) !void {
pub fn setTextContent(self: *Node, data: []const u8, frame: *Frame) !void {
switch (self._type) {
.element => |el| {
if (data.len == 0) {
return el.replaceChildren(&.{}, page);
return el.replaceChildren(&.{}, frame);
}
return el.replaceChildren(&.{.{ .text = data }}, page);
return el.replaceChildren(&.{.{ .text = data }}, frame);
},
// Per spec, setting textContent on CharacterData runs replaceData(0, length, value)
.cdata => |c| try c.replaceData(0, c.getLength(), data, page),
.cdata => |c| try c.replaceData(0, c.getLength(), data, frame),
.document => {},
.document_type => {},
.document_fragment => |frag| {
if (data.len == 0) {
return frag.replaceChildren(&.{}, page);
return frag.replaceChildren(&.{}, frame);
}
return frag.replaceChildren(&.{.{ .text = data }}, page);
return frag.replaceChildren(&.{.{ .text = data }}, frame);
},
.attribute => |attr| return attr.setValue(.wrap(data), page),
.attribute => |attr| return attr.setValue(.wrap(data), frame),
}
}
@@ -354,30 +354,30 @@ pub fn getNodeType(self: *const Node) u8 {
};
}
pub fn lookupNamespaceURI(self: *Node, prefix_arg: ?[]const u8, page: *Page) ?[]const u8 {
pub fn lookupNamespaceURI(self: *Node, prefix_arg: ?[]const u8, frame: *Frame) ?[]const u8 {
const prefix: ?[]const u8 = if (prefix_arg) |p| (if (p.len == 0) null else p) else null;
switch (self._type) {
.element => |el| return el.lookupNamespaceURIForElement(prefix, page),
.element => |el| return el.lookupNamespaceURIForElement(prefix, frame),
.document => |doc| {
const de = doc.getDocumentElement() orelse return null;
return de.lookupNamespaceURIForElement(prefix, page);
return de.lookupNamespaceURIForElement(prefix, frame);
},
.document_type, .document_fragment => return null,
.attribute => |attr| {
const owner = attr.getOwnerElement() orelse return null;
return owner.lookupNamespaceURIForElement(prefix, page);
return owner.lookupNamespaceURIForElement(prefix, frame);
},
.cdata => {
const parent = self.parentElement() orelse return null;
return parent.lookupNamespaceURIForElement(prefix, page);
return parent.lookupNamespaceURIForElement(prefix, frame);
},
}
}
pub fn isDefaultNamespace(self: *Node, namespace_arg: ?[]const u8, page: *Page) bool {
pub fn isDefaultNamespace(self: *Node, namespace_arg: ?[]const u8, frame: *Frame) bool {
const namespace: ?[]const u8 = if (namespace_arg) |ns| (if (ns.len == 0) null else ns) else null;
const default_ns = self.lookupNamespaceURI(null, page);
const default_ns = self.lookupNamespaceURI(null, frame);
if (default_ns == null and namespace == null) return true;
if (default_ns != null and namespace != null) return std.mem.eql(u8, default_ns.?, namespace.?);
return false;
@@ -481,7 +481,7 @@ pub fn contains(self: *const Node, child_: ?*const Node) bool {
return false;
}
pub fn ownerDocument(self: *const Node, page: *const Page) ?*Document {
pub fn ownerDocument(self: *const Node, frame: *const Frame) ?*Document {
// A document node does not have an owner.
if (self._type == .document) {
return null;
@@ -500,17 +500,17 @@ pub fn ownerDocument(self: *const Node, page: *const Page) ?*Document {
// Otherwise, this is a detached node. Check if it has a specific owner
// document registered (for nodes created via non-main documents).
if (page._node_owner_documents.get(@constCast(self))) |owner| {
if (frame._node_owner_documents.get(@constCast(self))) |owner| {
return owner;
}
// Default to the main document for detached nodes without a specific owner.
return page.document;
return frame.document;
}
pub fn ownerPage(self: *const Node, default: *Page) *Page {
pub fn ownerFrame(self: *const Node, default: *Frame) *Frame {
const doc = self.ownerDocument(default) orelse return default;
return doc._page orelse default;
return doc._frame orelse default;
}
pub const ResolveURLOpts = struct {
@@ -519,16 +519,16 @@ pub const ResolveURLOpts = struct {
// Resolve a URL relative to this node's owning document.
// Uses the document's charset for query string encoding (with NCR fallback for unmappable chars).
pub fn resolveURL(self: *const Node, url: anytype, page: *Page, opts: ResolveURLOpts) ![:0]const u8 {
const owner_page = self.ownerPage(page);
const allocator = opts.allocator orelse page.call_arena;
return URL.resolve(allocator, owner_page.base(), url, .{ .encoding = owner_page.charset });
pub fn resolveURL(self: *const Node, url: anytype, frame: *Frame, opts: ResolveURLOpts) ![:0]const u8 {
const owner_frame = self.ownerFrame(frame);
const allocator = opts.allocator orelse frame.call_arena;
return URL.resolve(allocator, owner_frame.base(), url, .{ .encoding = owner_frame.charset });
}
pub fn isSameDocumentAs(self: *const Node, other: *const Node, page: *const Page) bool {
pub fn isSameDocumentAs(self: *const Node, other: *const Node, frame: *const Frame) bool {
// Get the root document for each node
const self_doc = if (self._type == .document) self._type.document else self.ownerDocument(page);
const other_doc = if (other._type == .document) other._type.document else other.ownerDocument(page);
const self_doc = if (self._type == .document) self._type.document else self.ownerDocument(frame);
const other_doc = if (other._type == .document) other._type.document else other.ownerDocument(frame);
return self_doc == other_doc;
}
@@ -540,33 +540,33 @@ pub fn isSameNode(self: *const Node, other: ?*Node) bool {
return self == other;
}
pub fn removeChild(self: *Node, child: *Node, page: *Page) !*Node {
pub fn removeChild(self: *Node, child: *Node, frame: *Frame) !*Node {
var it = self.childrenIterator();
while (it.next()) |n| {
if (n == child) {
page.domChanged();
page.removeNode(self, child, .{ .will_be_reconnected = false });
frame.domChanged();
frame.removeNode(self, child, .{ .will_be_reconnected = false });
return child;
}
}
return error.NotFound;
}
pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, page: *Page) !*Node {
pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, frame: *Frame) !*Node {
const ref_node = ref_node_ orelse {
return self.appendChild(new_node, page);
return self.appendChild(new_node, frame);
};
// special case: if nodes are the same, ignore the change.
if (new_node == ref_node_) {
page.domChanged();
frame.domChanged();
if (page.hasMutationObservers()) {
if (frame.hasMutationObservers()) {
const parent = new_node._parent.?;
const previous_sibling = new_node.previousSibling();
const next_sibling = new_node.nextSibling();
const replaced = [_]*Node{new_node};
page.childListChange(parent, &replaced, &replaced, previous_sibling, next_sibling);
frame.childListChange(parent, &replaced, &replaced, previous_sibling, next_sibling);
}
return new_node;
@@ -577,7 +577,7 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, page: *Page
}
if (new_node.is(DocumentFragment)) |_| {
try page.insertAllChildrenBefore(new_node, self, ref_node);
try frame.insertAllChildrenBefore(new_node, self, ref_node);
return new_node;
}
@@ -586,22 +586,22 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, page: *Page
const child_already_connected = new_node.isConnected();
// Check if we're adopting the node to a different document
const child_owner = new_node.ownerDocument(page);
const parent_owner = self.ownerDocument(page) orelse self.as(Document);
const child_owner = new_node.ownerDocument(frame);
const parent_owner = self.ownerDocument(frame) orelse self.as(Document);
const adopting_to_new_document = child_owner != null and child_owner.? != parent_owner;
page.domChanged();
frame.domChanged();
const will_be_reconnected = self.isConnected();
if (new_node._parent) |parent| {
page.removeNode(parent, new_node, .{ .will_be_reconnected = will_be_reconnected });
frame.removeNode(parent, new_node, .{ .will_be_reconnected = will_be_reconnected });
}
// Adopt the node tree if moving between documents
if (adopting_to_new_document) {
try page.adoptNodeTree(new_node, parent_owner);
try frame.adoptNodeTree(new_node, parent_owner);
}
try page.insertNodeRelative(
try frame.insertNodeRelative(
self,
new_node,
.{ .before = ref_node },
@@ -614,21 +614,21 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, page: *Page
return new_node;
}
pub fn replaceChild(self: *Node, new_child: *Node, old_child: *Node, page: *Page) !*Node {
pub fn replaceChild(self: *Node, new_child: *Node, old_child: *Node, frame: *Frame) !*Node {
if (old_child._parent == null or old_child._parent.? != self) {
return error.HierarchyError;
}
try validateNodeInsertion(self, new_child);
_ = try self.insertBefore(new_child, old_child, page);
_ = try self.insertBefore(new_child, old_child, frame);
// Special case: if we replace a node by itself, we don't remove it.
// insertBefore is an noop in this case.
// Re-check parent after insertBefore since callbacks (e.g. connectedCallback)
// could have already removed old_child from self.
if (new_child != old_child and old_child._parent == self) {
page.removeNode(self, old_child, .{ .will_be_reconnected = false });
frame.removeNode(self, old_child, .{ .will_be_reconnected = false });
}
return old_child;
@@ -645,14 +645,14 @@ pub fn getNodeValue(self: *const Node) ?String {
};
}
pub fn setNodeValue(self: *const Node, value: ?String, page: *Page) !void {
pub fn setNodeValue(self: *const Node, value: ?String, frame: *Frame) !void {
switch (self._type) {
// Per spec, setting nodeValue on CharacterData runs replaceData(0, length, value)
.cdata => |c| {
const new_value: []const u8 = if (value) |v| v.str() else "";
try c.replaceData(0, c.getLength(), new_value, page);
try c.replaceData(0, c.getLength(), new_value, frame);
},
.attribute => |attr| try attr.setValue(value, page),
.attribute => |attr| try attr.setValue(value, frame),
.element => {},
.document => {},
.document_type => {},
@@ -740,16 +740,16 @@ pub fn getData(self: *const Node) String {
};
}
pub fn setData(self: *Node, data: []const u8, page: *Page) !void {
pub fn setData(self: *Node, data: []const u8, frame: *Frame) !void {
switch (self._type) {
.cdata => |c| try c.setData(data, page),
.cdata => |c| try c.setData(data, frame),
else => {},
}
}
pub fn normalize(self: *Node, page: *Page) !void {
pub fn normalize(self: *Node, frame: *Frame) !void {
var buffer: std.ArrayList(u8) = .empty;
return self._normalize(page.call_arena, &buffer, page);
return self._normalize(frame.call_arena, &buffer, frame);
}
const CloneError = error{
@@ -767,27 +767,27 @@ const CloneError = error{
CompilationError,
JsException,
};
pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) CloneError!*Node {
pub fn cloneNode(self: *Node, deep_: ?bool, frame: *Frame) CloneError!*Node {
const deep = deep_ orelse false;
switch (self._type) {
.cdata => |cd| {
const data = cd.getData().str();
return switch (cd._type) {
.text => page.createTextNode(data),
.cdata_section => page.createCDATASection(data),
.comment => page.createComment(data),
.processing_instruction => |pi| page.createProcessingInstruction(pi._target, data),
.text => frame.createTextNode(data),
.cdata_section => frame.createCDATASection(data),
.comment => frame.createComment(data),
.processing_instruction => |pi| frame.createProcessingInstruction(pi._target, data),
};
},
.element => |el| return el.clone(deep, page),
.element => |el| return el.clone(deep, frame),
.document => return error.NotSupported,
.document_type => |dt| {
const cloned = dt.clone(page) catch return error.CloneError;
const cloned = dt.clone(frame) catch return error.CloneError;
return cloned.asNode();
},
.document_fragment => |frag| return frag.cloneFragment(deep, page),
.document_fragment => |frag| return frag.cloneFragment(deep, frame),
.attribute => |attr| {
const cloned = attr.clone(page) catch return error.CloneError;
const cloned = attr.clone(frame) catch return error.CloneError;
return cloned._proto;
},
}
@@ -799,8 +799,8 @@ pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) CloneError!*Node {
///
/// This helper is used when iterating over children to clone them. The typical pattern is:
/// while (child_it.next()) |child| {
/// if (try child.cloneNodeForAppending(true, page)) |cloned| {
/// try page.appendNode(parent, cloned, opts);
/// if (try child.cloneNodeForAppending(true, frame)) |cloned| {
/// try frame.appendNode(parent, cloned, opts);
/// }
/// }
///
@@ -808,8 +808,8 @@ pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) CloneError!*Node {
/// constructor (which runs during cloning per the HTML spec) explicitly attaches the element
/// somewhere. In that case, we respect the constructor's decision and return null to signal
/// that the cloned node should not be appended to our intended parent.
pub fn cloneNodeForAppending(self: *Node, deep: bool, page: *Page) CloneError!?*Node {
const cloned = try self.cloneNode(deep, page);
pub fn cloneNodeForAppending(self: *Node, deep: bool, frame: *Frame) CloneError!?*Node {
const cloned = try self.cloneNode(deep, frame);
if (cloned._parent != null) {
return null;
}
@@ -916,10 +916,10 @@ fn isNodeBefore(node1: *const Node, node2: *const Node) bool {
return false;
}
fn _normalize(self: *Node, allocator: Allocator, buffer: *std.ArrayList(u8), page: *Page) !void {
fn _normalize(self: *Node, allocator: Allocator, buffer: *std.ArrayList(u8), frame: *Frame) !void {
var it = self.childrenIterator();
while (it.next()) |child| {
try child._normalize(allocator, buffer, page);
try child._normalize(allocator, buffer, frame);
}
var child = self.firstChild();
@@ -932,7 +932,7 @@ fn _normalize(self: *Node, allocator: Allocator, buffer: *std.ArrayList(u8), pag
};
if (text_node._proto.getData().len == 0) {
page.removeNode(self, current_node, .{ .will_be_reconnected = false });
frame.removeNode(self, current_node, .{ .will_be_reconnected = false });
child = next_node;
continue;
}
@@ -947,9 +947,9 @@ fn _normalize(self: *Node, allocator: Allocator, buffer: *std.ArrayList(u8), pag
const to_remove = node_to_merge;
next_node = node_to_merge.nextSibling();
page.removeNode(self, to_remove, .{ .will_be_reconnected = false });
frame.removeNode(self, to_remove, .{ .will_be_reconnected = false });
}
text_node._proto._data = try page.dupeSSO(buffer.items);
text_node._proto._data = try frame.dupeSSO(buffer.items);
buffer.clearRetainingCapacity();
}
}
@@ -964,7 +964,7 @@ pub const GetElementsByTagNameResult = union(enum) {
all_elements: collections.NodeLive(.all_elements),
};
// Not exposed in the WebAPI, but used by both Element and Document
pub fn getElementsByTagName(self: *Node, tag_name: []const u8, page: *Page) !GetElementsByTagNameResult {
pub fn getElementsByTagName(self: *Node, tag_name: []const u8, frame: *Frame) !GetElementsByTagNameResult {
if (tag_name.len > 256) {
// 256 seems generous.
return error.InvalidTagName;
@@ -972,25 +972,25 @@ pub fn getElementsByTagName(self: *Node, tag_name: []const u8, page: *Page) !Get
if (std.mem.eql(u8, tag_name, "*")) {
return .{
.all_elements = collections.NodeLive(.all_elements).init(self, {}, page),
.all_elements = collections.NodeLive(.all_elements).init(self, {}, frame),
};
}
const lower = std.ascii.lowerString(&page.buf, tag_name);
const lower = std.ascii.lowerString(&frame.buf, tag_name);
if (Node.Element.Tag.parseForMatch(lower)) |known| {
// optimized for known tag names, comparis
return .{
.tag = collections.NodeLive(.tag).init(self, known, page),
.tag = collections.NodeLive(.tag).init(self, known, frame),
};
}
const arena = page.arena;
const arena = frame.arena;
const filter = try String.init(arena, lower, .{});
return .{ .tag_name = collections.NodeLive(.tag_name).init(self, filter, page) };
return .{ .tag_name = collections.NodeLive(.tag_name).init(self, filter, frame) };
}
// Not exposed in the WebAPI, but used by both Element and Document
pub fn getElementsByTagNameNS(self: *Node, namespace: ?[]const u8, local_name: []const u8, page: *Page) !collections.NodeLive(.tag_name_ns) {
pub fn getElementsByTagNameNS(self: *Node, namespace: ?[]const u8, local_name: []const u8, frame: *Frame) !collections.NodeLive(.tag_name_ns) {
if (local_name.len > 256) {
return error.InvalidTagName;
}
@@ -1003,53 +1003,53 @@ pub fn getElementsByTagNameNS(self: *Node, namespace: ?[]const u8, local_name: [
return collections.NodeLive(.tag_name_ns).init(self, .{
.namespace = ns,
.local_name = try String.init(page.arena, local_name, .{}),
}, page);
.local_name = try String.init(frame.arena, local_name, .{}),
}, frame);
}
// Not exposed in the WebAPI, but used by both Element and Document
pub fn getElementsByClassName(self: *Node, class_name: []const u8, page: *Page) !collections.NodeLive(.class_name) {
const arena = page.arena;
pub fn getElementsByClassName(self: *Node, class_name: []const u8, frame: *Frame) !collections.NodeLive(.class_name) {
const arena = frame.arena;
// Parse space-separated class names
var class_names: std.ArrayList([]const u8) = .empty;
var it = std.mem.tokenizeAny(u8, class_name, "\t\n\x0C\r ");
while (it.next()) |name| {
try class_names.append(arena, try page.dupeString(name));
try class_names.append(arena, try frame.dupeString(name));
}
return collections.NodeLive(.class_name).init(self, class_names.items, page);
return collections.NodeLive(.class_name).init(self, class_names.items, frame);
}
/// Shared implementation of replaceChildren for Element, Document, and DocumentFragment.
/// Validates all nodes, removes existing children, then appends new children.
pub fn replaceChildren(self: *Node, nodes: []const NodeOrText, page: *Page) !void {
pub fn replaceChildren(self: *Node, nodes: []const NodeOrText, frame: *Frame) !void {
// First pass: validate all nodes and collect them
// We need to collect because DocumentFragments contribute their children, not themselves
var children_to_add: std.ArrayList(*Node) = .empty;
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
const child = try node_or_text.toNode(frame);
// DocumentFragments contribute their children, not themselves
if (child.is(DocumentFragment)) |frag| {
var frag_it = frag.asNode().childrenIterator();
while (frag_it.next()) |frag_child| {
try validateNodeInsertion(self, frag_child);
try children_to_add.append(page.call_arena, frag_child);
try children_to_add.append(frame.call_arena, frag_child);
}
} else {
try validateNodeInsertion(self, child);
try children_to_add.append(page.call_arena, child);
try children_to_add.append(frame.call_arena, child);
}
}
page.domChanged();
frame.domChanged();
// Remove all existing children
var it = self.childrenIterator();
while (it.next()) |child| {
page.removeNode(self, child, .{ .will_be_reconnected = false });
frame.removeNode(self, child, .{ .will_be_reconnected = false });
}
// Append new children
@@ -1058,9 +1058,9 @@ pub fn replaceChildren(self: *Node, nodes: []const NodeOrText, page: *Page) !voi
var child_connected = false;
if (child._parent) |previous_parent| {
child_connected = child.isConnected();
page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
frame.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
}
try page.appendNode(self, child, .{ .child_already_connected = child_connected });
try frame.appendNode(self, child, .{ .child_already_connected = child_connected });
}
}
@@ -1121,18 +1121,18 @@ pub const JsApi = struct {
pub const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = bridge.property(0x20, .{ .template = true });
pub const nodeName = bridge.accessor(struct {
fn wrap(self: *const Node, page: *Page) []const u8 {
return self.getNodeName(&page.buf);
fn wrap(self: *const Node, frame: *Frame) []const u8 {
return self.getNodeName(&frame.buf);
}
}.wrap, null, .{});
pub const nodeType = bridge.accessor(Node.getNodeType, null, .{});
pub const textContent = bridge.accessor(_textContext, Node.setTextContent, .{});
fn _textContext(self: *Node, page: *const Page) !?[]const u8 {
fn _textContext(self: *Node, frame: *const Frame) !?[]const u8 {
// cdata and attributes can return value directly, avoiding the copy
switch (self._type) {
.element, .document_fragment => {
var buf = std.Io.Writer.Allocating.init(page.call_arena);
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
try self.getTextContent(&buf.writer);
return buf.written();
},
@@ -1168,8 +1168,8 @@ pub const JsApi = struct {
pub const lookupNamespaceURI = bridge.function(Node.lookupNamespaceURI, .{});
pub const isDefaultNamespace = bridge.function(Node.isDefaultNamespace, .{});
fn _baseURI(_: *Node, page: *const Page) []const u8 {
return page.base();
fn _baseURI(_: *Node, frame: *const Frame) []const u8 {
return frame.base();
}
pub const baseURI = bridge.accessor(_baseURI, null, .{});
};
@@ -1219,10 +1219,10 @@ pub const NodeOrText = union(enum) {
}
}
pub fn toNode(self: *const NodeOrText, page: *Page) !*Node {
pub fn toNode(self: *const NodeOrText, frame: *Frame) !*Node {
return switch (self.*) {
.node => |n| n,
.text => |txt| page.createTextNode(txt),
.text => |txt| frame.createTextNode(txt),
};
}

View File

@@ -1,5 +1,5 @@
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const datetime = @import("../../datetime.zig");
const EventCounts = @import("EventCounts.zig");
@@ -65,12 +65,12 @@ pub fn mark(
self: *Performance,
name: []const u8,
_options: ?Mark.Options,
page: *Page,
frame: *Frame,
) !*Mark {
const m = try Mark.init(name, _options, page);
try self._entries.append(page.arena, m._proto);
const m = try Mark.init(name, _options, frame);
try self._entries.append(frame.arena, m._proto);
// Notify about the change.
try page.notifyPerformanceObservers(m._proto);
try frame.notifyPerformanceObservers(m._proto);
return m;
}
@@ -84,7 +84,7 @@ pub fn measure(
name: []const u8,
maybe_options_or_start: ?MeasureOptionsOrStartMark,
maybe_end_mark: ?[]const u8,
page: *Page,
frame: *Frame,
) !*Measure {
if (maybe_options_or_start) |options_or_start| switch (options_or_start) {
.measure_options => |options| {
@@ -118,11 +118,11 @@ pub fn measure(
start_timestamp,
end_timestamp,
options.duration,
page,
frame,
);
try self._entries.append(page.arena, m._proto);
try self._entries.append(frame.arena, m._proto);
// Notify about the change.
try page.notifyPerformanceObservers(m._proto);
try frame.notifyPerformanceObservers(m._proto);
return m;
},
.start_mark => |start_mark| {
@@ -143,19 +143,19 @@ pub fn measure(
start_timestamp,
end_timestamp,
null,
page,
frame,
);
try self._entries.append(page.arena, m._proto);
try self._entries.append(frame.arena, m._proto);
// Notify about the change.
try page.notifyPerformanceObservers(m._proto);
try frame.notifyPerformanceObservers(m._proto);
return m;
},
};
const m = try Measure.init(name, null, 0.0, self.now(), null, page);
try self._entries.append(page.arena, m._proto);
const m = try Measure.init(name, null, 0.0, self.now(), null, frame);
try self._entries.append(frame.arena, m._proto);
// Notify about the change.
try page.notifyPerformanceObservers(m._proto);
try frame.notifyPerformanceObservers(m._proto);
return m;
}
@@ -187,19 +187,19 @@ pub fn getEntries(self: *const Performance) []*Entry {
return self._entries.items;
}
pub fn getEntriesByType(self: *const Performance, entry_type: []const u8, page: *Page) ![]const *Entry {
pub fn getEntriesByType(self: *const Performance, entry_type: []const u8, frame: *Frame) ![]const *Entry {
var result: std.ArrayList(*Entry) = .empty;
for (self._entries.items) |entry| {
if (std.mem.eql(u8, entry.getEntryType(), entry_type)) {
try result.append(page.call_arena, entry);
try result.append(frame.call_arena, entry);
}
}
return result.items;
}
pub fn getEntriesByName(self: *const Performance, name: []const u8, entry_type: ?[]const u8, page: *Page) ![]const *Entry {
pub fn getEntriesByName(self: *const Performance, name: []const u8, entry_type: ?[]const u8, frame: *Frame) ![]const *Entry {
var result: std.ArrayList(*Entry) = .empty;
for (self._entries.items) |entry| {
@@ -208,12 +208,12 @@ pub fn getEntriesByName(self: *const Performance, name: []const u8, entry_type:
}
const et = entry_type orelse {
try result.append(page.call_arena, entry);
try result.append(frame.call_arena, entry);
continue;
};
if (std.mem.eql(u8, entry.getEntryType(), et)) {
try result.append(page.call_arena, entry);
try result.append(frame.call_arena, entry);
}
}
@@ -371,23 +371,23 @@ pub const Mark = struct {
startTime: ?f64 = null,
};
pub fn init(name: []const u8, _opts: ?Options, page: *Page) !*Mark {
pub fn init(name: []const u8, _opts: ?Options, frame: *Frame) !*Mark {
const opts = _opts orelse Options{};
const start_time = opts.startTime orelse page.window._performance.now();
const start_time = opts.startTime orelse frame.window._performance.now();
if (start_time < 0.0) {
return error.TypeError;
}
const detail = if (opts.detail) |d| try d.persist() else null;
const m = try page._factory.create(Mark{
const m = try frame._factory.create(Mark{
._proto = undefined,
._detail = detail,
});
const entry = try page._factory.create(Entry{
const entry = try frame._factory.create(Entry{
._start_time = start_time,
._name = try page.dupeString(name),
._name = try frame.dupeString(name),
._type = .{ .mark = m },
});
m._proto = entry;
@@ -432,7 +432,7 @@ pub const Measure = struct {
start_timestamp: f64,
end_timestamp: f64,
maybe_duration: ?f64,
page: *Page,
frame: *Frame,
) !*Measure {
const duration = maybe_duration orelse (end_timestamp - start_timestamp);
if (duration < 0.0) {
@@ -440,15 +440,15 @@ pub const Measure = struct {
}
const detail = if (maybe_detail) |d| try d.persist() else null;
const m = try page._factory.create(Measure{
const m = try frame._factory.create(Measure{
._proto = undefined,
._detail = detail,
});
const entry = try page._factory.create(Entry{
const entry = try frame._factory.create(Entry{
._start_time = start_timestamp,
._duration = duration,
._name = try page.dupeString(name),
._name = try frame.dupeString(name),
._type = .{ .measure = m },
});
m._proto = entry;

View File

@@ -20,7 +20,7 @@ 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 Performance = @import("Performance.zig");
const log = lp.log;
@@ -45,8 +45,8 @@ _entries: std.ArrayList(*Performance.Entry),
const DefaultDurationThreshold: f64 = 104;
/// Creates a new PerformanceObserver object with the given observer callback.
pub fn init(callback: js.Function.Global, page: *Page) !*PerformanceObserver {
return page._factory.create(PerformanceObserver{
pub fn init(callback: js.Function.Global, frame: *Frame) !*PerformanceObserver {
return frame._factory.create(PerformanceObserver{
._callback = callback,
._duration_threshold = DefaultDurationThreshold,
._interests = 0,
@@ -65,7 +65,7 @@ const ObserveOptions = struct {
pub fn observe(
self: *PerformanceObserver,
maybe_options: ?ObserveOptions,
page: *Page,
frame: *Frame,
) !void {
const options: ObserveOptions = maybe_options orelse .{};
// Update threshold.
@@ -109,7 +109,7 @@ pub fn observe(
// If we had no interests before, it means Page is not aware of
// this observer.
if (self._interests == 0) {
try page.registerPerformanceObserver(self);
try frame.registerPerformanceObserver(self);
}
// Update interests.
@@ -119,19 +119,19 @@ pub fn observe(
// Per spec, buffered is only valid with the type option, not entryTypes.
// Delivery is async via a queued task, not synchronous.
if (options.buffered and options.type != null and !self.hasRecords()) {
for (page.window._performance._entries.items) |entry| {
for (frame.window._performance._entries.items) |entry| {
if (self.interested(entry)) {
try self._entries.append(page.arena, entry);
try self._entries.append(frame.arena, entry);
}
}
if (self.hasRecords()) {
try page.schedulePerformanceObserverDelivery();
try frame.schedulePerformanceObserverDelivery();
}
}
}
pub fn disconnect(self: *PerformanceObserver, page: *Page) void {
page.unregisterPerformanceObserver(self);
pub fn disconnect(self: *PerformanceObserver, frame: *Frame) void {
frame.unregisterPerformanceObserver(self);
// Reset observer.
self._duration_threshold = DefaultDurationThreshold;
self._interests = 0;
@@ -140,10 +140,10 @@ pub fn disconnect(self: *PerformanceObserver, page: *Page) void {
/// Returns the current list of PerformanceEntry objects
/// stored in the performance observer, emptying it out.
pub fn takeRecords(self: *PerformanceObserver, page: *Page) ![]*Performance.Entry {
// Use page.arena instead of call_arena because this slice is wrapped in EntryList
pub fn takeRecords(self: *PerformanceObserver, frame: *Frame) ![]*Performance.Entry {
// Use frame.arena instead of call_arena because this slice is wrapped in EntryList
// and may be accessed later.
const records = try page.arena.dupe(*Performance.Entry, self._entries.items);
const records = try frame.arena.dupe(*Performance.Entry, self._entries.items);
self._entries.clearRetainingCapacity();
return records;
}
@@ -166,16 +166,16 @@ pub inline fn hasRecords(self: *const PerformanceObserver) bool {
}
/// Runs the PerformanceObserver's callback with records; emptying it out.
pub fn dispatch(self: *PerformanceObserver, page: *Page) !void {
const records = try self.takeRecords(page);
pub fn dispatch(self: *PerformanceObserver, frame: *Frame) !void {
const records = try self.takeRecords(frame);
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
frame.js.localScope(&ls);
defer ls.deinit();
var caught: js.TryCatch.Caught = undefined;
ls.toLocal(self._callback).tryCall(void, .{ EntryList{ ._entries = records }, self }, &caught) catch |err| {
log.err(.page, "PerfObserver.dispatch", .{ .err = err, .caught = caught });
log.err(.frame, "PerfObserver.dispatch", .{ .err = err, .caught = caught });
return err;
};
}

View File

@@ -19,7 +19,7 @@
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;
@@ -37,9 +37,9 @@ const QueryDescriptor = struct {
name: []const u8,
};
// We always report 'prompt' (the default safe value — neither granted nor denied).
pub fn query(_: *const Permissions, qd: QueryDescriptor, page: *Page) !js.Promise {
const arena = try page.getArena(.tiny, "PermissionStatus");
errdefer page.releaseArena(arena);
pub fn query(_: *const Permissions, qd: QueryDescriptor, frame: *Frame) !js.Promise {
const arena = try frame.getArena(.tiny, "PermissionStatus");
errdefer frame.releaseArena(arena);
const status = try arena.create(PermissionStatus);
status.* = .{
@@ -47,7 +47,7 @@ pub fn query(_: *const Permissions, qd: QueryDescriptor, page: *Page) !js.Promis
._state = "prompt",
._name = try arena.dupe(u8, qd.name),
};
return page.js.local.?.resolvePromise(status);
return frame.js.local.?.resolvePromise(status);
}
const PermissionStatus = struct {

View File

@@ -20,7 +20,7 @@ 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");
@@ -34,10 +34,10 @@ const Range = @This();
_proto: *AbstractRange,
pub fn init(page: *Page) !*Range {
const arena = try page.getArena(.medium, "Range");
errdefer page.releaseArena(arena);
return page._factory.abstractRange(arena, Range{ ._proto = undefined }, page);
pub fn init(frame: *Frame) !*Range {
const arena = try frame.getArena(.medium, "Range");
errdefer frame.releaseArena(arena);
return frame._factory.abstractRange(arena, Range{ ._proto = undefined }, frame);
}
pub fn asAbstractRange(self: *Range) *AbstractRange {
@@ -313,11 +313,11 @@ pub fn intersectsNode(self: *const Range, node: *Node) bool {
return false;
}
pub fn cloneRange(self: *const Range, page: *Page) !*Range {
const arena = try page.getArena(.medium, "Range.clone");
errdefer page.releaseArena(arena);
pub fn cloneRange(self: *const Range, frame: *Frame) !*Range {
const arena = try frame.getArena(.medium, "Range.clone");
errdefer frame.releaseArena(arena);
const clone = try page._factory.abstractRange(arena, Range{ ._proto = undefined }, page);
const clone = try frame._factory.abstractRange(arena, Range{ ._proto = undefined }, frame);
clone._proto._end_offset = self._proto._end_offset;
clone._proto._start_offset = self._proto._start_offset;
clone._proto._end_container = self._proto._end_container;
@@ -325,7 +325,7 @@ pub fn cloneRange(self: *const Range, page: *Page) !*Range {
return clone;
}
pub fn insertNode(self: *Range, node: *Node, page: *Page) !void {
pub fn insertNode(self: *Range, node: *Node, frame: *Frame) !void {
// Insert node at the start of the range
const container = self._proto._start_container;
const offset = self._proto._start_offset;
@@ -340,28 +340,28 @@ pub fn insertNode(self: *Range, node: *Node, page: *Page) !void {
const parent = container.parentNode() orelse return error.InvalidNodeType;
if (offset == 0) {
_ = try parent.insertBefore(node, container, page);
_ = try parent.insertBefore(node, container, frame);
} else {
const text_data = container.getData().str();
if (offset >= text_data.len) {
_ = try parent.insertBefore(node, container.nextSibling(), page);
_ = try parent.insertBefore(node, container.nextSibling(), frame);
} else {
// Split the text node into before and after parts
const before_text = text_data[0..offset];
const after_text = text_data[offset..];
const before = try page.createTextNode(before_text);
const after = try page.createTextNode(after_text);
const before = try frame.createTextNode(before_text);
const after = try frame.createTextNode(after_text);
_ = try parent.replaceChild(before, container, page);
_ = try parent.insertBefore(node, before.nextSibling(), page);
_ = try parent.insertBefore(after, node.nextSibling(), page);
_ = try parent.replaceChild(before, container, frame);
_ = try parent.insertBefore(node, before.nextSibling(), frame);
_ = try parent.insertBefore(after, node.nextSibling(), frame);
}
}
} else {
// Container is an element, insert at offset
const ref_child = container.getChildAt(offset);
_ = try container.insertBefore(node, ref_child, page);
_ = try container.insertBefore(node, ref_child, frame);
}
// Per spec step 11: if range was collapsed, extend end to include inserted node.
@@ -371,11 +371,11 @@ pub fn insertNode(self: *Range, node: *Node, page: *Page) !void {
}
}
pub fn deleteContents(self: *Range, page: *Page) !void {
pub fn deleteContents(self: *Range, frame: *Frame) !void {
if (self._proto.getCollapsed()) {
return;
}
page.domChanged();
frame.domChanged();
// Simple case: same container
if (self._proto._start_container == self._proto._end_container) {
@@ -384,10 +384,10 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
const old_value = cdata.getData();
const text_data = old_value.str();
cdata._data = try String.concat(
page.arena,
frame.arena,
&.{ text_data[0..self._proto._start_offset], text_data[self._proto._end_offset..] },
);
page.characterDataChange(self._proto._start_container, old_value);
frame.characterDataChange(self._proto._start_container, old_value);
} else {
// Delete child nodes in range.
// Capture count before the loop: removeChild triggers live range
@@ -396,7 +396,7 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
var i: u32 = 0;
while (i < count) : (i += 1) {
if (self._proto._start_container.getChildAt(self._proto._start_offset)) |child| {
_ = try self._proto._start_container.removeChild(child, page);
_ = try self._proto._start_container.removeChild(child, frame);
}
}
}
@@ -411,7 +411,7 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
if (self._proto._start_offset < text_data.len) {
// Keep only the part before start_offset
const new_text = text_data[0..self._proto._start_offset];
try self._proto._start_container.setData(new_text, page);
try self._proto._start_container.setData(new_text, frame);
}
}
@@ -421,10 +421,10 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
if (self._proto._end_offset < text_data.len) {
// Keep only the part from end_offset onwards
const new_text = text_data[self._proto._end_offset..];
try self._proto._end_container.setData(new_text, page);
try self._proto._end_container.setData(new_text, frame);
} else if (self._proto._end_offset == text_data.len) {
// If we're at the end, set to empty (will be removed if needed)
try self._proto._end_container.setData("", page);
try self._proto._end_container.setData("", frame);
}
}
@@ -435,7 +435,7 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
while (current != null and current != self._proto._end_container) {
const next = current.?.nextSibling();
if (current.?.parentNode()) |parent| {
_ = try parent.removeChild(current.?, page);
_ = try parent.removeChild(current.?, frame);
}
current = next;
}
@@ -444,8 +444,8 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
self.collapse(true);
}
pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
const fragment = try DocumentFragment.init(page);
pub fn cloneContents(self: *const Range, frame: *Frame) !*DocumentFragment {
const fragment = try DocumentFragment.init(frame);
if (self._proto.getCollapsed()) return fragment;
@@ -456,16 +456,16 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
const text_data = self._proto._start_container.getData().str();
if (self._proto._start_offset < text_data.len and self._proto._end_offset <= text_data.len) {
const cloned_text = text_data[self._proto._start_offset..self._proto._end_offset];
const text_node = try page.createTextNode(cloned_text);
_ = try fragment.asNode().appendChild(text_node, page);
const text_node = try frame.createTextNode(cloned_text);
_ = try fragment.asNode().appendChild(text_node, frame);
}
} else {
// Clone child nodes in range
var offset = self._proto._start_offset;
while (offset < self._proto._end_offset) : (offset += 1) {
if (self._proto._start_container.getChildAt(offset)) |child| {
if (try child.cloneNodeForAppending(true, page)) |cloned| {
_ = try fragment.asNode().appendChild(cloned, page);
if (try child.cloneNodeForAppending(true, frame)) |cloned| {
_ = try fragment.asNode().appendChild(cloned, frame);
}
}
}
@@ -478,8 +478,8 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
if (self._proto._start_offset < text_data.len) {
// Clone from start_offset to end of text
const cloned_text = text_data[self._proto._start_offset..];
const text_node = try page.createTextNode(cloned_text);
_ = try fragment.asNode().appendChild(text_node, page);
const text_node = try frame.createTextNode(cloned_text);
_ = try fragment.asNode().appendChild(text_node, frame);
}
}
@@ -488,8 +488,8 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
var current = self._proto._start_container.nextSibling();
while (current != null and current != self._proto._end_container) {
const next = current.?.nextSibling();
if (try current.?.cloneNodeForAppending(true, page)) |cloned| {
_ = try fragment.asNode().appendChild(cloned, page);
if (try current.?.cloneNodeForAppending(true, frame)) |cloned| {
_ = try fragment.asNode().appendChild(cloned, frame);
}
current = next;
}
@@ -501,8 +501,8 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
if (self._proto._end_offset > 0 and self._proto._end_offset <= text_data.len) {
// Clone from start to end_offset
const cloned_text = text_data[0..self._proto._end_offset];
const text_node = try page.createTextNode(cloned_text);
_ = try fragment.asNode().appendChild(text_node, page);
const text_node = try frame.createTextNode(cloned_text);
_ = try fragment.asNode().appendChild(text_node, frame);
}
}
}
@@ -510,27 +510,27 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
return fragment;
}
pub fn extractContents(self: *Range, page: *Page) !*DocumentFragment {
const fragment = try self.cloneContents(page);
try self.deleteContents(page);
pub fn extractContents(self: *Range, frame: *Frame) !*DocumentFragment {
const fragment = try self.cloneContents(frame);
try self.deleteContents(frame);
return fragment;
}
pub fn surroundContents(self: *Range, new_parent: *Node, page: *Page) !void {
pub fn surroundContents(self: *Range, new_parent: *Node, frame: *Frame) !void {
// Extract contents
const contents = try self.extractContents(page);
const contents = try self.extractContents(frame);
// Insert the new parent
try self.insertNode(new_parent, page);
try self.insertNode(new_parent, frame);
// Move contents into new parent
_ = try new_parent.appendChild(contents.asNode(), page);
_ = try new_parent.appendChild(contents.asNode(), frame);
// Select the new parent's contents
try self.selectNodeContents(new_parent);
}
pub fn createContextualFragment(self: *const Range, html: []const u8, page: *Page) !*DocumentFragment {
pub fn createContextualFragment(self: *const Range, html: []const u8, frame: *Frame) !*DocumentFragment {
var context_node = self._proto._start_container;
// If start container is a text node, use its parent as context
@@ -538,7 +538,7 @@ pub fn createContextualFragment(self: *const Range, html: []const u8, page: *Pag
context_node = context_node.parentNode() orelse context_node;
}
const fragment = try DocumentFragment.init(page);
const fragment = try DocumentFragment.init(frame);
if (html.len == 0) {
return fragment;
@@ -547,26 +547,26 @@ pub fn createContextualFragment(self: *const Range, html: []const u8, page: *Pag
// Create a temporary element of the same type as the context for parsing
// This preserves the parsing context without modifying the original node
const temp_node = if (context_node.is(Node.Element)) |el|
try page.createElementNS(el._namespace, el.getTagNameLower(), null)
try frame.createElementNS(el._namespace, el.getTagNameLower(), null)
else
try page.createElementNS(.html, "div", null);
try frame.createElementNS(.html, "div", null);
try page.parseHtmlAsChildren(temp_node, html);
try frame.parseHtmlAsChildren(temp_node, html);
// Move all parsed children to the fragment
// Keep removing first child until temp element is empty
const fragment_node = fragment.asNode();
while (temp_node.firstChild()) |child| {
page.removeNode(temp_node, child, .{ .will_be_reconnected = true });
try page.appendNode(fragment_node, child, .{ .child_already_connected = false });
frame.removeNode(temp_node, child, .{ .will_be_reconnected = true });
try frame.appendNode(fragment_node, child, .{ .child_already_connected = false });
}
return fragment;
}
pub fn toString(self: *const Range, page: *Page) ![]const u8 {
pub fn toString(self: *const Range, frame: *Frame) ![]const u8 {
// Simplified implementation: just extract text content
var buf = std.Io.Writer.Allocating.init(page.call_arena);
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
try self.writeTextContent(&buf.writer);
return buf.written();
}
@@ -661,24 +661,24 @@ fn nextAfterSubtree(node: *Node, root: *Node) ?*Node {
return null;
}
pub fn getBoundingClientRect(self: *const Range, page: *Page) DOMRect {
pub fn getBoundingClientRect(self: *const Range, frame: *Frame) DOMRect {
if (self._proto.getCollapsed()) {
return .{ ._x = 0, ._y = 0, ._width = 0, ._height = 0 };
}
const element = self.getContainerElement() orelse {
return .{ ._x = 0, ._y = 0, ._width = 0, ._height = 0 };
};
return element.getBoundingClientRect(page);
return element.getBoundingClientRect(frame);
}
pub fn getClientRects(self: *const Range, page: *Page) ![]DOMRect {
pub fn getClientRects(self: *const Range, frame: *Frame) ![]DOMRect {
if (self._proto.getCollapsed()) {
return &.{};
}
const element = self.getContainerElement() orelse {
return &.{};
};
return element.getClientRects(page);
return element.getClientRects(frame);
}
fn getContainerElement(self: *const Range) ?*Node.Element {

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const EventTarget = @import("EventTarget.zig");
pub fn registerTypes() []const type {
@@ -36,11 +36,11 @@ pub fn asEventTarget(self: *Screen) *EventTarget {
return self._proto;
}
pub fn getOrientation(self: *Screen, page: *Page) !*Orientation {
pub fn getOrientation(self: *Screen, frame: *Frame) !*Orientation {
if (self._orientation) |orientation| {
return orientation;
}
const orientation = try Orientation.init(page);
const orientation = try Orientation.init(frame);
self._orientation = orientation;
return orientation;
}
@@ -66,8 +66,8 @@ pub const JsApi = struct {
pub const Orientation = struct {
_proto: *EventTarget,
pub fn init(page: *Page) !*Orientation {
return page._factory.eventTarget(Orientation{
pub fn init(frame: *Frame) !*Orientation {
return frame._factory.eventTarget(Orientation{
._proto = undefined,
});
}

View File

@@ -20,7 +20,7 @@ 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");
@@ -54,9 +54,9 @@ pub fn acquireRef(self: *Selection) void {
self._rc.acquire();
}
fn dispatchSelectionChangeEvent(page: *Page) !void {
const event = try Event.init("selectionchange", .{}, page);
try page._event_manager.dispatch(page.document.asEventTarget(), event);
fn dispatchSelectionChangeEvent(frame: *Frame) !void {
const event = try Event.init("selectionchange", .{}, frame);
try frame._event_manager.dispatch(frame.document.asEventTarget(), event);
}
fn isInTree(self: *const Selection) bool {
@@ -146,70 +146,70 @@ pub fn getType(self: *const Selection) []const u8 {
return "Range";
}
pub fn addRange(self: *Selection, range: *Range, page: *Page) !void {
pub fn addRange(self: *Selection, range: *Range, frame: *Frame) !void {
if (self._range != null) {
return;
}
// Only add the range if its root node is in the document associated with this selection
const start_node = range.asAbstractRange().getStartContainer();
if (!page.document.asNode().contains(start_node)) {
if (!frame.document.asNode().contains(start_node)) {
return;
}
self.setRange(range, page);
try dispatchSelectionChangeEvent(page);
self.setRange(range, frame);
try dispatchSelectionChangeEvent(frame);
}
pub fn removeRange(self: *Selection, range: *Range, page: *Page) !void {
pub fn removeRange(self: *Selection, range: *Range, frame: *Frame) !void {
const existing_range = self._range orelse return error.NotFound;
if (existing_range != range) {
return error.NotFound;
}
self.setRange(null, page);
try dispatchSelectionChangeEvent(page);
self.setRange(null, frame);
try dispatchSelectionChangeEvent(frame);
}
pub fn removeAllRanges(self: *Selection, page: *Page) !void {
pub fn removeAllRanges(self: *Selection, frame: *Frame) !void {
if (self._range == null) {
return;
}
self.setRange(null, page);
self.setRange(null, frame);
self._direction = .none;
try dispatchSelectionChangeEvent(page);
try dispatchSelectionChangeEvent(frame);
}
pub fn collapseToEnd(self: *Selection, page: *Page) !void {
pub fn collapseToEnd(self: *Selection, frame: *Frame) !void {
const range = self._range orelse return;
const abstract = range.asAbstractRange();
const last_node = abstract.getEndContainer();
const last_offset = abstract.getEndOffset();
const new_range = try Range.init(page);
const new_range = try Range.init(frame);
try new_range.setStart(last_node, last_offset);
try new_range.setEnd(last_node, last_offset);
self.setRange(new_range, page);
self.setRange(new_range, frame);
self._direction = .none;
try dispatchSelectionChangeEvent(page);
try dispatchSelectionChangeEvent(frame);
}
pub fn collapseToStart(self: *Selection, page: *Page) !void {
pub fn collapseToStart(self: *Selection, frame: *Frame) !void {
const range = self._range orelse return error.InvalidStateError;
const abstract = range.asAbstractRange();
const first_node = abstract.getStartContainer();
const first_offset = abstract.getStartOffset();
const new_range = try Range.init(page);
const new_range = try Range.init(frame);
try new_range.setStart(first_node, first_offset);
try new_range.setEnd(first_node, first_offset);
self.setRange(new_range, page);
self.setRange(new_range, frame);
self._direction = .none;
try dispatchSelectionChangeEvent(page);
try dispatchSelectionChangeEvent(frame);
}
pub fn containsNode(self: *const Selection, node: *Node, partial: bool) !bool {
@@ -238,18 +238,18 @@ pub fn containsNode(self: *const Selection, node: *Node, partial: bool) !bool {
return false;
}
pub fn deleteFromDocument(self: *Selection, page: *Page) !void {
pub fn deleteFromDocument(self: *Selection, frame: *Frame) !void {
const range = self._range orelse return;
try range.deleteContents(page);
try dispatchSelectionChangeEvent(page);
try range.deleteContents(frame);
try dispatchSelectionChangeEvent(frame);
}
pub fn extend(self: *Selection, node: *Node, _offset: ?u32, page: *Page) !void {
pub fn extend(self: *Selection, node: *Node, _offset: ?u32, frame: *Frame) !void {
const range = self._range orelse return error.InvalidState;
const offset = _offset orelse 0;
// If the node is not contained in the document, do not change the selection
if (!page.document.asNode().contains(node)) {
if (!frame.document.asNode().contains(node)) {
return;
}
@@ -268,7 +268,7 @@ pub fn extend(self: *Selection, node: *Node, _offset: ?u32, page: *Page) !void {
.forward, .none => range.asAbstractRange().getStartOffset(),
};
const new_range = try Range.init(page);
const new_range = try Range.init(frame);
const cmp = AbstractRange.compareBoundaryPoints(node, offset, old_anchor, old_anchor_offset);
switch (cmp) {
@@ -289,8 +289,8 @@ pub fn extend(self: *Selection, node: *Node, _offset: ?u32, page: *Page) !void {
},
}
self.setRange(new_range, page);
try dispatchSelectionChangeEvent(page);
self.setRange(new_range, frame);
try dispatchSelectionChangeEvent(frame);
}
pub fn getRangeAt(self: *Selection, index: u32) !*Range {
@@ -338,7 +338,7 @@ pub fn modify(
alter_str: []const u8,
direction_str: []const u8,
granularity_str: []const u8,
page: *Page,
frame: *Frame,
) !void {
const alter = ModifyAlter.fromString(alter_str) orelse return;
const direction = ModifyDirection.fromString(direction_str) orelse return;
@@ -352,8 +352,8 @@ pub fn modify(
};
switch (granularity) {
.character => try self.modifyByCharacter(alter, is_forward, range, page),
.word => try self.modifyByWord(alter, is_forward, range, page),
.character => try self.modifyByCharacter(alter, is_forward, range, frame),
.word => try self.modifyByWord(alter, is_forward, range, frame),
}
}
@@ -428,7 +428,7 @@ fn prevTextNode(node: *Node) ?*Node {
}
}
fn modifyByCharacter(self: *Selection, alter: ModifyAlter, forward: bool, range: *Range, page: *Page) !void {
fn modifyByCharacter(self: *Selection, alter: ModifyAlter, forward: bool, range: *Range, frame: *Frame) !void {
const abstract = range.asAbstractRange();
const focus_node = switch (self._direction) {
@@ -491,7 +491,7 @@ fn modifyByCharacter(self: *Selection, alter: ModifyAlter, forward: bool, range:
}
}
try self.applyModify(alter, new_node, new_offset, page);
try self.applyModify(alter, new_node, new_offset, frame);
}
fn isWordChar(c: u8) bool {
@@ -517,7 +517,7 @@ fn prevWordStart(text: []const u8, offset: u32) u32 {
return i;
}
fn modifyByWord(self: *Selection, alter: ModifyAlter, forward: bool, range: *Range, page: *Page) !void {
fn modifyByWord(self: *Selection, alter: ModifyAlter, forward: bool, range: *Range, frame: *Frame) !void {
const abstract = range.asAbstractRange();
const focus_node = switch (self._direction) {
@@ -560,11 +560,11 @@ fn modifyByWord(self: *Selection, alter: ModifyAlter, forward: bool, range: *Ran
new_node = next;
new_offset = nextWordEnd(next.getData().str(), 0);
}
return self.applyModify(alter, new_node, new_offset, page);
return self.applyModify(alter, new_node, new_offset, frame);
};
const t = if (isTextNode(child)) child else nextTextNode(child) orelse {
return self.applyModify(alter, new_node, new_offset, page);
return self.applyModify(alter, new_node, new_offset, frame);
};
new_node = t;
@@ -585,41 +585,41 @@ fn modifyByWord(self: *Selection, alter: ModifyAlter, forward: bool, range: *Ran
}
}
try self.applyModify(alter, new_node, new_offset, page);
try self.applyModify(alter, new_node, new_offset, frame);
}
fn applyModify(self: *Selection, alter: ModifyAlter, new_node: *Node, new_offset: u32, page: *Page) !void {
fn applyModify(self: *Selection, alter: ModifyAlter, new_node: *Node, new_offset: u32, frame: *Frame) !void {
switch (alter) {
.move => {
const new_range = try Range.init(page);
const new_range = try Range.init(frame);
try new_range.setStart(new_node, new_offset);
try new_range.setEnd(new_node, new_offset);
self.setRange(new_range, page);
self.setRange(new_range, frame);
self._direction = .none;
try dispatchSelectionChangeEvent(page);
try dispatchSelectionChangeEvent(frame);
},
.extend => try self.extend(new_node, new_offset, page),
.extend => try self.extend(new_node, new_offset, frame),
}
}
pub fn selectAllChildren(self: *Selection, parent: *Node, page: *Page) !void {
pub fn selectAllChildren(self: *Selection, parent: *Node, frame: *Frame) !void {
if (parent._type == .document_type) return error.InvalidNodeType;
// If the node is not contained in the document, do not change the selection
if (!page.document.asNode().contains(parent)) {
if (!frame.document.asNode().contains(parent)) {
return;
}
const range = try Range.init(page);
const range = try Range.init(frame);
try range.setStart(parent, 0);
const child_count = parent.getChildrenCount();
try range.setEnd(parent, @intCast(child_count));
self.setRange(range, page);
self.setRange(range, frame);
self._direction = .forward;
try dispatchSelectionChangeEvent(page);
try dispatchSelectionChangeEvent(frame);
}
pub fn setBaseAndExtent(
@@ -628,7 +628,7 @@ pub fn setBaseAndExtent(
anchor_offset: u32,
focus_node: *Node,
focus_offset: u32,
page: *Page,
frame: *Frame,
) !void {
if (anchor_offset > anchor_node.getLength()) {
return error.IndexSizeError;
@@ -645,7 +645,7 @@ pub fn setBaseAndExtent(
focus_offset,
);
const range = try Range.init(page);
const range = try Range.init(frame);
switch (cmp) {
.before => {
@@ -665,13 +665,13 @@ pub fn setBaseAndExtent(
},
}
self.setRange(range, page);
try dispatchSelectionChangeEvent(page);
self.setRange(range, frame);
try dispatchSelectionChangeEvent(frame);
}
pub fn collapse(self: *Selection, _node: ?*Node, _offset: ?u32, page: *Page) !void {
pub fn collapse(self: *Selection, _node: ?*Node, _offset: ?u32, frame: *Frame) !void {
const node = _node orelse {
try self.removeAllRanges(page);
try self.removeAllRanges(frame);
return;
};
@@ -683,27 +683,27 @@ pub fn collapse(self: *Selection, _node: ?*Node, _offset: ?u32, page: *Page) !vo
}
// If the node is not contained in the document, do not change the selection
if (!page.document.asNode().contains(node)) {
if (!frame.document.asNode().contains(node)) {
return;
}
const range = try Range.init(page);
const range = try Range.init(frame);
try range.setStart(node, offset);
try range.setEnd(node, offset);
self.setRange(range, page);
self.setRange(range, frame);
self._direction = .none;
try dispatchSelectionChangeEvent(page);
try dispatchSelectionChangeEvent(frame);
}
pub fn toString(self: *const Selection, page: *Page) ![]const u8 {
pub fn toString(self: *const Selection, frame: *Frame) ![]const u8 {
const range = self._range orelse return "";
return try range.toString(page);
return try range.toString(frame);
}
fn setRange(self: *Selection, new_range: ?*Range, page: *Page) void {
fn setRange(self: *Selection, new_range: ?*Range, frame: *Frame) void {
if (self._range) |existing| {
_ = existing.asAbstractRange().releaseRef(page._session);
_ = existing.asAbstractRange().releaseRef(frame._session);
}
if (new_range) |nr| {
nr.asAbstractRange().acquireRef();

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Node = @import("Node.zig");
const DocumentFragment = @import("DocumentFragment.zig");
const Element = @import("Element.zig");
@@ -42,8 +42,8 @@ _elements_by_id: std.StringHashMapUnmanaged(*Element) = .{},
_removed_ids: std.StringHashMapUnmanaged(void) = .{},
_adopted_style_sheets: ?js.Object.Global = null,
pub fn init(host: *Element, mode: Mode, page: *Page) !*ShadowRoot {
return page._factory.documentFragment(ShadowRoot{
pub fn init(host: *Element, mode: Mode, frame: *Frame) !*ShadowRoot {
return frame._factory.documentFragment(ShadowRoot{
._proto = undefined,
._mode = mode,
._host = host,
@@ -70,7 +70,7 @@ pub fn getHost(self: *const ShadowRoot) *Element {
return self._host;
}
pub fn getElementById(self: *ShadowRoot, id: []const u8, page: *Page) ?*Element {
pub fn getElementById(self: *ShadowRoot, id: []const u8, frame: *Frame) ?*Element {
if (id.len == 0) {
return null;
}
@@ -90,8 +90,8 @@ pub fn getElementById(self: *ShadowRoot, id: []const u8, page: *Page) ?*Element
// we ignore this error to keep getElementById easy to call
// if it really failed, then we're out of memory and nothing's
// going to work like it should anyways.
const owned_id = page.dupeString(id) catch return null;
self._elements_by_id.put(page.arena, owned_id, el) catch return null;
const owned_id = frame.dupeString(id) catch return null;
self._elements_by_id.put(frame.arena, owned_id, el) catch return null;
return el;
}
}
@@ -100,11 +100,11 @@ pub fn getElementById(self: *ShadowRoot, id: []const u8, page: *Page) ?*Element
return null;
}
pub fn getAdoptedStyleSheets(self: *ShadowRoot, page: *Page) !js.Object.Global {
pub fn getAdoptedStyleSheets(self: *ShadowRoot, frame: *Frame) !js.Object.Global {
if (self._adopted_style_sheets) |ass| {
return ass;
}
const js_arr = page.js.local.?.newArray(0);
const js_arr = frame.js.local.?.newArray(0);
const js_obj = js_arr.toObject();
self._adopted_style_sheets = try js_obj.persist();
return self._adopted_style_sheets.?;
@@ -126,15 +126,15 @@ pub const JsApi = struct {
pub const mode = bridge.accessor(ShadowRoot.getMode, null, .{});
pub const host = bridge.accessor(ShadowRoot.getHost, null, .{});
pub const getElementById = bridge.function(_getElementById, .{});
fn _getElementById(self: *ShadowRoot, value_: ?js.Value, page: *Page) !?*Element {
fn _getElementById(self: *ShadowRoot, value_: ?js.Value, frame: *Frame) !?*Element {
const value = value_ orelse return null;
if (value.isNull()) {
return self.getElementById("null", page);
return self.getElementById("null", frame);
}
if (value.isUndefined()) {
return self.getElementById("undefined", page);
return self.getElementById("undefined", frame);
}
return self.getElementById(try value.toZig([]const u8), page);
return self.getElementById(try value.toZig([]const u8), frame);
}
pub const adoptedStyleSheets = bridge.accessor(ShadowRoot.getAdoptedStyleSheets, ShadowRoot.setAdoptedStyleSheets, .{});
};

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
pub fn registerTypes() []const type {
return &.{ StorageManager, StorageEstimate };
@@ -27,12 +27,12 @@ const StorageManager = @This();
_pad: bool = false,
pub fn estimate(_: *const StorageManager, page: *Page) !js.Promise {
const est = try page._factory.create(StorageEstimate{
pub fn estimate(_: *const StorageManager, frame: *Frame) !js.Promise {
const est = try frame._factory.create(StorageEstimate{
._usage = 0,
._quota = 1024 * 1024 * 1024, // 1 GiB
});
return page.js.local.?.resolvePromise(est);
return frame.js.local.?.resolvePromise(est);
}
const StorageEstimate = struct {

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const lp = @import("lightpanda");
const crypto = @import("../../sys/libcrypto.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const js = @import("../js/js.zig");
const CryptoKey = @import("CryptoKey.zig");
@@ -45,13 +45,13 @@ pub fn generateKey(
algo: algorithm.Init,
extractable: bool,
key_usages: []const []const u8,
page: *Page,
frame: *Frame,
) !js.Promise {
switch (algo) {
.hmac_key_gen => |params| return HMAC.init(params, extractable, key_usages, page),
.hmac_key_gen => |params| return HMAC.init(params, extractable, key_usages, frame),
.name => |name| {
if (std.mem.eql(u8, "X25519", name)) {
return X25519.init(extractable, key_usages, page);
return X25519.init(extractable, key_usages, frame);
}
log.warn(.not_implemented, "generateKey", .{ .name = name });
@@ -60,7 +60,7 @@ pub fn generateKey(
// Ditto.
const name = object.name;
if (std.mem.eql(u8, "X25519", name)) {
return X25519.init(extractable, key_usages, page);
return X25519.init(extractable, key_usages, frame);
}
log.warn(.not_implemented, "generateKey", .{ .name = name });
@@ -68,7 +68,7 @@ pub fn generateKey(
else => log.warn(.not_implemented, "generateKey", .{}),
}
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.SyntaxError } });
return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.SyntaxError } });
}
/// Exports a key: that is, it takes as input a CryptoKey object and gives you
@@ -77,14 +77,14 @@ pub fn exportKey(
_: *const SubtleCrypto,
format: []const u8,
key: *CryptoKey,
page: *Page,
frame: *Frame,
) !js.Promise {
if (!key.canExportKey()) {
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
}
if (std.mem.eql(u8, format, "raw")) {
return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = key._key });
return frame.js.local.?.resolvePromise(js.ArrayBuffer{ .values = key._key });
}
const is_unsupported = std.mem.eql(u8, format, "pkcs8") or
@@ -92,10 +92,10 @@ pub fn exportKey(
if (is_unsupported) {
log.warn(.not_implemented, "SubtleCrypto.exportKey", .{ .format = format });
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
}
return page.js.local.?.rejectPromise(.{ .type_error = "invalid format" });
return frame.js.local.?.rejectPromise(.{ .type_error = "invalid format" });
}
/// Derive a secret key from a master key.
@@ -104,27 +104,27 @@ pub fn deriveBits(
algo: algorithm.Derive,
base_key: *const CryptoKey, // Private key.
length: usize,
page: *Page,
frame: *Frame,
) !js.Promise {
return switch (algo) {
.ecdh_or_x25519 => |params| {
const name = params.name;
if (std.mem.eql(u8, name, "X25519")) {
const result = X25519.deriveBits(base_key, params.public, length, page) catch |err| switch (err) {
error.InvalidAccessError => return page.js.local.?.rejectPromise(.{
const result = X25519.deriveBits(base_key, params.public, length, frame) catch |err| switch (err) {
error.InvalidAccessError => return frame.js.local.?.rejectPromise(.{
.dom_exception = .{ .err = error.InvalidAccessError },
}),
else => return err,
};
return page.js.local.?.resolvePromise(result);
return frame.js.local.?.resolvePromise(result);
}
if (std.mem.eql(u8, name, "ECDH")) {
log.warn(.not_implemented, "SubtleCrypto.deriveBits", .{ .name = name });
}
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
},
};
}
@@ -136,14 +136,14 @@ pub fn sign(
algo: algorithm.Sign,
key: *CryptoKey,
data: []const u8, // ArrayBuffer.
page: *Page,
frame: *Frame,
) !js.Promise {
return switch (key._type) {
// Call sign for HMAC.
.hmac => return HMAC.sign(algo, key, data, page),
.hmac => return HMAC.sign(algo, key, data, frame),
else => {
log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type });
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
},
};
}
@@ -155,33 +155,33 @@ pub fn verify(
key: *const CryptoKey,
signature: []const u8, // ArrayBuffer.
data: []const u8, // ArrayBuffer.
page: *Page,
frame: *Frame,
) !js.Promise {
if (!algo.isHMAC()) {
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
}
return switch (key._type) {
.hmac => HMAC.verify(key, signature, data, page),
else => page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }),
.hmac => HMAC.verify(key, signature, data, frame),
else => frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }),
};
}
/// Generates a digest of the given data, using the specified hash function.
pub fn digest(_: *const SubtleCrypto, algo: []const u8, data: js.TypedArray(u8), page: *Page) !js.Promise {
const local = page.js.local.?;
pub fn digest(_: *const SubtleCrypto, algo: []const u8, data: js.TypedArray(u8), frame: *Frame) !js.Promise {
const local = frame.js.local.?;
if (algo.len > 10) {
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
}
const normalized = std.ascii.upperString(&page.buf, algo);
const normalized = std.ascii.upperString(&frame.buf, algo);
const digest_type = crypto.findDigest(normalized) catch {
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
};
const bytes = data.values;
const out = page.buf[0..crypto.EVP_MAX_MD_SIZE];
const out = frame.buf[0..crypto.EVP_MAX_MD_SIZE];
var out_size: c_uint = 0;
const result = crypto.EVP_Digest(bytes.ptr, bytes.len, out, &out_size, digest_type, null);
lp.assert(result == 1, "SubtleCrypto.digest", .{ .algo = algo });

View File

@@ -160,9 +160,9 @@ pub fn TreeWalker(comptime mode: Mode) type {
test "TreeWalker: skipChildren" {
const testing = @import("../../testing.zig");
const page = try testing.test_session.createPage();
defer testing.test_session.removePage();
const doc = page.window._document;
const frame = try testing.test_session.createFrame();
defer testing.test_session.removeFrame();
const doc = frame.window._document;
// <div>
// <span>
@@ -170,13 +170,13 @@ test "TreeWalker: skipChildren" {
// </span>
// <p>B</p>
// </div>
const div = try doc.createElement("div", null, page);
const span = try doc.createElement("span", null, page);
const b = try doc.createElement("b", null, page);
const p = try doc.createElement("p", null, page);
_ = try span.asNode().appendChild(b.asNode(), page);
_ = try div.asNode().appendChild(span.asNode(), page);
_ = try div.asNode().appendChild(p.asNode(), page);
const div = try doc.createElement("div", null, frame);
const span = try doc.createElement("span", null, frame);
const b = try doc.createElement("b", null, frame);
const p = try doc.createElement("p", null, frame);
_ = try span.asNode().appendChild(b.asNode(), frame);
_ = try div.asNode().appendChild(span.asNode(), frame);
_ = try div.asNode().appendChild(p.asNode(), frame);
var tw = Full.init(div.asNode(), .{});

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const js = @import("../js/js.zig");
const U = @import("../URL.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const URLSearchParams = @import("net/URLSearchParams.zig");
const Blob = @import("Blob.zig");
const Execution = js.Execution;
@@ -248,28 +248,28 @@ pub fn canParse(url: []const u8, base_: ?[]const u8) bool {
return U.isCompleteHTTPUrl(url);
}
pub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 {
pub fn createObjectURL(blob: *Blob, frame: *Frame) ![]const u8 {
var uuid_buf: [36]u8 = undefined;
@import("../../id.zig").uuidv4(&uuid_buf);
const blob_url = try std.fmt.allocPrint(
page.arena,
frame.arena,
"blob:{s}/{s}",
.{ page.origin orelse "null", uuid_buf },
.{ frame.origin orelse "null", uuid_buf },
);
try page._blob_urls.put(page.arena, blob_url, blob);
try frame._blob_urls.put(frame.arena, blob_url, blob);
blob.acquireRef();
return blob_url;
}
pub fn revokeObjectURL(url: []const u8, page: *Page) void {
pub fn revokeObjectURL(url: []const u8, frame: *Frame) void {
// Per spec: silently ignore non-blob URLs
if (!std.mem.startsWith(u8, url, "blob:")) {
return;
}
if (page._blob_urls.fetchRemove(url)) |entry| {
entry.value.releaseRef(page._session);
if (frame._blob_urls.fetchRemove(url)) |entry| {
entry.value.releaseRef(frame._session);
}
}

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const EventTarget = @import("EventTarget.zig");
const VisualViewport = @This();
@@ -28,12 +28,12 @@ pub fn asEventTarget(self: *VisualViewport) *EventTarget {
return self._proto;
}
pub fn getPageLeft(_: *const VisualViewport, page: *Page) u32 {
return page.window.getScrollX();
pub fn getPageLeft(_: *const VisualViewport, frame: *Frame) u32 {
return frame.window.getScrollX();
}
pub fn getPageTop(_: *const VisualViewport, page: *Page) u32 {
return page.window.getScrollY();
pub fn getPageTop(_: *const VisualViewport, frame: *Frame) u32 {
return frame.window.getScrollY();
}
pub const JsApi = struct {

View File

@@ -21,7 +21,7 @@ const lp = @import("lightpanda");
const builtin = @import("builtin");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Console = @import("Console.zig");
const History = @import("History.zig");
const Navigation = @import("navigation/Navigation.zig");
@@ -57,7 +57,7 @@ pub fn registerTypes() []const type {
const Window = @This();
_proto: *EventTarget,
_page: *Page,
_frame: *Frame,
_document: *Document,
_css: CSS = .init,
_crypto: Crypto = .init,
@@ -111,17 +111,17 @@ pub fn getWindow(self: *Window) *Window {
return self;
}
pub fn getTop(self: *Window, page: *Page) Access {
var p = self._page;
pub fn getTop(self: *Window, frame: *Frame) Access {
var p = self._frame;
while (p.parent) |parent| {
p = parent;
}
return Access.init(page.window, p.window);
return Access.init(frame.window, p.window);
}
pub fn getParent(self: *Window, page: *Page) Access {
if (self._page.parent) |p| {
return Access.init(page.window, p.window);
pub fn getParent(self: *Window, frame: *Frame) Access {
if (self._frame.parent) |p| {
return Access.init(frame.window, p.window);
}
return .{ .window = self };
}
@@ -167,7 +167,7 @@ pub fn getSessionStorage(self: *Window) *storage.Lookup {
}
pub fn getOrigin(self: *const Window) []const u8 {
return self._page.origin orelse "null";
return self._frame.origin orelse "null";
}
pub fn getLocation(self: *const Window) *Location {
@@ -178,16 +178,16 @@ pub fn getSelection(self: *const Window) *Selection {
return &self._document._selection;
}
pub fn setLocation(self: *Window, url: [:0]const u8, page: *Page) !void {
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = self._page });
pub fn setLocation(self: *Window, url: [:0]const u8, frame: *Frame) !void {
return frame.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = self._frame });
}
pub fn getHistory(_: *Window, page: *Page) *History {
return &page._session.history;
pub fn getHistory(_: *Window, frame: *Frame) *History {
return &frame._session.history;
}
pub fn getNavigation(_: *Window, page: *Page) *Navigation {
return &page._session.navigation;
pub fn getNavigation(_: *Window, frame: *Frame) *Navigation {
return &frame._session.navigation;
}
pub fn getCustomElements(self: *Window) *CustomElementRegistry {
@@ -250,8 +250,8 @@ pub fn setOnUnhandledRejection(self: *Window, setter: ?FunctionSetter) void {
self._on_unhandled_rejection = getFunctionFromSetter(setter);
}
pub fn fetch(_: *const Window, input: Fetch.Input, options: ?Fetch.InitOpts, page: *Page) !js.Promise {
return Fetch.init(input, options, page);
pub fn fetch(_: *const Window, input: Fetch.Input, options: ?Fetch.InitOpts, frame: *Frame) !js.Promise {
return Fetch.init(input, options, frame);
}
const LegacyHandler = union(enum) {
@@ -259,24 +259,24 @@ const LegacyHandler = union(enum) {
string: js.String,
};
pub fn setTimeout(self: *Window, handler: LegacyHandler, delay_ms: ?u32, params: []js.Value.Temp, page: *Page) !u32 {
const cb = try resolveTimerHandler(handler, page);
pub fn setTimeout(self: *Window, handler: LegacyHandler, delay_ms: ?u32, params: []js.Value.Temp, frame: *Frame) !u32 {
const cb = try resolveTimerHandler(handler, frame);
return self.scheduleCallback(cb, delay_ms orelse 0, .{
.repeat = false,
.params = params,
.low_priority = false,
.name = "window.setTimeout",
}, page);
}, frame);
}
pub fn setInterval(self: *Window, handler: LegacyHandler, delay_ms: ?u32, params: []js.Value.Temp, page: *Page) !u32 {
const cb = try resolveTimerHandler(handler, page);
pub fn setInterval(self: *Window, handler: LegacyHandler, delay_ms: ?u32, params: []js.Value.Temp, frame: *Frame) !u32 {
const cb = try resolveTimerHandler(handler, frame);
return self.scheduleCallback(cb, delay_ms orelse 0, .{
.repeat = true,
.params = params,
.low_priority = false,
.name = "window.setInterval",
}, page);
}, frame);
}
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-settimeout
@@ -284,37 +284,37 @@ pub fn setInterval(self: *Window, handler: LegacyHandler, delay_ms: ?u32, params
// TimerHandler = Function or DOMString. When a string is passed, it is
// compiled into an anonymous function body, matching how legacy browsers
// (and all current UAs) interpret `setTimeout("foo()", 100)`.
fn resolveTimerHandler(handler: LegacyHandler, page: *Page) !js.Function.Temp {
fn resolveTimerHandler(handler: LegacyHandler, frame: *Frame) !js.Function.Temp {
switch (handler) {
.function => |fun| return fun,
.string => |str| {
const fun = try page.js.local.?.compileFunction(str, &.{}, &.{});
const fun = try frame.js.local.?.compileFunction(str, &.{}, &.{});
return fun.temp();
},
}
}
pub fn setImmediate(self: *Window, cb: js.Function.Temp, params: []js.Value.Temp, page: *Page) !u32 {
pub fn setImmediate(self: *Window, cb: js.Function.Temp, params: []js.Value.Temp, frame: *Frame) !u32 {
return self.scheduleCallback(cb, 0, .{
.repeat = false,
.params = params,
.low_priority = false,
.name = "window.setImmediate",
}, page);
}, frame);
}
pub fn requestAnimationFrame(self: *Window, cb: js.Function.Temp, page: *Page) !u32 {
pub fn requestAnimationFrame(self: *Window, cb: js.Function.Temp, frame: *Frame) !u32 {
return self.scheduleCallback(cb, 5, .{
.repeat = false,
.params = &.{},
.low_priority = false,
.mode = .animation_frame,
.name = "window.requestAnimationFrame",
}, page);
}, frame);
}
pub fn queueMicrotask(_: *Window, cb: js.Function, page: *Page) void {
page.js.queueMicrotaskFunc(cb);
pub fn queueMicrotask(_: *Window, cb: js.Function, frame: *Frame) void {
frame.js.queueMicrotaskFunc(cb);
}
pub fn clearTimeout(self: *Window, id: u32) void {
@@ -340,7 +340,7 @@ pub fn cancelAnimationFrame(self: *Window, id: u32) void {
const RequestIdleCallbackOpts = struct {
timeout: ?u32 = null,
};
pub fn requestIdleCallback(self: *Window, cb: js.Function.Temp, opts_: ?RequestIdleCallbackOpts, page: *Page) !u32 {
pub fn requestIdleCallback(self: *Window, cb: js.Function.Temp, opts_: ?RequestIdleCallbackOpts, frame: *Frame) !u32 {
const opts = opts_ orelse RequestIdleCallbackOpts{};
return self.scheduleCallback(cb, opts.timeout orelse 50, .{
.mode = .idle,
@@ -348,7 +348,7 @@ pub fn requestIdleCallback(self: *Window, cb: js.Function.Temp, opts_: ?RequestI
.params = &.{},
.low_priority = true,
.name = "window.requestIdleCallback",
}, page);
}, frame);
}
pub fn cancelIdleCallback(self: *Window, id: u32) void {
@@ -356,13 +356,13 @@ pub fn cancelIdleCallback(self: *Window, id: u32) void {
sc.value.removed = true;
}
pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
pub fn reportError(self: *Window, err: js.Value, frame: *Frame) !void {
const error_event = try ErrorEvent.initTrusted(comptime .wrap("error"), .{
.@"error" = try err.temp(),
.message = err.toStringSlice() catch "Unknown error",
.bubbles = false,
.cancelable = true,
}, page._session);
}, frame._session);
// Invoke window.onerror callback if set (per WHATWG spec, this is called
// with 5 arguments: message, source, lineno, colno, error)
@@ -370,7 +370,7 @@ pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
var prevent_default = false;
if (self._on_error) |on_error| {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
frame.js.localScope(&ls);
defer ls.deinit();
const local_func = ls.toLocal(on_error);
@@ -392,7 +392,7 @@ pub fn reportError(self: *Window, err: js.Value, page: *Page) !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 page._event_manager.dispatchDirect(self.asEventTarget(), event, null, .{
try frame._event_manager.dispatchDirect(self.asEventTarget(), event, null, .{
.context = "window.reportError",
});
@@ -408,59 +408,57 @@ pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
}
}
pub fn matchMedia(_: *const Window, query: []const u8, page: *Page) !*MediaQueryList {
return page._factory.eventTarget(MediaQueryList{
pub fn matchMedia(_: *const Window, query: []const u8, frame: *Frame) !*MediaQueryList {
return frame._factory.eventTarget(MediaQueryList{
._proto = undefined,
._media = try page.dupeString(query),
._media = try frame.dupeString(query),
});
}
pub fn getComputedStyle(_: *const Window, element: *Element, pseudo_element: ?[]const u8, page: *Page) !*CSSStyleProperties {
pub fn getComputedStyle(_: *const Window, element: *Element, pseudo_element: ?[]const u8, frame: *Frame) !*CSSStyleProperties {
if (pseudo_element) |pe| {
if (pe.len != 0) {
log.warn(.not_implemented, "window.GetComputedStyle", .{ .pseudo_element = pe });
}
}
return CSSStyleProperties.init(element, true, page);
return CSSStyleProperties.init(element, true, frame);
}
pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]const u8, page: *Page) !void {
pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]const u8, frame: *Frame) !void {
// For now, we ignore targetOrigin checking and just dispatch the message
// In a full implementation, we would validate the origin
_ = target_origin;
// self = the window that will get the message
// page = the context calling postMessage
const target_page = self._page;
const source_window = target_page.js.getIncumbent().window;
const target_frame = self._frame;
const source_window = target_frame.js.getIncumbent().window;
const arena = try target_page.getArena(.medium, "Window.postMessage");
errdefer target_page.releaseArena(arena);
const arena = try target_frame.getArena(.medium, "Window.postMessage");
errdefer target_frame.releaseArena(arena);
// Origin should be the source window's origin (where the message came from)
const origin = try source_window._location.getOrigin(&page.js.execution);
const origin = try source_window._location.getOrigin(&frame.js.execution);
const callback = try arena.create(PostMessageCallback);
callback.* = .{
.arena = arena,
.message = message,
.page = target_page,
.frame = target_frame,
.source = source_window,
.origin = try arena.dupe(u8, origin),
};
try target_page.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
try target_frame.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
.name = "postMessage",
.low_priority = false,
.finalizer = PostMessageCallback.cancelled,
});
}
pub fn btoa(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
return @import("encoding/base64.zig").encode(page.call_arena, input);
pub fn btoa(_: *const Window, input: []const u8, frame: *Frame) ![]const u8 {
return @import("encoding/base64.zig").encode(frame.call_arena, input);
}
pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
return @import("encoding/base64.zig").decode(page.call_arena, input);
pub fn atob(_: *const Window, input: []const u8, frame: *Frame) ![]const u8 {
return @import("encoding/base64.zig").decode(frame.call_arena, input);
}
pub fn structuredClone(_: *const Window, value: js.Value) !js.Value {
@@ -468,15 +466,15 @@ pub fn structuredClone(_: *const Window, value: js.Value) !js.Value {
}
pub fn getFrame(self: *Window, idx: usize) !?*Window {
const page = self._page;
const frames = page.frames.items;
const frame = self._frame;
const frames = frame.child_frames.items;
if (idx >= frames.len) {
return null;
}
if (page.frames_sorted == false) {
std.mem.sort(*Page, frames, {}, struct {
fn lessThan(_: void, a: *Page, b: *Page) bool {
if (frame.child_frames_sorted == false) {
std.mem.sort(*Frame, frames, {}, struct {
fn lessThan(_: void, a: *Frame, b: *Frame) bool {
const iframe_a = a.iframe orelse return false;
const iframe_b = b.iframe orelse return true;
@@ -485,13 +483,13 @@ pub fn getFrame(self: *Window, idx: usize) !?*Window {
return (pos & 0x04) != 0; // FOLLOWING bit: b follows a
}
}.lessThan);
page.frames_sorted = true;
frame.child_frames_sorted = true;
}
return frames[idx].window;
}
pub fn getFramesLength(self: *const Window) u32 {
return @intCast(self._page.frames.items.len);
return @intCast(self._frame.child_frames.items.len);
}
pub fn getScrollX(self: *const Window) u32 {
@@ -512,7 +510,7 @@ const ScrollToOpts = union(enum) {
behavior: []const u8 = "",
};
};
pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, frame: *Frame) !void {
switch (opts) {
.x => |x| {
self._scroll_pos.x = @intCast(@max(x, 0));
@@ -528,11 +526,11 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
// We dispatch scroll event asynchronously after 10ms. So we can throttle
// them.
try page.js.scheduler.add(
page,
try frame.js.scheduler.add(
frame,
struct {
fn dispatch(_page: *anyopaque) anyerror!?u32 {
const p: *Page = @ptrCast(@alignCast(_page));
fn dispatch(_frame: *anyopaque) anyerror!?u32 {
const p: *Frame = @ptrCast(@alignCast(_frame));
const pos = &p.window._scroll_pos;
// If the state isn't scroll, we can ignore safely to throttle
// the events.
@@ -552,11 +550,11 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
.{ .low_priority = true },
);
// We dispatch scrollend event asynchronously after 20ms.
try page.js.scheduler.add(
page,
try frame.js.scheduler.add(
frame,
struct {
fn dispatch(_page: *anyopaque) anyerror!?u32 {
const p: *Page = @ptrCast(@alignCast(_page));
fn dispatch(_frame: *anyopaque) anyerror!?u32 {
const p: *Frame = @ptrCast(@alignCast(_frame));
const pos = &p.window._scroll_pos;
// Dispatch only if the state is .end.
// If a scroll is pending, retry in 10ms.
@@ -580,7 +578,7 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
);
}
pub fn scrollBy(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
pub fn scrollBy(self: *Window, opts: ScrollToOpts, y: ?i32, frame: *Frame) !void {
// The scroll is relative to the current position. So compute to new
// absolute position.
var absx: i32 = undefined;
@@ -595,7 +593,7 @@ pub fn scrollBy(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
absy = @as(i32, @intCast(self._scroll_pos.y)) + o.top;
},
}
return self.scrollTo(.{ .x = absx }, absy, page);
return self.scrollTo(.{ .x = absx }, absy, frame);
}
// only exposed when the binary is built with the -Dwpt_extensions flag
@@ -603,7 +601,7 @@ pub fn getWebDriver(_: *const Window) @import("WebDriver.zig") {
return .{};
}
pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js.PromiseRejection, page: *Page) !void {
pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js.PromiseRejection, frame: *Frame) !void {
if (comptime IS_DEBUG) {
log.debug(.js, "unhandled rejection", .{
.target = "window",
@@ -620,12 +618,12 @@ pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js.
};
const target = self.asEventTarget();
if (page._event_manager.hasDirectListeners(target, event_name, attribute_callback)) {
if (frame._event_manager.hasDirectListeners(target, event_name, attribute_callback)) {
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(),
}, page._session)).asEvent();
try page._event_manager.dispatchDirect(target, event, attribute_callback, .{ .context = "window.unhandledrejection" });
}, frame._session)).asEvent();
try frame._event_manager.dispatchDirect(target, event, attribute_callback, .{ .context = "window.unhandledrejection" });
}
}
@@ -639,7 +637,7 @@ pub const Access = union(enum) {
return .{ .window = accessing };
}
if (callee._page.js.origin == accessing._page.js.origin) {
if (callee._frame.js.origin == accessing._frame.js.origin) {
// two different windows, but same origin, return the full window
return .{ .window = accessing };
}
@@ -656,14 +654,14 @@ const ScheduleOpts = struct {
animation_frame: bool = false,
mode: ScheduleCallback.Mode = .normal,
};
fn scheduleCallback(self: *Window, cb: js.Function.Temp, delay_ms: u32, opts: ScheduleOpts, page: *Page) !u32 {
fn scheduleCallback(self: *Window, cb: js.Function.Temp, delay_ms: u32, opts: ScheduleOpts, frame: *Frame) !u32 {
if (self._timers.count() > 512) {
// these are active
return error.TooManyTimeout;
}
const arena = try page.getArena(.tiny, "Window.schedule");
errdefer page.releaseArena(arena);
const arena = try frame.getArena(.tiny, "Window.schedule");
errdefer frame.releaseArena(arena);
const timer_id = self._timer_id +% 1;
self._timer_id = timer_id;
@@ -674,7 +672,7 @@ fn scheduleCallback(self: *Window, cb: js.Function.Temp, delay_ms: u32, opts: Sc
persisted_params = try arena.dupe(js.Value.Temp, params);
}
const gop = try self._timers.getOrPut(page.arena, timer_id);
const gop = try self._timers.getOrPut(frame.arena, timer_id);
if (gop.found_existing) {
// 2^31 would have to wrap for this to happen.
return error.TooManyTimeout;
@@ -684,7 +682,7 @@ fn scheduleCallback(self: *Window, cb: js.Function.Temp, delay_ms: u32, opts: Sc
const callback = try arena.create(ScheduleCallback);
callback.* = .{
.cb = cb,
.page = page,
.frame = frame,
.arena = arena,
.mode = opts.mode,
.name = opts.name,
@@ -694,7 +692,7 @@ fn scheduleCallback(self: *Window, cb: js.Function.Temp, delay_ms: u32, opts: Sc
};
gop.value_ptr.* = callback;
try page.js.scheduler.add(callback, ScheduleCallback.run, delay_ms, .{
try frame.js.scheduler.add(callback, ScheduleCallback.run, delay_ms, .{
.name = opts.name,
.low_priority = opts.low_priority,
.finalizer = ScheduleCallback.cancelled,
@@ -716,7 +714,7 @@ const ScheduleCallback = struct {
cb: js.Function.Temp,
mode: Mode,
page: *Page,
frame: *Frame,
arena: Allocator,
removed: bool = false,
params: []const js.Value.Temp,
@@ -737,13 +735,13 @@ const ScheduleCallback = struct {
for (self.params) |param| {
param.release();
}
self.page.releaseArena(self.arena);
self.frame.releaseArena(self.arena);
}
fn run(ctx: *anyopaque) !?u32 {
const self: *ScheduleCallback = @ptrCast(@alignCast(ctx));
const page = self.page;
const window = page.window;
const frame = self.frame;
const window = frame.window;
if (self.removed) {
self.deinit();
@@ -751,7 +749,7 @@ const ScheduleCallback = struct {
}
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
frame.js.localScope(&ls);
defer ls.deinit();
switch (self.mode) {
@@ -783,14 +781,14 @@ const ScheduleCallback = struct {
};
const PostMessageCallback = struct {
page: *Page,
frame: *Frame,
source: *Window,
arena: Allocator,
origin: []const u8,
message: js.Value.Temp,
fn deinit(self: *PostMessageCallback) void {
self.page.releaseArena(self.arena);
self.frame.releaseArena(self.arena);
}
fn cancelled(ctx: *anyopaque) void {
@@ -802,19 +800,19 @@ const PostMessageCallback = struct {
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
defer self.deinit();
const page = self.page;
const window = page.window;
const frame = self.frame;
const window = frame.window;
const event_target = window.asEventTarget();
if (page._event_manager.hasDirectListeners(event_target, "message", window._on_message)) {
if (frame._event_manager.hasDirectListeners(event_target, "message", window._on_message)) {
const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{
.data = .{ .value = self.message },
.origin = self.origin,
.source = self.source,
.bubbles = false,
.cancelable = false,
}, page._session)).asEvent();
try page._event_manager.dispatchDirect(event_target, event, window._on_message, .{ .context = "window.postMessage" });
}, frame._session)).asEvent();
try frame._event_manager.dispatchDirect(event_target, event, window._on_message, .{ .context = "window.postMessage" });
}
return null;
@@ -921,18 +919,18 @@ pub const JsApi = struct {
pub const opener = bridge.property(null, .{ .template = false });
pub const alert = bridge.function(struct {
fn alert(_: *const Window, message: ?[]const u8, page: *Page) void {
page._session.notification.dispatch(.javascript_dialog_opening, &.{
.url = page.url,
fn alert(_: *const Window, message: ?[]const u8, frame: *Frame) void {
frame._session.notification.dispatch(.javascript_dialog_opening, &.{
.url = frame.url,
.message = message orelse "",
.dialog_type = "alert",
});
}
}.alert, .{});
pub const confirm = bridge.function(struct {
fn confirm(_: *const Window, message: ?[]const u8, page: *Page) bool {
page._session.notification.dispatch(.javascript_dialog_opening, &.{
.url = page.url,
fn confirm(_: *const Window, message: ?[]const u8, frame: *Frame) bool {
frame._session.notification.dispatch(.javascript_dialog_opening, &.{
.url = frame.url,
.message = message orelse "",
.dialog_type = "confirm",
});
@@ -940,9 +938,9 @@ pub const JsApi = struct {
}
}.confirm, .{});
pub const prompt = bridge.function(struct {
fn prompt(_: *const Window, message: ?[]const u8, _: ?[]const u8, page: *Page) ?[]const u8 {
page._session.notification.dispatch(.javascript_dialog_opening, &.{
.url = page.url,
fn prompt(_: *const Window, message: ?[]const u8, _: ?[]const u8, frame: *Frame) ?[]const u8 {
frame._session.notification.dispatch(.javascript_dialog_opening, &.{
.url = frame.url,
.message = message orelse "",
.dialog_type = "prompt",
});
@@ -956,16 +954,16 @@ pub const JsApi = struct {
const CrossOriginWindow = struct {
window: *Window,
pub fn postMessage(self: *CrossOriginWindow, message: js.Value.Temp, target_origin: ?[]const u8, page: *Page) !void {
return self.window.postMessage(message, target_origin, page);
pub fn postMessage(self: *CrossOriginWindow, message: js.Value.Temp, target_origin: ?[]const u8, frame: *Frame) !void {
return self.window.postMessage(message, target_origin, frame);
}
pub fn getTop(self: *CrossOriginWindow, page: *Page) Access {
return self.window.getParent(page);
pub fn getTop(self: *CrossOriginWindow, frame: *Frame) Access {
return self.window.getParent(frame);
}
pub fn getParent(self: *CrossOriginWindow, page: *Page) Access {
return self.window.getParent(page);
pub fn getParent(self: *CrossOriginWindow, frame: *Frame) Access {
return self.window.getParent(frame);
}
pub fn getFramesLength(self: *const CrossOriginWindow) u32 {

View File

@@ -23,7 +23,7 @@ const js = @import("../js/js.zig");
const http = @import("../../network/http.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");
@@ -42,11 +42,11 @@ const Worker = @This();
// used by HttpClient when generating notification
// Ultimately used by CDP to generate request/loader ids.
id: u32,
_pseudo_frame_id: u32,
_frame_id: u32,
_loader_id: u32,
_proto: *EventTarget,
_page: *Page,
_frame: *Frame,
_arena: Allocator,
_worker_scope: *WorkerGlobalScope,
@@ -61,32 +61,32 @@ _on_message: ?js.Function.Global = null,
_on_messageerror: ?js.Function.Global = null,
pub fn init(url: []const u8, exec: *Execution) !*Worker {
const page = switch (exec.context.global) {
.page => |p| p,
const frame = switch (exec.context.global) {
.frame => |f| f,
.worker => return error.WorkerCannotCreateWorker,
};
const session = page._session;
const session = frame._session;
const arena = try session.getArena(.large, "Worker");
errdefer session.releaseArena(arena);
const resolved_url = try URL.resolve(arena, exec.url.*, url, .{});
const self = try session.factory.eventTargetWithAllocator(arena, Worker{
.id = session.nextPageId(),
._pseudo_frame_id = session.nextFrameId(),
._arena = arena,
._proto = undefined,
._page = page,
._frame = frame,
._url = resolved_url,
._worker_scope = undefined,
._frame_id = session.nextFrameId(),
._loader_id = session.nextLoaderId(),
});
self._worker_scope = try WorkerGlobalScope.init(self, resolved_url);
errdefer self._worker_scope.deinit();
try page.trackWorker(self);
try frame.trackWorker(self);
if (std.mem.startsWith(u8, url, "blob:")) {
errdefer page.removeWorker(self);
const blob: *Blob = page.lookupBlobUrl(url) orelse {
errdefer frame.removeWorker(self);
const blob: *Blob = frame.lookupBlobUrl(url) orelse {
log.warn(.js, "invalid blob", .{ .target = "worker" });
return error.BlobNotFound;
};
@@ -100,8 +100,8 @@ pub fn init(url: []const u8, exec: *Execution) !*Worker {
.url = resolved_url,
.method = .GET,
.headers = try http_client.newHeaders(),
.page_id = self.id,
.frame_id = self._pseudo_frame_id,
.frame_id = self._frame_id,
.loader_id = self._loader_id,
.resource_type = .script,
.cookie_jar = &session.cookie_jar,
.cookie_origin = resolved_url,
@@ -112,21 +112,21 @@ pub fn init(url: []const u8, exec: *Execution) !*Worker {
.error_callback = httpErrorCallback,
}) catch |err| {
log.err(.browser, "Worker request", .{ .url = resolved_url, .err = err });
page.removeWorker(self);
frame.removeWorker(self);
return err;
};
return self;
}
// Called from Page.deinit when the page is destroyed, so we don't need to
// remove from the page's worker list.
// Called from Frame.deinit when the frame is destroyed, so we don't need to
// remove from the frame's worker list.
pub fn deinit(self: *Worker) void {
if (self._http_response) |res| {
res.abort(error.Abort);
self._http_response = null;
}
self._worker_scope.deinit();
self._page._session.releaseArena(self._arena);
self._frame._session.releaseArena(self._arena);
}
pub fn asEventTarget(self: *Worker) *EventTarget {
@@ -215,13 +215,13 @@ 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 page = self._page;
const session = page._session;
const frame = self._frame;
const session = frame._session;
const target = self.asEventTarget();
const on_error = self._on_error;
// Check if there are any listeners
if (!page._event_manager.hasDirectListeners(target, "error", on_error)) {
if (!frame._event_manager.hasDirectListeners(target, "error", on_error)) {
if (error_value) |ev| ev.release();
return;
}
@@ -234,7 +234,7 @@ fn _fireErrorEvent(self: *Worker, message: []const u8, error_value: ?js.Value.Te
.cancelable = true,
}, session);
try page._event_manager.dispatchDirect(target, error_event.asEvent(), on_error, .{
try frame._event_manager.dispatchDirect(target, error_event.asEvent(), on_error, .{
.context = "Worker.onerror",
});
}
@@ -247,17 +247,17 @@ pub fn terminate(self: *Worker) void {
}
}
// Posts a message from the page to the worker.
// Posts a message from the frame to the worker.
pub fn postMessage(self: *Worker, data: js.Value) !void {
try self._worker_scope.receiveMessage(data);
}
// Called internally by WorkerGlobalScope when it wants to post a message to us
pub fn receiveMessage(self: *Worker, data: js.Value) !void {
const page = self._page;
const frame = self._frame;
const cloned_data = blk: {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
frame.js.localScope(&ls);
defer ls.deinit();
// clones from where it currently is (the Worker context) to our Page's context
@@ -265,8 +265,8 @@ pub fn receiveMessage(self: *Worker, data: js.Value) !void {
break :blk cloned.temp();
};
const message_arena = try page.getArena(.tiny, "Worker.receiveMessage");
errdefer page.releaseArena(message_arena);
const message_arena = try frame.getArena(.tiny, "Worker.receiveMessage");
errdefer frame.releaseArena(message_arena);
const callback = try message_arena.create(ReceiveMessageCallback);
callback.* = .{
@@ -275,7 +275,7 @@ pub fn receiveMessage(self: *Worker, data: js.Value) !void {
.arena = message_arena,
};
try page.js.scheduler.add(callback, ReceiveMessageCallback.run, 0, .{
try frame.js.scheduler.add(callback, ReceiveMessageCallback.run, 0, .{
.name = "Worker.receiveMessage",
.low_priority = false,
.finalizer = ReceiveMessageCallback.cancelled,
@@ -333,7 +333,7 @@ const ReceiveMessageCallback = struct {
}
fn deinit(self: *ReceiveMessageCallback) void {
self.worker._page._session.releaseArena(self.arena);
self.worker._frame._session.releaseArena(self.arena);
}
fn run(ctx: *anyopaque) !?u32 {
@@ -341,28 +341,28 @@ const ReceiveMessageCallback = struct {
defer self.deinit();
const worker = self.worker;
const page = worker._page;
const frame = worker._frame;
const target = worker.asEventTarget();
// If data is null, structured clone failed - fire messageerror
const data = self.data catch |err| {
const on_messageerror = worker._on_messageerror;
if (!page._event_manager.hasDirectListeners(target, "messageerror", on_messageerror)) {
if (!frame._event_manager.hasDirectListeners(target, "messageerror", on_messageerror)) {
return null;
}
const event = (try MessageEvent.initTrusted(comptime .wrap("messageerror"), .{
.data = .{ .string = @errorName(err) },
.bubbles = false,
.cancelable = false,
}, page._session)).asEvent();
try page._event_manager.dispatchDirect(target, event, on_messageerror, .{ .context = "Worker.messageerror" });
}, frame._session)).asEvent();
try frame._event_manager.dispatchDirect(target, event, on_messageerror, .{ .context = "Worker.messageerror" });
return null;
};
const on_message = worker._on_message;
// Check if there are any listeners before creating the event
if (!page._event_manager.hasDirectListeners(target, "message", on_message)) {
if (!frame._event_manager.hasDirectListeners(target, "message", on_message)) {
data.release();
return null;
}
@@ -371,9 +371,9 @@ const ReceiveMessageCallback = struct {
.data = .{ .value = data },
.bubbles = false,
.cancelable = false,
}, page._session)).asEvent();
}, frame._session)).asEvent();
try page._event_manager.dispatchDirect(target, event, on_message, .{ .context = "Worker.receiveMessage" });
try frame._event_manager.dispatchDirect(target, event, on_message, .{ .context = "Worker.receiveMessage" });
return null;
}

View File

@@ -52,12 +52,12 @@ _identity: JS.Identity = .{},
arena: Allocator,
call_arena: Allocator,
url: [:0]const u8,
buf: [1024]u8 = undefined, // same size as page.buf
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,
// Reference back to the Worker object (for postMessage to page)
// Reference back to the Worker object (for postMessage to frame)
_worker: *Worker,
// Event management for non-DOM targets in worker context
@@ -76,7 +76,7 @@ _on_messageerror: ?JS.Function.Global = null,
pub fn init(worker: *Worker, url: [:0]const u8) !*WorkerGlobalScope {
const arena = worker._arena;
const session = worker._page._session;
const session = worker._frame._session;
const factory = &session.factory;
const call_arena = try session.getArena(.small, "WorkerGlobalScope.call_arena");
@@ -187,7 +187,7 @@ pub fn setOnMessageError(self: *WorkerGlobalScope, setter: ?FunctionSetter) void
self._on_messageerror = getFunctionFromSetter(setter);
}
// Posts a message from the worker back to the page.
// Posts a message from the worker back to the frame.
// The message is cloned via structured clone and dispatched on the Worker object.
pub fn postMessage(self: *WorkerGlobalScope, data: JS.Value) !void {
try self._worker.receiveMessage(data);

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Node = @import("Node.zig");
const dump = @import("../dump.zig");
@@ -32,13 +32,13 @@ pub fn init() XMLSerializer {
return .{};
}
pub fn serializeToString(self: *const XMLSerializer, node: *Node, page: *Page) ![]const u8 {
pub fn serializeToString(self: *const XMLSerializer, node: *Node, frame: *Frame) ![]const u8 {
_ = self;
var buf = std.Io.Writer.Allocating.init(page.call_arena);
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
if (node.is(Node.Document)) |doc| {
try dump.root(doc, .{ .shadow = .skip }, &buf.writer, page);
try dump.root(doc, .{ .shadow = .skip }, &buf.writer, frame);
} else {
try dump.deep(node, .{ .shadow = .skip }, &buf.writer, page);
try dump.deep(node, .{ .shadow = .skip }, &buf.writer, frame);
}
// Not sure about this trim. But `dump` is meant to display relatively
// pretty HTML, so it does include newlines, which can result in a trailing

View File

@@ -19,7 +19,7 @@
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;
@@ -35,7 +35,7 @@ const PlayState = enum {
};
_rc: lp.RC(u32) = .{},
_page: *Page,
_frame: *Frame,
_arena: Allocator,
_effect: ?js.Object.Global = null,
@@ -51,13 +51,13 @@ _playState: PlayState = .idle,
// .running => .finished after 10ms when update() is callback.
//
// TODO add support for effect and timeline
pub fn init(page: *Page) !*Animation {
const arena = try page.getArena(.tiny, "Animation");
errdefer page.releaseArena(arena);
pub fn init(frame: *Frame) !*Animation {
const arena = try frame.getArena(.tiny, "Animation");
errdefer frame.releaseArena(arena);
const self = try arena.create(Animation);
self.* = .{
._page = page,
._frame = frame,
._arena = arena,
};
@@ -76,7 +76,7 @@ pub fn acquireRef(self: *Animation) void {
self._rc.acquire();
}
pub fn play(self: *Animation, page: *Page) !void {
pub fn play(self: *Animation, frame: *Frame) !void {
if (self._playState == .running) {
return;
}
@@ -86,7 +86,7 @@ pub fn play(self: *Animation, page: *Page) !void {
// Schedule the transition from .running => .finished in 10ms.
self.acquireRef();
try page.js.scheduler.add(
try frame.js.scheduler.add(
self,
Animation.update,
10,
@@ -105,7 +105,7 @@ pub fn cancel(self: *Animation) void {
self._playState = .idle;
}
pub fn finish(self: *Animation, page: *Page) void {
pub fn finish(self: *Animation, frame: *Frame) void {
if (self._playState == .finished) {
return;
}
@@ -114,11 +114,11 @@ pub fn finish(self: *Animation, page: *Page) void {
// resolve finished
if (self._finished_resolver) |resolver| {
page.js.local.?.toLocal(resolver).resolve("Animation.getFinished", self);
frame.js.local.?.toLocal(resolver).resolve("Animation.getFinished", self);
}
// call onfinish
if (self._onFinish) |func| {
page.js.local.?.toLocal(func).call(void, .{}) catch |err| {
frame.js.local.?.toLocal(func).call(void, .{}) catch |err| {
log.warn(.js, "Animation._onFinish", .{ .err = err });
};
}
@@ -128,24 +128,24 @@ pub fn reverse(_: *Animation) void {
log.warn(.not_implemented, "Animation.reverse", .{});
}
pub fn getFinished(self: *Animation, page: *Page) !js.Promise {
pub fn getFinished(self: *Animation, frame: *Frame) !js.Promise {
if (self._finished_resolver == null) {
const resolver = page.js.local.?.createPromiseResolver();
const resolver = frame.js.local.?.createPromiseResolver();
self._finished_resolver = try resolver.persist();
return resolver.promise();
}
return page.js.toLocal(self._finished_resolver).?.promise();
return frame.js.toLocal(self._finished_resolver).?.promise();
}
// The ready promise is immediately resolved.
pub fn getReady(self: *Animation, page: *Page) !js.Promise {
pub fn getReady(self: *Animation, frame: *Frame) !js.Promise {
if (self._ready_resolver == null) {
const resolver = page.js.local.?.createPromiseResolver();
const resolver = frame.js.local.?.createPromiseResolver();
resolver.resolve("Animation.getReady", self);
self._ready_resolver = try resolver.persist();
return resolver.promise();
}
return page.js.toLocal(self._ready_resolver).?.promise();
return frame.js.toLocal(self._ready_resolver).?.promise();
}
pub fn getEffect(self: *const Animation) ?js.Object.Global {
@@ -168,7 +168,7 @@ pub fn getStartTime(self: *const Animation) ?f64 {
return self._startTime;
}
pub fn setStartTime(self: *Animation, value: ?f64, page: *Page) !void {
pub fn setStartTime(self: *Animation, value: ?f64, frame: *Frame) !void {
self._startTime = value;
// if the startTime is null, don't play the animation.
@@ -176,7 +176,7 @@ pub fn setStartTime(self: *Animation, value: ?f64, page: *Page) !void {
return;
}
return self.play(page);
return self.play(frame);
}
pub fn getOnFinish(self: *const Animation) ?js.Function.Temp {
@@ -193,7 +193,7 @@ fn update(ctx: *anyopaque) !?u32 {
self._playState = .finished;
var ls: js.Local.Scope = undefined;
self._page.js.localScope(&ls);
self._frame.js.localScope(&ls);
defer ls.deinit();
// resolve finished
@@ -211,7 +211,7 @@ fn update(ctx: *anyopaque) !?u32 {
}
// No future change scheduled, set the object weak for garbage collection.
self.releaseRef(self._page._session);
self.releaseRef(self._frame._session);
return null;
}

View File

@@ -21,7 +21,7 @@ const std = @import("std");
const js = @import("../../js/js.zig");
const color = @import("../../color.zig");
const Page = @import("../../Page.zig");
const Frame = @import("../../Frame.zig");
const Canvas = @import("../element/html/Canvas.zig");
const ImageData = @import("../ImageData.zig");
@@ -41,8 +41,8 @@ pub fn getCanvas(self: *const CanvasRenderingContext2D) *Canvas {
return self._canvas;
}
pub fn getFillStyle(self: *const CanvasRenderingContext2D, page: *Page) ![]const u8 {
var w = std.Io.Writer.Allocating.init(page.call_arena);
pub fn getFillStyle(self: *const CanvasRenderingContext2D, frame: *Frame) ![]const u8 {
var w = std.Io.Writer.Allocating.init(frame.call_arena);
try self._fill_style.format(&w.writer);
return w.written();
}
@@ -67,15 +67,15 @@ pub fn createImageData(
maybe_height: ?u32,
/// Can be used if width and height provided.
maybe_settings: ?ImageData.ConstructorSettings,
page: *Page,
frame: *Frame,
) !*ImageData {
switch (width_or_image_data) {
.width => |width| {
const height = maybe_height orelse return error.TypeError;
return ImageData.init(width, height, maybe_settings, page);
return ImageData.init(width, height, maybe_settings, frame);
},
.image_data => |image_data| {
return ImageData.init(image_data._width, image_data._height, null, page);
return ImageData.init(image_data._width, image_data._height, null, frame);
},
}
}
@@ -88,12 +88,12 @@ pub fn getImageData(
_: i32, // sy
sw: i32,
sh: i32,
page: *Page,
frame: *Frame,
) !*ImageData {
if (sw <= 0 or sh <= 0) {
return error.IndexSizeError;
}
return ImageData.init(@intCast(sw), @intCast(sh), null, page);
return ImageData.init(@intCast(sw), @intCast(sh), null, frame);
}
pub fn save(_: *CanvasRenderingContext2D) void {}

View File

@@ -18,7 +18,7 @@
const std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Frame = @import("../../Frame.zig");
const Blob = @import("../Blob.zig");
const OffscreenCanvasRenderingContext2D = @import("OffscreenCanvasRenderingContext2D.zig");
@@ -37,8 +37,8 @@ const DrawingContext = union(enum) {
@"2d": *OffscreenCanvasRenderingContext2D,
};
pub fn constructor(width: u32, height: u32, page: *Page) !*OffscreenCanvas {
return page._factory.create(OffscreenCanvas{
pub fn constructor(width: u32, height: u32, frame: *Frame) !*OffscreenCanvas {
return frame._factory.create(OffscreenCanvas{
._width = width,
._height = height,
});
@@ -60,9 +60,9 @@ pub fn setHeight(self: *OffscreenCanvas, value: u32) void {
self._height = value;
}
pub fn getContext(_: *OffscreenCanvas, context_type: []const u8, page: *Page) !?DrawingContext {
pub fn getContext(_: *OffscreenCanvas, context_type: []const u8, frame: *Frame) !?DrawingContext {
if (std.mem.eql(u8, context_type, "2d")) {
const ctx = try page._factory.create(OffscreenCanvasRenderingContext2D{});
const ctx = try frame._factory.create(OffscreenCanvasRenderingContext2D{});
return .{ .@"2d" = ctx };
}
@@ -71,9 +71,9 @@ pub fn getContext(_: *OffscreenCanvas, context_type: []const u8, page: *Page) !?
/// 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, page: *Page) !js.Promise {
const blob = try Blob.init(null, null, page);
return page.js.local.?.resolvePromise(blob);
pub fn convertToBlob(_: *OffscreenCanvas, frame: *Frame) !js.Promise {
const blob = try Blob.init(null, null, frame._session);
return frame.js.local.?.resolvePromise(blob);
}
/// Returns an ImageBitmap with the rendered content (stub).

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const js = @import("../../js/js.zig");
const color = @import("../../color.zig");
const Page = @import("../../Page.zig");
const Frame = @import("../../Frame.zig");
const ImageData = @import("../ImageData.zig");
@@ -32,8 +32,8 @@ const OffscreenCanvasRenderingContext2D = @This();
/// TODO: Add support for `CanvasGradient` and `CanvasPattern`.
_fill_style: color.RGBA = color.RGBA.Named.black,
pub fn getFillStyle(self: *const OffscreenCanvasRenderingContext2D, page: *Page) ![]const u8 {
var w = std.Io.Writer.Allocating.init(page.call_arena);
pub fn getFillStyle(self: *const OffscreenCanvasRenderingContext2D, frame: *Frame) ![]const u8 {
var w = std.Io.Writer.Allocating.init(frame.call_arena);
try self._fill_style.format(&w.writer);
return w.written();
}
@@ -58,15 +58,15 @@ pub fn createImageData(
maybe_height: ?u32,
/// Can be used if width and height provided.
maybe_settings: ?ImageData.ConstructorSettings,
page: *Page,
frame: *Frame,
) !*ImageData {
switch (width_or_image_data) {
.width => |width| {
const height = maybe_height orelse return error.TypeError;
return ImageData.init(width, height, maybe_settings, page);
return ImageData.init(width, height, maybe_settings, frame);
},
.image_data => |image_data| {
return ImageData.init(image_data._width, image_data._height, null, page);
return ImageData.init(image_data._width, image_data._height, null, frame);
},
}
}
@@ -79,12 +79,12 @@ pub fn getImageData(
_: i32, // sy
sw: i32,
sh: i32,
page: *Page,
frame: *Frame,
) !*ImageData {
if (sw <= 0 or sh <= 0) {
return error.IndexSizeError;
}
return ImageData.init(@intCast(sw), @intCast(sh), null, page);
return ImageData.init(@intCast(sw), @intCast(sh), null, frame);
}
pub fn save(_: *OffscreenCanvasRenderingContext2D) void {}

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Frame = @import("../../Frame.zig");
pub fn registerTypes() []const type {
return &.{
@@ -168,16 +168,16 @@ pub fn getParameter(_: *const WebGLRenderingContext, pname: u32) []const u8 {
}
/// Enables a WebGL extension.
pub fn getExtension(_: *const WebGLRenderingContext, name: []const u8, page: *Page) !?Extension {
pub fn getExtension(_: *const WebGLRenderingContext, name: []const u8, frame: *Frame) !?Extension {
const tag = Extension.find(name) orelse return null;
return switch (tag) {
.WEBGL_debug_renderer_info => {
const info = try page._factory.create(Extension.Type.WEBGL_debug_renderer_info{});
const info = try frame._factory.create(Extension.Type.WEBGL_debug_renderer_info{});
return .{ .WEBGL_debug_renderer_info = info };
},
.WEBGL_lose_context => {
const ctx = try page._factory.create(Extension.Type.WEBGL_lose_context{});
const ctx = try frame._factory.create(Extension.Type.WEBGL_lose_context{});
return .{ .WEBGL_lose_context = ctx };
},
inline else => |comptime_enum| @unionInit(Extension, @tagName(comptime_enum), {}),

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Frame = @import("../../Frame.zig");
const CData = @import("../CData.zig");
@@ -25,8 +25,8 @@ const Comment = @This();
_proto: *CData,
pub fn init(str: ?js.NullableString, page: *Page) !*Comment {
const node = try page.createComment(if (str) |s| s.value else "");
pub fn init(str: ?js.NullableString, frame: *Frame) !*Comment {
const node = try frame.createComment(if (str) |s| s.value else "");
return node.as(Comment);
}

View File

@@ -17,15 +17,15 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Frame = @import("../../Frame.zig");
const CData = @import("../CData.zig");
const Text = @This();
_proto: *CData,
pub fn init(str: ?js.NullableString, page: *Page) !*Text {
const node = try page.createTextNode(if (str) |s| s.value else "");
pub fn init(str: ?js.NullableString, frame: *Frame) !*Text {
const node = try frame.createTextNode(if (str) |s| s.value else "");
return node.as(Text);
}
@@ -33,13 +33,13 @@ pub fn getWholeText(self: *Text) []const u8 {
return self._proto._data.str();
}
pub fn splitText(self: *Text, offset: usize, page: *Page) !*Text {
pub fn splitText(self: *Text, offset: usize, frame: *Frame) !*Text {
const data = self._proto._data.str();
const byte_offset = CData.utf16OffsetToUtf8(data, offset) catch return error.IndexSizeError;
const new_data = data[byte_offset..];
const new_node = try page.createTextNode(new_data);
const new_node = try frame.createTextNode(new_data);
const new_text = new_node.as(Text);
const node = self._proto.asNode();
@@ -48,11 +48,11 @@ pub fn splitText(self: *Text, offset: usize, page: *Page) !*Text {
// then truncate original node (step 8).
if (node.parentNode()) |parent| {
const next_sibling = node.nextSibling();
_ = try parent.insertBefore(new_node, next_sibling, page);
_ = try parent.insertBefore(new_node, next_sibling, frame);
// splitText-specific range updates (steps 7b-7e)
if (parent.getChildIndex(node)) |node_index| {
page.updateRangesForSplitText(node, new_node, @intCast(offset), parent, node_index);
frame.updateRangesForSplitText(node, new_node, @intCast(offset), parent, node_index);
}
}
@@ -60,7 +60,7 @@ pub fn splitText(self: *Text, offset: usize, page: *Page) !*Text {
// Use replaceData instead of setData so live range updates fire
// (matters for detached text nodes where steps 7b-7e were skipped).
const length = self._proto.getLength();
try self._proto.replaceData(offset, length - offset, "", page);
try self._proto.replaceData(offset, length - offset, "", frame);
return new_text;
}

View File

@@ -20,7 +20,7 @@ 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 GenericIterator = @import("iterator.zig").Entry;
@@ -39,9 +39,9 @@ pub const KeyIterator = GenericIterator(Iterator, "0");
pub const ValueIterator = GenericIterator(Iterator, "1");
pub const EntryIterator = GenericIterator(Iterator, null);
pub fn init(node: *Node, page: *Page) !*ChildNodes {
const arena = try page.getArena(.small, "ChildNodes");
errdefer page.releaseArena(arena);
pub fn init(node: *Node, frame: *Frame) !*ChildNodes {
const arena = try frame.getArena(.small, "ChildNodes");
errdefer frame.releaseArena(arena);
const self = try arena.create(ChildNodes);
self.* = .{
@@ -50,7 +50,7 @@ pub fn init(node: *Node, page: *Page) !*ChildNodes {
._last_index = 0,
._last_node = null,
._last_length = null,
._cached_version = page.version,
._cached_version = frame.version,
};
return self;
}
@@ -59,8 +59,8 @@ pub fn deinit(self: *const ChildNodes, session: *Session) void {
session.releaseArena(self._arena);
}
pub fn length(self: *ChildNodes, page: *Page) !u32 {
if (self.versionCheck(page)) {
pub fn length(self: *ChildNodes, frame: *Frame) !u32 {
if (self.versionCheck(frame)) {
if (self._last_length) |cached_length| {
return cached_length;
}
@@ -73,8 +73,8 @@ pub fn length(self: *ChildNodes, page: *Page) !u32 {
return len;
}
pub fn getAtIndex(self: *ChildNodes, index: usize, page: *Page) !?*Node {
_ = self.versionCheck(page);
pub fn getAtIndex(self: *ChildNodes, index: usize, frame: *Frame) !?*Node {
_ = self.versionCheck(frame);
var current = self._last_index;
var node: ?*std.DoublyLinkedList.Node = null;
@@ -102,20 +102,20 @@ pub fn first(self: *const ChildNodes) ?*std.DoublyLinkedList.Node {
return &(self._node._children orelse return null).first()._child_link;
}
pub fn keys(self: *ChildNodes, page: *Page) !*KeyIterator {
return .init(.{ .list = self }, page);
pub fn keys(self: *ChildNodes, frame: *Frame) !*KeyIterator {
return .init(.{ .list = self }, frame);
}
pub fn values(self: *ChildNodes, page: *Page) !*ValueIterator {
return .init(.{ .list = self }, page);
pub fn values(self: *ChildNodes, frame: *Frame) !*ValueIterator {
return .init(.{ .list = self }, frame);
}
pub fn entries(self: *ChildNodes, page: *Page) !*EntryIterator {
return .init(.{ .list = self }, page);
pub fn entries(self: *ChildNodes, frame: *Frame) !*EntryIterator {
return .init(.{ .list = self }, frame);
}
fn versionCheck(self: *ChildNodes, page: *Page) bool {
const current = page.version;
fn versionCheck(self: *ChildNodes, frame: *Frame) bool {
const current = frame.version;
if (current == self._cached_version) {
return true;
}
@@ -127,7 +127,7 @@ fn versionCheck(self: *ChildNodes, page: *Page) bool {
}
const NodeList = @import("NodeList.zig");
pub fn runtimeGenericWrap(self: *ChildNodes, _: *const Page) !*NodeList {
pub fn runtimeGenericWrap(self: *ChildNodes, _: *const Frame) !*NodeList {
const nl = try self._arena.create(NodeList);
nl.* = .{
._data = .{ .child_nodes = self },
@@ -141,9 +141,9 @@ const Iterator = struct {
const Entry = struct { u32, *Node };
pub fn next(self: *Iterator, page: *const Page) !?Entry {
pub fn next(self: *Iterator, frame: *const Frame) !?Entry {
const index = self.index;
const node = try self.list.getAtIndex(index, page) orelse return null;
const node = try self.list.getAtIndex(index, frame) orelse return null;
self.index = index + 1;
return .{ index, node };
}

View File

@@ -20,7 +20,7 @@ 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 Element = @import("../Element.zig");
const GenericIterator = @import("iterator.zig").Entry;
@@ -44,16 +44,16 @@ const Lookup = std.StringArrayHashMapUnmanaged(void);
const WHITESPACE = " \t\n\r\x0C";
pub fn length(self: *const DOMTokenList, page: *Page) !u32 {
const tokens = try self.getTokens(page.call_arena);
pub fn length(self: *const DOMTokenList, frame: *Frame) !u32 {
const tokens = try self.getTokens(frame.call_arena);
return @intCast(tokens.count());
}
// TODO: soooo..inefficient
pub fn item(self: *const DOMTokenList, index: usize, page: *Page) !?[]const u8 {
pub fn item(self: *const DOMTokenList, index: usize, frame: *Frame) !?[]const u8 {
var i: usize = 0;
const allocator = page.call_arena;
const allocator = frame.call_arena;
var seen: std.StringArrayHashMapUnmanaged(void) = .empty;
var it = std.mem.tokenizeAny(u8, self.getValue(), WHITESPACE);
@@ -79,12 +79,12 @@ pub fn contains(self: *const DOMTokenList, search: []const u8) !bool {
return false;
}
pub fn add(self: *DOMTokenList, tokens: []const []const u8, page: *Page) !void {
pub fn add(self: *DOMTokenList, tokens: []const []const u8, frame: *Frame) !void {
for (tokens) |token| {
try validateToken(token);
}
const allocator = page.call_arena;
const allocator = frame.call_arena;
var lookup = try self.getTokens(allocator);
try lookup.ensureUnusedCapacity(allocator, tokens.len);
@@ -92,22 +92,22 @@ pub fn add(self: *DOMTokenList, tokens: []const []const u8, page: *Page) !void {
try lookup.put(allocator, token, {});
}
try self.updateAttribute(lookup, page);
try self.updateAttribute(lookup, frame);
}
pub fn remove(self: *DOMTokenList, tokens: []const []const u8, page: *Page) !void {
pub fn remove(self: *DOMTokenList, tokens: []const []const u8, frame: *Frame) !void {
for (tokens) |token| {
try validateToken(token);
}
var lookup = try self.getTokens(page.call_arena);
var lookup = try self.getTokens(frame.call_arena);
for (tokens) |token| {
_ = lookup.orderedRemove(token);
}
try self.updateAttribute(lookup, page);
try self.updateAttribute(lookup, frame);
}
pub fn toggle(self: *DOMTokenList, token: []const u8, force: ?bool, page: *Page) !bool {
pub fn toggle(self: *DOMTokenList, token: []const u8, force: ?bool, frame: *Frame) !bool {
try validateToken(token);
const has_token = try self.contains(token);
@@ -116,30 +116,30 @@ pub fn toggle(self: *DOMTokenList, token: []const u8, force: ?bool, page: *Page)
if (f) {
if (!has_token) {
const tokens_to_add = [_][]const u8{token};
try self.add(&tokens_to_add, page);
try self.add(&tokens_to_add, frame);
}
return true;
} else {
if (has_token) {
const tokens_to_remove = [_][]const u8{token};
try self.remove(&tokens_to_remove, page);
try self.remove(&tokens_to_remove, frame);
}
return false;
}
} else {
if (has_token) {
const tokens_to_remove = [_][]const u8{token};
try self.remove(tokens_to_remove[0..], page);
try self.remove(tokens_to_remove[0..], frame);
return false;
} else {
const tokens_to_add = [_][]const u8{token};
try self.add(tokens_to_add[0..], page);
try self.add(tokens_to_add[0..], frame);
return true;
}
}
}
pub fn replace(self: *DOMTokenList, old_token: []const u8, new_token: []const u8, page: *Page) !bool {
pub fn replace(self: *DOMTokenList, old_token: []const u8, new_token: []const u8, frame: *Frame) !bool {
// Validate in spec order: both empty first, then both whitespace
if (old_token.len == 0 or new_token.len == 0) {
return error.SyntaxError;
@@ -151,8 +151,8 @@ pub fn replace(self: *DOMTokenList, old_token: []const u8, new_token: []const u8
return error.InvalidCharacterError;
}
const allocator = page.call_arena;
var lookup = try self.getTokens(page.call_arena);
const allocator = frame.call_arena;
var lookup = try self.getTokens(frame.call_arena);
// Check if old_token exists
if (!lookup.contains(old_token)) {
@@ -161,7 +161,7 @@ pub fn replace(self: *DOMTokenList, old_token: []const u8, new_token: []const u8
// If replacing with the same token, still need to trigger mutation
if (std.mem.eql(u8, new_token, old_token)) {
try self.updateAttribute(lookup, page);
try self.updateAttribute(lookup, frame);
return true;
}
@@ -192,7 +192,7 @@ pub fn replace(self: *DOMTokenList, old_token: []const u8, new_token: []const u8
try new_lookup.put(allocator, token, {});
}
try self.updateAttribute(new_lookup, page);
try self.updateAttribute(new_lookup, frame);
return true;
}
@@ -200,26 +200,26 @@ pub fn getValue(self: *const DOMTokenList) []const u8 {
return self._element.getAttributeSafe(self._attribute_name) orelse "";
}
pub fn setValue(self: *DOMTokenList, value: String, page: *Page) !void {
try self._element.setAttribute(self._attribute_name, value, page);
pub fn setValue(self: *DOMTokenList, value: String, frame: *Frame) !void {
try self._element.setAttribute(self._attribute_name, value, frame);
}
pub fn keys(self: *DOMTokenList, page: *Page) !*KeyIterator {
return .init(.{ .list = self }, page);
pub fn keys(self: *DOMTokenList, frame: *Frame) !*KeyIterator {
return .init(.{ .list = self }, frame);
}
pub fn values(self: *DOMTokenList, page: *Page) !*ValueIterator {
return .init(.{ .list = self }, page);
pub fn values(self: *DOMTokenList, frame: *Frame) !*ValueIterator {
return .init(.{ .list = self }, frame);
}
pub fn entries(self: *DOMTokenList, page: *Page) !*EntryIterator {
return .init(.{ .list = self }, page);
pub fn entries(self: *DOMTokenList, frame: *Frame) !*EntryIterator {
return .init(.{ .list = self }, frame);
}
pub fn forEach(self: *DOMTokenList, cb_: js.Function, js_this_: ?js.Object, page: *Page) !void {
pub fn forEach(self: *DOMTokenList, cb_: js.Function, js_this_: ?js.Object, frame: *Frame) !void {
const cb = if (js_this_) |js_this| try cb_.withThis(js_this) else cb_;
const allocator = page.call_arena;
const allocator = frame.call_arena;
var i: i32 = 0;
var seen: std.StringArrayHashMapUnmanaged(void) = .empty;
@@ -264,16 +264,16 @@ fn validateToken(token: []const u8) !void {
}
}
fn updateAttribute(self: *DOMTokenList, tokens: Lookup, page: *Page) !void {
fn updateAttribute(self: *DOMTokenList, tokens: Lookup, frame: *Frame) !void {
if (tokens.count() > 0) {
const joined = try std.mem.join(page.call_arena, " ", tokens.keys());
return self._element.setAttribute(self._attribute_name, .wrap(joined), page);
const joined = try std.mem.join(frame.call_arena, " ", tokens.keys());
return self._element.setAttribute(self._attribute_name, .wrap(joined), frame);
}
// Only remove attribute if it didn't exist before (was null)
// If it existed (even as ""), set it to "" to preserve its existence
if (self._element.hasAttributeSafe(self._attribute_name)) {
try self._element.setAttribute(self._attribute_name, .wrap(""), page);
try self._element.setAttribute(self._attribute_name, .wrap(""), frame);
}
}
@@ -283,9 +283,9 @@ const Iterator = struct {
const Entry = struct { u32, []const u8 };
pub fn next(self: *Iterator, page: *Page) !?Entry {
pub fn next(self: *Iterator, frame: *Frame) !?Entry {
const index = self.index;
const node = try self.list.item(index, page) orelse return null;
const node = try self.list.item(index, frame) orelse return null;
self.index = index + 1;
return .{ index, node };
}
@@ -303,11 +303,11 @@ pub const JsApi = struct {
pub const length = bridge.accessor(DOMTokenList.length, null, .{});
pub const item = bridge.function(_item, .{});
fn _item(self: *const DOMTokenList, index: i32, page: *Page) !?[]const u8 {
fn _item(self: *const DOMTokenList, index: i32, frame: *Frame) !?[]const u8 {
if (index < 0) {
return null;
}
return self.item(@intCast(index), page);
return self.item(@intCast(index), frame);
}
pub const contains = bridge.function(DOMTokenList.contains, .{ .dom_exception = true });

View File

@@ -20,7 +20,7 @@ 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 Node = @import("../Node.zig");
const Element = @import("../Element.zig");
const TreeWalker = @import("../TreeWalker.zig");
@@ -33,18 +33,18 @@ _last_index: usize,
_last_length: ?u32,
_cached_version: usize,
pub fn init(root: *Node, page: *Page) HTMLAllCollection {
pub fn init(root: *Node, frame: *Frame) HTMLAllCollection {
return .{
._last_index = 0,
._last_length = null,
._tw = TreeWalker.FullExcludeSelf.init(root, .{}),
._cached_version = page.version,
._cached_version = frame.version,
};
}
fn versionCheck(self: *HTMLAllCollection, page: *const Page) bool {
if (self._cached_version != page.version) {
self._cached_version = page.version;
fn versionCheck(self: *HTMLAllCollection, frame: *const Frame) bool {
if (self._cached_version != frame.version) {
self._cached_version = frame.version;
self._last_index = 0;
self._last_length = null;
self._tw.reset();
@@ -53,8 +53,8 @@ fn versionCheck(self: *HTMLAllCollection, page: *const Page) bool {
return true;
}
pub fn length(self: *HTMLAllCollection, page: *const Page) u32 {
if (self.versionCheck(page)) {
pub fn length(self: *HTMLAllCollection, frame: *const Frame) u32 {
if (self.versionCheck(frame)) {
if (self._last_length) |cached_length| {
return cached_length;
}
@@ -76,8 +76,8 @@ pub fn length(self: *HTMLAllCollection, page: *const Page) u32 {
return l;
}
pub fn getAtIndex(self: *HTMLAllCollection, index: usize, page: *const Page) ?*Element {
_ = self.versionCheck(page);
pub fn getAtIndex(self: *HTMLAllCollection, index: usize, frame: *const Frame) ?*Element {
_ = self.versionCheck(frame);
var current = self._last_index;
if (index <= current) {
current = 0;
@@ -98,15 +98,15 @@ pub fn getAtIndex(self: *HTMLAllCollection, index: usize, page: *const Page) ?*E
return null;
}
pub fn getByName(self: *HTMLAllCollection, name: []const u8, page: *Page) ?*Element {
pub fn getByName(self: *HTMLAllCollection, name: []const u8, frame: *Frame) ?*Element {
// First, try fast ID lookup using the document's element map
if (page.document._elements_by_id.get(name)) |el| {
if (frame.document._elements_by_id.get(name)) |el| {
return el;
}
// Fall back to searching by name attribute
// Clone the tree walker to preserve _last_index optimization
_ = self.versionCheck(page);
_ = self.versionCheck(frame);
var tw = self._tw.clone();
tw.reset();
@@ -127,10 +127,10 @@ const CAllAsFunctionArg = union(enum) {
index: u32,
id: []const u8,
};
pub fn callable(self: *HTMLAllCollection, arg: CAllAsFunctionArg, page: *Page) ?*Element {
pub fn callable(self: *HTMLAllCollection, arg: CAllAsFunctionArg, frame: *Frame) ?*Element {
return switch (arg) {
.index => |i| self.getAtIndex(i, page),
.id => |id| self.getByName(id, page),
.index => |i| self.getAtIndex(i, frame),
.id => |id| self.getByName(id, frame),
};
}
@@ -175,11 +175,11 @@ pub const JsApi = struct {
pub const @"[str]" = bridge.namedIndexed(HTMLAllCollection.getByName, null, null, .{ .null_as_undefined = true });
pub const item = bridge.function(_item, .{});
fn _item(self: *HTMLAllCollection, index: i32, page: *Page) ?*Element {
fn _item(self: *HTMLAllCollection, index: i32, frame: *Frame) ?*Element {
if (index < 0) {
return null;
}
return self.getAtIndex(@intCast(index), page);
return self.getAtIndex(@intCast(index), frame);
}
pub const namedItem = bridge.function(HTMLAllCollection.getByName, .{});

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Frame = @import("../../Frame.zig");
const Element = @import("../Element.zig");
const TreeWalker = @import("../TreeWalker.zig");
const NodeLive = @import("node_live.zig").NodeLive;
@@ -57,24 +57,24 @@ _data: union(Mode) {
empty: void,
},
pub fn length(self: *HTMLCollection, page: *const Page) u32 {
pub fn length(self: *HTMLCollection, frame: *const Frame) u32 {
return switch (self._data) {
.empty => 0,
inline else => |*impl| impl.length(page),
inline else => |*impl| impl.length(frame),
};
}
pub fn getAtIndex(self: *HTMLCollection, index: usize, page: *const Page) ?*Element {
pub fn getAtIndex(self: *HTMLCollection, index: usize, frame: *const Frame) ?*Element {
return switch (self._data) {
.empty => null,
inline else => |*impl| impl.getAtIndex(index, page),
inline else => |*impl| impl.getAtIndex(index, frame),
};
}
pub fn getByName(self: *HTMLCollection, name: []const u8, page: *Page) ?*Element {
pub fn getByName(self: *HTMLCollection, name: []const u8, frame: *Frame) ?*Element {
return switch (self._data) {
.empty => null,
inline else => |*impl| impl.getByName(name, page),
inline else => |*impl| impl.getByName(name, frame),
};
}
@@ -149,11 +149,11 @@ pub const JsApi = struct {
pub const @"[str]" = bridge.namedIndexed(HTMLCollection.getByName, null, null, .{ .null_as_undefined = true });
pub const item = bridge.function(_item, .{});
fn _item(self: *HTMLCollection, index: i32, page: *Page) ?*Element {
fn _item(self: *HTMLCollection, index: i32, frame: *Frame) ?*Element {
if (index < 0) {
return null;
}
return self.getAtIndex(@intCast(index), page);
return self.getAtIndex(@intCast(index), frame);
}
pub const namedItem = bridge.function(HTMLCollection.getByName, .{});

View File

@@ -18,7 +18,7 @@
const std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Frame = @import("../../Frame.zig");
const Element = @import("../Element.zig");
const NodeList = @import("NodeList.zig");
@@ -36,15 +36,15 @@ pub const NamedItemResult = union(enum) {
radio_node_list: *RadioNodeList,
};
pub fn length(self: *HTMLFormControlsCollection, page: *Page) u32 {
return self._proto.length(page);
pub fn length(self: *HTMLFormControlsCollection, frame: *Frame) u32 {
return self._proto.length(frame);
}
pub fn getAtIndex(self: *HTMLFormControlsCollection, index: usize, page: *Page) ?*Element {
return self._proto.getAtIndex(index, page);
pub fn getAtIndex(self: *HTMLFormControlsCollection, index: usize, frame: *Frame) ?*Element {
return self._proto.getAtIndex(index, frame);
}
pub fn namedItem(self: *HTMLFormControlsCollection, name: []const u8, page: *Page) !?NamedItemResult {
pub fn namedItem(self: *HTMLFormControlsCollection, name: []const u8, frame: *Frame) !?NamedItemResult {
if (name.len == 0) {
return null;
}
@@ -79,13 +79,13 @@ pub fn namedItem(self: *HTMLFormControlsCollection, name: []const u8, page: *Pag
count += 1;
if (count == 2) {
const radio_node_list = try page._factory.create(RadioNodeList{
const radio_node_list = try frame._factory.create(RadioNodeList{
._proto = undefined,
._form_collection = self,
._name = try page.dupeString(name),
._name = try frame.dupeString(name),
});
radio_node_list._proto = try page._factory.create(NodeList{ ._data = .{ .radio_node_list = radio_node_list } });
radio_node_list._proto = try frame._factory.create(NodeList{ ._data = .{ .radio_node_list = radio_node_list } });
return .{ .radio_node_list = radio_node_list };
}

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Frame = @import("../../Frame.zig");
const Node = @import("../Node.zig");
const Element = @import("../Element.zig");
const HTMLCollection = @import("HTMLCollection.zig");
@@ -30,17 +30,17 @@ _proto: *HTMLCollection,
_select: *@import("../element/html/Select.zig"),
// Forward length to HTMLCollection
pub fn length(self: *HTMLOptionsCollection, page: *Page) u32 {
return self._proto.length(page);
pub fn length(self: *HTMLOptionsCollection, frame: *Frame) u32 {
return self._proto.length(frame);
}
// Forward indexed access to HTMLCollection
pub fn getAtIndex(self: *HTMLOptionsCollection, index: usize, page: *Page) ?*Element {
return self._proto.getAtIndex(index, page);
pub fn getAtIndex(self: *HTMLOptionsCollection, index: usize, frame: *Frame) ?*Element {
return self._proto.getAtIndex(index, frame);
}
pub fn getByName(self: *HTMLOptionsCollection, name: []const u8, page: *Page) ?*Element {
return self._proto.getByName(name, page);
pub fn getByName(self: *HTMLOptionsCollection, name: []const u8, frame: *Frame) ?*Element {
return self._proto.getByName(name, frame);
}
// Forward selectedIndex to the owning select element
@@ -60,7 +60,7 @@ const AddBeforeOption = union(enum) {
};
// Add a new option element
pub fn add(self: *HTMLOptionsCollection, element: *Option, before_: ?AddBeforeOption, page: *Page) !void {
pub fn add(self: *HTMLOptionsCollection, element: *Option, before_: ?AddBeforeOption, frame: *Frame) !void {
const select_node = self._select.asNode();
const element_node = element.asElement().asNode();
@@ -68,24 +68,24 @@ pub fn add(self: *HTMLOptionsCollection, element: *Option, before_: ?AddBeforeOp
if (before_) |before| {
switch (before) {
.index => |idx| {
if (self.getAtIndex(idx, page)) |el| {
if (self.getAtIndex(idx, frame)) |el| {
before_node = el.asNode();
}
},
.option => |before_option| before_node = before_option.asNode(),
}
}
_ = try select_node.insertBefore(element_node, before_node, page);
_ = try select_node.insertBefore(element_node, before_node, frame);
}
// Remove an option element by index
pub fn remove(self: *HTMLOptionsCollection, index: i32, page: *Page) void {
pub fn remove(self: *HTMLOptionsCollection, index: i32, frame: *Frame) void {
if (index < 0) {
return;
}
if (self._proto.getAtIndex(@intCast(index), page)) |element| {
element.remove(page);
if (self._proto.getAtIndex(@intCast(index), frame)) |element| {
element.remove(frame);
}
}

View File

@@ -20,7 +20,7 @@ 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");
@@ -57,44 +57,44 @@ pub fn acquireRef(self: *NodeList) void {
self._rc.acquire();
}
pub fn length(self: *NodeList, page: *Page) !u32 {
pub fn length(self: *NodeList, frame: *Frame) !u32 {
return switch (self._data) {
.child_nodes => |impl| impl.length(page),
.child_nodes => |impl| impl.length(frame),
.selector_list => |impl| @intCast(impl.getLength()),
.radio_node_list => |impl| impl.getLength(),
.name => |*impl| impl.length(page),
.name => |*impl| impl.length(frame),
};
}
pub fn indexedGet(self: *NodeList, index: usize, page: *Page) !*Node {
return try self.getAtIndex(index, page) orelse return error.NotHandled;
pub fn indexedGet(self: *NodeList, index: usize, frame: *Frame) !*Node {
return try self.getAtIndex(index, frame) orelse return error.NotHandled;
}
pub fn getAtIndex(self: *NodeList, index: usize, page: *Page) !?*Node {
pub fn getAtIndex(self: *NodeList, index: usize, frame: *Frame) !?*Node {
return switch (self._data) {
.child_nodes => |impl| impl.getAtIndex(index, page),
.child_nodes => |impl| impl.getAtIndex(index, frame),
.selector_list => |impl| impl.getAtIndex(index),
.radio_node_list => |impl| impl.getAtIndex(index, page),
.name => |*impl| if (impl.getAtIndex(index, page)) |el| el.asNode() else null,
.radio_node_list => |impl| impl.getAtIndex(index, frame),
.name => |*impl| if (impl.getAtIndex(index, frame)) |el| el.asNode() else null,
};
}
pub fn keys(self: *NodeList, page: *Page) !*KeyIterator {
return .init(.{ .list = self }, page);
pub fn keys(self: *NodeList, frame: *Frame) !*KeyIterator {
return .init(.{ .list = self }, frame);
}
pub fn values(self: *NodeList, page: *Page) !*ValueIterator {
return .init(.{ .list = self }, page);
pub fn values(self: *NodeList, frame: *Frame) !*ValueIterator {
return .init(.{ .list = self }, frame);
}
pub fn entries(self: *NodeList, page: *Page) !*EntryIterator {
return .init(.{ .list = self }, page);
pub fn entries(self: *NodeList, frame: *Frame) !*EntryIterator {
return .init(.{ .list = self }, frame);
}
pub fn forEach(self: *NodeList, cb: js.Function, page: *Page) !void {
pub fn forEach(self: *NodeList, cb: js.Function, frame: *Frame) !void {
var i: i32 = 0;
while (true) : (i += 1) {
const node = try self.getAtIndex(@intCast(i), page) orelse return;
const node = try self.getAtIndex(@intCast(i), frame) orelse return;
var caught: js.TryCatch.Caught = undefined;
cb.tryCall(void, .{ node, i, self }, &caught) catch {
@@ -127,9 +127,9 @@ const Iterator = struct {
self.list.acquireRef();
}
pub fn next(self: *Iterator, page: *Page) !?Entry {
pub fn next(self: *Iterator, frame: *Frame) !?Entry {
const index = self.index;
const node = try self.list.getAtIndex(index, page) orelse return null;
const node = try self.list.getAtIndex(index, frame) orelse return null;
self.index = index + 1;
return .{ index, node };
}
@@ -154,9 +154,9 @@ pub const JsApi = struct {
pub const forEach = bridge.function(NodeList.forEach, .{});
pub const symbol_iterator = bridge.iterator(NodeList.values, .{});
fn getIndexes(self: *NodeList, page: *Page) !js.Array {
const len = try self.length(page);
var arr = page.js.local.?.newArray(len);
fn getIndexes(self: *NodeList, frame: *Frame) !js.Array {
const len = try self.length(frame);
var arr = frame.js.local.?.newArray(len);
for (0..len) |i| {
_ = try arr.set(@intCast(i), i, .{});
}

View File

@@ -18,7 +18,7 @@
const std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Frame = @import("../../Frame.zig");
const Node = @import("../Node.zig");
const Element = @import("../Element.zig");
@@ -44,10 +44,10 @@ pub fn getLength(self: *RadioNodeList) !u32 {
return i;
}
pub fn getAtIndex(self: *RadioNodeList, index: usize, page: *Page) !?*Node {
pub fn getAtIndex(self: *RadioNodeList, index: usize, frame: *Frame) !?*Node {
var i: usize = 0;
var current: usize = 0;
while (self._form_collection.getAtIndex(i, page)) |element| : (i += 1) {
while (self._form_collection.getAtIndex(i, frame)) |element| : (i += 1) {
if (!self.matches(element)) {
continue;
}
@@ -74,7 +74,7 @@ pub fn getValue(self: *RadioNodeList) ![]const u8 {
return "";
}
pub fn setValue(self: *RadioNodeList, value: []const u8, page: *Page) !void {
pub fn setValue(self: *RadioNodeList, value: []const u8, frame: *Frame) !void {
var it = try self._form_collection.iterator();
while (it.next()) |element| {
const input = element.is(Input) orelse continue;
@@ -92,7 +92,7 @@ pub fn setValue(self: *RadioNodeList, value: []const u8, page: *Page) !void {
};
if (matches_value) {
try input.setChecked(true, page);
try input.setChecked(true, frame);
return;
}
}

View File

@@ -19,7 +19,7 @@
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;

Some files were not shown because too many files have changed in this diff Show More