Merge branch 'main' into agent-regression

This commit is contained in:
Adrià Arrufat
2026-07-07 11:43:12 +02:00
139 changed files with 2681 additions and 1127 deletions

View File

@@ -13,7 +13,7 @@ inputs:
zig-v8:
description: 'zig v8 version to install'
required: false
default: 'v0.4.9'
default: 'v0.5.0'
v8:
description: 'v8 version to install'
required: false

View File

@@ -13,7 +13,7 @@ inputs:
zig-v8:
description: 'zig-v8 release tag the prebuilt lib came from'
required: false
default: 'v0.4.9'
default: 'v0.5.0'
runs:
using: "composite"

View File

@@ -281,7 +281,7 @@ jobs:
fi
mkdir -p $CG_ROOT/$CG
cgexec -g memory:$CG ./lightpanda serve & echo $! > LPD.pid
cgexec -g memory:$CG ./lightpanda serve --insecure-disable-tls-host-verification & echo $! > LPD.pid
sleep 2

View File

@@ -4,7 +4,7 @@ FROM debian:stable-slim
ARG MINISIG=0.12
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG V8=14.9.207.35
ARG ZIG_V8=v0.4.9
ARG ZIG_V8=v0.5.0
ARG TARGETPLATFORM
RUN apt-get update -yq && \

View File

