mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-30 17:25:58 -04:00
Merge branch 'main' into agent-regression
This commit is contained in:
2
.github/actions/install/action.yml
vendored
2
.github/actions/install/action.yml
vendored
@@ -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
|
||||
|
||||
2
.github/actions/v8-snapshot/action.yml
vendored
2
.github/actions/v8-snapshot/action.yml
vendored
@@ -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"
|
||||
|
||||
2
.github/workflows/e2e-test.yml
vendored
2
.github/workflows/e2e-test.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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 && \
|
||||
|
||||
11
README.md
11
README.md
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@ js: *Context,
|
||||
buf: []u8,
|
||||
arena: Allocator,
|
||||
call_arena: Allocator,
|
||||
local_arena: Allocator,
|
||||
|
||||
page: *Page,
|
||||
session: *Session,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
173
src/browser/tests/dompoint.html
Normal file
173
src/browser/tests/dompoint.html
Normal 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>
|
||||
12
src/browser/tests/element/html/script/supports.html
Normal file
12
src/browser/tests/element/html/script/supports.html
Normal 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>
|
||||
23
src/browser/tests/element/rel_list.html
Normal file
23
src/browser/tests/element/rel_list.html
Normal 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>
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
5
src/browser/tests/page/encoding/unicode_label.html
Normal file
5
src/browser/tests/page/encoding/unicode_label.html
Normal 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>
|
||||
4
src/browser/tests/runner/iframe_idle.html
Normal file
4
src/browser/tests/runner/iframe_idle.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset="UTF-8">
|
||||
<iframe src="about:blank"></iframe>
|
||||
<iframe src="runner1.html"></iframe>
|
||||
@@ -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);
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
108
src/browser/webapi/DOMPoint.zig
Normal file
108
src/browser/webapi/DOMPoint.zig
Normal 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, .{});
|
||||
};
|
||||
167
src/browser/webapi/DOMPointReadOnly.zig
Normal file
167
src/browser/webapi/DOMPointReadOnly.zig
Normal 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", .{});
|
||||
}
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user