@@ -168,10 +168,11 @@ Run `/save` to export one from your current session, then replay it with
you can prototype with the LLM and ship the output to production without a
model at runtime.
It supports Anthropic, OpenAI, Gemini, Hugging Face, and local models via
Ollama. You can also run without an LLM using `--no-llm`, which drops you into
the REPL. See the [agent documentation](https://lightpanda.io/docs/usage/agent)
for the full reference.
It supports Anthropic, OpenAI, Gemini, Google Vertex AI, Hugging Face, and
local models via Ollama. You can also run without an LLM using `--no-llm`,
which drops you into the REPL. See the
[agent documentation](https://lightpanda.io/docs/usage/agent) for the full
reference.
```console
./lightpanda agent # auto-detects API key from env
@@ -179,6 +180,8 @@ for the full reference.
./lightpanda agent --no-llm # basic REPL, no LLM
./lightpanda agent session.js # run a recorded script
./lightpanda agent --provider gemini --task "..." # force a specific provider
VERTEX_API_KEY=... ./lightpanda agent --provider vertex # Vertex AI, express mode
GOOGLE_CLOUD_PROJECT=my-proj ./lightpanda agent --provider vertex # Vertex AI, token via gcloud auth
```
### Native MCP and skill

View File

@@ -5,8 +5,8 @@
.minimum_zig_version = "0.15.2",
.dependencies = .{
.v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.4.9.tar.gz",
.hash = "v8-0.0.0-xddH63jnAgB8guO5jPzJD4pnX-CI5od-g4v-fIIjmQXr",
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.5.0.tar.gz",
.hash = "v8-0.0.0-xddH613pAgDFIxC9Xq1zCPVj2n-8Dxsm-TVMfSRVzq6Z",
},
// .v8 = .{ .path = "../zig-v8-fork" },
.brotli = .{
@@ -35,8 +35,8 @@
.hash = "sqlite3-3.51.0-DMxLWssOAABZ8cAvU_LfBIbp0kZjm824PU8sSLXpEDdr",
},
.zenai = .{
.url = "git+https://github.com/lightpanda-io/zenai.git#a8c031222f7545c363b91197394b50024d370a7e",
.hash = "zenai-0.0.0-iOY_VB6zBABxSlIgM38xQQv53QEzy8PuDgHopcQ4mwbV",
.url = "git+https://github.com/lightpanda-io/zenai.git#e58635146733157eb4936388b21a071fcabfa687",
.hash = "zenai-0.0.0-iOY_VIf0BADH7PvSOlnVf564krXnAlns_1IIhh9ZDdwz",
},
.isocline = .{
.url = "git+https://github.com/arrufat/isocline?ref=lightpanda#832a9fe25f5f4458fcc47b5acc7c21db669c2f47",

View File

@@ -353,14 +353,14 @@ pub fn proxyBearerToken(self: *const Config) ?[:0]const u8 {
pub fn httpMaxConcurrent(self: *const Config) u8 {
return switch (self.mode) {
inline .serve, .fetch, .mcp, .agent => |opts| opts.http_max_concurrent orelse 10,
inline .serve, .fetch, .mcp, .agent => |opts| opts.http_max_concurrent orelse 40,
else => unreachable,
};
}
pub fn httpMaxHostOpen(self: *const Config) u8 {
return switch (self.mode) {
inline .serve, .fetch, .mcp, .agent => |opts| opts.http_max_host_open orelse 4,
inline .serve, .fetch, .mcp, .agent => |opts| opts.http_max_host_open orelse 6,
else => unreachable,
};
}

View File

@@ -162,7 +162,7 @@ const script_skill =
\\ await Promise.all(pages.map((p, i) => p.goto(items[i].url))); // all in flight at once
\\ return pages.map((p, i) => ({ ...items[i], ...p.extract({ /* schema */ }) }));
\\ ```
\\ - Concurrency is bounded by the HTTP connection pool (10 by default, `--http-max-concurrent`); extra navigations queue rather than fail. For long lists, fan out in batches and `page.close()` each page once read so its memory is reclaimed.
\\ - Concurrency is bounded by the HTTP connection pool (40 by default, `--http-max-concurrent`); extra navigations queue rather than fail. For long lists, fan out in batches and `page.close()` each page once read so its memory is reclaimed.
\\ - `Promise.all` rejects the whole batch if any `goto` *fails* (a timeout does not reject); use `Promise.allSettled` when partial results are fine.
\\ - Walk **serially** on one page (`for (const it of items) { await page.goto(it.url); … }`) only when the steps depend on each other — each page decides the next URL, or they share login/session state.
\\3. **`evaluate` is a last resort, not a reading tool.** A `querySelectorAll`-and-parse `page.evaluate` block is always wrong: lift the raw strings with `page.extract`, then trim/split/parse them in top-level JS. Reserve `page.evaluate` for behavior that must run inside the page and no builtin covers — and remember its state dies on every navigation/reload, while script variables persist.
@@ -222,6 +222,9 @@ const synthesis_prompt =
allocator: std.mem.Allocator,
ai_client: ?zenai.provider.Client,
model_credentials: ?Credentials,
/// Allocated credentials key (Vertex gcloud token) — other keys are unowned
/// env pointers. The AI client references it: free only after client deinit.
owned_key: ?[:0]const u8,
/// True when the no-LLM state is a persisted preference (remembered null
/// provider or runtime `/provider null`), so `reportSaved` writes
/// `provider = null`. A transient `--no-llm` run leaves it false so saving
@@ -340,6 +343,8 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent
if (banner_before) welcome.print(resolve);
const resolved: ?settings.ResolvedProvider = if (resolve) try settings.resolveCredentials(allocator, opts, remembered, will_repl) else null;
// Before the ai_client errdefer, so on unwind the client goes first.
errdefer if (resolved) |r| if (r.key_owned) allocator.free(r.credentials.key);
if (will_repl and !banner_before and resolved != null) welcome.print(resolve);
const llm: ?Credentials = if (resolved) |r| r.credentials else null;
@@ -394,6 +399,7 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent
.allocator = allocator,
.ai_client = null,
.model_credentials = llm,
.owned_key = if (resolved) |r| (if (r.key_owned) r.credentials.key else null) else null,
.no_llm_persisted = remembered_no_llm,
.model_base_url = opts.base_url,
.model_completions = null,
@@ -455,6 +461,7 @@ pub fn deinit(self: *Agent) void {
self.browser.deinit();
self.notification.deinit();
if (self.ai_client) |ai_client| ai_client.deinit(self.allocator);
if (self.owned_key) |k| self.allocator.free(k);
self.allocator.free(self.model);
for (self.available_providers) |p| self.allocator.free(p);
self.allocator.free(self.available_providers);
@@ -915,11 +922,28 @@ fn handleProvider(self: *Agent, _: std.mem.Allocator, rest: []const u8) void {
self.terminal.printError("unknown provider: {s} (or 'null' to disable the LLM)", .{trimmed});
return;
};
if (self.model_credentials) |current| if (provider == current.provider) {
// Re-selecting vertex falls through — that's the token-refresh path.
const vertex_project = provider == .vertex and settings.vertexProjectMode();
if (self.model_credentials) |current| if (provider == current.provider and !vertex_project) {
self.terminal.printInfo("provider: {s}", .{@tagName(provider)});
return;
};
if (vertex_project) {
const token = settings.gcloudAccessToken(self.allocator) catch |err| {
self.terminal.printError("could not obtain a Vertex access token: {s} (details above)", .{@errorName(err)});
return;
};
self.setProvider(.{ .provider = .vertex, .key = token }, token) catch |err| {
self.allocator.free(token);
self.terminal.printError("failed to set provider: {s}", .{@errorName(err)});
};
return;
}
const key = zenai.provider.envApiKey(provider) orelse {
if (provider == .vertex) {
self.terminal.printError("vertex needs VERTEX_API_KEY (express mode) or GOOGLE_CLOUD_PROJECT (project mode, token via gcloud)", .{});
return;
}
self.terminal.printError("no API key for {s}; set {s}", .{ @tagName(provider), zenai.provider.envVarName(provider) });
return;
};
@@ -932,7 +956,7 @@ fn handleProvider(self: *Agent, _: std.mem.Allocator, rest: []const u8) void {
self.terminal.printError("no llama.cpp server with a loaded model at {s}", .{self.model_base_url orelse zenai.provider.llama_cpp_default_base_url});
return;
}
self.setProvider(.{ .provider = provider, .key = key }) catch |err| {
self.setProvider(.{ .provider = provider, .key = key }, null) catch |err| {
self.terminal.printError("failed to set provider: {s}", .{@errorName(err)});
};
}
@@ -942,6 +966,8 @@ fn handleProvider(self: *Agent, _: std.mem.Allocator, rest: []const u8) void {
fn disableProvider(self: *Agent) void {
if (self.ai_client) |client| client.deinit(self.allocator);
self.ai_client = null;
if (self.owned_key) |k| self.allocator.free(k);
self.owned_key = null;
self.model_credentials = null;
self.model_completions = null;
self.no_llm_persisted = true;
@@ -955,12 +981,18 @@ fn hfBillTo(provider: Config.AiProvider) ?[]const u8 {
return std.posix.getenv("HF_BILL_TO");
}
fn setProvider(self: *Agent, credentials: Credentials) !void {
/// `owned_key` transfers ownership of an allocated `credentials.key` (Vertex
/// gcloud token) on success; on error the caller still owns it.
fn setProvider(self: *Agent, credentials: Credentials, owned_key: ?[:0]const u8) !void {
const new_client = try zenai.provider.Client.init(self.allocator, credentials, .{ .base_url = self.model_base_url, .retry_policy = .long_running, .bill_to = hfBillTo(credentials.provider) });
errdefer new_client.deinit(self.allocator);
const new_model = try self.allocator.dupe(u8, zenai.provider.defaultModel(credentials.provider));
// A same-provider re-select (vertex token refresh) must not reset the model.
const same_provider = if (self.model_credentials) |c| c.provider == credentials.provider else false;
const new_model = try self.allocator.dupe(u8, if (same_provider) self.model else zenai.provider.defaultModel(credentials.provider));
if (self.ai_client) |client| client.deinit(self.allocator);
if (self.owned_key) |k| self.allocator.free(k);
self.owned_key = owned_key;
new_client.setInterrupt(&self.http_interrupt);
self.ai_client = new_client;
self.model_credentials = credentials;
@@ -1527,10 +1559,17 @@ fn recordSlashToolCall(
fn formatApiError(self: *Agent, client: zenai.provider.Client, err: anyerror) []const u8 {
const e = client.lastError();
const status = e.status orelse return @errorName(err);
const hint = if (status == 401 and client == .vertex)
if (self.owned_key != null)
" (Vertex token may have expired; run /provider vertex to refresh)"
else
" (Vertex express mode needs an express API key — a Gemini Developer key won't work)"
else
"";
if (e.message) |m| {
if (std.fmt.bufPrint(&self.api_error_buf, "HTTP {d} — {s}", .{ status, m })) |s| return s else |_| {}
if (std.fmt.bufPrint(&self.api_error_buf, "HTTP {d} — {s}{s}", .{ status, m, hint })) |s| return s else |_| {}
}
return std.fmt.bufPrint(&self.api_error_buf, "HTTP {d}", .{status}) catch @errorName(err);
return std.fmt.bufPrint(&self.api_error_buf, "HTTP {d}{s}", .{ status, hint }) catch @errorName(err);
}
/// Returned text lives in `conversation.arena`, valid only until the next prune.
@@ -1797,10 +1836,16 @@ pub fn listModels(allocator: std.mem.Allocator, opts: Config.Agent) !void {
}
const resolved = (try settings.resolveCredentials(allocator, opts, null, false)) orelse return error.MissingProvider;
const llm = resolved.credentials;
defer if (resolved.key_owned) allocator.free(llm.key);
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
const ids = try zenai.provider.listChatModelIds(allocator, arena.allocator(), llm.provider, llm.key, opts.base_url);
const ids = zenai.provider.listChatModelIds(allocator, arena.allocator(), llm.provider, llm.key, opts.base_url) catch |err| {
if (llm.provider == .vertex and !settings.vertexProjectMode()) {
std.debug.print("Vertex express mode cannot list models (the endpoint requires OAuth); set GOOGLE_CLOUD_PROJECT for project mode.\n", .{});
}
return err;
};
var stdout_file = std.fs.File.stdout().writer(&.{});
const w = &stdout_file.interface;

View File

@@ -30,7 +30,7 @@ const Terminal = @import("Terminal.zig");
const string = @import("../string.zig");
const Credentials = zenai.provider.Credentials;
pub const api_keys_hint = "ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY, HF_TOKEN, AI_GATEWAY_API_KEY, or MISTRAL_API_KEY";
pub const api_keys_hint = "ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY, HF_TOKEN, AI_GATEWAY_API_KEY, or MISTRAL_API_KEY (Vertex AI: VERTEX_API_KEY, or GOOGLE_CLOUD_PROJECT via gcloud)";
/// Determine which provider to use and read its env key. Returns null
/// only when no `--provider` was given AND no env key exists (the caller
@@ -38,6 +38,9 @@ pub const api_keys_hint = "ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY, HF
pub const ResolvedProvider = struct {
credentials: Credentials,
source: enum { flag, remembered, detected, picked },
/// Key allocated (Vertex gcloud token) rather than an env pointer; the
/// caller frees it, only after the client that references it is gone.
key_owned: bool = false,
};
/// Probe a keyless local provider (Ollama, llama.cpp): its env key is a
@@ -52,21 +55,69 @@ pub fn detectLocalProvider(allocator: std.mem.Allocator, tag: Config.AiProvider,
return .{ .provider = tag, .key = key };
}
/// With GOOGLE_CLOUD_PROJECT set, zenai's client always sends Bearer auth —
/// an API key can never work, so the credential must be an OAuth token.
pub fn vertexProjectMode() bool {
return std.posix.getenv("GOOGLE_CLOUD_PROJECT") != null;
}
/// Caller owns the result. Failure prints gcloud's own stderr so the real
/// cause (not logged in, missing SDK) reaches the user.
pub fn gcloudAccessToken(allocator: std.mem.Allocator) ![:0]const u8 {
const result = std.process.Child.run(.{
.allocator = allocator,
.argv = &.{ "gcloud", "auth", "print-access-token" },
.max_output_bytes = 64 * 1024,
}) catch |err| {
if (err == error.FileNotFound) {
std.debug.print("gcloud not found on PATH; install the Google Cloud SDK, or unset GOOGLE_CLOUD_PROJECT to use Vertex express mode with GOOGLE_API_KEY.\n", .{});
return error.GcloudNotFound;
}
return err;
};
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
const failed = switch (result.term) {
.Exited => |code| code != 0,
else => true,
};
const token = std.mem.trim(u8, result.stdout, &std.ascii.whitespace);
if (failed or token.len == 0) {
std.debug.print("`gcloud auth print-access-token` failed:\n{s}", .{result.stderr});
return error.GcloudTokenFailed;
}
return allocator.dupeZ(u8, token);
}
/// True when a non-Ollama provider key is available (flag, remembered, or
/// env-detected). Skips the Ollama probe so it isn't run twice at startup; the
/// interactive picker only fires on detected keys, which this still catches.
pub fn hasDetectableKey(opts: Config.Agent, remembered: ?Remembered) bool {
if (opts.provider) |p| return zenai.provider.envApiKey(p) != null;
if (remembered) |r| if (r.provider) |p| if (zenai.provider.envApiKey(p)) |_| return true;
if (opts.provider) |p| return zenai.provider.envApiKey(p) != null or (p == .vertex and vertexProjectMode());
if (remembered) |r| if (r.provider) |p| {
if (zenai.provider.envApiKey(p) != null) return true;
if (p == .vertex and vertexProjectMode()) return true;
};
var buf: [zenai.provider.default_candidates.len]Credentials = undefined;
return zenai.provider.detectKeys(&buf, zenai.provider.default_candidates).len > 0;
return availableProviders(&buf).len > 0;
}
/// Precedence: `--provider` > remembered (if its key is still set) > first
/// detected. Null means no key at all (the reason is already printed).
pub fn resolveCredentials(allocator: std.mem.Allocator, opts: Config.Agent, remembered: ?Remembered, allow_pick: bool) !?ResolvedProvider {
if (opts.provider) |p| {
if (p == .vertex and vertexProjectMode()) {
const token = try gcloudAccessToken(allocator);
return .{ .credentials = .{ .provider = p, .key = token }, .source = .flag, .key_owned = true };
}
const key = zenai.provider.envApiKey(p) orelse {
if (p == .vertex) {
std.debug.print(
"Vertex needs VERTEX_API_KEY (express mode) or GOOGLE_CLOUD_PROJECT (project mode, token via gcloud) — or pass --no-llm for the basic REPL.\n",
.{},
);
return error.MissingApiKey;
}
std.debug.print(
"Missing API key for --provider {s}: set {s} — or pass --no-llm for the basic REPL.\n",
.{ @tagName(p), zenai.provider.envVarName(p) },
@@ -76,12 +127,19 @@ pub fn resolveCredentials(allocator: std.mem.Allocator, opts: Config.Agent, reme
return .{ .credentials = .{ .provider = p, .key = key }, .source = .flag };
}
if (remembered) |r| if (r.provider) |p| if (zenai.provider.envApiKey(p)) |key| {
return .{ .credentials = .{ .provider = p, .key = key }, .source = .remembered };
if (remembered) |r| if (r.provider) |p| {
if (p == .vertex and vertexProjectMode()) {
// On failure the reason is already printed; fall through to detection.
if (gcloudAccessToken(allocator)) |token| {
return .{ .credentials = .{ .provider = p, .key = token }, .source = .remembered, .key_owned = true };
} else |_| {}
} else if (zenai.provider.envApiKey(p)) |key| {
return .{ .credentials = .{ .provider = p, .key = key }, .source = .remembered };
}
};
var buf: [zenai.provider.default_candidates.len]Credentials = undefined;
const found = zenai.provider.detectKeys(&buf, zenai.provider.default_candidates);
const found = availableProviders(&buf);
if (found.len == 0) {
if (detectLocalProvider(allocator, .ollama, opts.base_url)) |creds| {
return .{ .credentials = creds, .source = .detected };
@@ -99,16 +157,26 @@ pub fn resolveCredentials(allocator: std.mem.Allocator, opts: Config.Agent, reme
// A single key needs no choice; non-interactive callers (--list-models,
// one-shot tasks, pipes) must not block on a prompt — take the first.
if (!allow_pick or found.len == 1 or !Terminal.interactiveTty()) {
return .{ .credentials = found[0], .source = .detected };
return try finishResolved(allocator, found[0], .detected);
}
var names: [zenai.provider.default_candidates.len][:0]const u8 = undefined;
for (found, 0..) |cred, i| names[i] = @tagName(cred.provider);
std.debug.print("\n", .{});
const idx = Terminal.promptNumberedChoice(" Select a provider:", names[0..found.len], 0) catch {
return .{ .credentials = found[0], .source = .detected };
return try finishResolved(allocator, found[0], .detected);
};
return .{ .credentials = found[idx], .source = .picked };
return try finishResolved(allocator, found[idx], .picked);
}
/// Swaps the placeholder key of a detected project-mode Vertex for a real
/// gcloud token.
fn finishResolved(allocator: std.mem.Allocator, credentials: Credentials, source: @FieldType(ResolvedProvider, "source")) !ResolvedProvider {
if (credentials.provider == .vertex and vertexProjectMode()) {
const token = try gcloudAccessToken(allocator);
return .{ .credentials = .{ .provider = .vertex, .key = token }, .source = source, .key_owned = true };
}
return .{ .credentials = credentials, .source = source };
}
pub const remembered_path = ".lp-agent.zon";
@@ -157,8 +225,15 @@ pub fn saveRemembered(remembered: Remembered) !void {
/// Cloud providers with a key set. Ollama is excluded — its availability needs
/// a live probe (`detectLocalProvider`), too costly for an unconditional startup scan.
/// Vertex project mode joins with a placeholder key — no subprocess during a
/// scan; the gcloud token is fetched on selection (`finishResolved`).
pub fn availableProviders(buf: []Credentials) []Credentials {
return zenai.provider.detectKeys(buf, zenai.provider.default_candidates);
const found = zenai.provider.detectKeys(buf, zenai.provider.default_candidates);
if (zenai.provider.useVertex() and vertexProjectMode() and found.len < buf.len) {
buf[found.len] = .{ .provider = .vertex, .key = "" };
return buf[0 .. found.len + 1];
}
return found;
}
pub fn resolveModelName(opts: Config.Agent, resolved: ?ResolvedProvider, remembered: ?Remembered) []const u8 {

View File

@@ -119,6 +119,10 @@ _attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attri
// Lazily-created style, classList, and dataset objects. Only stored for elements
// that actually access these features via JavaScript, saving 24 bytes per element.
_element_styles: Element.StyleLookup = .empty,
// Computed-style views handed out by window.getComputedStyle. The computed
// variant is a stateless lazy view, so one per element suffices — and Chrome
// returns the same object for repeated calls, so identity is also conformance.
_element_computed_styles: Element.StyleLookup = .empty,
_element_datasets: Element.DatasetLookup = .empty,
_element_class_lists: Element.ClassListLookup = .empty,
_element_rel_lists: Element.RelListLookup = .empty,
@@ -251,10 +255,15 @@ js: *JS.Context,
// An arena for the lifetime of the frame.
arena: Allocator,
// An arena with a lifetime guaranteed to be for 1 invoking of a Zig function
// from JS. Best arena to use, when possible.
// An arena with a lifetime for at least the scope of one Zig invocation from
// JS. Prefer local_arena where possible. Use call_arena when allocations may
// need to call back into JS (event dispatch, forEach callback, ....)
call_arena: Allocator,
// An arena with a lifetime guaranteed to be for exactly 1 invoking of a Zig
// function from JS. Best arena to use, when possible.
local_arena: Allocator,
parent: ?*Frame,
window: *Window,
document: *Document,
@@ -306,6 +315,9 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, opts: InitOpts) !void {
const call_arena = try session.getArena(.medium, "call_arena");
errdefer session.releaseArena(call_arena);
const local_arena = try session.getArena(.medium, "local_arena");
errdefer session.releaseArena(local_arena);
const factory = &page.factory;
const document = (try factory.document(Node.Document.HTMLDocument{
._proto = undefined,
@@ -320,6 +332,7 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, opts: InitOpts) !void {
.document = document,
.window = undefined,
.call_arena = call_arena,
.local_arena = local_arena,
._frame_id = frame_id,
._page = page,
._session = session,
@@ -382,6 +395,7 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, opts: InitOpts) !void {
.identity = &page.identity,
.identity_arena = arena,
.call_arena = self.call_arena,
.local_arena = self.local_arena,
});
errdefer browser.env.destroyContext(self.js);
@@ -480,6 +494,7 @@ pub fn deinit(self: *Frame) void {
self._style_manager.deinit();
page.releaseArena(self.call_arena);
page.releaseArena(self.local_arena);
}
pub fn trackWorker(self: *Frame, worker: *Worker) !void {
@@ -1329,6 +1344,10 @@ fn urlBasename(arena: Allocator, url: []const u8) !?[]const u8 {
return try arena.dupe(u8, name);
}
fn isUtf16Encoding(charset: []const u8) bool {
return std.mem.eql(u8, charset, "UTF-16LE") or std.mem.eql(u8, charset, "UTF-16BE");
}
fn frameDataCallback(response: HttpClient.Response, data: []const u8) !void {
var self: *Frame = @ptrCast(@alignCast(response.ctx));
@@ -1344,8 +1363,10 @@ fn frameDataCallback(response: HttpClient.Response, data: []const u8) !void {
// If the HTTP Content-Type header didn't specify a charset and this is HTML,
// prescan the first 1024 bytes for a <meta charset> declaration.
var html_prescan_found_charset = false;
if (mime.content_type == .text_html and mime.is_default_charset) {
if (Mime.prescanCharset(data)) |charset| {
html_prescan_found_charset = true;
if (charset.len <= 40) {
@memcpy(mime.charset[0..charset.len], charset);
mime.charset[charset.len] = 0;
@@ -1369,14 +1390,15 @@ fn frameDataCallback(response: HttpClient.Response, data: []const u8) !void {
const charset_str = mime.charsetString();
const info = h5e.encoding_for_label(charset_str.ptr, charset_str.len);
if (info.isValid()) {
self.charset = info.name();
const name = info.name();
self.charset = if (html_prescan_found_charset and isUtf16Encoding(name)) "UTF-8" else name;
}
self._parse_state = .{ .html = .{
.buffer = .empty,
.arena = try self.getArena(.large, "Frame.navigate"),
} };
},
.application_json, .text_javascript, .text_css, .text_plain => {
.application_json, .text_javascript, .text_css, .text_plain, .text_markdown => {
var arr: std.ArrayList(u8) = .empty;
try arr.appendSlice(self.arena, "<html><head><meta charset=\"utf-8\"></head><body><pre>");
self._parse_state = .{ .text = arr };
@@ -2188,6 +2210,27 @@ pub fn scheduleCustomElementBackupDrain(self: *Frame) !void {
try self.js.queueCustomElementBackupDrain();
}
// Run the network-idle notification checks for this frame and, recursively,
// its child frames. CDP clients (e.g. puppeteer's networkidle0) expect the
// networkIdle/networkAlmostIdle lifecycle events on every frame, like Chrome
// emits them, not just on the root frame.
pub fn checkIdleNotifications(self: *Frame, total_http_activity: usize) void {
switch (self._parse_state) {
.html, .complete => {
if (self._notified_network_almost_idle.check(total_http_activity <= 2)) {
self.notifyNetworkAlmostIdle();
}
if (self._notified_network_idle.check(total_http_activity == 0)) {
self.notifyNetworkIdle();
}
},
else => {},
}
for (self.child_frames.items) |child| {
child.checkIdleNotifications(total_http_activity);
}
}
pub fn notifyNetworkIdle(self: *Frame) void {
lp.assert(self._notified_network_idle == .done, "Frame.notifyNetworkIdle", .{});
self._session.notification.dispatch(.frame_network_idle, &.{
@@ -2474,50 +2517,19 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No
}
}
const parent_is_connected = parent.isConnected();
// Tri-state behavior for mutations:
// 1. from_parser=true, parse_mode=document -> no mutations (initial document parse)
// 2. from_parser=true, parse_mode=fragment -> mutations (innerHTML additions)
// 3. from_parser=false, parse_mode=document -> mutation (js manipulation)
// split like this because from_parser can be comptime known.
const should_notify = if (comptime from_parser)
self._parse_mode == .fragment
else
true;
if (should_notify) {
if (comptime from_parser == false) {
// When the parser adds the node, nodeIsReady is only called when the
// nodeComplete() callback is executed. nodeIsReady resolves the
// node's owning frame itself (only for the few node types that have
// ready work), so pass the incumbent `self`.
try self.nodeIsReady(false, child);
// Check if text was added to a script that hasn't started yet.
if (child._type == .cdata and parent_is_connected) {
if (parent.is(Element.Html.Script)) |script| {
if (!script._executed) {
try self.nodeIsReady(false, parent);
}
}
}
}
// Notify mutation observers about childList change
if (observers.hasMutationObservers(self)) {
const previous_sibling = child.previousSibling();
const next_sibling = child.nextSibling();
const added = [_]*Node{child};
observers.notifyChildListChange(self, parent, &added, &.{}, previous_sibling, next_sibling);
}
}
// The parser path does its own (limited) notification and connected-callback
// work, then returns.
if (comptime from_parser) {
// Of the parser insertions, only fragment parses (innerHTML) mutate a
// live tree; the initial document parse suppresses notifications.
if (self._parse_mode == .fragment) {
self.notifyChildInserted(parent, child);
}
if (child.is(Element)) |el| {
// Invoke connectedCallback for custom elements during parsing
// For main document parsing, we know nodes are connected (fast path)
// For fragment parsing (innerHTML), we need to check connectivity
// Invoke connectedCallback for custom elements during parsing.
// For main document parsing we know nodes are connected (fast path);
// for fragment parsing (innerHTML) we check connectivity.
if (child.isConnected() or child.isInShadowTree()) {
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
try self.addElementId(parent, el, id);
@@ -2528,6 +2540,23 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No
return;
}
const parent_is_connected = parent.isConnected();
// nodeIsReady resolves the node's owning frame itself (only for the few node
// types that have ready work), so pass the incumbent `self`.
try self.nodeIsReady(false, child);
// Check if text was added to a script that hasn't started yet.
if (child._type == .cdata and parent_is_connected) {
if (parent.is(Element.Html.Script)) |script| {
if (!script._executed) {
try self.nodeIsReady(false, parent);
}
}
}
self.notifyChildInserted(parent, child);
if (opts.child_already_connected and !opts.adopting_to_new_document) {
// The child is already connected in the same document, we don't have to reconnect it.
// On cross-document adoption the child has already fired
@@ -2567,6 +2596,17 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No
}
}
fn notifyChildInserted(self: *Frame, parent: *Node, child: *Node) void {
if (!observers.hasMutationObservers(self)) {
return;
}
const previous_sibling = child.previousSibling();
const next_sibling = child.nextSibling();
const added = [_]*Node{child};
observers.notifyChildListChange(self, parent, &added, &.{}, previous_sibling, next_sibling);
}
pub fn attributeChange(self: *Frame, element: *Element, name: String, value: String, old_value: ?String) void {
_ = Element.Build.call(element, "attributeChange", .{ element, name, value, self }) catch |err| {
log.err(.bug, "build.attributeChange", .{ .tag = element.getTag(), .name = name, .value = value, .err = err, .type = self._type, .url = self.url });

View File

@@ -578,6 +578,16 @@ fn requestT(self: *Client, req: Request, owner: ?*Owner) !*Transfer {
if (req.body_outlives_request == false) {
owned.body = try arena.dupe(u8, b);
}
// Browsers never send Expect: 100-continue; libcurl generates it
// for HTTP/1.1 requests whose body exceeds 1MB
// (EXPECT_100_THRESHOLD), which stalls the request ~1s against
// servers/proxies that never answer the interim response. An
// empty value ("Expect:") suppresses the generated header. Only
// requests with a body can trigger it, and over HTTP/2 curl never
// generates it, so the entry is inert there. Added here (not
// configureConn) so redirect/auth retries don't append duplicates.
try owned.headers.add("Expect:");
}
const t = try arena.create(Transfer);
@@ -2088,7 +2098,7 @@ pub const Transfer = struct {
return result;
}
fn dataCallback(buffer: [*]const u8, chunk_count: usize, chunk_len: usize, data: *anyopaque) usize {
fn dataCallback(buffer: [*]const u8, chunk_count: usize, chunk_len: usize, data: *anyopaque) callconv(.c) usize {
// libcurl should only ever emit 1 chunk at a time
if (comptime IS_DEBUG) {
std.debug.assert(chunk_count == 1);

View File

@@ -40,6 +40,7 @@ pub const ContentTypeEnum = enum {
text_javascript,
text_plain,
text_css,
text_markdown,
image_jpeg,
image_gif,
image_png,
@@ -55,6 +56,7 @@ pub const ContentType = union(ContentTypeEnum) {
text_javascript: void,
text_plain: void,
text_css: void,
text_markdown: void,
image_jpeg: void,
image_gif: void,
image_png: void,
@@ -72,6 +74,7 @@ pub fn contentTypeString(mime: *const Mime) []const u8 {
.text_html => "text/html",
.text_javascript => "application/javascript",
.text_plain => "text/plain",
.text_markdown => "text/markdown",
.text_css => "text/css",
.image_jpeg => "image/jpeg",
.image_png => "image/png",
@@ -342,7 +345,7 @@ pub fn isHTML(self: *const Mime) bool {
pub fn isText(mime: *const Mime) bool {
return switch (mime.content_type) {
.text_xml, .text_html, .text_javascript, .text_plain, .text_css => true,
.text_xml, .text_html, .text_javascript, .text_plain, .text_css, .text_markdown => true,
.application_json => true,
else => false,
};
@@ -359,6 +362,7 @@ fn parseContentType(value: []const u8) !struct { ContentType, usize } {
@"text/html",
@"text/css",
@"text/plain",
@"text/markdown",
@"text/javascript",
@"application/javascript",
@@ -377,6 +381,7 @@ fn parseContentType(value: []const u8) !struct { ContentType, usize } {
.@"text/javascript", .@"application/javascript", .@"application/x-javascript" => .{ .text_javascript = {} },
.@"text/plain" => .{ .text_plain = {} },
.@"text/css" => .{ .text_css = {} },
.@"text/markdown" => .{ .text_markdown = {} },
.@"image/jpeg" => .{ .image_jpeg = {} },
.@"image/png" => .{ .image_png = {} },
.@"image/gif" => .{ .image_gif = {} },
@@ -790,6 +795,7 @@ test "Mime: parse common" {
try expect(.{ .content_type = .{ .application_json = {} } }, "application/json");
try expect(.{ .content_type = .{ .text_css = {} } }, "text/css");
try expect(.{ .content_type = .{ .text_markdown = {} } }, "text/markdown");
try expect(.{ .content_type = .{ .image_jpeg = {} } }, "image/jpeg");
try expect(.{ .content_type = .{ .image_png = {} } }, "image/png");

View File

@@ -263,12 +263,7 @@ fn _tick(self: *Runner, comptime is_cdp: bool, timeout_ms: u32, conditions: []Wa
}
},
.html, .complete => {
if (frame._notified_network_almost_idle.check(total_http_activity <= 2)) {
frame.notifyNetworkAlmostIdle();
}
if (frame._notified_network_idle.check(total_http_activity == 0)) {
frame.notifyNetworkIdle();
}
frame.checkIdleNotifications(total_http_activity);
const met = switch (condition.until) {
.done => is_done,
@@ -306,13 +301,13 @@ fn _tick(self: *Runner, comptime is_cdp: bool, timeout_ms: u32, conditions: []Wa
return .done;
}
pub fn waitForSelector(self: *Runner, frame_id: u32, selector: [:0]const u8, timeout_ms: u32) !*Node.Element {
pub fn waitForSelector(self: *Runner, frame_id: u32, input: [:0]const u8, timeout_ms: u32) !*Node.Element {
const session = self.session;
const arena = try session.getArena(.small, "Runner.waitForSelector");
defer session.releaseArena(arena);
var timer = try std.time.Timer.start();
const parsed_selector = try Selector.parseLeaky(arena, selector);
const selector = try Selector.parseLeaky(arena, input);
while (true) {
if (session.isCancelled()) {
@@ -324,7 +319,7 @@ pub fn waitForSelector(self: *Runner, frame_id: u32, selector: [:0]const u8, tim
};
const frame = &page.frame;
if (try parsed_selector.query(frame.document.asNode(), frame)) |el| {
if (try Selector.query(selector, frame.document.asNode(), frame)) |el| {
return el;
}
@@ -368,7 +363,7 @@ pub fn waitForScript(self: *Runner, frame_id: u32, src: [:0]const u8, timeout_ms
defer try_catch.deinit();
const s = ls.local.compile(src, "wait_script") catch |err| {
const caught = try_catch.caughtOrError(frame.call_arena, err);
const caught = try_catch.caughtOrError(frame.local_arena, err);
log.err(.app, "wait script error", .{ .err = caught });
return error.ScriptError;
};
@@ -396,7 +391,7 @@ pub fn waitForScript(self: *Runner, frame_id: u32, src: [:0]const u8, timeout_ms
const script = compiled.get(ls.local.isolate).bindToCurrentContext(&ls.local);
const value = script.run() catch |err| {
const caught = try_catch.caughtOrError(frame.call_arena, err);
const caught = try_catch.caughtOrError(frame.local_arena, err);
log.err(.app, "wait script error", .{ .err = caught });
return error.ScriptError;
};
@@ -473,3 +468,27 @@ test "Runner: waitForScript" {
var runner = page.session.runner(.{});
try runner.waitForScript(page.frame_id, "document.querySelector('#sel1')", 10);
}
test "Runner: networkidle notifies child frames" {
const page = try testing.pageTest("runner/iframe_idle.html", .{});
defer page.close();
var runner = page.session.runner(.{});
const frame = page.frame().?;
try testing.expectEqual(2, frame.child_frames.items.len);
// A `.networkidle` wait resolves via `is_done` once the page is fully
// idle, which can happen before the 500ms idle-notification hold. Keep
// ticking (like the CDP serve loop does) until the notifications fire.
var attempts: usize = 0;
while (frame._notified_network_idle != .done and attempts < 50) : (attempts += 1) {
_ = try runner.tickForFrame(page.frame_id, 20, .{ .until = .networkidle });
std.Thread.sleep(25 * std.time.ns_per_ms);
}
try testing.expectEqual(true, frame._notified_network_idle == .done);
for (frame.child_frames.items) |child| {
try testing.expectEqual(true, child._notified_network_almost_idle == .done);
try testing.expectEqual(true, child._notified_network_idle == .done);
}
}

View File

@@ -306,18 +306,19 @@ pub fn preloadModuleHint(self: *ScriptManagerBase, element: *Element.Html, url:
}
pub fn waitForImport(self: *ScriptManagerBase, url: [:0]const u8) !ModuleSource {
const entry = self.imported_modules.getEntry(url) orelse {
// It shouldn't be possible for v8 to ask for a module that we didn't
// `preloadImport` above.
return error.UnknownModule;
};
const was_evaluating = self.is_evaluating;
self.is_evaluating = true;
defer self.endEvaluationWindow(was_evaluating);
var client = self.client;
while (true) {
// imported_modules can be mutated by client.tick, so we need to lookup
// the entry on each iteration.
const entry = self.imported_modules.getEntry(url) orelse {
// It shouldn't be possible for v8 to ask for a module that we
// didn't `preloadImport` above.
return error.UnknownModule;
};
switch (entry.value_ptr.state) {
.loading => {
_ = try client.tick(200, .sync_wait);
@@ -935,7 +936,7 @@ pub const Script = struct {
return;
}
const caught = try_catch.caughtOrError(frame.call_arena, error.Unknown);
const caught = try_catch.caughtOrError(frame.local_arena, error.Unknown);
log.warn(.js, "eval script", .{
.url = url,
.caught = caught,

View File

@@ -72,7 +72,10 @@ _nav_cursor: usize = 0,
// Set by the agent script Runtime around one tool call so each `Page` handle's
// tools act on its own frame, not `pages[0]`. Null for all other callers.
_tool_frame_override: ?*Frame = null,
// A frame id, not a pointer: a tool-triggered navigation commits a replacement
// Page mid-call, freeing the old Frame while keeping its frame id (see
// `commitPendingPage`).
_tool_frame_override: ?u32 = null,
// Loader IDs are scoped to the Session: each new BrowserContext gets a
// fresh counter. Frame IDs (`frame_id_gen`) live on `Browser` instead so
@@ -444,8 +447,9 @@ pub fn primaryPage(self: *Session) ?PageHandle {
// DEPRECATED. Exists during our transition to multi-page sessions.
pub fn currentFrame(self: *Session) ?*Frame {
if (self._tool_frame_override) |frame| {
return frame;
if (self._tool_frame_override) |frame_id| {
// No pages[0] fallthrough: the override targets one specific page.
return self.findFrameByFrameId(frame_id);
}
if (self.pages.items.len == 0) {
return null;
@@ -458,8 +462,8 @@ pub fn currentFrame(self: *Session) ?*Frame {
}
/// See `_tool_frame_override`. Pass null to clear.
pub fn setToolFrameOverride(self: *Session, frame: ?*Frame) void {
self._tool_frame_override = frame;
pub fn setToolFrameOverride(self: *Session, frame_id: ?u32) void {
self._tool_frame_override = frame_id;
}
// Multi-page aware: frame ids are globally unique (monotonic on `Browser`).
@@ -660,6 +664,7 @@ fn _processFrameNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation)
const frame_id = frame._frame_id;
const reuse_window = frame.window;
const page = frame._page;
frame.js.detachGlobal();
frame.deinit();
frame.* = undefined;
@@ -705,6 +710,7 @@ fn processPopupNavigation(_: *Session, frame: *Frame, qn: *QueuedNavigation) !vo
const frame_id = frame._frame_id;
const page = frame._page;
frame.js.detachGlobal();
frame.deinit();
frame.* = undefined;

View File

@@ -356,7 +356,7 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons
defer try_catch.deinit();
ls.local.eval(inject_script, "inject_script") catch |err| {
const caught = try_catch.caughtOrError(frame.call_arena, err);
const caught = try_catch.caughtOrError(frame.local_arena, err);
log.err(.app, "inject script error", .{ .err = caught });
};
}

View File

@@ -33,6 +33,7 @@ const v8 = js.v8;
const log = lp.log;
const ArenaAllocator = std.heap.ArenaAllocator;
const CALL_ARENA_RETAIN = 1024 * 16;
const LOCAL_ARENA_RETAIN = 1024 * 16;
const IS_DEBUG = @import("builtin").mode == .Debug;
const Caller = @This();
@@ -101,13 +102,22 @@ pub fn deinit(self: *Caller) void {
_ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
}
// Unlike call_arena, local_arena is reset on _every_ return, since its
// users promise not to hold data across a nested call. In debug, free
// back to the backing allocator so a stale pointer trips the
// DebugAllocator's use-after-free detection; in release, retain a buffer
// to avoid realloc churn.
{
const local_arena: *ArenaAllocator = @ptrCast(@alignCast(ctx.local_arena.ptr));
_ = local_arena.reset(if (comptime IS_DEBUG) .free_all else .{ .retain_with_limit = LOCAL_ARENA_RETAIN });
}
ctx.call_depth = call_depth;
ctx.local = self.prev_local;
ctx.global.setJs(self.prev_context);
}
pub const CallOpts = struct {
dom_exception: bool = false,
null_as_undefined: bool = false,
as_typed_array: bool = false,
// Constructor-only. When true, `new.target` is pulled from the
@@ -126,12 +136,12 @@ pub fn constructor(self: *Caller, comptime T: type, func: anytype, handle: *cons
const info = FunctionCallbackInfo{ .handle = handle };
if (!info.isConstructCall()) {
handleError(T, @TypeOf(func), local, error.InvalidArgument, info, opts);
handleError(T, @TypeOf(func), local, error.InvalidArgument, info);
return;
}
self._constructor(func, info, opts) catch |err| {
handleError(T, @TypeOf(func), local, err, info, opts);
handleError(T, @TypeOf(func), local, err, info);
};
}
@@ -182,7 +192,7 @@ pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, handle
const info = PropertyCallbackInfo{ .handle = handle };
return _getIndex(T, local, func, idx, info, opts) catch |err| {
handleError(T, @TypeOf(func), local, err, info, opts);
handleError(T, @TypeOf(func), local, err, info);
return js.Intercepted.no;
};
}
@@ -208,7 +218,7 @@ pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *cons
const info = PropertyCallbackInfo{ .handle = handle };
return _getNamedIndex(T, local, func, name, info, opts) catch |err| {
handleError(T, @TypeOf(func), local, err, info, opts);
handleError(T, @TypeOf(func), local, err, info);
return js.Intercepted.no;
};
}
@@ -234,7 +244,7 @@ pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *cons
const info = PropertyCallbackInfo{ .handle = handle };
return _setNamedIndex(T, local, func, name, .{ .local = &self.local, .handle = js_value }, info, opts) catch |err| {
handleError(T, @TypeOf(func), local, err, info, opts);
handleError(T, @TypeOf(func), local, err, info);
return js.Intercepted.no;
};
}
@@ -261,7 +271,7 @@ pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *c
const info = PropertyCallbackInfo{ .handle = handle };
return _deleteNamedIndex(T, local, func, name, info, opts) catch |err| {
handleError(T, @TypeOf(func), local, err, info, opts);
handleError(T, @TypeOf(func), local, err, info);
return js.Intercepted.no;
};
}
@@ -287,7 +297,7 @@ pub fn getEnumerator(self: *Caller, comptime T: type, func: anytype, handle: *co
const info = PropertyCallbackInfo{ .handle = handle };
return _getEnumerator(T, local, func, info, opts) catch |err| {
handleError(T, @TypeOf(func), local, err, info, opts);
handleError(T, @TypeOf(func), local, err, info);
return js.Intercepted.no;
};
}
@@ -317,7 +327,7 @@ fn handleIndexedReturn(comptime T: type, comptime F: type, comptime with_value:
return js.Intercepted.no;
}
}
handleError(T, F, local, err, info, opts);
handleError(T, F, local, err, info);
return js.Intercepted.no;
};
},
@@ -348,7 +358,7 @@ fn nameToString(local: *const Local, comptime T: type, name: *const v8.Name) !T
return try js.String.toSlice(.{ .local = local, .handle = handle });
}
fn handleError(comptime T: type, comptime F: type, local: *const Local, err: anyerror, info: anytype, comptime opts: CallOpts) void {
fn handleError(comptime T: type, comptime F: type, local: *const Local, err: anyerror, info: anytype) void {
const isolate = local.isolate;
if (comptime IS_DEBUG and @TypeOf(info) == FunctionCallbackInfo) {
@@ -368,22 +378,21 @@ fn handleError(comptime T: type, comptime F: type, local: *const Local, err: any
error.RangeError => isolate.createRangeError(""),
error.OutOfMemory => isolate.createError("out of memory"),
error.IllegalConstructor => isolate.createError("Illegal Constructor"),
else => blk: {
if (comptime opts.dom_exception) {
const DOMException = @import("../webapi/DOMException.zig");
if (DOMException.fromError(err)) |ex| {
const value = local.zigValueToJs(ex, .{}) catch break :blk isolate.createError("internal error");
break :blk value.handle;
}
}
break :blk isolate.createError(@errorName(err));
},
else => domExceptionToJs(local, err) orelse isolate.createError(@errorName(err)),
};
const js_exception = isolate.throwException(js_err);
info.getReturnValue().setValueHandle(js_exception);
}
// Convert a Zig error to a DOMException. If the error is unknown, return null.
fn domExceptionToJs(local: *const Local, err: anyerror) ?*const v8.Value {
const DOMException = @import("../webapi/DOMException.zig");
const ex = DOMException.fromError(err) orelse return null;
const value = local.zigValueToJs(ex, .{}) catch return local.isolate.createError("internal error");
return value.handle;
}
// This is extracted to speed up compilation. When left inlined in handleError,
// this can add as much as 10 seconds of compilation time.
fn logFunctionCallError(local: *const Local, type_name: []const u8, func: []const u8, err: anyerror, info: FunctionCallbackInfo) void {
@@ -557,13 +566,13 @@ pub const Function = struct {
static: bool = false,
wpt_only: bool = false,
deletable: bool = true,
dom_exception: bool = false,
as_typed_array: bool = false,
null_as_undefined: bool = false,
cache: ?Caching = null,
embedded_receiver: bool = false,
exposed: Exposed = .both,
ce_reactions: bool = false,
js_name: ?[:0]const u8 = null,
pub const Exposed = enum { both, window, worker };
@@ -639,11 +648,7 @@ pub const Function = struct {
};
const js_value = _call(T, &caller.local, info, func, opts) catch |err| {
handleError(T, @TypeOf(func), &caller.local, err, info, .{
.dom_exception = opts.dom_exception,
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
handleError(T, @TypeOf(func), &caller.local, err, info);
return;
};
@@ -666,7 +671,6 @@ pub const Function = struct {
}
const res = @call(.auto, func, args);
const js_value = try local.zigValueToJs(res, .{
.dom_exception = opts.dom_exception,
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
@@ -872,12 +876,6 @@ fn getArgs(comptime F: type, comptime offset: usize, local: *const Local, info:
@field(args, tupleFieldName(field_index)) = local.jsValueToZig(param.type.?, js_val) catch |err| {
const DOMException = @import("../webapi/DOMException.zig");
if (DOMException.fromError(err) != null) {
// I don't love this. But we have [a few] cases when trying to
// map a JS Value that we have a specific DOMException to throw.
// Ideally we should only do this if dom_exception = true in the
// bridge definition. But we don't have access to that here.
// Instead, we just rely on the fact that local.jsValueToZig
// only throws a DOMException-known error when it should.
return err;
}
return error.InvalidArgument;

View File

@@ -100,6 +100,10 @@ arena: Allocator,
// owned by the IsolatedWorld.
call_arena: Allocator,
// Like call_arena, but reset on _every_ Caller.deinit rather than only at
// call_depth 0.
local_arena: Allocator,
// Because calls can be nested (i.e.a function calling a callback),
// we can only reset the call_arena when call_depth == 0. If we were
// to reset it within a callback, it would invalidate the data of
@@ -229,6 +233,24 @@ pub fn deinit(self: *Context) void {
v8.v8__MicrotaskQueue__DELETE(self.microtask_queue);
}
// The global (e.g. Window) can be reused across contexts. If you do:
//
// var w = iframe.contentWindow;
// iframe.src = 'two.html';
// w === iframe.contentWindow (must be true)
//
// so when we navigate, the Window/Global is re-used. That's fine with v8, but
// we need to explicitly detach it from the original before we can safely attach
// it to the new
pub fn detachGlobal(self: *Context) void {
var hs: js.HandleScope = undefined;
hs.init(self.isolate);
defer hs.deinit();
const local_v8_context: *const v8.Context = @ptrCast(v8.v8__Global__Get(&self.handle, self.isolate.handle));
v8.v8__Context__DetachGlobal(local_v8_context);
}
// setOrigin is called at navigation (opaque -> real origin) and again when a
// script sets document.domain (real origin -> '!'-marked effective domain).
pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
@@ -294,6 +316,18 @@ pub fn toLocal(self: *Context, global: anytype) js.Local.ToLocalReturnType(@Type
return l.toLocal(global);
}
// This context's global (proxy) object, bound to an already-active local.
// Lets a caller running in a different context — e.g. a [Replaceable] setter
// invoked on another same-origin window — target this context's global rather
// than its own.
pub fn globalObject(self: *Context, local: *const js.Local) js.Object {
const local_v8_context: *const v8.Context = @ptrCast(v8.v8__Global__Get(&self.handle, self.isolate.handle));
return .{
.local = local,
.handle = v8.v8__Context__Global(local_v8_context).?,
};
}
pub fn getIncumbent(self: *Context) *Frame {
const ctx = fromC(v8.v8__Isolate__GetIncumbentContext(self.env.isolate.handle).?).?;
return switch (ctx.global) {
@@ -700,7 +734,7 @@ fn importMetaResolveCallback(callback_handle: ?*const v8.FunctionCallbackInfo) c
return;
};
const resolved = ctx.script_manager.resolveSpecifier(ctx.call_arena, data.base, specifier) catch {
const resolved = ctx.script_manager.resolveSpecifier(ctx.local_arena, data.base, specifier) catch {
_ = isolate.throwException(isolate.createTypeError("failed to resolve module specifier"));
return;
};
@@ -724,8 +758,7 @@ fn _resolveModuleCallback(self: *Context, referrer: js.Module, specifier: [:0]co
specifier,
);
const entry = self.module_cache.getPtr(normalized_specifier).?;
if (entry.module) |m| {
if (self.module_cache.getPtr(normalized_specifier).?.module) |m| {
// This import registered a waiter via preloadImport when it was discovered
// but the compiled module is already cached so we don't have to call
// waitForImport. Release our waiter so we no longer hold on waiter on
@@ -751,6 +784,9 @@ fn _resolveModuleCallback(self: *Context, referrer: js.Module, specifier: [:0]co
const mod = try compileModule(local, source.src(), normalized_specifier);
try self.postCompileModule(mod, normalized_specifier, local);
// waitForImport can cause module_cache to be mutated (via HttpClient.tick),
// so we need to refetch this incase the hashmap changed
const entry = self.module_cache.getPtr(normalized_specifier).?;
entry.module = try mod.persist();
// Note: We don't instantiate/evaluate here - V8 will handle instantiation
// as part of the parent module's dependency chain. If there's a resolver
@@ -904,7 +940,7 @@ fn dynamicModuleSourceCallback(ctx: *anyopaque, module_source_: anyerror!ScriptM
defer try_catch.deinit();
break :blk self.module(true, local, ms.src(), state.specifier, true) catch |err| {
const caught = try_catch.caughtOrError(self.call_arena, err);
const caught = try_catch.caughtOrError(self.local_arena, err);
log.err(.js, "module compilation failed", .{
.caught = caught,
.specifier = state.specifier,

View File

@@ -229,6 +229,7 @@ pub const ContextParams = struct {
identity: *js.Identity,
identity_arena: Allocator,
call_arena: Allocator,
local_arena: Allocator,
debug_name: []const u8 = "Context",
};
@@ -315,6 +316,7 @@ fn _createContext(self: *Env, global: anytype, params: ContextParams) !*Context
.handle = context_global,
.templates = self.templates,
.call_arena = params.call_arena,
.local_arena = params.local_arena,
.microtask_queue = microtask_queue,
.script_manager = if (comptime is_frame) &global._script_manager.base else &global._script_manager,
.scheduler = .init(context_arena),
@@ -332,6 +334,7 @@ fn _createContext(self: *Env, global: anytype, params: ContextParams) !*Context
.page = context.page,
.session = page.session,
.call_arena = params.call_arena,
.local_arena = params.local_arena,
._factory = global._factory,
._scheduler = &context.scheduler,
};

View File

@@ -51,6 +51,7 @@ js: *Context,
buf: []u8,
arena: Allocator,
call_arena: Allocator,
local_arena: Allocator,
page: *Page,
session: *Session,

View File

@@ -314,13 +314,8 @@ fn createSnapshotContext(
const name = JsApi.Meta.name;
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
var maybe_result: v8.MaybeBool = undefined;
// Web IDL: interface objects on the global are non-enumerable
// by default. Opt back in via JsApi.Meta.enumerable = true.
var properties: v8.PropertyAttribute = v8.DontEnum;
if (@hasDecl(JsApi.Meta, "enumerable") and JsApi.Meta.enumerable == true) {
properties = v8.None;
}
v8.v8__Object__DefineOwnProperty(global_obj, context, v8_class_name, func, properties, &maybe_result);
// Web IDL: interface objects on the global are non-enumerable.
v8.v8__Object__DefineOwnProperty(global_obj, context, v8_class_name, func, v8.DontEnum, &maybe_result);
}
}
@@ -759,12 +754,13 @@ fn attachClass(comptime JsApi: type, comptime flatten: bool, isolate: *v8.Isolat
if (value.static) {
v8.v8__Template__SetAccessorProperty(@ptrCast(template), js_name, getter_callback, setter_callback, attribute);
} else {
const accessor_attr = if (own_properties) attribute else attribute | v8.DontEnum;
// Web IDL: attributes on the interface prototype object
// (and mirrored onto [Global] instances) are enumerable.
v8.v8__ObjectTemplate__SetAccessorProperty__Config(define_on orelse prototype, &.{
.key = js_name,
.getter = getter_callback,
.setter = setter_callback,
.attribute = accessor_attr,
.attribute = attribute,
});
}
},
@@ -780,13 +776,16 @@ fn attachClass(comptime JsApi: type, comptime flatten: bool, isolate: *v8.Isolat
.length = value.arity,
.signature = func_signature,
}).?;
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
// A member may override its JS-visible name (see Response.json_static)
const fn_name = value.js_name orelse name;
const js_name = v8.v8__String__NewFromUtf8(isolate, fn_name.ptr, v8.kNormal, @intCast(fn_name.len));
v8.v8__FunctionTemplate__SetClassName(function_template, js_name);
if (value.static and !own_properties) {
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
} else {
const fn_attr: v8.PropertyAttribute = if (own_properties) v8.None else v8.DontEnum;
v8.v8__Template__Set(@ptrCast(define_on orelse member_template), js_name, @ptrCast(function_template), fn_attr);
// Web IDL: operations on the interface prototype object
// (and mirrored onto [Global] instances) are enumerable.
v8.v8__Template__Set(@ptrCast(define_on orelse member_template), js_name, @ptrCast(function_template), v8.None);
}
},
bridge.Indexed => {
@@ -825,7 +824,8 @@ fn attachClass(comptime JsApi: type, comptime flatten: bool, isolate: *v8.Isolat
v8.v8__Symbol__GetAsyncIterator(isolate)
else
v8.v8__Symbol__GetIterator(isolate);
v8.v8__Template__Set(@ptrCast(prototype), js_name, @ptrCast(function_template), v8.None);
// Web IDL: @@iterator is { writable, enumerable: false, configurable }.
v8.v8__Template__Set(@ptrCast(prototype), js_name, @ptrCast(function_template), v8.DontEnum);
},
bridge.Property => {
const js_value = switch (value.value) {

View File

@@ -20,8 +20,10 @@ const std = @import("std");
const lp = @import("lightpanda");
const js = @import("js.zig");
const TaggedOpaque = @import("TaggedOpaque.zig");
const v8 = js.v8;
const bridge = js.bridge;
const IS_DEBUG = @import("builtin").mode == .Debug;
@@ -324,8 +326,9 @@ pub fn jsonStringify(self: Value, jws: anytype) !void {
jws.endWriteRaw();
}
// Throws a DataCloneError for host objects (Blob, File, etc.) that cannot be serialized.
// Does not support transferables which require additional delegate callbacks.
// Clones host objects listed in cloneable_types; throws a DataCloneError for
// any other. Does not support transferables which require additional delegate
// callbacks.
pub fn structuredClone(self: Value) !Value {
return self.structuredCloneTo(self.local);
}
@@ -333,76 +336,286 @@ pub fn structuredClone(self: Value) !Value {
// Clone a value to a different context (within the same isolate).
// Used for cross-context messaging (e.g., Worker <-> Page).
pub fn structuredCloneTo(self: Value, target: *const js.Local) !Value {
const source_context = self.local.handle;
const target_context = target.handle;
const v8_isolate = target.isolate.handle;
const SerializerDelegate = struct {
// Called when V8 encounters a host object it doesn't know how to serialize.
// Returns false to indicate the object cannot be cloned, and throws a DataCloneError.
// V8 asserts has_exception() after this returns false, so we must throw here.
fn writeHostObject(_: ?*anyopaque, isolate: ?*v8.Isolate, _: ?*const v8.Object) callconv(.c) v8.MaybeBool {
const iso = isolate orelse return .{ .has_value = true, .value = false };
const message = v8.v8__String__NewFromUtf8(iso, "The object cannot be cloned.", v8.kNormal, -1);
const error_value = v8.v8__Exception__Error(message) orelse return .{ .has_value = true, .value = false };
_ = v8.v8__Isolate__ThrowException(iso, error_value);
return .{ .has_value = true, .value = false };
}
// 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 = SerializerDelegate.getSharedArrayBufferId,
.write_host_object = SerializerDelegate.writeHostObject,
.throw_data_clone_error = SerializerDelegate.throwDataCloneError,
}) orelse return error.JsException;
defer v8.v8__ValueSerializer__DELETE(serializer);
var write_result: v8.MaybeBool = undefined;
v8.v8__ValueSerializer__WriteHeader(serializer);
v8.v8__ValueSerializer__WriteValue(serializer, source_context, self.handle, &write_result);
if (!write_result.has_value or !write_result.value) {
return error.JsException;
}
var size: usize = undefined;
const data = v8.v8__ValueSerializer__Release(serializer, &size) orelse return error.JsException;
break :blk .{ size, data };
};
defer v8.v8__ValueSerializer__FreeBuffer(data);
const cloned_handle = blk: {
const deserializer = v8.v8__ValueDeserializer__New(v8_isolate, data, size, null) orelse return error.JsException;
defer v8.v8__ValueDeserializer__DELETE(deserializer);
var read_header_result: v8.MaybeBool = undefined;
v8.v8__ValueDeserializer__ReadHeader(deserializer, target_context, &read_header_result);
if (!read_header_result.has_value or !read_header_result.value) {
return error.JsException;
}
break :blk v8.v8__ValueDeserializer__ReadValue(deserializer, target_context) orelse return error.JsException;
};
return .{ .local = target, .handle = cloned_handle };
const serialized = try self.serialize();
defer serialized.deinit();
return deserialize(target, serialized.bytes());
}
// A structured-serialized value: a V8-owned byte buffer. Caller must free it
// and must dupe the bytes if they want it to outlive the current local scope.
pub const Serialized = struct {
data: [*c]u8,
size: usize,
pub fn bytes(self: Serialized) []const u8 {
return self.data[0..self.size];
}
pub fn deinit(self: Serialized) void {
v8.v8__ValueSerializer__FreeBuffer(self.data);
}
};
// Serialize `self` into a V8-owned buffer. The caller must call deinit on the
// result. Raises a JS exception (DataCloneError) for unserializable values.
pub fn serialize(self: Value) !Serialized {
var delegate_ctx = CloneDelegate.SerializeContext{
.local = self.local,
.serializer = undefined,
};
const serializer = v8.v8__ValueSerializer__New(self.local.isolate.handle, &.{
.data = &delegate_ctx,
.get_shared_array_buffer_id = CloneDelegate.getSharedArrayBufferId,
.write_host_object = CloneDelegate.writeHostObject,
.throw_data_clone_error = CloneDelegate.throwDataCloneError,
}) orelse return error.JsException;
defer v8.v8__ValueSerializer__DELETE(serializer);
// the delegate callbacks only fire during WriteValue, after this is set
delegate_ctx.serializer = serializer;
var write_result: v8.MaybeBool = undefined;
v8.v8__ValueSerializer__WriteHeader(serializer);
v8.v8__ValueSerializer__WriteValue(serializer, self.local.handle, self.handle, &write_result);
if (!write_result.has_value or !write_result.value) {
return error.JsException;
}
var size: usize = undefined;
const data = v8.v8__ValueSerializer__Release(serializer, &size) orelse return error.JsException;
return .{ .data = data, .size = size };
}
// Deserialize a structured-serialized buffer (from `serialize`) into a value in
// `local`'s context. A malformed buffer surfaces as error.JsException.
pub fn deserialize(local: *const js.Local, bytes: []const u8) !Value {
var delegate_ctx = CloneDelegate.DeserializeContext{
.local = local,
.deserializer = undefined,
};
const deserializer = v8.v8__ValueDeserializer__New(local.isolate.handle, bytes.ptr, bytes.len, &.{
.data = &delegate_ctx,
.read_host_object = CloneDelegate.readHostObject,
}) orelse return error.JsException;
defer v8.v8__ValueDeserializer__DELETE(deserializer);
delegate_ctx.deserializer = deserializer;
var read_header_result: v8.MaybeBool = undefined;
v8.v8__ValueDeserializer__ReadHeader(deserializer, local.handle, &read_header_result);
if (!read_header_result.has_value or !read_header_result.value) {
return error.JsException;
}
const handle = v8.v8__ValueDeserializer__ReadValue(deserializer, local.handle) orelse return error.JsException;
return .{ .local = local, .handle = handle };
}
// Host object types that support structured cloning via structuredSerialize /
// structuredDeserialize hooks. The serialized payload tags each host object
// with its position in this list; buffers never outlive the process, so the
// order only has to be consistent within a build.
const cloneable_types = .{
@import("../webapi/Blob.zig"),
@import("../webapi/File.zig"),
@import("../webapi/FileList.zig"),
@import("../webapi/ImageData.zig"),
@import("../webapi/DOMPointReadOnly.zig"),
@import("../webapi/DOMPoint.zig"),
};
// Passed to a type's structuredSerialize hook to write its payload into the
// V8 serialization buffer.
pub const StructuredWriter = struct {
local: *const js.Local,
serializer: *v8.ValueSerializer,
pub fn writeUint32(self: *const StructuredWriter, value: u32) void {
v8.v8__ValueSerializer__WriteUint32(self.serializer, value);
}
pub fn writeUint64(self: *const StructuredWriter, value: u64) void {
v8.v8__ValueSerializer__WriteUint64(self.serializer, value);
}
pub fn writeBytes(self: *const StructuredWriter, bytes: []const u8) void {
v8.v8__ValueSerializer__WriteUint32(self.serializer, @intCast(bytes.len));
if (bytes.len > 0) {
v8.v8__ValueSerializer__WriteRawBytes(self.serializer, bytes.ptr, bytes.len);
}
}
};
// Passed to a type's structuredDeserialize hook to read back the payload its
// structuredSerialize hook wrote.
pub const StructuredReader = struct {
local: *const js.Local,
deserializer: *v8.ValueDeserializer,
pub fn readUint32(self: *const StructuredReader) !u32 {
var out: u32 = undefined;
if (!v8.v8__ValueDeserializer__ReadUint32(self.deserializer, &out)) {
return error.DataClone;
}
return out;
}
pub fn readUint64(self: *const StructuredReader) !u64 {
var out: u64 = undefined;
if (!v8.v8__ValueDeserializer__ReadUint64(self.deserializer, &out)) {
return error.DataClone;
}
return out;
}
// The returned slice points into the serialization buffer; dupe anything
// that must outlive deserialization.
pub fn readBytes(self: *const StructuredReader) ![]const u8 {
const len = try self.readUint32();
if (len == 0) {
return "";
}
var ptr: ?*const anyopaque = null;
if (!v8.v8__ValueDeserializer__ReadRawBytes(self.deserializer, len, &ptr)) {
return error.DataClone;
}
return @as([*]const u8, @ptrCast(ptr.?))[0..len];
}
};
const CloneDelegate = struct {
const SerializeContext = struct {
local: *const js.Local,
serializer: *v8.ValueSerializer,
};
const DeserializeContext = struct {
local: *const js.Local,
deserializer: *v8.ValueDeserializer,
};
// Called when V8 encounters an object with embedder fields, i.e. one of
// our wrapped Zig instances. Serialize it if its type (or a prototype)
// is in cloneable_types, otherwise throw a DataCloneError. V8 asserts
// has_exception() after a false return, so we must throw here.
fn writeHostObject(data: ?*anyopaque, _: ?*v8.Isolate, object: ?*const v8.Object) callconv(.c) v8.MaybeBool {
const ctx: *SerializeContext = @ptrCast(@alignCast(data.?));
blk: {
const obj = object orelse break :blk;
if (v8.v8__Object__InternalFieldCount(obj) == 0) {
break :blk;
}
const tao_ptr = v8.v8__Object__GetAlignedPointerFromInternalField(obj, 0) orelse break :blk;
const tao: *TaggedOpaque = @ptrCast(@alignCast(tao_ptr));
const prototype_chain = tao.prototype_chain[0..tao.prototype_len];
if (writeCloneable(ctx, prototype_chain[0].index, tao.value)) |result| {
return result;
}
// Walk up the prototype chain so a subtype serializes as its
// closest cloneable supertype (mirrors TaggedOpaque.fromJS).
var ptr = @intFromPtr(tao.value);
for (prototype_chain[1..]) |proto| {
ptr += proto.offset;
const proto_ptr: **anyopaque = @ptrFromInt(ptr);
if (writeCloneable(ctx, proto.index, proto_ptr.*)) |result| {
return result;
}
ptr = @intFromPtr(proto_ptr.*);
}
}
throwDataCloneException(ctx.local, null);
return .{ .has_value = true, .value = false };
}
fn writeCloneable(
ctx: *SerializeContext,
type_index: bridge.JsApiLookup.BackingInt,
value_ptr: *anyopaque,
) ?v8.MaybeBool {
inline for (cloneable_types, 0..) |T, tag| {
if (type_index == bridge.JsApiLookup.getId(T.JsApi)) {
v8.v8__ValueSerializer__WriteUint32(ctx.serializer, tag);
var writer = StructuredWriter{ .local = ctx.local, .serializer = ctx.serializer };
const instance: *const T = @ptrCast(@alignCast(value_ptr));
instance.structuredSerialize(&writer) catch {
throwDataCloneException(ctx.local, null);
return .{ .has_value = true, .value = false };
};
return .{ .has_value = true, .value = true };
}
}
return null;
}
// Called by V8 to read back what writeHostObject wrote. Returning null
// aborts deserialization, so we throw first to surface a proper error.
fn readHostObject(data: ?*anyopaque, _: ?*v8.Isolate) callconv(.c) ?*const v8.Object {
const ctx: *DeserializeContext = @ptrCast(@alignCast(data.?));
const local = ctx.local;
var tag: u32 = undefined;
if (v8.v8__ValueDeserializer__ReadUint32(ctx.deserializer, &tag)) {
var reader = StructuredReader{ .local = local, .deserializer = ctx.deserializer };
inline for (cloneable_types, 0..) |T, i| {
if (tag == i) {
return readCloneable(T, &reader) orelse {
throwDataCloneException(local, null);
return null;
};
}
}
}
throwDataCloneException(local, null);
return null;
}
fn readCloneable(comptime T: type, reader: *StructuredReader) ?*const v8.Object {
const local = reader.local;
const instance = T.structuredDeserialize(reader, local.ctx.page) catch return null;
const js_obj = local.mapZigInstanceToJs(null, instance) catch return null;
return js_obj.handle;
}
// Called by V8 when a built-in can't be serialized (e.g. an out-of-bounds
// TypedArray). The delegate is responsible for actually throwing.
fn throwDataCloneError(data: ?*anyopaque, message: ?*const v8.String) callconv(.c) void {
const ctx: *SerializeContext = @ptrCast(@alignCast(data.?));
const local = ctx.local;
const msg: ?[]const u8 = blk: {
const handle = message orelse break :blk null;
const str = js.String{ .local = local, .handle = handle };
// the exception can outlive this call; dupe onto the context arena
break :blk str.toSliceWithAlloc(local.ctx.arena) catch null;
};
throwDataCloneException(local, msg);
}
// 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(data: ?*anyopaque, _: ?*v8.Isolate, _: ?*const v8.SharedArrayBuffer, _: ?*u32) callconv(.c) bool {
const ctx: *SerializeContext = @ptrCast(@alignCast(data.?));
throwDataCloneException(ctx.local, "SharedArrayBuffer cannot be cloned");
return false;
}
fn throwDataCloneException(local: *const js.Local, message: ?[]const u8) void {
const DOMException = @import("../webapi/DOMException.zig");
const isolate = local.isolate;
const js_value = local.zigValueToJs(DOMException.init(message, "DataCloneError"), .{}) catch {
const str = v8.v8__String__NewFromUtf8(isolate.handle, "The object can not be cloned", v8.kNormal, -1);
const error_value = v8.v8__Exception__Error(str) orelse return;
_ = v8.v8__Isolate__ThrowException(isolate.handle, error_value);
return;
};
_ = isolate.throwException(js_value.handle);
}
};
pub fn persist(self: Value) !Global {
return self._persist(true);
}

View File

@@ -110,7 +110,6 @@ pub const Constructor = struct {
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
const Opts = struct {
dom_exception: bool = false,
// When true, the constructor function receives `new.target` (as a
// js.Function) as its first parameter. Used by HTMLElement to support
// direct instantiation of custom elements via `new MyElement()`.
@@ -142,7 +141,6 @@ pub const Constructor = struct {
defer if (ce_frame) |frame| frame._ce_reactions.popAndInvoke(ce_checkpoint, frame);
caller.constructor(T, func, handle.?, .{
.dom_exception = opts.dom_exception,
.new_target = opts.new_target,
});
}
@@ -156,6 +154,7 @@ pub const Function = struct {
arity: usize,
noop: bool = false,
wpt_only: bool = false,
js_name: ?[:0]const u8 = null,
exposed: Caller.Function.Opts.Exposed = .both,
cache: ?Caller.Function.Opts.Caching = null,
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
@@ -165,6 +164,7 @@ pub const Function = struct {
.cache = opts.cache,
.static = opts.static,
.wpt_only = opts.wpt_only,
.js_name = opts.js_name,
.exposed = opts.exposed,
// Non-static methods receive `self` as their first param; static
// methods don't, so don't skip the first param for them.
@@ -822,6 +822,8 @@ pub const PageJsApis = flattenTypes(&.{
@import("../webapi/DOMRect.zig"),
@import("../webapi/DOMMatrixReadOnly.zig"),
@import("../webapi/DOMMatrix.zig"),
@import("../webapi/DOMPointReadOnly.zig"),
@import("../webapi/DOMPoint.zig"),
@import("../webapi/DOMParser.zig"),
@import("../webapi/XMLSerializer.zig"),
@import("../webapi/AbstractRange.zig"),
@@ -1021,6 +1023,8 @@ pub const WorkerJsApis = flattenTypes(&.{
@import("../webapi/DOMException.zig"),
@import("../webapi/DOMMatrixReadOnly.zig"),
@import("../webapi/DOMMatrix.zig"),
@import("../webapi/DOMPointReadOnly.zig"),
@import("../webapi/DOMPoint.zig"),
@import("../webapi/net/URLSearchParams.zig"),
@import("../webapi/encoding/TextEncoder.zig"),
@import("../webapi/encoding/TextDecoder.zig"),
@@ -1059,6 +1063,12 @@ pub const WorkerJsApis = flattenTypes(&.{
@import("../webapi/storage/CookieStore.zig"),
@import("../webapi/event/CookieChangeEvent.zig"),
@import("../webapi/BroadcastChannel.zig"),
@import("../webapi/event/CustomEvent.zig"),
@import("../webapi/event/ProgressEvent.zig"),
@import("../webapi/Notification.zig"),
@import("../webapi/FileList.zig"),
@import("../webapi/MessageChannel.zig"),
@import("../webapi/MessagePort.zig"),
});
// Master list of ALL JS APIs across all contexts.

View File

@@ -43,6 +43,8 @@ pub const Isolate = @import("Isolate.zig");
pub const HandleScope = @import("HandleScope.zig");
pub const Value = @import("Value.zig");
pub const StructuredWriter = Value.StructuredWriter;
pub const StructuredReader = Value.StructuredReader;
pub const Array = @import("Array.zig");
pub const String = @import("String.zig");
pub const Object = @import("Object.zig");
@@ -178,6 +180,22 @@ pub fn ArrayBufferRef(comptime kind: ArrayType) type {
return .{ .handle = global };
}
// Direct view into the typed array's backing memory.
pub fn slice(self: *const Self) []BackingInt {
const view: *const v8.ArrayBufferView = @ptrCast(self.handle);
const byte_len = v8.v8__ArrayBufferView__ByteLength(view);
if (byte_len == 0) {
return @constCast(&[_]BackingInt{});
}
const byte_offset = v8.v8__ArrayBufferView__ByteOffset(view);
const array_buffer = v8.v8__ArrayBufferView__Buffer(view).?;
const backing_store_ptr = v8.v8__ArrayBuffer__GetBackingStore(array_buffer);
const backing_store = v8.std__shared_ptr__v8__BackingStore__get(&backing_store_ptr).?;
const data = v8.v8__BackingStore__Data(backing_store).?;
const base = @as([*]u8, @ptrCast(data)) + byte_offset;
return @as([*]BackingInt, @ptrCast(@alignCast(base)))[0 .. byte_len / @sizeOf(BackingInt)];
}
};
}

View File

@@ -381,7 +381,7 @@ const Context = struct {
if (!info.has_visible and label == null and href_raw == null) return;
const href = if (href_raw) |h| URL.resolve(frame.call_arena, frame.base(), h, .{ .encoding = frame.charset }) catch h else null;
const href = if (href_raw) |h| URL.resolve(frame.local_arena, frame.base(), h, .{ .encoding = frame.charset }) catch h else null;
if (info.has_block) {
try self.renderChildren(el.asNode());

View File

@@ -0,0 +1,173 @@
<!DOCTYPE html>
<head>
<title>DOMPoint Test</title>
<script src="testing.js"></script>
</head>
<body>
</body>
<script id=default_construct>
{
const p = new DOMPoint();
testing.expectEqual(0, p.x);
testing.expectEqual(0, p.y);
testing.expectEqual(0, p.z);
testing.expectEqual(1, p.w);
}
</script>
<script id=construct_with_args>
{
const p = new DOMPoint(1, 2, 3, 4);
testing.expectEqual(1, p.x);
testing.expectEqual(2, p.y);
testing.expectEqual(3, p.z);
testing.expectEqual(4, p.w);
}
</script>
<script id=readonly_default>
{
const p = new DOMPointReadOnly(5, 6);
testing.expectEqual(5, p.x);
testing.expectEqual(6, p.y);
testing.expectEqual(0, p.z);
testing.expectEqual(1, p.w);
}
</script>
<script id=mutable_setters>
{
const p = new DOMPoint(1, 1, 1, 1);
p.x = 10;
p.y = 20;
p.z = 30;
p.w = 40;
testing.expectEqual(10, p.x);
testing.expectEqual(20, p.y);
testing.expectEqual(30, p.z);
testing.expectEqual(40, p.w);
}
</script>
<script id=readonly_is_not_writable>
{
const p = new DOMPointReadOnly(1, 2, 3, 4);
// Non-strict assignment to an accessor without a setter is a silent no-op.
p.x = 99;
testing.expectEqual(1, p.x);
}
</script>
<script id=prototype_chain>
{
const p = new DOMPoint(1, 2, 3, 4);
testing.expectTrue(p instanceof DOMPoint);
testing.expectTrue(p instanceof DOMPointReadOnly);
const ro = new DOMPointReadOnly(1, 2, 3, 4);
testing.expectTrue(ro instanceof DOMPointReadOnly);
testing.expectFalse(ro instanceof DOMPoint);
}
</script>
<script id=from_point>
{
const p = DOMPoint.fromPoint({ x: 7, y: 8, z: 9 });
testing.expectTrue(p instanceof DOMPoint);
testing.expectEqual(7, p.x);
testing.expectEqual(8, p.y);
testing.expectEqual(9, p.z);
testing.expectEqual(1, p.w);
const ro = DOMPointReadOnly.fromPoint({ x: 1, w: 5 });
testing.expectTrue(ro instanceof DOMPointReadOnly);
testing.expectFalse(ro instanceof DOMPoint);
testing.expectEqual(1, ro.x);
testing.expectEqual(5, ro.w);
}
</script>
<script id=from_point_empty>
{
const p = DOMPoint.fromPoint();
testing.expectEqual(0, p.x);
testing.expectEqual(1, p.w);
}
</script>
<script id=to_json>
{
const p = new DOMPoint(1, 2, 3, 4);
const j = p.toJSON();
testing.expectEqual(1, j.x);
testing.expectEqual(2, j.y);
testing.expectEqual(3, j.z);
testing.expectEqual(4, j.w);
}
</script>
<script id=matrix_transform_translate>
{
const p = new DOMPoint(1, 2, 0, 1);
const t = p.matrixTransform(new DOMMatrix().translate(10, 20));
testing.expectTrue(t instanceof DOMPoint);
testing.expectEqual(11, t.x);
testing.expectEqual(22, t.y);
testing.expectEqual(0, t.z);
testing.expectEqual(1, t.w);
// Original is unchanged.
testing.expectEqual(1, p.x);
testing.expectEqual(2, p.y);
}
</script>
<script id=matrix_transform_scale>
{
const p = new DOMPointReadOnly(2, 3, 4, 1);
const t = p.matrixTransform(new DOMMatrix().scale(2, 3));
testing.expectEqual(4, t.x);
testing.expectEqual(9, t.y);
}
</script>
<script id=matrix_transform_dict>
{
const p = new DOMPoint(1, 0, 0, 1);
// matrix(a,b,c,d,e,f) = scaleX 2, translateX 5
const t = p.matrixTransform({ a: 2, e: 5 });
testing.expectEqual(7, t.x);
}
</script>
<script id=structured_clone_readonly>
{
const p = new DOMPointReadOnly(1.5, 2.5, 3.5, 4.5);
const c = structuredClone(p);
testing.expectTrue(c instanceof DOMPointReadOnly);
testing.expectFalse(c instanceof DOMPoint);
testing.expectEqual(1.5, c.x);
testing.expectEqual(2.5, c.y);
testing.expectEqual(3.5, c.z);
testing.expectEqual(4.5, c.w);
testing.expectFalse(c === p);
}
</script>
<script id=structured_clone_mutable>
{
const p = new DOMPoint(-1, -2, -3, -4);
const c = structuredClone(p);
testing.expectTrue(c instanceof DOMPoint);
testing.expectTrue(c instanceof DOMPointReadOnly);
testing.expectEqual(-1, c.x);
testing.expectEqual(-2, c.y);
testing.expectEqual(-3, c.z);
testing.expectEqual(-4, c.w);
// The clone is independent and writable.
c.x = 100;
testing.expectEqual(100, c.x);
testing.expectEqual(-1, p.x);
}
</script>

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<script src="../../../testing.js"></script>
<script id=supports>
{
testing.expectEqual(true, HTMLScriptElement.supports('classic'));
testing.expectEqual(true, HTMLScriptElement.supports('module'));
testing.expectEqual(true, HTMLScriptElement.supports('importmap'));
testing.expectEqual(false, HTMLScriptElement.supports('speculationrules'));
testing.expectEqual(false, HTMLScriptElement.supports('Module'));
}
</script>

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<link id=l1 rel="stylesheet" href="data:text/css,">
<div id=d1 class="foo"></div>
<script id=supports>
{
const rl = $('#l1').relList;
testing.expectEqual(true, rl.supports('modulepreload'));
testing.expectEqual(true, rl.supports('MODULEPRELOAD'));
testing.expectEqual(true, rl.supports('preload'));
testing.expectEqual(true, rl.supports('stylesheet'));
testing.expectEqual(false, rl.supports('prefetch'));
}
</script>
<script id=supports_undefined_tokens>
{
// classList's backing attribute defines no supported tokens.
testing.expectError('TypeError', () => $('#d1').classList.supports('foo'));
}
</script>

View File

@@ -216,6 +216,14 @@
document.body.appendChild(plainDiv);
testing.expectEqual('block', window.getComputedStyle(plainDiv).getPropertyValue('display'));
// Repeated calls return the same object (as in Chrome); distinct per element.
testing.expectTrue(window.getComputedStyle(div) === window.getComputedStyle(div));
testing.expectTrue(window.getComputedStyle(div) === cs);
testing.expectFalse(window.getComputedStyle(div) === window.getComputedStyle(plainDiv));
// The cached object stays a live view.
div.style.setProperty('text-transform', 'lowercase');
testing.expectEqual('lowercase', window.getComputedStyle(div).getPropertyValue('text-transform'));
// A normal declaration must not override an earlier !important one, and the
// computed and inline (el.style) paths must agree on the resolved value.
const impDiv = document.createElement('div');

View File

@@ -251,3 +251,53 @@
testing.expectTrue(new Response(new Uint8Array([1, 2, 3])).headers.get('content-type') === null);
}
</script>
<script id=static_error>
{
const r = Response.error();
testing.expectEqual('error', r.type);
testing.expectEqual(0, r.status);
testing.expectEqual('', r.statusText);
testing.expectEqual(false, r.ok);
testing.expectEqual(null, r.body);
}
</script>
<script id=static_redirect>
{
const r = Response.redirect('https://example.com/a');
testing.expectEqual(302, r.status);
testing.expectEqual('https://example.com/a', r.headers.get('location'));
testing.expectEqual('default', r.type);
}
{
const r = Response.redirect('https://example.com/a', 301);
testing.expectEqual(301, r.status);
}
{
let threw = false;
try { Response.redirect('https://example.com/a', 200); } catch (e) { threw = e instanceof RangeError; }
testing.expectTrue(threw);
}
</script>
<script id=static_json type=module>
{
const r = Response.json({ a: 1, b: 'two' });
testing.expectEqual(200, r.status);
testing.expectEqual('application/json', r.headers.get('content-type'));
testing.expectEqual('{"a":1,"b":"two"}', await r.text());
}
{
const r = Response.json([1, 2], { status: 201, headers: { 'content-type': 'application/vnd.api+json' } });
testing.expectEqual(201, r.status);
// An explicit content-type is not overridden.
testing.expectEqual('application/vnd.api+json', r.headers.get('content-type'));
testing.expectEqual('[1,2]', await r.text());
}
{
let threw = false;
try { Response.json(undefined); } catch (e) { threw = e instanceof TypeError; }
testing.expectTrue(threw);
}
</script>

View File

@@ -489,23 +489,30 @@
}
</script>
<script id=prototypeMembersAreNonEnumerable>
<script id=prototypeMembersAreEnumerable>
{
// Per WebIDL, interface prototype members are non-enumerable. Several
// libraries iterate caller-supplied objects with `for..in` or
// `Object.keys` to build querystrings; if prototype methods leak in,
// they end up serialized as URL params with values like
// "function has() { [native code] }". Real Chrome/Firefox return [].
testing.expectEqual([], Object.keys(URLSearchParams.prototype));
// Per WebIDL, operations and attributes on an interface prototype object
// are enumerable (unlike ES class methods). Real Chrome/Firefox list them
// in Object.keys(URLSearchParams.prototype) and in for..in over instances.
const proto_keys = Object.keys(URLSearchParams.prototype);
testing.expectEqual(true, proto_keys.includes('append'));
testing.expectEqual(true, proto_keys.includes('has'));
testing.expectEqual(true, proto_keys.includes('size'));
const usp = new URLSearchParams('a=1&b=2');
const keys = [];
for (const k in usp) keys.push(k);
testing.expectEqual([], keys);
testing.expectEqual(true, keys.includes('get'));
// Sanity: instance own enumerable keys are still empty (entries live in
// the C++ side, exposed only via the iterator protocol).
// Instance own enumerable keys are still empty (entries live in the
// native side, exposed only via the iterator protocol).
testing.expectEqual([], Object.keys(usp));
// But @@iterator and the interface object itself are non-enumerable.
const it_desc = Object.getOwnPropertyDescriptor(URLSearchParams.prototype, Symbol.iterator);
testing.expectEqual(false, it_desc.enumerable);
const iface_desc = Object.getOwnPropertyDescriptor(window, 'URLSearchParams');
testing.expectEqual(false, iface_desc.enumerable);
}
</script>

View File

@@ -69,6 +69,30 @@
}
</script>
<script id="unicode_label_falls_back_to_utf8" type=module>
{
const state = await testing.async();
// The legacy "unicode" label maps to UTF-16LE, but HTML prescan maps
// UTF-16 labels back to UTF-8 so ASCII pages stay readable.
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
iframe.src = 'encoding/unicode_label.html';
iframe.onload = () => { state.resolve(); }
await state.done(() => {
testing.expectEqual('Hello world', iframe.contentDocument.getElementById('title').textContent);
testing.expectEqual(
'This page is plain ASCII but declares charset=unicode.',
iframe.contentDocument.getElementById('body').textContent,
);
testing.expectEqual('UTF-8', iframe.contentDocument.characterSet);
testing.expectEqual('UTF-8', iframe.contentDocument.charset);
testing.expectEqual('UTF-8', iframe.contentDocument.inputEncoding);
});
}
</script>
<script id="no_charset_fallback" type=module>
{
const state = await testing.async();

View File

@@ -0,0 +1,5 @@
<!DOCTYPE html>
<META http-equiv=Content-Type content="text/html; charset=unicode">
<title>Hello</title>
<h1 id="title">Hello world</h1>
<p id="body">This page is plain ASCII but declares charset=unicode.</p>

View File

@@ -0,0 +1,4 @@
<!DOCTYPE html>
<meta charset="UTF-8">
<iframe src="about:blank"></iframe>
<iframe src="runner1.html"></iframe>

View File

@@ -1856,7 +1856,8 @@ pub fn startGoto(
arguments: ?std.json.Value,
receiver_frame_id: ?u32,
) ToolError!StartedGoto {
const args = try parseArgs(GotoParams, arena, arguments);
// This path bypasses `call`, so it needs its own `$LP_*` substitution pass.
const args = try parseArgs(GotoParams, arena, try substituteStringArgs(arena, .goto, arguments));
if (receiver_frame_id) |fid| {
if (session.livePage(fid)) |page| {
registry.resetFrame(arena, &page.frame);

View File

@@ -49,7 +49,6 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const constructor = bridge.constructor(AbortController.init, .{});

View File

@@ -233,7 +233,6 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const Prototype = EventTarget;

View File

@@ -96,7 +96,8 @@ pub fn init(parts_: ?[]const js.Value, opts_: ?InitOptions, page: *Page) !*Blob
/// Creates a new Blob from raw byte slices (for internal Zig use).
pub fn initFromBytes(data: []const u8, content_type: []const u8, page: *Page) !*Blob {
const arena = try page.getArena(.large, "Blob");
// + 256 because the arena might also be used by File
const arena = try page.getArena(data.len + content_type.len + 256, "Blob");
errdefer page.releaseArena(arena);
const mime = try Mime.serialize(arena, content_type);
@@ -124,6 +125,30 @@ pub fn acquireRef(self: *Blob) void {
self._rc.acquire();
}
pub fn structuredSerialize(self: *const Blob, writer: *js.StructuredWriter) !void {
writer.writeBytes(self._mime);
writer.writeBytes(self._slice);
}
pub fn structuredDeserialize(reader: *js.StructuredReader, page: *Page) !*Blob {
const mime = try reader.readBytes();
const data = try reader.readBytes();
const arena = try page.getArena(data.len + mime.len + 256, "Blob.clone");
errdefer page.releaseArena(arena);
const self = try arena.create(Blob);
self.* = .{
._rc = .{},
._arena = arena,
._type = .generic,
._slice = try arena.dupe(u8, data),
// the serialized mime is already in normalized form; copy it verbatim
._mime = try arena.dupe(u8, mime),
};
return self;
}
const largest_vector = @max(std.simd.suggestVectorLength(u8) orelse 1, 8);
/// Array of possible vector sizes for the current arch in decrementing order.
/// We may move this to some file for SIMD helpers in the future.

View File

@@ -258,7 +258,7 @@ pub const JsApi = struct {
pub const constructor = bridge.constructor(BroadcastChannel.init, .{});
pub const name = bridge.accessor(BroadcastChannel.getName, null, .{});
pub const postMessage = bridge.function(BroadcastChannel.postMessage, .{ .dom_exception = true });
pub const postMessage = bridge.function(BroadcastChannel.postMessage, .{});
pub const close = bridge.function(BroadcastChannel.close, .{});
pub const onmessage = bridge.accessor(BroadcastChannel.getOnMessage, BroadcastChannel.setOnMessage, .{});

View File

@@ -420,17 +420,16 @@ pub const JsApi = struct {
pub const name = "CharacterData";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const data = bridge.accessor(CData.getData, CData._setData, .{ .ce_reactions = true });
pub const length = bridge.accessor(CData.getLength, null, .{});
pub const appendData = bridge.function(CData.appendData, .{ .ce_reactions = true });
pub const deleteData = bridge.function(CData.deleteData, .{ .dom_exception = true, .ce_reactions = true });
pub const insertData = bridge.function(CData.insertData, .{ .dom_exception = true, .ce_reactions = true });
pub const replaceData = bridge.function(CData.replaceData, .{ .dom_exception = true, .ce_reactions = true });
pub const substringData = bridge.function(CData.substringData, .{ .dom_exception = true });
pub const deleteData = bridge.function(CData.deleteData, .{ .ce_reactions = true });
pub const insertData = bridge.function(CData.insertData, .{ .ce_reactions = true });
pub const replaceData = bridge.function(CData.replaceData, .{ .ce_reactions = true });
pub const substringData = bridge.function(CData.substringData, .{});
pub const remove = bridge.function(CData.remove, .{ .ce_reactions = true });
pub const before = bridge.function(CData.before, .{ .ce_reactions = true });

View File

@@ -65,7 +65,7 @@ pub fn escape(value: []const u8, frame: *Frame) ![]const u8 {
return value;
}
const result = try frame.call_arena.alloc(u8, out_len);
const result = try frame.local_arena.alloc(u8, out_len);
var pos: usize = 0;
if (needsEscape(true, first)) {

View File

@@ -82,7 +82,7 @@ pub const JsApi = struct {
pub const empty_with_no_proto = true;
};
pub const getRandomValues = bridge.function(Crypto.getRandomValues, .{ .dom_exception = true });
pub const getRandomValues = bridge.function(Crypto.getRandomValues, .{});
pub const randomUUID = bridge.function(Crypto.randomUUID, .{});
pub const subtle = bridge.accessor(Crypto.getSubtle, null, .{});
};

View File

@@ -190,7 +190,7 @@ pub fn getUsages(self: *const CryptoKey, exec: *const Execution) ![]const []cons
n += 1;
}
}
return exec.call_arena.dupe([]const u8, buf[0..n]);
return exec.local_arena.dupe([]const u8, buf[0..n]);
}
pub const JsApi = struct {

View File

@@ -281,10 +281,10 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain();
};
pub const define = bridge.function(CustomElementRegistry.define, .{ .dom_exception = true, .ce_reactions = true });
pub const define = bridge.function(CustomElementRegistry.define, .{ .ce_reactions = true });
pub const get = bridge.function(CustomElementRegistry.get, .{ .null_as_undefined = true });
pub const upgrade = bridge.function(CustomElementRegistry.upgrade, .{ .ce_reactions = true });
pub const whenDefined = bridge.function(CustomElementRegistry.whenDefined, .{ .dom_exception = true });
pub const whenDefined = bridge.function(CustomElementRegistry.whenDefined, .{});
};
const testing = @import("../../testing.zig");

View File

@@ -99,10 +99,9 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const empty_with_no_proto = true;
pub const enumerable = false;
};
pub const createDocumentType = bridge.function(DOMImplementation.createDocumentType, .{ .dom_exception = true });
pub const createDocumentType = bridge.function(DOMImplementation.createDocumentType, .{});
pub const createDocument = bridge.function(DOMImplementation.createDocument, .{});
pub const createHTMLDocument = bridge.function(DOMImplementation.createHTMLDocument, .{});
pub const hasFeature = bridge.function(DOMImplementation.hasFeature, .{});

View File

@@ -58,9 +58,12 @@ pub fn fromFloat64Array(array: js.TypedArray(f64), page: *Page) !*DOMMatrix {
return create(parsed.m, parsed.is_2d, page);
}
// The base already exposes read-only getters, but a redeclared accessor's
// getter must be typed to this (owner) struct, so we provide DOMMatrix-typed
// getters that read through `_proto`.
// DOMMatrix redeclares a/b/.../m44 as writable, so DOMMatrix.prototype needs
// its own read-write accessors, distinct from the read-only ones on
// DOMMatrixReadOnly.prototype. The setters are the point; each accessor bundles
// a getter too, so we pair them with DOMMatrix-typed getters that read through
// `_proto` (they could reuse the base getters — the receiver unwraps down the
// prototype chain either way — but keeping the pair symmetric reads cleaner).
pub fn getA(self: *const DOMMatrix) f64 {
return self._proto._m[0];
@@ -224,7 +227,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(DOMMatrix.init, .{ .dom_exception = true });
pub const constructor = bridge.constructor(DOMMatrix.init, .{});
pub const fromMatrix = bridge.function(DOMMatrix.fromMatrix, .{ .static = true });
pub const fromFloat32Array = bridge.function(DOMMatrix.fromFloat32Array, .{ .static = true });
@@ -268,7 +271,7 @@ pub const JsApi = struct {
pub const preMultiplySelf = bridge.function(DOMMatrix.preMultiplySelf, .{});
pub const invertSelf = bridge.function(DOMMatrix.invertSelf, .{});
// setMatrixValue parses a CSS transform string; Window-only.
pub const setMatrixValue = bridge.function(DOMMatrix.setMatrixValue, .{ .dom_exception = true, .exposed = .window });
pub const setMatrixValue = bridge.function(DOMMatrix.setMatrixValue, .{ .exposed = .window });
fn getM(comptime idx: usize) fn (*const DOMMatrix) f64 {
return struct {

View File

@@ -627,7 +627,7 @@ pub fn inverse(self: *const DOMMatrixReadOnly, page: *Page) !*DOMMatrix {
}
pub fn toFloat32Array(self: *const DOMMatrixReadOnly, exec: *const js.Execution) !js.TypedArray(f32) {
const out = try exec.call_arena.alloc(f32, 16);
const out = try exec.local_arena.alloc(f32, 16);
for (0..16) |i| {
out[i] = @floatCast(self._m[i]);
}
@@ -635,7 +635,7 @@ pub fn toFloat32Array(self: *const DOMMatrixReadOnly, exec: *const js.Execution)
}
pub fn toFloat64Array(self: *const DOMMatrixReadOnly, exec: *const js.Execution) !js.TypedArray(f64) {
const out = try exec.call_arena.dupe(f64, &self._m);
const out = try exec.local_arena.dupe(f64, &self._m);
return .{ .values = out };
}
@@ -648,7 +648,7 @@ pub fn toString(self: *const DOMMatrixReadOnly, exec: *const js.Execution) ![]co
return error.InvalidStateError;
}
}
return std.fmt.allocPrint(exec.call_arena, "matrix({d}, {d}, {d}, {d}, {d}, {d})", .{
return std.fmt.allocPrint(exec.local_arena, "matrix({d}, {d}, {d}, {d}, {d}, {d})", .{
m[0], m[1], m[4], m[5], m[12], m[13],
});
}
@@ -657,7 +657,7 @@ pub fn toString(self: *const DOMMatrixReadOnly, exec: *const js.Execution) ![]co
return error.InvalidStateError;
}
}
return std.fmt.allocPrint(exec.call_arena, "matrix3d({d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d})", .{
return std.fmt.allocPrint(exec.local_arena, "matrix3d({d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d})", .{
m[0], m[1], m[2], m[3],
m[4], m[5], m[6], m[7],
m[8], m[9], m[10], m[11],
@@ -803,7 +803,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(DOMMatrixReadOnly.init, .{ .dom_exception = true });
pub const constructor = bridge.constructor(DOMMatrixReadOnly.init, .{});
pub const fromMatrix = bridge.function(DOMMatrixReadOnly.fromMatrix, .{ .static = true });
pub const fromFloat32Array = bridge.function(DOMMatrixReadOnly.fromFloat32Array, .{ .static = true });
@@ -852,7 +852,7 @@ pub const JsApi = struct {
pub const toFloat32Array = bridge.function(DOMMatrixReadOnly.toFloat32Array, .{});
pub const toFloat64Array = bridge.function(DOMMatrixReadOnly.toFloat64Array, .{});
// The stringifier depends on CSS serialization and is Window-only.
pub const toString = bridge.function(DOMMatrixReadOnly.toString, .{ .dom_exception = true, .exposed = .window });
pub const toString = bridge.function(DOMMatrixReadOnly.toString, .{ .exposed = .window });
// m11..m44 getters are generated from the storage index.
fn getM(comptime idx: usize) fn (*const DOMMatrixReadOnly) f64 {

View File

@@ -191,7 +191,6 @@ pub const JsApi = struct {
pub const name = "NodeIterator";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const root = bridge.accessor(DOMNodeIterator.getRoot, null, .{});
@@ -200,7 +199,7 @@ pub const JsApi = struct {
pub const whatToShow = bridge.accessor(DOMNodeIterator.getWhatToShow, null, .{});
pub const filter = bridge.accessor(DOMNodeIterator.getFilter, null, .{});
pub const nextNode = bridge.function(DOMNodeIterator.nextNode, .{ .dom_exception = true });
pub const previousNode = bridge.function(DOMNodeIterator.previousNode, .{ .dom_exception = true });
pub const nextNode = bridge.function(DOMNodeIterator.nextNode, .{});
pub const previousNode = bridge.function(DOMNodeIterator.previousNode, .{});
pub const detach = bridge.function(DOMNodeIterator.detach, .{});
};

View File

@@ -0,0 +1,108 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const RO = @import("DOMPointReadOnly.zig");
const DOMPoint = @This();
_proto: *RO,
pub fn init(x_: ?f64, y_: ?f64, z_: ?f64, w_: ?f64, exec: *const js.Execution) !*DOMPoint {
return create(x_ orelse 0, y_ orelse 0, z_ orelse 0, w_ orelse 1, exec.page);
}
pub fn create(x: f64, y: f64, z: f64, w: f64, page: *Page) !*DOMPoint {
const proto = try RO.createBare(x, y, z, w, page);
errdefer proto.deinit(page);
const self = try proto._arena.create(DOMPoint);
self.* = .{ ._proto = proto };
proto._type = .{ .mutable = self };
return self;
}
pub fn fromPoint(other_: ?RO.DOMPointInit, page: *Page) !*DOMPoint {
const other: RO.DOMPointInit = other_ orelse .{};
return create(other.x, other.y, other.z, other.w, page);
}
pub fn structuredSerialize(self: *const DOMPoint, writer: *js.StructuredWriter) !void {
try self._proto.structuredSerialize(writer);
}
pub fn structuredDeserialize(reader: *js.StructuredReader, page: *Page) !*DOMPoint {
const proto = try RO.structuredDeserialize(reader, page);
errdefer proto.deinit(page);
const self = try proto._arena.create(DOMPoint);
self.* = .{ ._proto = proto };
proto._type = .{ .mutable = self };
return self;
}
// DOMPoint redeclares x/y/z/w as writable (`inherit attribute` in the IDL), so
// DOMPoint.prototype needs its own read-write accessors, distinct from the
// read-only ones on DOMPointReadOnly.prototype. The setters are the point; each
// accessor bundles a getter too, so we pair them with DOMPoint-typed getters
// that read through `_proto` (they could reuse the base getters — the receiver
// unwraps down the prototype chain either way — but keeping the pair symmetric
// mirrors DOMMatrix).
pub fn getX(self: *const DOMPoint) f64 {
return self._proto._x;
}
pub fn getY(self: *const DOMPoint) f64 {
return self._proto._y;
}
pub fn getZ(self: *const DOMPoint) f64 {
return self._proto._z;
}
pub fn getW(self: *const DOMPoint) f64 {
return self._proto._w;
}
pub fn setX(self: *DOMPoint, v: f64) void {
self._proto._x = v;
}
pub fn setY(self: *DOMPoint, v: f64) void {
self._proto._y = v;
}
pub fn setZ(self: *DOMPoint, v: f64) void {
self._proto._z = v;
}
pub fn setW(self: *DOMPoint, v: f64) void {
self._proto._w = v;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(DOMPoint);
pub const Meta = struct {
pub const name = "DOMPoint";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(DOMPoint.init, .{});
pub const fromPoint = bridge.function(DOMPoint.fromPoint, .{ .static = true });
pub const x = bridge.accessor(DOMPoint.getX, DOMPoint.setX, .{});
pub const y = bridge.accessor(DOMPoint.getY, DOMPoint.setY, .{});
pub const z = bridge.accessor(DOMPoint.getZ, DOMPoint.setZ, .{});
pub const w = bridge.accessor(DOMPoint.getW, DOMPoint.setW, .{});
};

View File

@@ -0,0 +1,167 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const DOMPoint = @import("DOMPoint.zig");
const Matrix = @import("DOMMatrixReadOnly.zig");
const Allocator = std.mem.Allocator;
const DOMPointReadOnly = @This();
pub const _prototype_root = true;
_type: Type,
_rc: lp.RC(u8),
_arena: Allocator,
_x: f64,
_y: f64,
_z: f64,
_w: f64,
pub const Type = union(enum) {
generic,
mutable: *DOMPoint,
};
pub const DOMPointInit = struct {
x: f64 = 0,
y: f64 = 0,
z: f64 = 0,
w: f64 = 1,
};
pub fn init(x_: ?f64, y_: ?f64, z_: ?f64, w_: ?f64, exec: *const js.Execution) !*DOMPointReadOnly {
return createBare(x_ orelse 0, y_ orelse 0, z_ orelse 0, w_ orelse 1, exec.page);
}
pub fn deinit(self: *DOMPointReadOnly, page: *Page) void {
page.releaseArena(self._arena);
}
pub fn acquireRef(self: *DOMPointReadOnly) void {
self._rc.acquire();
}
pub fn releaseRef(self: *DOMPointReadOnly, page: *Page) void {
self._rc.release(self, page);
}
pub fn createBare(x: f64, y: f64, z: f64, w: f64, page: *Page) !*DOMPointReadOnly {
const arena = try page.getArena(.tiny, "DOMPoint");
errdefer page.releaseArena(arena);
const self = try arena.create(DOMPointReadOnly);
self.* = .{
._rc = .{},
._arena = arena,
._type = .generic,
._x = x,
._y = y,
._z = z,
._w = w,
};
return self;
}
pub fn fromPoint(other_: ?DOMPointInit, page: *Page) !*DOMPointReadOnly {
const other: DOMPointInit = other_ orelse .{};
return createBare(other.x, other.y, other.z, other.w, page);
}
pub fn structuredSerialize(self: *const DOMPointReadOnly, writer: *js.StructuredWriter) !void {
writer.writeUint64(@bitCast(self._x));
writer.writeUint64(@bitCast(self._y));
writer.writeUint64(@bitCast(self._z));
writer.writeUint64(@bitCast(self._w));
}
pub fn structuredDeserialize(reader: *js.StructuredReader, page: *Page) !*DOMPointReadOnly {
const x: f64 = @bitCast(try reader.readUint64());
const y: f64 = @bitCast(try reader.readUint64());
const z: f64 = @bitCast(try reader.readUint64());
const w: f64 = @bitCast(try reader.readUint64());
return createBare(x, y, z, w, page);
}
pub fn matrixTransform(self: *const DOMPointReadOnly, matrix_: ?Matrix.DOMMatrixInit, page: *Page) !*DOMPoint {
const m = (try Matrix.fixupDict(matrix_ orelse .{})).m;
const x = self._x;
const y = self._y;
const z = self._z;
const w = self._w;
return DOMPoint.create(
m[0] * x + m[4] * y + m[8] * z + m[12] * w,
m[1] * x + m[5] * y + m[9] * z + m[13] * w,
m[2] * x + m[6] * y + m[10] * z + m[14] * w,
m[3] * x + m[7] * y + m[11] * z + m[15] * w,
page,
);
}
pub fn getX(self: *const DOMPointReadOnly) f64 {
return self._x;
}
pub fn getY(self: *const DOMPointReadOnly) f64 {
return self._y;
}
pub fn getZ(self: *const DOMPointReadOnly) f64 {
return self._z;
}
pub fn getW(self: *const DOMPointReadOnly) f64 {
return self._w;
}
pub fn toJSON(self: *const DOMPointReadOnly) struct {
x: f64,
y: f64,
z: f64,
w: f64,
} {
return .{ .x = self._x, .y = self._y, .z = self._z, .w = self._w };
}
pub const JsApi = struct {
pub const bridge = js.Bridge(DOMPointReadOnly);
pub const Meta = struct {
pub const name = "DOMPointReadOnly";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(DOMPointReadOnly.init, .{});
pub const fromPoint = bridge.function(DOMPointReadOnly.fromPoint, .{ .static = true });
pub const x = bridge.accessor(DOMPointReadOnly.getX, null, .{});
pub const y = bridge.accessor(DOMPointReadOnly.getY, null, .{});
pub const z = bridge.accessor(DOMPointReadOnly.getZ, null, .{});
pub const w = bridge.accessor(DOMPointReadOnly.getW, null, .{});
pub const matrixTransform = bridge.function(DOMPointReadOnly.matrixTransform, .{});
pub const toJSON = bridge.function(DOMPointReadOnly.toJSON, .{});
};
const testing = @import("../../testing.zig");
test "WebApi: DOMPoint" {
try testing.htmlRunner("dompoint.html", .{});
}

View File

@@ -343,7 +343,6 @@ pub const JsApi = struct {
pub const name = "TreeWalker";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const root = bridge.accessor(DOMTreeWalker.getRoot, null, .{});

View File

@@ -102,7 +102,7 @@ fn normalizeFormat(arena: Allocator, format: []const u8) ![]const u8 {
}
pub fn getData(self: *const DataTransfer, format: []const u8, frame: *Frame) ![]const u8 {
const norm = try normalizeFormat(frame.call_arena, format);
const norm = try normalizeFormat(frame.local_arena, format);
for (self._items.items) |it| {
if (it._kind == .string and std.mem.eql(u8, it._type, norm)) {
return it._payload.string;
@@ -127,7 +127,7 @@ pub fn setData(self: *DataTransfer, format: []const u8, data: []const u8) !void
pub fn clearData(self: *DataTransfer, format_: ?[]const u8, frame: *Frame) !void {
if (format_) |format| {
const norm = try normalizeFormat(frame.call_arena, format);
const norm = try normalizeFormat(frame.local_arena, format);
var i: usize = 0;
while (i < self._items.items.len) {
const it = self._items.items[i];
@@ -222,14 +222,14 @@ pub fn getTypes(self: *DataTransfer, frame: *Frame) ![][]const u8 {
var has_files = false;
for (self._items.items) |it| {
switch (it._kind) {
.string => try out.append(frame.call_arena, it._type),
.string => try out.append(frame.local_arena, it._type),
.file => has_files = true,
}
}
if (has_files) {
try out.append(frame.call_arena, "Files");
try out.append(frame.local_arena, "Files");
}
return out.toOwnedSlice(frame.call_arena);
return out.toOwnedSlice(frame.local_arena);
}
pub fn getDropEffect(self: *const DataTransfer) []const u8 {

View File

@@ -171,7 +171,7 @@ pub fn setDomain(self: *Document, value: []const u8) !void {
const doc_frame = self._frame orelse return error.SecurityError;
const origin = doc_frame.origin orelse return error.SecurityError;
const arena = doc_frame.call_arena;
const arena = doc_frame.local_arena;
const requested = if (idna.needsAscii(value)) try idna.toAscii(arena, value) else value;
// Validate against the current effective domain. Once relaxed,
@@ -193,7 +193,7 @@ pub fn setDomain(self: *Document, value: []const u8) !void {
pub fn getCookie(_: *Document, frame: *Frame) ![]const u8 {
var buf: std.ArrayList(u8) = .empty;
try frame._session.cookie_jar.forRequest(frame.url, buf.writer(frame.call_arena), .{
try frame._session.cookie_jar.forRequest(frame.url, buf.writer(frame.local_arena), .{
.is_http = false,
.is_navigation = true,
});
@@ -738,7 +738,7 @@ pub fn elementFromPoint(self: *Document, x: f64, y: f64, frame: *Frame) !?*Eleme
const root = self.asNode();
var stack: std.ArrayList(*Node) = .empty;
try stack.append(frame.call_arena, root);
try stack.append(frame.local_arena, root);
var visibility_cache: Element.VisibilityCache = .{};
var preorder_index: f64 = 0;
@@ -770,7 +770,7 @@ pub fn elementFromPoint(self: *Document, x: f64, y: f64, frame: *Frame) !?*Eleme
// Add children to stack in reverse order so we process them in document order
var child = node.lastChild();
while (child) |c| {
try stack.append(frame.call_arena, c);
try stack.append(frame.local_arena, c);
child = c.previousSibling();
}
}
@@ -783,7 +783,7 @@ pub fn elementsFromPoint(self: *Document, x: f64, y: f64, frame: *Frame) ![]cons
var current: ?*Element = (try self.elementFromPoint(x, y, frame)) orelse return &.{};
var result: std.ArrayList(*Element) = .empty;
while (current) |el| {
try result.append(frame.call_arena, el);
try result.append(frame.local_arena, el);
current = el.parentElement();
}
return result.items;
@@ -1254,7 +1254,6 @@ pub const JsApi = struct {
pub const name = "Document";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const constructor = bridge.constructor(_constructor, .{});
@@ -1278,23 +1277,23 @@ pub const JsApi = struct {
pub const styleSheets = bridge.accessor(Document.getStyleSheets, null, .{});
pub const fonts = bridge.accessor(Document.getFonts, null, .{});
pub const contentType = bridge.accessor(Document.getContentType, null, .{});
pub const domain = bridge.accessor(Document.getDomain, Document.setDomain, .{ .dom_exception = true });
pub const domain = bridge.accessor(Document.getDomain, Document.setDomain, .{});
pub const cookie = bridge.accessor(Document.getCookie, Document.setCookie, .{});
pub const createElement = bridge.function(Document.createElement, .{ .dom_exception = true });
pub const createElementNS = bridge.function(Document.createElementNS, .{ .dom_exception = true });
pub const createElement = bridge.function(Document.createElement, .{});
pub const createElementNS = bridge.function(Document.createElementNS, .{});
pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{});
pub const createComment = bridge.function(Document.createComment, .{});
pub const createTextNode = bridge.function(Document.createTextNode, .{});
pub const createAttribute = bridge.function(Document.createAttribute, .{ .dom_exception = true });
pub const createAttributeNS = bridge.function(Document.createAttributeNS, .{ .dom_exception = true });
pub const createCDATASection = bridge.function(Document.createCDATASection, .{ .dom_exception = true });
pub const createProcessingInstruction = bridge.function(Document.createProcessingInstruction, .{ .dom_exception = true });
pub const createAttribute = bridge.function(Document.createAttribute, .{});
pub const createAttributeNS = bridge.function(Document.createAttributeNS, .{});
pub const createCDATASection = bridge.function(Document.createCDATASection, .{});
pub const createProcessingInstruction = bridge.function(Document.createProcessingInstruction, .{});
pub const createRange = bridge.function(Document.createRange, .{});
pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true });
pub const createEvent = bridge.function(Document.createEvent, .{});
pub const createTreeWalker = bridge.function(Document.createTreeWalker, .{});
pub const createNodeIterator = bridge.function(Document.createNodeIterator, .{});
pub const evaluate = bridge.function(Document.evaluate, .{ .dom_exception = true });
pub const createExpression = bridge.function(Document.createExpression, .{ .dom_exception = true });
pub const evaluate = bridge.function(Document.evaluate, .{});
pub const createExpression = bridge.function(Document.createExpression, .{});
pub const createNSResolver = bridge.function(Document.createNSResolver, .{});
pub const getElementById = bridge.function(_getElementById, .{});
fn _getElementById(self: *Document, value_: ?js.Value, frame: *Frame) !?*Element {
@@ -1307,25 +1306,25 @@ pub const JsApi = struct {
}
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 });
pub const querySelector = bridge.function(Document.querySelector, .{});
pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{});
pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{});
pub const getElementsByTagNameNS = bridge.function(Document.getElementsByTagNameNS, .{});
pub const getSelection = bridge.function(Document.getSelection, .{});
pub const getElementsByClassName = bridge.function(Document.getElementsByClassName, .{});
pub const getElementsByName = bridge.function(Document.getElementsByName, .{});
pub const adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true, .ce_reactions = true });
pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true, .ce_reactions = true });
pub const append = bridge.function(Document.append, .{ .dom_exception = true, .ce_reactions = true });
pub const prepend = bridge.function(Document.prepend, .{ .dom_exception = true, .ce_reactions = true });
pub const moveBefore = bridge.function(Document.moveBefore, .{ .dom_exception = true, .ce_reactions = true });
pub const replaceChildren = bridge.function(Document.replaceChildren, .{ .dom_exception = true, .ce_reactions = true });
pub const adoptNode = bridge.function(Document.adoptNode, .{ .ce_reactions = true });
pub const importNode = bridge.function(Document.importNode, .{ .ce_reactions = true });
pub const append = bridge.function(Document.append, .{ .ce_reactions = true });
pub const prepend = bridge.function(Document.prepend, .{ .ce_reactions = true });
pub const moveBefore = bridge.function(Document.moveBefore, .{ .ce_reactions = true });
pub const replaceChildren = bridge.function(Document.replaceChildren, .{ .ce_reactions = true });
pub const elementFromPoint = bridge.function(Document.elementFromPoint, .{});
pub const elementsFromPoint = bridge.function(Document.elementsFromPoint, .{});
pub const write = bridge.function(Document.write, .{ .dom_exception = true, .ce_reactions = true });
pub const writeln = bridge.function(Document.writeln, .{ .dom_exception = true, .ce_reactions = true });
pub const open = bridge.function(Document.open, .{ .dom_exception = true, .ce_reactions = true });
pub const close = bridge.function(Document.close, .{ .dom_exception = true, .ce_reactions = true });
pub const write = bridge.function(Document.write, .{ .ce_reactions = true });
pub const writeln = bridge.function(Document.writeln, .{ .ce_reactions = true });
pub const open = bridge.function(Document.open, .{ .ce_reactions = true });
pub const close = bridge.function(Document.close, .{ .ce_reactions = true });
pub const doctype = bridge.accessor(Document.getDocType, null, .{});
pub const firstElementChild = bridge.accessor(Document.getFirstElementChild, null, .{});
pub const lastElementChild = bridge.accessor(Document.getLastElementChild, null, .{});

View File

@@ -192,7 +192,6 @@ pub const JsApi = struct {
pub const name = "DocumentFragment";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const constructor = bridge.constructor(DocumentFragment.init, .{});
@@ -209,20 +208,20 @@ pub const JsApi = struct {
return self.getElementById(try value.toZig([]const u8));
}
pub const querySelector = bridge.function(DocumentFragment.querySelector, .{ .dom_exception = true });
pub const querySelectorAll = bridge.function(DocumentFragment.querySelectorAll, .{ .dom_exception = true });
pub const querySelector = bridge.function(DocumentFragment.querySelector, .{});
pub const querySelectorAll = bridge.function(DocumentFragment.querySelectorAll, .{});
pub const children = bridge.accessor(DocumentFragment.getChildren, null, .{});
pub const childElementCount = bridge.accessor(DocumentFragment.getChildElementCount, null, .{});
pub const firstElementChild = bridge.accessor(DocumentFragment.firstElementChild, null, .{});
pub const lastElementChild = bridge.accessor(DocumentFragment.lastElementChild, null, .{});
pub const append = bridge.function(DocumentFragment.append, .{ .dom_exception = true, .ce_reactions = true });
pub const prepend = bridge.function(DocumentFragment.prepend, .{ .dom_exception = true, .ce_reactions = true });
pub const moveBefore = bridge.function(DocumentFragment.moveBefore, .{ .dom_exception = true, .ce_reactions = true });
pub const replaceChildren = bridge.function(DocumentFragment.replaceChildren, .{ .dom_exception = true, .ce_reactions = true });
pub const append = bridge.function(DocumentFragment.append, .{ .ce_reactions = true });
pub const prepend = bridge.function(DocumentFragment.prepend, .{ .ce_reactions = true });
pub const moveBefore = bridge.function(DocumentFragment.moveBefore, .{ .ce_reactions = true });
pub const replaceChildren = bridge.function(DocumentFragment.replaceChildren, .{ .ce_reactions = true });
pub const innerHTML = bridge.accessor(_getInnerHTML, _setInnerHTML, .{ .ce_reactions = true });
fn _getInnerHTML(self: *DocumentFragment, frame: *Frame) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
var buf = std.Io.Writer.Allocating.init(frame.local_arena);
try self.getInnerHTML(&buf.writer, frame);
return buf.written();
}

View File

@@ -87,7 +87,6 @@ pub const JsApi = struct {
pub const name = "DocumentType";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const name = bridge.accessor(DocumentType.getName, null, .{});

View File

@@ -1143,11 +1143,13 @@ pub fn animate(_: *Element, _: ?js.Object, _: ?js.Object, frame: *Frame) !*Anima
return Animation.init(frame);
}
pub fn closest(self: *Element, selector: []const u8, frame: *Frame) !?*Element {
if (selector.len == 0) {
pub fn closest(self: *Element, input: []const u8, frame: *Frame) !?*Element {
if (input.len == 0) {
return error.SyntaxError;
}
const selector = try Selector.cachedParse(frame._session.browser, input);
var current: ?*Element = self;
while (current) |el| {
if (try Selector.matchesWithScope(el, selector, self, frame)) {
@@ -1280,7 +1282,7 @@ pub fn getClientRects(self: *Element, frame: *Frame) ![]DOMRect {
if (!self.checkVisibilityCached(null, frame)) {
return &.{};
}
const rects = try frame.call_arena.alloc(DOMRect, 1);
const rects = try frame.local_arena.alloc(DOMRect, 1);
rects[0] = self.getBoundingClientRectForVisible(frame);
return rects;
}
@@ -1913,7 +1915,6 @@ pub const JsApi = struct {
pub const name = "Element";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const tagName = bridge.accessor(_tagName, null, .{});
@@ -1924,14 +1925,16 @@ pub const JsApi = struct {
pub const innerText = bridge.accessor(_innerText, Element.setInnerText, .{ .ce_reactions = true });
fn _innerText(self: *Element, frame: *Frame) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
var buf = std.Io.Writer.Allocating.init(frame.local_arena);
try self.getInnerText(&buf.writer, frame);
return buf.written();
}
pub const outerHTML = bridge.accessor(_getOuterHTML, _setOuterHTML, .{ .ce_reactions = true });
fn _getOuterHTML(self: *Element, frame: *Frame) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
// local_arena: serialization is read-only and the returned string is
// converted to v8 before this call returns. No JS runs in between.
var buf = std.Io.Writer.Allocating.init(frame.local_arena);
try self.getOuterHTML(&buf.writer, frame);
return buf.written();
}
@@ -1942,7 +1945,9 @@ pub const JsApi = struct {
pub const innerHTML = bridge.accessor(_getInnerHTML, _setInnerHTML, .{ .ce_reactions = true });
fn _getInnerHTML(self: *Element, frame: *Frame) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
// local_arena: read-only serialization, result converted to v8 before
// returning; no JS runs in between.
var buf = std.Io.Writer.Allocating.init(frame.local_arena);
try self.getInnerHTML(&buf.writer, frame);
return buf.written();
}
@@ -1953,12 +1958,12 @@ pub const JsApi = struct {
pub const prefix = bridge.accessor(Element._prefix, null, .{});
pub const setAttribute = bridge.function(_setAttribute, .{ .dom_exception = true, .ce_reactions = true });
pub const setAttribute = bridge.function(_setAttribute, .{ .ce_reactions = true });
fn _setAttribute(self: *Element, name: String, value: js.Value, frame: *Frame) !void {
return self.setAttribute(name, .wrap(try value.toStringSlice()), frame);
}
pub const setAttributeNS = bridge.function(_setAttributeNS, .{ .dom_exception = true, .ce_reactions = true });
pub const setAttributeNS = bridge.function(_setAttributeNS, .{ .ce_reactions = true });
fn _setAttributeNS(self: *Element, maybe_ns: ?[]const u8, qn: []const u8, value: js.Value, frame: *Frame) !void {
return self.setAttributeNS(maybe_ns, qn, .wrap(try value.toStringSlice()), frame);
}
@@ -1981,16 +1986,16 @@ pub const JsApi = struct {
pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{});
pub const setAttributeNode = bridge.function(Element.setAttributeNode, .{ .ce_reactions = true });
pub const removeAttribute = bridge.function(Element.removeAttribute, .{ .ce_reactions = true });
pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true, .ce_reactions = true });
pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .ce_reactions = true });
pub const getAttributeNames = bridge.function(Element.getAttributeNames, .{});
pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .dom_exception = true, .ce_reactions = true });
pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .ce_reactions = true });
pub const shadowRoot = bridge.accessor(Element.getShadowRoot, null, .{});
pub const assignedSlot = bridge.accessor(Element.getAssignedSlot, null, .{});
pub const attachShadow = bridge.function(_attachShadow, .{ .dom_exception = true });
pub const insertAdjacentHTML = bridge.function(Element.insertAdjacentHTML, .{ .dom_exception = true, .ce_reactions = true });
pub const setHTMLUnsafe = bridge.function(Element.setHTMLUnsafe, .{ .dom_exception = true, .ce_reactions = true });
pub const insertAdjacentElement = bridge.function(Element.insertAdjacentElement, .{ .dom_exception = true, .ce_reactions = true });
pub const insertAdjacentText = bridge.function(Element.insertAdjacentText, .{ .dom_exception = true, .ce_reactions = true });
pub const attachShadow = bridge.function(_attachShadow, .{});
pub const insertAdjacentHTML = bridge.function(Element.insertAdjacentHTML, .{ .ce_reactions = true });
pub const setHTMLUnsafe = bridge.function(Element.setHTMLUnsafe, .{ .ce_reactions = true });
pub const insertAdjacentElement = bridge.function(Element.insertAdjacentElement, .{ .ce_reactions = true });
pub const insertAdjacentText = bridge.function(Element.insertAdjacentText, .{ .ce_reactions = true });
const ShadowRootInit = struct {
mode: String,
@@ -2019,23 +2024,23 @@ pub const JsApi = struct {
.serializable = init.serializable,
}, frame);
}
pub const replaceChildren = bridge.function(Element.replaceChildren, .{ .dom_exception = true, .ce_reactions = true });
pub const replaceWith = bridge.function(Element.replaceWith, .{ .dom_exception = true, .ce_reactions = true });
pub const replaceChildren = bridge.function(Element.replaceChildren, .{ .ce_reactions = true });
pub const replaceWith = bridge.function(Element.replaceWith, .{ .ce_reactions = true });
pub const remove = bridge.function(Element.remove, .{ .ce_reactions = true });
pub const append = bridge.function(Element.append, .{ .dom_exception = true, .ce_reactions = true });
pub const prepend = bridge.function(Element.prepend, .{ .dom_exception = true, .ce_reactions = true });
pub const moveBefore = bridge.function(Element.moveBefore, .{ .dom_exception = true, .ce_reactions = true });
pub const before = bridge.function(Element.before, .{ .dom_exception = true, .ce_reactions = true });
pub const after = bridge.function(Element.after, .{ .dom_exception = true, .ce_reactions = true });
pub const append = bridge.function(Element.append, .{ .ce_reactions = true });
pub const prepend = bridge.function(Element.prepend, .{ .ce_reactions = true });
pub const moveBefore = bridge.function(Element.moveBefore, .{ .ce_reactions = true });
pub const before = bridge.function(Element.before, .{ .ce_reactions = true });
pub const after = bridge.function(Element.after, .{ .ce_reactions = true });
pub const firstElementChild = bridge.accessor(Element.firstElementChild, null, .{});
pub const lastElementChild = bridge.accessor(Element.lastElementChild, null, .{});
pub const nextElementSibling = bridge.accessor(Element.nextElementSibling, null, .{});
pub const previousElementSibling = bridge.accessor(Element.previousElementSibling, null, .{});
pub const childElementCount = bridge.accessor(Element.getChildElementCount, null, .{});
pub const matches = bridge.function(Element.matches, .{ .dom_exception = true });
pub const querySelector = bridge.function(Element.querySelector, .{ .dom_exception = true });
pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{ .dom_exception = true });
pub const closest = bridge.function(Element.closest, .{ .dom_exception = true });
pub const matches = bridge.function(Element.matches, .{});
pub const querySelector = bridge.function(Element.querySelector, .{});
pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{});
pub const closest = bridge.function(Element.closest, .{});
pub const getAnimations = bridge.function(Element.getAnimations, .{});
pub const animate = bridge.function(Element.animate, .{});
pub const checkVisibility = bridge.function(Element.checkVisibility, .{});

View File

@@ -465,7 +465,6 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const constructor = bridge.constructor(Event.init, .{});

View File

@@ -201,11 +201,10 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const constructor = bridge.constructor(EventTarget.init, .{});
pub const dispatchEvent = bridge.function(EventTarget.dispatchEvent, .{ .dom_exception = true });
pub const dispatchEvent = bridge.function(EventTarget.dispatchEvent, .{});
pub const addEventListener = bridge.function(EventTarget.addEventListener, .{});
pub const removeEventListener = bridge.function(EventTarget.removeEventListener, .{});
};

View File

@@ -72,6 +72,29 @@ pub fn acquireRef(self: *File) void {
self._proto.acquireRef();
}
pub fn structuredSerialize(self: *const File, writer: *js.StructuredWriter) !void {
try self._proto.structuredSerialize(writer);
writer.writeBytes(self._name);
writer.writeUint64(@bitCast(self._last_modified));
}
pub fn structuredDeserialize(reader: *js.StructuredReader, page: *Page) !*File {
const blob = try Blob.structuredDeserialize(reader, page);
errdefer blob.deinit(page);
const name = try reader.readBytes();
const last_modified = try reader.readUint64();
const file = try blob._arena.create(File);
file.* = .{
._proto = blob,
._name = try blob._arena.dupe(u8, name),
._last_modified = @bitCast(last_modified),
};
blob._type = .{ .file = file };
return file;
}
pub fn getName(self: *const File) []const u8 {
return self._name;
}

View File

@@ -1,4 +1,23 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const File = @import("File.zig");
@@ -24,6 +43,22 @@ pub fn item(self: *const FileList, index: u32) ?*File {
return self._files[index];
}
pub fn structuredSerialize(self: *const FileList, writer: *js.StructuredWriter) !void {
writer.writeUint32(@intCast(self._files.len));
for (self._files) |file| {
try file.structuredSerialize(writer);
}
}
pub fn structuredDeserialize(reader: *js.StructuredReader, page: *Page) !FileList {
const count = try reader.readUint32();
const files = try reader.local.ctx.arena.alloc(*File, count);
for (files) |*file| {
file.* = try File.structuredDeserialize(reader, page);
}
return .{ ._files = files };
}
pub fn iterator(self: *FileList, exec: *const js.Execution) !*Iterator {
return Iterator.init(.{
.index = 0,

View File

@@ -342,10 +342,10 @@ pub const JsApi = struct {
pub const onprogress = bridge.accessor(FileReader.getOnProgress, FileReader.setOnProgress, .{});
// Methods
pub const readAsArrayBuffer = bridge.function(FileReader.readAsArrayBuffer, .{ .dom_exception = true });
pub const readAsBinaryString = bridge.function(FileReader.readAsBinaryString, .{ .dom_exception = true });
pub const readAsText = bridge.function(FileReader.readAsText, .{ .dom_exception = true });
pub const readAsDataURL = bridge.function(FileReader.readAsDataURL, .{ .dom_exception = true });
pub const readAsArrayBuffer = bridge.function(FileReader.readAsArrayBuffer, .{});
pub const readAsBinaryString = bridge.function(FileReader.readAsBinaryString, .{});
pub const readAsText = bridge.function(FileReader.readAsText, .{});
pub const readAsDataURL = bridge.function(FileReader.readAsDataURL, .{});
pub const abort = bridge.function(FileReader.abort, .{});
};

View File

@@ -103,7 +103,7 @@ pub fn getTitle(self: *HTMLDocument, frame: *Frame) ![]const u8 {
return "";
};
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
var buf = std.Io.Writer.Allocating.init(frame.local_arena);
try title_element.asNode().getTextContent(&buf.writer);
const text = buf.written();
@@ -114,7 +114,7 @@ pub fn getTitle(self: *HTMLDocument, frame: *Frame) ![]const u8 {
var started = false;
var in_whitespace = false;
var result: std.ArrayList(u8) = .empty;
try result.ensureTotalCapacity(frame.call_arena, text.len);
try result.ensureTotalCapacity(frame.local_arena, text.len);
for (text) |c| {
const is_ascii_ws = c == ' ' or c == '\t' or c == '\n' or c == '\r' or c == '\x0C';
@@ -264,7 +264,7 @@ pub const JsApi = struct {
pub const dir = bridge.accessor(HTMLDocument.getDir, HTMLDocument.setDir, .{ .ce_reactions = true });
pub const head = bridge.accessor(HTMLDocument.getHead, null, .{});
pub const body = bridge.accessor(HTMLDocument.getBody, HTMLDocument.setBody, .{ .dom_exception = true, .ce_reactions = true });
pub const body = bridge.accessor(HTMLDocument.getBody, HTMLDocument.setBody, .{ .ce_reactions = true });
pub const lang = bridge.accessor(HTMLDocument.getLang, HTMLDocument.setLang, .{});
pub const title = bridge.accessor(HTMLDocument.getTitle, HTMLDocument.setTitle, .{ .ce_reactions = true });
pub const images = bridge.accessor(HTMLDocument.getImages, null, .{});

View File

@@ -20,6 +20,7 @@ const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const String = lp.String;
const Execution = js.Execution;
@@ -84,6 +85,27 @@ pub fn init(
});
}
pub fn structuredSerialize(self: *const ImageData, writer: *js.StructuredWriter) !void {
writer.writeUint32(self._width);
writer.writeUint32(self._height);
writer.writeBytes(self._data.local(writer.local).slice());
}
pub fn structuredDeserialize(reader: *js.StructuredReader, page: *Page) !*ImageData {
const width = try reader.readUint32();
const height = try reader.readUint32();
const bytes = try reader.readBytes();
const data = reader.local.createTypedArray(.uint8_clamped, bytes.len);
@memcpy(data.slice(), bytes);
return page.factory.create(ImageData{
._width = width,
._height = height,
._data = try data.persist(),
});
}
pub fn getWidth(self: *const ImageData) u32 {
return self._width;
}
@@ -105,7 +127,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(ImageData.init, .{ .dom_exception = true });
pub const constructor = bridge.constructor(ImageData.init, .{});
pub const colorSpace = bridge.property("srgb", .{ .template = false, .readonly = true });
pub const pixelFormat = bridge.property("rgba-unorm8", .{ .template = false, .readonly = true });

View File

@@ -191,7 +191,7 @@ pub fn disconnect(self: *IntersectionObserver, frame: *Frame) void {
}
pub fn takeRecords(self: *IntersectionObserver, frame: *Frame) ![]*IntersectionObserverEntry {
const entries = try frame.call_arena.dupe(*IntersectionObserverEntry, self._pending_entries.items);
const entries = try frame.local_arena.dupe(*IntersectionObserverEntry, self._pending_entries.items);
self._pending_entries.clearRetainingCapacity();
return entries;
}

View File

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

View File

@@ -19,12 +19,12 @@
const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Frame = @import("../Frame.zig");
const EventTarget = @import("EventTarget.zig");
const MessageEvent = @import("event/MessageEvent.zig");
const log = lp.log;
const Execution = js.Execution;
const MessagePort = @This();
@@ -35,8 +35,8 @@ _on_message: ?js.Function.Global = null,
_on_message_error: ?js.Function.Global = null,
_entangled_port: ?*MessagePort = null,
pub fn init(frame: *Frame) !*MessagePort {
return frame._factory.eventTarget(MessagePort{
pub fn init(exec: *Execution) !*MessagePort {
return exec._factory.eventTarget(MessagePort{
._proto = undefined,
});
}
@@ -50,7 +50,7 @@ pub fn entangle(port1: *MessagePort, port2: *MessagePort) void {
port2._entangled_port = port1;
}
pub fn postMessage(self: *MessagePort, message: js.Value, frame: *Frame) !void {
pub fn postMessage(self: *MessagePort, message: js.Value, exec: *Execution) !void {
if (self._closed) {
return;
}
@@ -66,7 +66,7 @@ pub fn postMessage(self: *MessagePort, message: js.Value, frame: *Frame) !void {
// value throws a DataCloneError to the caller. Mirrors Worker.postMessage.
const cloned = blk: {
var ls: js.Local.Scope = undefined;
frame.js.localScope(&ls);
exec.js.localScope(&ls);
defer ls.deinit();
// Contain any V8 exception from a failed serialization so it surfaces as
@@ -83,13 +83,13 @@ pub fn postMessage(self: *MessagePort, message: js.Value, frame: *Frame) !void {
errdefer cloned.release();
// Create callback to deliver message
const callback = try frame._factory.create(PostMessageCallback{
.frame = frame,
const callback = try exec._factory.create(PostMessageCallback{
.exec = exec,
.port = other,
.message = cloned,
});
try frame.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
try exec.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
.name = "MessagePort.postMessage",
.low_priority = false,
.finalizer = PostMessageCallback.cancelled,
@@ -132,7 +132,7 @@ pub fn setOnMessageError(self: *MessagePort, cb: ?js.Function.Global) !void {
const PostMessageCallback = struct {
port: *MessagePort,
message: js.Value.Temp,
frame: *Frame,
exec: *Execution,
// Called by the scheduler if the task is dropped before it runs. `run` and
// `cancelled` are mutually exclusive, so the temp is released exactly once.
@@ -143,13 +143,13 @@ const PostMessageCallback = struct {
}
fn deinit(self: *PostMessageCallback) void {
self.frame._factory.destroy(self);
self.exec._factory.destroy(self);
}
fn run(ctx: *anyopaque) !?u32 {
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
defer self.deinit();
const frame = self.frame;
const exec = self.exec;
// The MessageEvent takes ownership of the cloned temp and releases it on
// teardown; on any path where we don't hand it over, release it here so
@@ -160,7 +160,7 @@ const PostMessageCallback = struct {
}
const target = self.port.asEventTarget();
if (!frame._event_manager.hasDirectListeners(target, "message", self.port._on_message)) {
if (!exec.hasDirectListeners(target, "message", self.port._on_message)) {
self.message.release();
return null;
}
@@ -169,13 +169,13 @@ const PostMessageCallback = struct {
.data = .{ .value = self.message },
.origin = "",
.source = null,
}, frame._page) catch |err| {
}, exec.page) catch |err| {
self.message.release();
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
return null;
}).asEvent();
frame._event_manager.dispatchDirect(target, event, self.port._on_message, .{ .context = "MessagePort message" }) catch |err| {
exec.dispatch(target, event, self.port._on_message, .{ .context = "MessagePort message" }) catch |err| {
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
};
@@ -192,7 +192,7 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain();
};
pub const postMessage = bridge.function(MessagePort.postMessage, .{ .dom_exception = true });
pub const postMessage = bridge.function(MessagePort.postMessage, .{});
pub const start = bridge.function(MessagePort.start, .{});
pub const close = bridge.function(MessagePort.close, .{});

View File

@@ -233,10 +233,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const registerTool = bridge.function(
ModelContext.registerTool,
.{ .dom_exception = true },
);
pub const registerTool = bridge.function(ModelContext.registerTool, .{});
};
const testing = @import("../../testing.zig");

View File

@@ -188,7 +188,7 @@ pub fn disconnect(self: *MutationObserver, frame: *Frame) void {
}
pub fn takeRecords(self: *MutationObserver, frame: *Frame) ![]*MutationRecord {
const records = try frame.call_arena.dupe(*MutationRecord, self._pending_records.items);
const records = try frame.local_arena.dupe(*MutationRecord, self._pending_records.items);
self._pending_records.clearRetainingCapacity();
return records;
}

View File

@@ -254,8 +254,8 @@ pub const JsApi = struct {
// window only
pub const plugins = bridge.accessor(Navigator.getPlugins, null, .{ .exposed = .window });
pub const modelContext = bridge.accessor(Navigator.getModelContext, null, .{ .exposed = .window });
pub const registerProtocolHandler = bridge.function(Navigator.registerProtocolHandler, .{ .dom_exception = true, .exposed = .window });
pub const unregisterProtocolHandler = bridge.function(Navigator.unregisterProtocolHandler, .{ .dom_exception = true, .exposed = .window });
pub const registerProtocolHandler = bridge.function(Navigator.registerProtocolHandler, .{ .exposed = .window });
pub const unregisterProtocolHandler = bridge.function(Navigator.unregisterProtocolHandler, .{ .exposed = .window });
};
const testing = @import("../../testing.zig");

View File

@@ -908,7 +908,7 @@ pub fn setData(self: *Node, data: []const u8, frame: *Frame) !void {
pub fn normalize(self: *Node, frame: *Frame) !void {
var buffer: std.ArrayList(u8) = .empty;
return self._normalize(frame.call_arena, &buffer, frame);
return self._normalize(frame.local_arena, &buffer, frame);
}
const CloneError = error{
@@ -1276,7 +1276,6 @@ pub const JsApi = struct {
pub const name = "Node";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const ELEMENT_NODE = bridge.property(1, .{ .template = true });
@@ -1311,7 +1310,9 @@ pub const JsApi = struct {
// cdata and attributes can return value directly, avoiding the copy
switch (self._type) {
.element, .document_fragment => {
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
// local_arena: read-only text collection, result converted to
// v8 before returning; no JS runs in between.
var buf = std.Io.Writer.Allocating.init(frame.local_arena);
try self.getTextContent(&buf.writer);
return buf.written();
},
@@ -1328,19 +1329,19 @@ pub const JsApi = struct {
pub const previousSibling = bridge.accessor(Node.previousSibling, null, .{});
pub const parentNode = bridge.accessor(Node.parentNode, null, .{});
pub const parentElement = bridge.accessor(Node.parentElement, null, .{});
pub const appendChild = bridge.function(Node.appendChild, .{ .dom_exception = true, .ce_reactions = true });
pub const appendChild = bridge.function(Node.appendChild, .{ .ce_reactions = true });
pub const childNodes = bridge.accessor(Node.childNodes, null, .{ .cache = .{ .private = "child_nodes" } });
pub const isConnected = bridge.accessor(Node.isConnected, null, .{});
pub const ownerDocument = bridge.accessor(Node.ownerDocument, null, .{});
pub const hasChildNodes = bridge.function(Node.hasChildNodes, .{});
pub const isSameNode = bridge.function(Node.isSameNode, .{});
pub const contains = bridge.function(Node.contains, .{});
pub const removeChild = bridge.function(Node.removeChild, .{ .dom_exception = true, .ce_reactions = true });
pub const removeChild = bridge.function(Node.removeChild, .{ .ce_reactions = true });
pub const nodeValue = bridge.accessor(Node.getNodeValue, Node.setNodeValue, .{ .ce_reactions = true });
pub const insertBefore = bridge.function(Node.insertBefore, .{ .dom_exception = true, .ce_reactions = true });
pub const replaceChild = bridge.function(Node.replaceChild, .{ .dom_exception = true, .ce_reactions = true });
pub const insertBefore = bridge.function(Node.insertBefore, .{ .ce_reactions = true });
pub const replaceChild = bridge.function(Node.replaceChild, .{ .ce_reactions = true });
pub const normalize = bridge.function(Node.normalize, .{ .ce_reactions = true });
pub const cloneNode = bridge.function(Node.cloneNode, .{ .dom_exception = true, .ce_reactions = true });
pub const cloneNode = bridge.function(Node.cloneNode, .{ .ce_reactions = true });
pub const compareDocumentPosition = bridge.function(Node.compareDocumentPosition, .{});
pub const getRootNode = bridge.function(_getRootNode, .{});
// The `options` argument is optional in JS; default it before calling the

View File

@@ -87,7 +87,6 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const empty_with_no_proto = true;
pub const enumerable = false;
};
pub const FILTER_ACCEPT = bridge.property(NodeFilter.FILTER_ACCEPT, .{ .template = true });

View File

@@ -215,11 +215,11 @@ pub fn getEntries(self: *const Performance) []*Entry {
}
pub fn getEntriesByType(self: *const Performance, entry_type: []const u8, exec: *const Execution) ![]const *Entry {
return filterEntriesByType(exec.call_arena, self._entries.items, entry_type);
return filterEntriesByType(exec.local_arena, self._entries.items, entry_type);
}
pub fn getEntriesByName(self: *const Performance, name: []const u8, entry_type: ?[]const u8, exec: *const Execution) ![]const *Entry {
return filterEntriesByName(exec.call_arena, self._entries.items, name, entry_type);
return filterEntriesByName(exec.local_arena, self._entries.items, name, entry_type);
}
// Also used by PerformanceObserver
@@ -357,7 +357,7 @@ pub const JsApi = struct {
pub const now = bridge.function(Performance.now, .{});
pub const mark = bridge.function(Performance.mark, .{});
pub const measure = bridge.function(Performance.measure, .{ .dom_exception = true });
pub const measure = bridge.function(Performance.measure, .{});
pub const clearMarks = bridge.function(Performance.clearMarks, .{});
pub const clearMeasures = bridge.function(Performance.clearMeasures, .{});
pub const setResourceTimingBufferSize = bridge.function(Performance.setResourceTimingBufferSize, .{ .noop = true });

View File

@@ -197,11 +197,11 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(PerformanceObserver.init, .{ .dom_exception = true });
pub const constructor = bridge.constructor(PerformanceObserver.init, .{});
pub const observe = bridge.function(PerformanceObserver.observe, .{ .dom_exception = true });
pub const observe = bridge.function(PerformanceObserver.observe, .{});
pub const disconnect = bridge.function(PerformanceObserver.disconnect, .{});
pub const takeRecords = bridge.function(PerformanceObserver.takeRecords, .{ .dom_exception = true });
pub const takeRecords = bridge.function(PerformanceObserver.takeRecords, .{});
pub const supportedEntryTypes = bridge.accessor(PerformanceObserver.getSupportedEntryTypes, null, .{ .static = true });
};
@@ -216,11 +216,11 @@ pub const EntryList = struct {
}
pub fn getEntriesByType(self: *const EntryList, entry_type: []const u8, exec: *Execution) ![]const *Performance.Entry {
return Performance.filterEntriesByType(exec.call_arena, self._entries, entry_type);
return Performance.filterEntriesByType(exec.local_arena, self._entries, entry_type);
}
pub fn getEntriesByName(self: *const EntryList, name: []const u8, entry_type: ?[]const u8, exec: *Execution) ![]const *Performance.Entry {
return Performance.filterEntriesByName(exec.call_arena, self._entries, name, entry_type);
return Performance.filterEntriesByName(exec.local_arena, self._entries, name, entry_type);
}
pub const JsApi = struct {

View File

@@ -108,5 +108,5 @@ pub const JsApi = struct {
pub const empty_with_no_proto = true;
};
pub const query = bridge.function(Permissions.query, .{ .dom_exception = true });
pub const query = bridge.function(Permissions.query, .{});
};

View File

@@ -569,7 +569,7 @@ pub fn createContextualFragment(self: *const Range, html: []const u8, frame: *Fr
pub fn toString(self: *const Range, frame: *Frame) ![]const u8 {
// Simplified implementation: just extract text content
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
var buf = std.Io.Writer.Allocating.init(frame.local_arena);
try self.writeTextContent(&buf.writer);
return buf.written();
}
@@ -708,28 +708,28 @@ pub const JsApi = struct {
pub const constructor = bridge.constructor(Range.init, .{});
pub const commonAncestorContainer = bridge.accessor(Range.getCommonAncestorContainer, null, .{});
pub const setStart = bridge.function(Range.setStart, .{ .dom_exception = true });
pub const setEnd = bridge.function(Range.setEnd, .{ .dom_exception = true });
pub const setStartBefore = bridge.function(Range.setStartBefore, .{ .dom_exception = true });
pub const setStartAfter = bridge.function(Range.setStartAfter, .{ .dom_exception = true });
pub const setEndBefore = bridge.function(Range.setEndBefore, .{ .dom_exception = true });
pub const setEndAfter = bridge.function(Range.setEndAfter, .{ .dom_exception = true });
pub const selectNode = bridge.function(Range.selectNode, .{ .dom_exception = true });
pub const setStart = bridge.function(Range.setStart, .{});
pub const setEnd = bridge.function(Range.setEnd, .{});
pub const setStartBefore = bridge.function(Range.setStartBefore, .{});
pub const setStartAfter = bridge.function(Range.setStartAfter, .{});
pub const setEndBefore = bridge.function(Range.setEndBefore, .{});
pub const setEndAfter = bridge.function(Range.setEndAfter, .{});
pub const selectNode = bridge.function(Range.selectNode, .{});
pub const selectNodeContents = bridge.function(Range.selectNodeContents, .{});
pub const collapse = bridge.function(Range.collapse, .{ .dom_exception = true });
pub const collapse = bridge.function(Range.collapse, .{});
pub const detach = bridge.function(Range.detach, .{});
pub const compareBoundaryPoints = bridge.function(Range.compareBoundaryPoints, .{ .dom_exception = true });
pub const comparePoint = bridge.function(Range.comparePoint, .{ .dom_exception = true });
pub const isPointInRange = bridge.function(Range.isPointInRange, .{ .dom_exception = true });
pub const compareBoundaryPoints = bridge.function(Range.compareBoundaryPoints, .{});
pub const comparePoint = bridge.function(Range.comparePoint, .{});
pub const isPointInRange = bridge.function(Range.isPointInRange, .{});
pub const intersectsNode = bridge.function(Range.intersectsNode, .{});
pub const cloneRange = bridge.function(Range.cloneRange, .{ .dom_exception = true });
pub const insertNode = bridge.function(Range.insertNode, .{ .dom_exception = true, .ce_reactions = true });
pub const deleteContents = bridge.function(Range.deleteContents, .{ .dom_exception = true, .ce_reactions = true });
pub const cloneContents = bridge.function(Range.cloneContents, .{ .dom_exception = true, .ce_reactions = true });
pub const extractContents = bridge.function(Range.extractContents, .{ .dom_exception = true, .ce_reactions = true });
pub const surroundContents = bridge.function(Range.surroundContents, .{ .dom_exception = true, .ce_reactions = true });
pub const createContextualFragment = bridge.function(Range.createContextualFragment, .{ .dom_exception = true, .ce_reactions = true });
pub const toString = bridge.function(Range.toString, .{ .dom_exception = true });
pub const cloneRange = bridge.function(Range.cloneRange, .{});
pub const insertNode = bridge.function(Range.insertNode, .{ .ce_reactions = true });
pub const deleteContents = bridge.function(Range.deleteContents, .{ .ce_reactions = true });
pub const cloneContents = bridge.function(Range.cloneContents, .{ .ce_reactions = true });
pub const extractContents = bridge.function(Range.extractContents, .{ .ce_reactions = true });
pub const surroundContents = bridge.function(Range.surroundContents, .{ .ce_reactions = true });
pub const createContextualFragment = bridge.function(Range.createContextualFragment, .{ .ce_reactions = true });
pub const toString = bridge.function(Range.toString, .{});
pub const getBoundingClientRect = bridge.function(Range.getBoundingClientRect, .{});
pub const getClientRects = bridge.function(Range.getClientRects, .{});
};

View File

@@ -730,21 +730,21 @@ pub const JsApi = struct {
pub const @"type" = bridge.accessor(Selection.getType, null, .{});
pub const addRange = bridge.function(Selection.addRange, .{});
pub const collapse = bridge.function(Selection.collapse, .{ .dom_exception = true });
pub const collapse = bridge.function(Selection.collapse, .{});
pub const collapseToEnd = bridge.function(Selection.collapseToEnd, .{});
pub const collapseToStart = bridge.function(Selection.collapseToStart, .{ .dom_exception = true });
pub const collapseToStart = bridge.function(Selection.collapseToStart, .{});
pub const containsNode = bridge.function(Selection.containsNode, .{});
pub const deleteFromDocument = bridge.function(Selection.deleteFromDocument, .{ .ce_reactions = true });
pub const empty = bridge.function(Selection.removeAllRanges, .{});
pub const extend = bridge.function(Selection.extend, .{ .dom_exception = true });
pub const extend = bridge.function(Selection.extend, .{});
// unimplemented: getComposedRanges
pub const getRangeAt = bridge.function(Selection.getRangeAt, .{ .dom_exception = true });
pub const getRangeAt = bridge.function(Selection.getRangeAt, .{});
pub const modify = bridge.function(Selection.modify, .{});
pub const removeAllRanges = bridge.function(Selection.removeAllRanges, .{});
pub const removeRange = bridge.function(Selection.removeRange, .{ .dom_exception = true });
pub const selectAllChildren = bridge.function(Selection.selectAllChildren, .{ .dom_exception = true });
pub const setBaseAndExtent = bridge.function(Selection.setBaseAndExtent, .{ .dom_exception = true });
pub const setPosition = bridge.function(Selection.collapse, .{ .dom_exception = true });
pub const removeRange = bridge.function(Selection.removeRange, .{});
pub const selectAllChildren = bridge.function(Selection.selectAllChildren, .{});
pub const setBaseAndExtent = bridge.function(Selection.setBaseAndExtent, .{});
pub const setPosition = bridge.function(Selection.collapse, .{});
pub const toString = bridge.function(Selection.toString, .{});
};

View File

@@ -193,7 +193,7 @@ pub const JsApi = struct {
return self.getElementById(try value.toZig([]const u8), frame);
}
pub const adoptedStyleSheets = bridge.accessor(ShadowRoot.getAdoptedStyleSheets, ShadowRoot.setAdoptedStyleSheets, .{});
pub const setHTMLUnsafe = bridge.function(ShadowRoot.setHTMLUnsafe, .{ .dom_exception = true, .ce_reactions = true });
pub const setHTMLUnsafe = bridge.function(ShadowRoot.setHTMLUnsafe, .{ .ce_reactions = true });
pub const onslotchange = bridge.accessor(ShadowRoot.getOnSlotChange, ShadowRoot.setOnSlotChange, .{});
};

View File

@@ -76,7 +76,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(StaticRange.init, .{ .dom_exception = true });
pub const constructor = bridge.constructor(StaticRange.init, .{});
};
const testing = @import("../../testing.zig");

View File

@@ -185,7 +185,7 @@ pub fn importKey(
const k = jwk.k orelse {
return local.rejectPromise(.{ .dom_exception = .{ .err = error.DataError } });
};
break :blk common.base64Decode(exec.call_arena, k) catch |err| switch (err) {
break :blk common.base64Decode(exec.local_arena, k) catch |err| switch (err) {
error.DataError => return local.rejectPromise(.{ .dom_exception = .{ .err = error.DataError } }),
else => |e| return e,
};
@@ -309,13 +309,13 @@ fn exportJwk(key: *CryptoKey, exec: *const Execution) !js.Promise {
// The `alg` registry value depends on the algorithm and key length.
const alg: []const u8 = switch (key._type) {
.aes => try std.fmt.allocPrint(exec.call_arena, "A{d}{s}", .{
.aes => try std.fmt.allocPrint(exec.local_arena, "A{d}{s}", .{
key._key.len * 8,
key._algorithm.name[4..], // strip "AES-"
}),
.hmac => blk: {
const hash: []const u8 = key._algorithm.hash orelse "SHA-";
break :blk try std.fmt.allocPrint(exec.call_arena, "HS{s}", .{hash[4..]}); // strip "SHA-"
break :blk try std.fmt.allocPrint(exec.local_arena, "HS{s}", .{hash[4..]}); // strip "SHA-"
},
else => {
log.warn(.not_implemented, "SubtleCrypto.exportKey", .{ .format = "jwk", .type = key._type });
@@ -324,7 +324,7 @@ fn exportJwk(key: *CryptoKey, exec: *const Execution) !js.Promise {
};
return local.resolvePromise(JwkSecret{
.k = try common.base64Encode(exec.call_arena, key._key),
.k = try common.base64Encode(exec.local_arena, key._key),
.alg = alg,
.ext = key._extractable,
.key_ops = try key.getUsages(exec),
@@ -569,14 +569,14 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain();
};
pub const generateKey = bridge.function(SubtleCrypto.generateKey, .{ .dom_exception = true });
pub const importKey = bridge.function(SubtleCrypto.importKey, .{ .dom_exception = true });
pub const exportKey = bridge.function(SubtleCrypto.exportKey, .{ .dom_exception = true });
pub const encrypt = bridge.function(SubtleCrypto.encrypt, .{ .dom_exception = true });
pub const decrypt = bridge.function(SubtleCrypto.decrypt, .{ .dom_exception = true });
pub const sign = bridge.function(SubtleCrypto.sign, .{ .dom_exception = true });
pub const verify = bridge.function(SubtleCrypto.verify, .{ .dom_exception = true });
pub const deriveBits = bridge.function(SubtleCrypto.deriveBits, .{ .dom_exception = true });
pub const deriveKey = bridge.function(SubtleCrypto.deriveKey, .{ .dom_exception = true });
pub const digest = bridge.function(SubtleCrypto.digest, .{ .dom_exception = true });
pub const generateKey = bridge.function(SubtleCrypto.generateKey, .{});
pub const importKey = bridge.function(SubtleCrypto.importKey, .{});
pub const exportKey = bridge.function(SubtleCrypto.exportKey, .{});
pub const encrypt = bridge.function(SubtleCrypto.encrypt, .{});
pub const decrypt = bridge.function(SubtleCrypto.decrypt, .{});
pub const sign = bridge.function(SubtleCrypto.sign, .{});
pub const verify = bridge.function(SubtleCrypto.verify, .{});
pub const deriveBits = bridge.function(SubtleCrypto.deriveBits, .{});
pub const deriveKey = bridge.function(SubtleCrypto.deriveKey, .{});
pub const digest = bridge.function(SubtleCrypto.digest, .{});
};

View File

@@ -29,11 +29,21 @@ const js = @import("../js/js.zig");
const log = lp.log;
const Allocator = std.mem.Allocator;
const CLAMP_MS = 4;
const CLAMP_NESTING = 5;
const Timers = @This();
_timer_id: u30 = 0,
_callbacks: CallbackHashMap = .{},
// We keep the depth of the timers (a setTimeout calling a setTimeout). When
// the depth reaches CLAMP_NESTING, the minimum timeout is 4ms. This is per-
// spec and it's necessary to prevent some sites from virtually breaking because
// they repeatedly do heavy work in endlessly looping setTimeout with a short
// timeout (often of 0ms)
_nesting_level: u8 = 0,
const Key = u32;
const CallbackHashMap = std.HashMapUnmanaged(
Key,
@@ -82,6 +92,9 @@ pub fn schedule(
const timer_id = self._timer_id +% 1;
self._timer_id = timer_id;
const nesting = @min(self._nesting_level + 1, CLAMP_NESTING + 1);
const delay = if (nesting > CLAMP_NESTING and delay_ms < CLAMP_MS) CLAMP_MS else delay_ms;
var persisted_params: []js.Value.Temp = &.{};
if (opts.params.len > 0) {
persisted_params = try arena.dupe(js.Value.Temp, opts.params);
@@ -102,13 +115,14 @@ pub fn schedule(
.arena = arena,
.mode = opts.mode,
.name = opts.name,
.nesting = nesting,
.timer_id = timer_id,
.params = persisted_params,
.repeat_ms = if (opts.repeat) if (delay_ms == 0) 1 else delay_ms else null,
.repeat_ms = if (opts.repeat) if (delay == 0) 1 else delay else null,
};
gop.value_ptr.* = callback;
try exec.js.scheduler.add(callback, ScheduleCallback.run, delay_ms, .{
try exec.js.scheduler.add(callback, ScheduleCallback.run, delay, .{
.name = opts.name,
.low_priority = opts.low_priority,
.finalizer = ScheduleCallback.cancelled,
@@ -152,6 +166,10 @@ const ScheduleCallback = struct {
// delay, in ms, to repeat. When null, removed after first invocation.
repeat_ms: ?u32,
// The nesting of this task. When it executes, this nesting will become
// the Timer's _nesting_level so that any new timers will become nesting + 1
nesting: u8,
cb: js.Function.Temp,
mode: Mode,
@@ -185,6 +203,11 @@ const ScheduleCallback = struct {
self.exec.js.localScope(&ls);
defer ls.deinit();
const timers = self.timers;
const prev_nesting = timers._nesting_level;
timers._nesting_level = self.nesting;
defer timers._nesting_level = prev_nesting;
switch (self.mode) {
.idle => {
const IdleDeadline = @import("IdleDeadline.zig");
@@ -210,6 +233,12 @@ const ScheduleCallback = struct {
ls.local.runMicrotasks();
if (self.repeat_ms) |ms| {
// each repeat re-enters the timer initialization steps, so the
// nesting level keeps growing and sub-4ms intervals get clamped.
self.nesting = @min(self.nesting + 1, CLAMP_NESTING + 1);
if (self.nesting > CLAMP_NESTING and ms < CLAMP_MS) {
return CLAMP_MS;
}
return ms;
}
defer self.deinit();

View File

@@ -174,7 +174,7 @@ pub fn getSearch(self: *const URL, exec: *const Execution) ![]const u8 {
return "";
}
var buf = std.Io.Writer.Allocating.init(exec.call_arena);
var buf = std.Io.Writer.Allocating.init(exec.local_arena);
try buf.writer.writeByte('?');
try search_params.toString(&buf.writer);
return buf.written();
@@ -262,7 +262,7 @@ pub fn getOrigin(self: *const URL, exec: *const Execution) ![]const u8 {
const origin = U.url_get_origin(self._url);
defer origin.deinit();
return exec.call_arena.dupe(u8, origin.slice());
return exec.local_arena.dupe(u8, origin.slice());
}
pub fn setHref(self: *URL, value: []const u8, exec: *const Execution) !void {
@@ -288,7 +288,7 @@ pub fn toString(self: *const URL, exec: *const Execution) ![]const u8 {
if (search_params.getSize() == 0) {
U.url_set_query_to_null(self._url);
} else {
var buf = std.Io.Writer.Allocating.init(exec.call_arena);
var buf = std.Io.Writer.Allocating.init(exec.local_arena);
defer buf.deinit();
try search_params.toString(&buf.writer);
const query = buf.written();

View File

@@ -125,6 +125,10 @@ pub fn getEvent(self: *const Window) ?*Event {
return self._current_event;
}
pub fn setEvent(self: *Window, value: js.Value) void {
self.replaceGlobalProperty(value, "event");
}
pub fn getSelf(self: *Window) *Window {
return self;
}
@@ -139,6 +143,17 @@ pub fn getOpener(self: *Window, frame: *Frame) ?Access {
return Access.init(frame.window, opener);
}
// Per the HTML spec's opener setter: null disowns the opener (the accessor
// stays in place and the getter now returns null); any other value redefines
// the property as an own data property, like [Replaceable].
pub fn setOpener(self: *Window, value: js.Value) void {
if (value.isNull()) {
self._opener = null;
return;
}
self.replaceGlobalProperty(value, "opener");
}
pub fn getClosed(self: *const Window) bool {
return self._closed;
}
@@ -175,48 +190,48 @@ pub fn getConsole(self: *Window) *Console {
return &self._console;
}
pub fn setConsole(_: *Window, value: js.Value) void {
replaceGlobalProperty(value, "console");
pub fn setConsole(self: *Window, value: js.Value) void {
self.replaceGlobalProperty(value, "console");
}
pub fn setSelf(_: *Window, value: js.Value) void {
replaceGlobalProperty(value, "self");
pub fn setSelf(self: *Window, value: js.Value) void {
self.replaceGlobalProperty(value, "self");
}
pub fn setFrames(_: *Window, value: js.Value) void {
replaceGlobalProperty(value, "frames");
pub fn setFrames(self: *Window, value: js.Value) void {
self.replaceGlobalProperty(value, "frames");
}
pub fn setParent(_: *Window, value: js.Value) void {
replaceGlobalProperty(value, "parent");
pub fn setParent(self: *Window, value: js.Value) void {
self.replaceGlobalProperty(value, "parent");
}
pub fn setLength(_: *Window, value: js.Value) void {
replaceGlobalProperty(value, "length");
pub fn setLength(self: *Window, value: js.Value) void {
self.replaceGlobalProperty(value, "length");
}
pub fn setInnerWidth(_: *Window, value: js.Value) void {
replaceGlobalProperty(value, "innerWidth");
pub fn setInnerWidth(self: *Window, value: js.Value) void {
self.replaceGlobalProperty(value, "innerWidth");
}
pub fn setInnerHeight(_: *Window, value: js.Value) void {
replaceGlobalProperty(value, "innerHeight");
pub fn setInnerHeight(self: *Window, value: js.Value) void {
self.replaceGlobalProperty(value, "innerHeight");
}
pub fn setScrollX(_: *Window, value: js.Value) void {
replaceGlobalProperty(value, "scrollX");
pub fn setScrollX(self: *Window, value: js.Value) void {
self.replaceGlobalProperty(value, "scrollX");
}
pub fn setScrollY(_: *Window, value: js.Value) void {
replaceGlobalProperty(value, "scrollY");
pub fn setScrollY(self: *Window, value: js.Value) void {
self.replaceGlobalProperty(value, "scrollY");
}
pub fn setPageXOffset(_: *Window, value: js.Value) void {
replaceGlobalProperty(value, "pageXOffset");
pub fn setPageXOffset(self: *Window, value: js.Value) void {
self.replaceGlobalProperty(value, "pageXOffset");
}
pub fn setPageYOffset(_: *Window, value: js.Value) void {
replaceGlobalProperty(value, "pageYOffset");
pub fn setPageYOffset(self: *Window, value: js.Value) void {
self.replaceGlobalProperty(value, "pageYOffset");
}
pub fn getNavigator(self: *Window) *Navigator {
@@ -231,10 +246,18 @@ pub fn getScreen(self: *Window) *Screen {
return self._screen;
}
pub fn setScreen(self: *Window, value: js.Value) void {
self.replaceGlobalProperty(value, "screen");
}
pub fn getVisualViewport(self: *const Window) *VisualViewport {
return self._visual_viewport;
}
pub fn setVisualViewport(self: *Window, value: js.Value) void {
self.replaceGlobalProperty(value, "visualViewport");
}
pub fn getCrypto(self: *Window) *Crypto {
return &self._crypto;
}
@@ -247,6 +270,10 @@ pub fn getPerformance(self: *Window) *Performance {
return &self._performance;
}
pub fn setPerformance(self: *Window, value: js.Value) void {
self.replaceGlobalProperty(value, "performance");
}
fn bucketForOrigin(self: *Window) *storage.Bucket {
return self._frame._session.storage_shed.getOrPut(
self._frame._session.browser.app.allocator,
@@ -274,6 +301,10 @@ pub fn getOrigin(self: *const Window) []const u8 {
return self._frame.origin orelse "null";
}
pub fn setOrigin(self: *Window, value: js.Value) void {
self.replaceGlobalProperty(value, "origin");
}
pub fn getSelection(self: *const Window) *Selection {
return &self._document._selection;
}
@@ -298,6 +329,10 @@ pub fn getNavigation(_: *Window, frame: *Frame) *Navigation {
return &frame._session.navigation;
}
pub fn setNavigation(self: *Window, value: js.Value) void {
self.replaceGlobalProperty(value, "navigation");
}
pub fn getCustomElements(self: *Window) *CustomElementRegistry {
return &self._custom_elements;
}
@@ -506,9 +541,16 @@ pub fn getComputedStyle(_: *const Window, element: *Element, pseudo_element: ?[]
if (pseudo_element) |pe| {
if (pe.len != 0) {
log.warn(.not_implemented, "window.GetComputedStyle", .{ .pseudo_element = pe });
// Chrome hands out a distinct object per pseudo-element, so these
// can't share the per-element cache entry.
return CSSStyleProperties.init(element, true, frame);
}
}
return CSSStyleProperties.init(element, true, frame);
const gop = try frame._element_computed_styles.getOrPut(frame.arena, element);
if (!gop.found_existing) {
gop.value_ptr.* = try CSSStyleProperties.init(element, true, frame);
}
return gop.value_ptr.*;
}
// window.open(url?, target?, features?) — v1 scope:
@@ -696,16 +738,17 @@ pub fn postMessage(self: *Window, message: js.Value, target_origin: ?[]const u8,
const base64 = @import("encoding/base64.zig");
pub fn btoa(_: *const Window, input: base64.BinInput, frame: *Frame) ![]const u8 {
return base64.encode(frame.call_arena, input);
return base64.encode(frame.local_arena, input);
}
pub fn atob(_: *const Window, input: base64.BinInput, frame: *Frame) !js.String.OneByte {
const decoded = try base64.decode(frame.call_arena, input);
const decoded = try base64.decode(frame.local_arena, input);
return .{ .bytes = decoded };
}
pub fn structuredClone(_: *const Window, value: js.Value) !js.Value {
return value.structuredClone();
// the serializer already threw (e.g. a DataCloneError); keep it
return value.structuredClone() catch error.TryCatchRethrow;
}
pub fn getFrame(self: *Window, idx: usize) !?*Window {
@@ -878,11 +921,11 @@ pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js.
}
}
// `console` and a handful of other Window attributes are [Replaceable]: assigning
// to them redefines the attribute as an own data property on the global instead
// of throwing (which a getter-only accessor does in strict mode / modules).
fn replaceGlobalProperty(value: js.Value, comptime name: []const u8) void {
const global = value.local.getGlobal();
// Some properties are readonly but [Replaceable]. They get assigned as own
// data properties on the underlying v8::object that represents the global (the
// Window)
fn replaceGlobalProperty(self: *Window, value: js.Value, comptime name: []const u8) void {
const global = self._frame.js.globalObject(value.local);
_ = global.defineOwnProperty(name, value, 0);
}
@@ -1002,16 +1045,16 @@ pub const JsApi = struct {
pub const window = bridge.accessor(Window.getWindow, null, .{});
pub const parent = bridge.accessor(Window.getParent, Window.setParent, .{});
pub const navigator = bridge.accessor(Window.getNavigator, null, .{});
pub const screen = bridge.accessor(Window.getScreen, null, .{});
pub const visualViewport = bridge.accessor(Window.getVisualViewport, null, .{});
pub const performance = bridge.accessor(Window.getPerformance, null, .{});
pub const screen = bridge.accessor(Window.getScreen, Window.setScreen, .{});
pub const visualViewport = bridge.accessor(Window.getVisualViewport, Window.setVisualViewport, .{});
pub const performance = bridge.accessor(Window.getPerformance, Window.setPerformance, .{});
pub const localStorage = bridge.accessor(Window.getLocalStorage, null, .{});
pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{});
pub const cookieStore = bridge.accessor(Window.getCookieStore, null, .{});
pub const origin = bridge.accessor(Window.getOrigin, null, .{});
pub const origin = bridge.accessor(Window.getOrigin, Window.setOrigin, .{});
pub const location = bridge.accessor(Window.getLocation, Window.setLocation, .{ .deletable = false });
pub const history = bridge.accessor(Window.getHistory, null, .{});
pub const navigation = bridge.accessor(Window.getNavigation, null, .{});
pub const navigation = bridge.accessor(Window.getNavigation, Window.setNavigation, .{});
pub const crypto = bridge.accessor(Window.getCrypto, null, .{});
pub const CSS = bridge.accessor(Window.getCSS, null, .{});
pub const customElements = bridge.accessor(Window.getCustomElements, null, .{});
@@ -1023,7 +1066,7 @@ pub const JsApi = struct {
pub const onmessage = bridge.accessor(Window.getOnMessage, Window.setOnMessage, .{});
pub const onrejectionhandled = bridge.accessor(Window.getOnRejectionHandled, Window.setOnRejectionHandled, .{});
pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});
pub const event = bridge.accessor(Window.getEvent, null, .{ .null_as_undefined = true });
pub const event = bridge.accessor(Window.getEvent, Window.setEvent, .{ .null_as_undefined = true });
pub const fetch = bridge.function(Window.fetch, .{});
pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{});
pub const setTimeout = bridge.function(Window.setTimeout, .{});
@@ -1037,9 +1080,9 @@ pub const JsApi = struct {
pub const requestIdleCallback = bridge.function(Window.requestIdleCallback, .{});
pub const cancelIdleCallback = bridge.function(Window.cancelIdleCallback, .{});
pub const matchMedia = bridge.function(Window.matchMedia, .{});
pub const postMessage = bridge.function(Window.postMessage, .{ .dom_exception = true });
pub const btoa = bridge.function(Window.btoa, .{ .dom_exception = true });
pub const atob = bridge.function(Window.atob, .{ .dom_exception = true });
pub const postMessage = bridge.function(Window.postMessage, .{});
pub const btoa = bridge.function(Window.btoa, .{});
pub const atob = bridge.function(Window.atob, .{});
pub const reportError = bridge.function(Window.reportError, .{});
pub const structuredClone = bridge.function(Window.structuredClone, .{});
pub const getComputedStyle = bridge.function(Window.getComputedStyle, .{});
@@ -1070,10 +1113,10 @@ pub const JsApi = struct {
pub const innerHeight = bridge.accessor(Window.getInnerHeight, Window.setInnerHeight, .{});
pub const devicePixelRatio = bridge.property(1, .{ .template = false, .readonly = false });
pub const opener = bridge.accessor(Window.getOpener, null, .{});
pub const opener = bridge.accessor(Window.getOpener, Window.setOpener, .{});
pub const closed = bridge.accessor(Window.getClosed, null, .{});
pub const name = bridge.accessor(Window.getName, Window.setName, .{});
pub const open = bridge.function(Window.open, .{ .dom_exception = true });
pub const open = bridge.function(Window.open, .{});
pub const close = bridge.function(Window.close, .{});
pub const alert = bridge.function(struct {
@@ -1151,7 +1194,7 @@ const CrossOriginWindow = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const postMessage = bridge.function(CrossOriginWindow.postMessage, .{ .dom_exception = true });
pub const postMessage = bridge.function(CrossOriginWindow.postMessage, .{});
pub const top = bridge.accessor(CrossOriginWindow.getTop, null, .{});
pub const parent = bridge.accessor(CrossOriginWindow.getParent, null, .{});
pub const length = bridge.accessor(CrossOriginWindow.getFramesLength, null, .{});

View File

@@ -70,6 +70,7 @@ _http_owner: HttpClient.Owner = .{},
arena: Allocator,
call_arena: Allocator,
local_arena: Allocator,
url: [:0]const u8,
// Same-origin constraint: a worker's origin is inherited from its parent frame.
origin: ?[]const u8 = null,
@@ -129,6 +130,9 @@ pub fn init(
const call_arena = try session.getArena(.small, "WorkerGlobalScope.call_arena");
errdefer session.releaseArena(call_arena);
const local_arena = try session.getArena(.small, "WorkerGlobalScope.local_arena");
errdefer session.releaseArena(local_arena);
const factory = frame._factory;
const self = try factory.eventTargetWithAllocator(arena, WorkerGlobalScope{
.url = url,
@@ -136,6 +140,7 @@ pub fn init(
.origin = frame.origin,
.js = undefined,
.call_arena = call_arena,
.local_arena = local_arena,
._frame = frame,
._page = frame._page,
._session = session,
@@ -162,6 +167,7 @@ pub fn init(
self.js = try session.browser.env.createWorkerContext(self, .{
.call_arena = call_arena,
.local_arena = local_arena,
.identity_arena = arena,
.identity = &self._identity,
});
@@ -191,6 +197,7 @@ pub fn deinit(self: *WorkerGlobalScope) void {
}
browser.env.destroyContext(self.js);
session.releaseArena(self.call_arena);
session.releaseArena(self.local_arena);
}
pub fn base(self: *const WorkerGlobalScope) [:0]const u8 {
@@ -247,16 +254,16 @@ pub fn getSelf(self: *WorkerGlobalScope) *WorkerGlobalScope {
return self;
}
pub fn setSelf(_: *WorkerGlobalScope, value: JS.Value) void {
replaceGlobalProperty(value, "self");
pub fn setSelf(self: *WorkerGlobalScope, value: JS.Value) void {
self.replaceGlobalProperty(value, "self");
}
pub fn getConsole(self: *WorkerGlobalScope) *Console {
return &self._console;
}
pub fn setConsole(_: *WorkerGlobalScope, value: JS.Value) void {
replaceGlobalProperty(value, "console");
pub fn setConsole(self: *WorkerGlobalScope, value: JS.Value) void {
self.replaceGlobalProperty(value, "console");
}
pub fn getCrypto(self: *WorkerGlobalScope) *Crypto {
@@ -496,11 +503,11 @@ pub fn clearInterval(self: *WorkerGlobalScope, id: u32) void {
self._timers.clear(id);
}
// `console` and `self` are [Replaceable]: assignment redefines them as own data
// properties on the global rather than throwing through the getter-only accessor
// in strict mode. Used here (console) and by DedicatedWorkerGlobalScope (self).
pub fn replaceGlobalProperty(value: JS.Value, comptime name: []const u8) void {
const global = value.local.getGlobal();
// Some properties are readonly but [Replaceable]. They get assigned as own
// data properties on the underlying v8::object that represents the global (the
// WorkerGlobalScope)
fn replaceGlobalProperty(self: *WorkerGlobalScope, value: JS.Value, comptime name: []const u8) void {
const global = self.js.globalObject(value.local);
_ = global.defineOwnProperty(name, value, 0);
}
@@ -546,12 +553,12 @@ pub const JsApi = struct {
pub const onrejectionhandled = bridge.accessor(WorkerGlobalScope.getOnRejectionHandled, WorkerGlobalScope.setOnRejectionHandled, .{});
pub const onunhandledrejection = bridge.accessor(WorkerGlobalScope.getOnUnhandledRejection, WorkerGlobalScope.setOnUnhandledRejection, .{});
pub const btoa = bridge.function(WorkerGlobalScope.btoa, .{ .dom_exception = true });
pub const atob = bridge.function(WorkerGlobalScope.atob, .{ .dom_exception = true });
pub const btoa = bridge.function(WorkerGlobalScope.btoa, .{});
pub const atob = bridge.function(WorkerGlobalScope.atob, .{});
pub const structuredClone = bridge.function(WorkerGlobalScope.structuredClone, .{});
pub const reportError = bridge.function(WorkerGlobalScope.reportError, .{});
pub const fetch = bridge.function(WorkerGlobalScope.fetch, .{});
pub const importScripts = bridge.function(WorkerGlobalScope.importScripts, .{ .dom_exception = true });
pub const importScripts = bridge.function(WorkerGlobalScope.importScripts, .{});
pub const queueMicrotask = bridge.function(WorkerGlobalScope.queueMicrotask, .{});
pub const setTimeout = bridge.function(WorkerGlobalScope.setTimeout, .{});
pub const clearTimeout = bridge.function(WorkerGlobalScope.clearTimeout, .{});

View File

@@ -34,7 +34,7 @@ pub fn init() XMLSerializer {
pub fn serializeToString(self: *const XMLSerializer, node: *Node, frame: *Frame) ![]const u8 {
_ = self;
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
var buf = std.Io.Writer.Allocating.init(frame.local_arena);
if (node.is(Node.Document)) |doc| {
try dump.root(doc, .{ .shadow = .skip }, &buf.writer, frame);
} else {

View File

@@ -83,8 +83,8 @@ pub const JsApi = struct {
};
pub const constructor = bridge.constructor(XPathEvaluator.init, .{});
pub const evaluate = bridge.function(XPathEvaluator.evaluate, .{ .dom_exception = true });
pub const createExpression = bridge.function(XPathEvaluator.createExpression, .{ .dom_exception = true });
pub const evaluate = bridge.function(XPathEvaluator.evaluate, .{});
pub const createExpression = bridge.function(XPathEvaluator.createExpression, .{});
pub const createNSResolver = bridge.function(XPathEvaluator.createNSResolver, .{});
};

View File

@@ -101,5 +101,5 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const evaluate = bridge.function(XPathExpression.evaluate, .{ .dom_exception = true });
pub const evaluate = bridge.function(XPathExpression.evaluate, .{});
};

View File

@@ -27,10 +27,10 @@
//! `deinit` once the JS wrapper's refcount hits zero.
//!
//! Type-mismatch accessor calls return `error.InvalidStateError` —
//! translated to a `DOMException` by `bridge.function(.., .{
//! .dom_exception = true })`. The WHATWG IDL technically specifies
//! `TypeError` for type mismatches, but `InvalidStateError` is what
//! decision #4 captures and what most legacy XPath consumers expect.
//! translated to a `DOMException` by the bridge. The WHATWG IDL
//! technically specifies `TypeError` for type mismatches, but
//! `InvalidStateError` is what decision #4 captures and what most
//! legacy XPath consumers expect.
const std = @import("std");
const lp = @import("lightpanda");
@@ -261,15 +261,15 @@ pub const JsApi = struct {
pub const FIRST_ORDERED_NODE_TYPE = bridge.property(XPathResult.FIRST_ORDERED_NODE_TYPE, .{ .template = true });
pub const resultType = bridge.accessor(XPathResult.getResultType, null, .{});
pub const numberValue = bridge.accessor(XPathResult.getNumberValue, null, .{ .dom_exception = true });
pub const stringValue = bridge.accessor(XPathResult.getStringValue, null, .{ .dom_exception = true });
pub const booleanValue = bridge.accessor(XPathResult.getBooleanValue, null, .{ .dom_exception = true });
pub const singleNodeValue = bridge.accessor(XPathResult.getSingleNodeValue, null, .{ .dom_exception = true });
pub const snapshotLength = bridge.accessor(XPathResult.getSnapshotLength, null, .{ .dom_exception = true });
pub const numberValue = bridge.accessor(XPathResult.getNumberValue, null, .{});
pub const stringValue = bridge.accessor(XPathResult.getStringValue, null, .{});
pub const booleanValue = bridge.accessor(XPathResult.getBooleanValue, null, .{});
pub const singleNodeValue = bridge.accessor(XPathResult.getSingleNodeValue, null, .{});
pub const snapshotLength = bridge.accessor(XPathResult.getSnapshotLength, null, .{});
pub const invalidIteratorState = bridge.accessor(XPathResult.getInvalidIteratorState, null, .{});
pub const iterateNext = bridge.function(XPathResult.iterateNext, .{ .dom_exception = true });
pub const snapshotItem = bridge.function(XPathResult.snapshotItem, .{ .dom_exception = true });
pub const iterateNext = bridge.function(XPathResult.iterateNext, .{});
pub const snapshotItem = bridge.function(XPathResult.snapshotItem, .{});
};
const testing = @import("../../testing.zig");

View File

@@ -151,11 +151,11 @@ pub const JsApi = struct {
pub const textBaseline = bridge.property("alphabetic", .{ .template = false, .readonly = false });
pub const fillStyle = bridge.accessor(CanvasRenderingContext2D.getFillStyle, CanvasRenderingContext2D.setFillStyle, .{});
pub const createImageData = bridge.function(CanvasRenderingContext2D.createImageData, .{ .dom_exception = true });
pub const createImageData = bridge.function(CanvasRenderingContext2D.createImageData, .{});
pub const putImageData = bridge.function(CanvasRenderingContext2D.putImageData, .{ .noop = true });
pub const drawImage = bridge.function(CanvasRenderingContext2D.drawImage, .{ .noop = true });
pub const getImageData = bridge.function(CanvasRenderingContext2D.getImageData, .{ .dom_exception = true });
pub const getImageData = bridge.function(CanvasRenderingContext2D.getImageData, .{});
pub const save = bridge.function(CanvasRenderingContext2D.save, .{ .noop = true });
pub const restore = bridge.function(CanvasRenderingContext2D.restore, .{ .noop = true });
pub const scale = bridge.function(CanvasRenderingContext2D.scale, .{ .noop = true });

View File

@@ -34,7 +34,7 @@ const OffscreenCanvasRenderingContext2D = @This();
_fill_style: color.RGBA = color.RGBA.Named.black,
pub fn getFillStyle(self: *const OffscreenCanvasRenderingContext2D, exec: *Execution) ![]const u8 {
var w = std.Io.Writer.Allocating.init(exec.call_arena);
var w = std.Io.Writer.Allocating.init(exec.local_arena);
try self._fill_style.format(&w.writer);
return w.written();
}
@@ -137,10 +137,10 @@ pub const JsApi = struct {
pub const textBaseline = bridge.property("alphabetic", .{ .template = false, .readonly = false });
pub const fillStyle = bridge.accessor(OffscreenCanvasRenderingContext2D.getFillStyle, OffscreenCanvasRenderingContext2D.setFillStyle, .{});
pub const createImageData = bridge.function(OffscreenCanvasRenderingContext2D.createImageData, .{ .dom_exception = true });
pub const createImageData = bridge.function(OffscreenCanvasRenderingContext2D.createImageData, .{});
pub const putImageData = bridge.function(OffscreenCanvasRenderingContext2D.putImageData, .{ .noop = true });
pub const getImageData = bridge.function(OffscreenCanvasRenderingContext2D.getImageData, .{ .dom_exception = true });
pub const getImageData = bridge.function(OffscreenCanvasRenderingContext2D.getImageData, .{});
pub const save = bridge.function(OffscreenCanvasRenderingContext2D.save, .{ .noop = true });
pub const restore = bridge.function(OffscreenCanvasRenderingContext2D.restore, .{ .noop = true });
pub const scale = bridge.function(OffscreenCanvasRenderingContext2D.scale, .{ .noop = true });

View File

@@ -37,7 +37,6 @@ pub const JsApi = struct {
pub const name = "Comment";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const constructor = bridge.constructor(Comment.init, .{});

View File

@@ -36,7 +36,6 @@ pub const JsApi = struct {
pub const name = "ProcessingInstruction";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const target = bridge.accessor(ProcessingInstruction.getTarget, null, .{});

View File

@@ -78,11 +78,10 @@ pub const JsApi = struct {
pub const name = "Text";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const constructor = bridge.constructor(Text.init, .{});
pub const wholeText = bridge.accessor(Text.getWholeText, null, .{});
pub const assignedSlot = bridge.accessor(Text.getAssignedSlot, null, .{});
pub const splitText = bridge.function(Text.splitText, .{ .dom_exception = true });
pub const splitText = bridge.function(Text.splitText, .{});
};

View File

@@ -45,7 +45,7 @@ const Lookup = std.StringArrayHashMapUnmanaged(void);
const WHITESPACE = " \t\n\r\x0C";
pub fn length(self: *const DOMTokenList, frame: *Frame) !u32 {
const tokens = try self.getTokens(frame.call_arena);
const tokens = try self.getTokens(frame.local_arena);
return @intCast(tokens.count());
}
@@ -53,7 +53,7 @@ pub fn length(self: *const DOMTokenList, frame: *Frame) !u32 {
pub fn item(self: *const DOMTokenList, index: usize, frame: *Frame) !?[]const u8 {
var i: usize = 0;
const allocator = frame.call_arena;
const allocator = frame.local_arena;
var seen: std.StringArrayHashMapUnmanaged(void) = .empty;
var it = std.mem.tokenizeAny(u8, self.getValue(), WHITESPACE);
@@ -69,6 +69,22 @@ pub fn item(self: *const DOMTokenList, index: usize, frame: *Frame) !?[]const u8
return null;
}
/// https://dom.spec.whatwg.org/#dom-domtokenlist-supports
/// Only `rel` defines supported tokens here; per spec every other backing
/// attribute throws. Loaders probe `relList.supports("modulepreload")` and
/// fall back to fetch()-based legacy loading when it fails.
pub fn supports(self: *const DOMTokenList, token: []const u8, frame: *Frame) !bool {
if (!std.ascii.eqlIgnoreCase(self._attribute_name.str(), "rel")) {
return error.TypeError;
}
const supported = [_][]const u8{ "stylesheet", "preload", "modulepreload" };
const lower = try std.ascii.allocLowerString(frame.local_arena, token);
for (supported) |s| {
if (std.mem.eql(u8, lower, s)) return true;
}
return false;
}
pub fn contains(self: *const DOMTokenList, search: []const u8) !bool {
var it = std.mem.tokenizeAny(u8, self.getValue(), WHITESPACE);
while (it.next()) |token| {
@@ -84,7 +100,7 @@ pub fn add(self: *DOMTokenList, tokens: []const []const u8, frame: *Frame) !void
try validateToken(token);
}
const allocator = frame.call_arena;
const allocator = frame.local_arena;
var lookup = try self.getTokens(allocator);
try lookup.ensureUnusedCapacity(allocator, tokens.len);
@@ -100,7 +116,7 @@ pub fn remove(self: *DOMTokenList, tokens: []const []const u8, frame: *Frame) !v
try validateToken(token);
}
var lookup = try self.getTokens(frame.call_arena);
var lookup = try self.getTokens(frame.local_arena);
for (tokens) |token| {
_ = lookup.orderedRemove(token);
}
@@ -151,8 +167,8 @@ pub fn replace(self: *DOMTokenList, old_token: []const u8, new_token: []const u8
return error.InvalidCharacterError;
}
const allocator = frame.call_arena;
var lookup = try self.getTokens(frame.call_arena);
const allocator = frame.local_arena;
var lookup = try self.getTokens(allocator);
// Check if old_token exists
if (!lookup.contains(old_token)) {
@@ -298,7 +314,6 @@ pub const JsApi = struct {
pub const name = "DOMTokenList";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const length = bridge.accessor(DOMTokenList.length, null, .{});
@@ -310,11 +325,12 @@ pub const JsApi = struct {
return self.item(@intCast(index), frame);
}
pub const contains = bridge.function(DOMTokenList.contains, .{ .dom_exception = true });
pub const add = bridge.function(DOMTokenList.add, .{ .dom_exception = true, .ce_reactions = true });
pub const remove = bridge.function(DOMTokenList.remove, .{ .dom_exception = true, .ce_reactions = true });
pub const toggle = bridge.function(DOMTokenList.toggle, .{ .dom_exception = true, .ce_reactions = true });
pub const replace = bridge.function(DOMTokenList.replace, .{ .dom_exception = true, .ce_reactions = true });
pub const contains = bridge.function(DOMTokenList.contains, .{});
pub const supports = bridge.function(DOMTokenList.supports, .{});
pub const add = bridge.function(DOMTokenList.add, .{ .ce_reactions = true });
pub const remove = bridge.function(DOMTokenList.remove, .{ .ce_reactions = true });
pub const toggle = bridge.function(DOMTokenList.toggle, .{ .ce_reactions = true });
pub const replace = bridge.function(DOMTokenList.replace, .{ .ce_reactions = true });
pub const value = bridge.accessor(DOMTokenList.getValue, DOMTokenList.setValue, .{ .ce_reactions = true });
pub const toString = bridge.function(DOMTokenList.getValue, .{});
pub const keys = bridge.function(DOMTokenList.keys, .{});

View File

@@ -143,7 +143,6 @@ pub const JsApi = struct {
pub const name = "HTMLCollection";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const length = bridge.accessor(HTMLCollection.length, null, .{});

View File

@@ -104,6 +104,6 @@ pub const JsApi = struct {
pub const @"[str]" = bridge.namedIndexed(HTMLOptionsCollection.getByName, null, null, .{ .null_as_undefined = true });
pub const selectedIndex = bridge.accessor(HTMLOptionsCollection.getSelectedIndex, HTMLOptionsCollection.setSelectedIndex, .{});
pub const add = bridge.function(HTMLOptionsCollection.add, .{ .dom_exception = true, .ce_reactions = true });
pub const add = bridge.function(HTMLOptionsCollection.add, .{ .ce_reactions = true });
pub const remove = bridge.function(HTMLOptionsCollection.remove, .{ .ce_reactions = true });
};

View File

@@ -142,7 +142,6 @@ pub const JsApi = struct {
pub const name = "NodeList";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const length = bridge.accessor(NodeList.length, null, .{});

View File

@@ -260,7 +260,7 @@ fn ctr(
std.mem.writeInt(u128, &wrapped, readBlock(counter) & ~mask, .big);
const second = try cbcOrCtr(cipher, key, &wrapped, data[split..], encrypting, false, exec);
const out = try exec.call_arena.alloc(u8, data.len);
const out = try exec.local_arena.alloc(u8, data.len);
@memcpy(out[0..split], first);
@memcpy(out[split..], second);
return out;
@@ -308,7 +308,7 @@ fn cbcOrCtr(
}
// Block ciphers may emit up to one extra block on top of the input.
const out = try exec.call_arena.alloc(u8, data.len + 16);
const out = try exec.local_arena.alloc(u8, data.len + 16);
var out_len: c_int = 0;
if (cipherUpdate(ctx, out.ptr, &out_len, @ptrCast(data.ptr), @intCast(data.len), encrypting) != 1) {
return error.OperationError;
@@ -365,7 +365,7 @@ fn gcm(
}
if (encrypting) {
const out = try exec.call_arena.alloc(u8, data.len + tag_len);
const out = try exec.local_arena.alloc(u8, data.len + tag_len);
var out_len: c_int = 0;
if (data.len > 0 and cipherUpdate(ctx, out.ptr, &out_len, @ptrCast(data.ptr), @intCast(data.len), true) != 1) {
return error.OperationError;
@@ -391,7 +391,7 @@ fn gcm(
return error.OperationError;
}
const out = try exec.call_arena.alloc(u8, ct.len + 16);
const out = try exec.local_arena.alloc(u8, ct.len + 16);
var out_len: c_int = 0;
if (ct.len > 0 and cipherUpdate(ctx, out.ptr, &out_len, @ptrCast(ct.ptr), @intCast(ct.len), false) != 1) {
return error.OperationError;

View File

@@ -236,7 +236,7 @@ pub fn deriveBits(
if (crypto.EVP_PKEY_derive(ctx, null, &secret_len) != 1 or secret_len == 0) {
return error.OperationError;
}
const secret = try exec.call_arena.alloc(u8, secret_len);
const secret = try exec.local_arena.alloc(u8, secret_len);
if (crypto.EVP_PKEY_derive(ctx, secret.ptr, &secret_len) != 1) {
return error.OperationError;
}

View File

@@ -151,7 +151,7 @@ pub fn sign(
return resolver.promise();
}
const buffer = try exec.call_arena.alloc(u8, crypto.EVP_MD_size(crypto_key.getDigest()));
const buffer = try exec.local_arena.alloc(u8, crypto.EVP_MD_size(crypto_key.getDigest()));
var out_len: u32 = 0;
// Try to sign.
_ = crypto.HMAC(
@@ -163,7 +163,6 @@ pub fn sign(
buffer.ptr,
&out_len,
) orelse {
exec.call_arena.free(buffer);
// Failure.
resolver.rejectError("HMAC.sign", .{ .dom_exception = .{ .err = error.InvalidAccessError } });
return resolver.promise();

View File

@@ -64,7 +64,7 @@ pub fn pbkdf2(
) Error![]const u8 {
try checkBaseKey(base_key, "PBKDF2", usage_ok);
const digest = crypto.findDigest(params.hash.name()) catch return error.NotSupported;
const out = try exec.call_arena.alloc(u8, try outputLen(length));
const out = try exec.local_arena.alloc(u8, try outputLen(length));
// A zero-length derivation is valid and yields an empty buffer; the C
// routines reject a zero output length, so short-circuit here.
if (out.len == 0) {
@@ -97,7 +97,7 @@ pub fn hkdf(
) Error![]const u8 {
try checkBaseKey(base_key, "HKDF", usage_ok);
const digest = crypto.findDigest(params.hash.name()) catch return error.NotSupported;
const out = try exec.call_arena.alloc(u8, try outputLen(length));
const out = try exec.local_arena.alloc(u8, try outputLen(length));
// A zero-length derivation is valid and yields an empty buffer; the C
// routines reject a zero output length, so short-circuit here.
if (out.len == 0) {

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