Merge branch 'main' into stylesheet-scroll-container

This commit is contained in:
Adrià Arrufat committed 2026-09-16 14:53:52 +02:00
commit 2cc537fc8c
127 files changed
+6163 -4557

No files matched your search

+1 -1
View File
@@ -13,7 +13,7 @@ inputs:
zig-v8:
description: 'zig v8 version to install'
required: false
default: 'v0.5.5'
default: 'v0.5.6'
v8:
description: 'v8 version to install'
required: false
+79
View File
@@ -0,0 +1,79 @@
name: "Orderfile"
description: "Regenerate orderfile/lightpanda.ld and orderfile/v8.txt from a profile of the CDP bench"
# Runs orderfile/tools/regen.sh, which writes the two files back into the
# checkout. What the caller does with them (commit, or just build) is its own
# business. See orderfile/README.md.
#
# Needs a Linux runner with sudo (the script flips
# /sys/kernel/debug/fault_around_bytes), plus node, go, python3 and binutils.
# Call it after ./.github/actions/install and after the v8 snapshot.
inputs:
build-args:
description: 'zig build args for the profiling build: the release build args minus -Dorderfile'
required: true
runs:
description: 'CDP bench iterations'
required: false
default: '100'
demo-repository:
description: 'Checkout holding the CDP bench (demo/puppeteer/cdp.js)'
required: false
default: 'lightpanda-io/demo'
outputs:
out-dir:
description: 'Scratch directory with the profile: resident.json, hot.text, hot.rodata, gen_order.stats, result.txt and the generated lightpanda.ld / v8.txt'
value: ${{ steps.out.outputs.dir }}
runs:
using: "composite"
steps:
# The bench the profile is taken from.
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: ${{ inputs.demo-repository }}
path: demo
- run: npm install
shell: bash
working-directory: demo
# Its own step, so the path is still an output when the profile fails.
- name: profile scratch directory
id: out
shell: bash
run: echo "dir=$RUNNER_TEMP/orderfile-regen" >> "$GITHUB_OUTPUT"
- name: regenerate the profile
shell: bash
env:
DEMO_DIR: demo
RUNS: ${{ inputs.runs }}
OUT: ${{ steps.out.outputs.dir }}
BUILD_ARGS: ${{ inputs.build-args }}
run: |
# BUILD_ARGS is a list of flags, it has to word-split.
# shellcheck disable=SC2086
orderfile/tools/regen.sh $BUILD_ARGS
# $OUT dies with the runner, and a failed profile is the one case where
# the page dump and the bench output are worth reading afterwards.
- name: upload profile
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: orderfile-profile
path: |
${{ steps.out.outputs.dir }}/result.txt
${{ steps.out.outputs.dir }}/gen_order.stats
${{ steps.out.outputs.dir }}/hot.text
${{ steps.out.outputs.dir }}/hot.rodata
${{ steps.out.outputs.dir }}/resident.json
${{ steps.out.outputs.dir }}/link.line
${{ steps.out.outputs.dir }}/bench.out
${{ steps.out.outputs.dir }}/lightpanda.ld
${{ steps.out.outputs.dir }}/v8.txt
retention-days: 10
+1 -1
View File
@@ -13,7 +13,7 @@ inputs:
zig-v8:
description: 'zig-v8 release tag the prebuilt lib came from'
required: false
default: 'v0.5.5'
default: 'v0.5.6'
runs:
using: "composite"
+84
View File
@@ -0,0 +1,84 @@
name: orderfile
# Regenerates orderfile/lightpanda.ld + orderfile/v8.txt from a profile of the
# CDP bench and opens a pull request with the result, every night or on demand
# from the Actions tab. See orderfile/README.md.
#
# release.yml runs the same ./.github/actions/orderfile, but builds with the
# profile instead of committing it.
env:
LIGHTPANDA_DISABLE_TELEMETRY: true
on:
schedule:
- cron: "2 0 * * *"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Two tokens, because neither can do the whole job:
# - the push uses GITHUB_TOKEN. The distribution app is installed here but
# its token has no write access ("Permission to lightpanda-io/browser.git
# denied to lightpanda-browser-distribution[bot]").
# - the pull request uses the app token. The repository has
# can_approve_pull_request_reviews off, so GITHUB_TOKEN gets "GitHub
# Actions is not permitted to create or approve pull requests".
# The app opening the PR also gets it CI checks, which GITHUB_TOKEN never
# would: GitHub starts no workflow run for an event caused by GITHUB_TOKEN.
permissions:
contents: write
jobs:
regen:
name: regenerate orderfile
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Generate token for browser
id: app-token
uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2
with:
app-id: ${{ secrets.DISTRIBUTION_APP_ID }}
private-key: ${{ secrets.DISTRIBUTION_APP_PRIVATE_KEY }}
owner: lightpanda-io
repositories: browser
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- uses: ./.github/actions/install
- uses: ./.github/actions/v8-snapshot
- uses: ./.github/actions/orderfile
id: orderfile
with:
build-args: -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64
- name: open a pull request
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
OUT: ${{ steps.orderfile.outputs.out-dir }}
BRANCH: orderfile-regen
run: |
if git diff --quiet -- orderfile/lightpanda.ld orderfile/v8.txt; then
echo "profile unchanged"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add orderfile/lightpanda.ld orderfile/v8.txt
git commit -m "orderfile: regenerate the hot-code profile" -m "$(cat "$OUT/result.txt")"
# One fixed branch, force-pushed: a re-run refreshes the open PR
# instead of stacking a second one.
git push --force origin "HEAD:refs/heads/$BRANCH"
if [ -n "$(gh pr list --head "$BRANCH" --base main --state open --json number --jq '.[].number')" ]; then
echo "refreshed the open pull request"
exit 0
fi
gh pr create --base main --head "$BRANCH" \
--title "orderfile: regenerate the hot-code profile" \
--body-file "$OUT/result.txt"
+10 -9
View File
@@ -233,10 +233,11 @@ jobs:
--field tag="${{ env.RELEASE }}"
update-python-package:
# Version tags only (never nightly): create the matching release on
# lightpanda-python. That repo's wheels workflow triggers on its release
# event, bundles this release's binaries, and publishes to PyPI once a
# maintainer approves the `pypi` environment deployment there.
# Version tags only (never nightly): start the wheels build on
# lightpanda-python. It bundles this release's binaries, publishes to PyPI
# once a maintainer approves the `pypi` environment deployment there, and
# then records the matching release on that repo itself — the app token
# below can start workflows there but cannot write repository contents.
if: github.ref_type == 'tag'
needs: [build-linux, build-macos]
runs-on: ubuntu-latest
@@ -251,7 +252,7 @@ jobs:
owner: lightpanda-io
repositories: lightpanda-python
- name: Create the matching lightpanda-python release
- name: Start the lightpanda-python wheels build
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PYTHON_REPO: lightpanda-io/lightpanda-python
@@ -264,7 +265,7 @@ jobs:
echo "release $RELEASE already exists on $PYTHON_REPO; nothing to do"
exit 0
fi
gh release create "$RELEASE" --repo "$PYTHON_REPO" \
--title "$RELEASE" \
--notes "Bundles [lightpanda-io/browser ${RELEASE}](https://github.com/lightpanda-io/browser/releases/tag/${RELEASE}). Install with \`pip install lightpanda\`."
echo "created; the release event starts the wheels build, whose publish waits for pypi environment approval"
gh workflow run wheels.yml --repo "$PYTHON_REPO" \
--field release="$RELEASE" \
--field publish=pypi
echo "dispatched; the publish waits for pypi environment approval on $PYTHON_REPO"
+1 -1
View File
@@ -4,7 +4,7 @@ FROM debian:stable-slim
ARG MINISIG=0.12
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG V8=14.9.207.35
ARG ZIG_V8=v0.5.5
ARG ZIG_V8=v0.5.6
ARG TARGETPLATFORM
RUN apt-get update -yq && \
+3 -1
View File
@@ -4,7 +4,8 @@
<h1 align="center">Lightpanda Browser</h1>
<p align="center">
<strong>The headless browser built from scratch for AI agents and automation.</strong><br>
Not a Chromium fork. Not a WebKit patch. A new browser, written in Zig.
Not a Chromium fork. Not a WebKit patch. A new browser, written in Zig.</strong><br>
16x lighter and 9x faster than Chromium.
</p>
</div>
@@ -192,6 +193,7 @@ reference.
./lightpanda agent --task "top story on news.ycombinator.com?"
./lightpanda agent --no-llm # basic REPL, no LLM
./lightpanda run session.js # run a recorded script
cat session.js | ./lightpanda run - # ...or pipe one in via stdin
./lightpanda agent --provider gemini --task "..." # force a specific provider
./lightpanda agent --list-models # models available for the detected provider
VERTEX_API_KEY=... ./lightpanda agent --provider vertex # Vertex AI, express mode
+2 -2
View File
@@ -5,8 +5,8 @@
.minimum_zig_version = "0.16.0",
.dependencies = .{
.v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/31b233ce6162c18eb37cacd001473057f75daad9.tar.gz",
.hash = "v8-0.0.0-xddH62IdAwCZid1vyAzg_8IiWG4ovW7DbcnSvZd12I9p",
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/d3d7b41677a0015fdfa55a8b1caa4f214de6d209.tar.gz",
.hash = "v8-0.0.0-xddH624yAwC5_H_8T303uTxiSAnAu2zrxv6MHLhvLo6t",
},
// .v8 = .{ .path = "../zig-v8-fork" },
.brotli = .{
+1 -1
View File
@@ -28,7 +28,7 @@ below), with no change in run duration.
gracefully: renamed or removed functions simply fall back into cold `.text`.
Zig's `__anon_NNN` names (~7% of the patterns) renumber on unrelated
changes, so the profile decays a little with every commit; it is
regenerated weekly (below).
regenerated nightly by CI (below).
- Link-time cost: LLD name-checks every input section against every pattern
of every input-section description, so the script scopes patterns to their
object file (`*api.o(...)`). The shipped script covers Zig/Rust/C only
+2999 -3012
View File
File diff suppressed because it is too large. Load diff
+743 -863
View File
File diff suppressed because it is too large. Load diff
+9
View File
@@ -20,6 +20,7 @@ const std = @import("std");
const lp = @import("lightpanda");
const Config = @import("Config.zig");
const Regex = @import("Regex.zig");
const Snapshot = @import("browser/js/Snapshot.zig");
const Platform = @import("browser/js/Platform.zig");
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
@@ -43,6 +44,8 @@ allocator: Allocator,
arena_pool: ArenaPool,
app_dir_path: ?[]const u8,
regex_context: *Regex.Context,
pub fn init(allocator: Allocator, config: *const Config) !*App {
const platform = try Platform.init(.{
.v8_flags = config.v8Flags(),
@@ -54,6 +57,9 @@ pub fn init(allocator: Allocator, config: *const Config) !*App {
const snapshot = try Snapshot.load();
errdefer snapshot.deinit();
const regex_context: *Regex.Context = try .init(allocator);
errdefer regex_context.deinit();
const app = try allocator.create(App);
errdefer allocator.destroy(app);
@@ -62,6 +68,7 @@ pub fn init(allocator: Allocator, config: *const Config) !*App {
.allocator = allocator,
.platform = platform,
.snapshot = snapshot,
.regex_context = regex_context,
.network = undefined,
.app_dir_path = undefined,
.telemetry = undefined,
@@ -96,6 +103,8 @@ pub fn deinit(self: *App) void {
}
self.telemetry.deinit(allocator);
self.network.deinit();
// After `network`: its adblock regexes free through this context.
self.regex_context.deinit();
self.snapshot.deinit();
self.platform.deinit();
self.arena_pool.deinit();
+1 -1
View File
@@ -1200,7 +1200,7 @@ pub fn parseArgs(allocator: Allocator, proc_args: std.process.Args) !Config {
if (command == .run) {
const run = command.run;
if (run.script_file == null) {
log.fatal(.app, "missing script file", .{ .hint = "usage: lightpanda run <script.js>" });
log.fatal(.app, "missing script file", .{ .hint = "usage: lightpanda run <script.js | ->" });
return error.MissingArgument;
}
// run's fields are a strict subset of Agent's (compile error otherwise).
+1 -1
View File
@@ -68,7 +68,7 @@ pub fn reset(self: *NodeRegistry) void {
/// IDs valid. Must run before the page's arena is freed — attribution reads
/// each node's document.
pub fn resetFrame(self: *NodeRegistry, arena: Allocator, frame: *Frame) void {
const page = frame._page;
const page = frame.page;
var doomed: std.ArrayListUnmanaged(*Node) = .empty;
var it = self.lookup_by_id.valueIterator();
while (it.next()) |node_ptr| {
+138 -60
View File
@@ -16,23 +16,18 @@
// 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/>.
//! A compiled `/regex/` filter body. Filter lists write them in JavaScript
//! `RegExp` syntax and uBO runs them with `new RegExp(src, 'i')` against the
//! raw request URL (no flag under `$match-case`); PCRE2 reads that syntax
//! as-is, escapes like `\/` included.
//! A pattern in JavaScript `RegExp` syntax, run by PCRE2, which reads that
//! syntax as-is, escapes like `\/` included.
//!
//! A compiled pattern and its `Context` are never modified after `compile`,
//! so one `Regex` can be shared by every HTTP client thread; the per-call
//! match data is what PCRE2 requires to be private.
//! so one `Regex` can be shared by every thread; the per-call match data is
//! what PCRE2 requires to be private.
const std = @import("std");
const lp = @import("lightpanda");
const pcre2 = @import("pcre2");
const Allocator = std.mem.Allocator;
const log = lp.log;
const Regex = @This();
code: *pcre2.pcre2_code_8,
@@ -40,21 +35,42 @@ context: *const Context,
pub const Error = error{ InvalidRegex, OutOfMemory };
/// What every `Regex` compiled through it shares: the allocator PCRE2 draws
/// from, and the compile and match settings. Outlives the regexes.
pub const Options = struct {
case_insensitive: bool = false,
/// UTF-8 aware matching: `.` consumes a code point and caseless folding
/// works beyond ASCII. An invalid sequence in the subject fails to match
/// rather than erroring. `\b` and `\w` stay ASCII, as in JavaScript.
unicode: bool = false,
/// JavaScript's `s`.
dot_all: bool = false,
/// JavaScript's `m`.
multiline: bool = false,
};
pub const Diagnostic = struct {
offset: usize = 0,
len: usize = 0,
buf: [256]u8 = undefined,
pub fn message(self: *const Diagnostic) []const u8 {
return self.buf[0..self.len];
}
};
/// Shared by every `Regex` compiled through it; outlives them.
///
/// PCRE2 would happily use libc's malloc; it is handed the blocker's
/// allocator so that a compiled pattern nobody freed fails a test the way
/// any other leak does.
/// PCRE2 would happily use libc's malloc; it is handed the owner's allocator
/// so that a compiled pattern nobody freed fails a test the way any other
/// leak does.
pub const Context = struct {
allocator: Allocator,
general: *pcre2.pcre2_general_context_8,
compile_context: *pcre2.pcre2_compile_context_8,
match_context: *pcre2.pcre2_match_context_8,
// A pattern from a list that backtracks this much on one URL is broken,
// not slow; giving up costs a false negative on that request, nothing
// more.
// Patterns come from lists and prompts, never from code: one that
// backtracks this much on a subject is broken, not slow, and giving up
// costs a false negative on that subject, nothing more.
const MATCH_LIMIT = 100_000;
const DEPTH_LIMIT = 10_000;
@@ -71,8 +87,7 @@ pub const Context = struct {
const compile_context = pcre2.pcre2_compile_context_create_8(general) orelse return error.OutOfMemory;
errdefer pcre2.pcre2_compile_context_free_8(compile_context);
// JavaScript without the `u` flag reads an unknown escape as the
// literal character, and that is the mode uBO compiles filters in.
// JavaScript reads an unknown escape as the literal character.
_ = pcre2.pcre2_set_compile_extra_options_8(compile_context, pcre2.PCRE2_EXTRA_BAD_ESCAPE_IS_LITERAL);
const match_context = pcre2.pcre2_match_context_create_8(general) orelse return error.OutOfMemory;
@@ -85,6 +100,37 @@ pub const Context = struct {
return self;
}
/// A failed compile fills `diag`, when given, with PCRE2's message and the
/// offset of the offending character.
pub fn compile(self: *const Context, pattern: []const u8, options: Options, diag: ?*Diagnostic) Error!Regex {
var flags: u32 = 0;
if (options.case_insensitive) flags |= pcre2.PCRE2_CASELESS;
if (options.unicode) flags |= pcre2.PCRE2_UTF | pcre2.PCRE2_MATCH_INVALID_UTF;
if (options.dot_all) flags |= pcre2.PCRE2_DOTALL;
if (options.multiline) flags |= pcre2.PCRE2_MULTILINE;
var err_code: c_int = 0;
var err_offset: usize = 0;
const code = pcre2.pcre2_compile_8(
pattern.ptr,
pattern.len,
flags,
&err_code,
&err_offset,
self.compile_context,
) orelse {
// A failed allocation is ours, not the pattern's.
if (err_code == pcre2.PCRE2_ERROR_HEAP_FAILED) return error.OutOfMemory;
if (diag) |d| {
const len = pcre2.pcre2_get_error_message_8(err_code, &d.buf, d.buf.len);
d.len = if (len < 0) 0 else @intCast(len);
d.offset = err_offset;
}
return error.InvalidRegex;
};
return .{ .code = code, .context = self };
}
pub fn deinit(self: *Context) void {
pcre2.pcre2_match_context_free_8(self.match_context);
pcre2.pcre2_compile_context_free_8(self.compile_context);
@@ -114,33 +160,6 @@ pub const Context = struct {
}
};
pub fn compile(context: *const Context, pattern: []const u8, case_insensitive: bool) Error!Regex {
const options: u32 = if (case_insensitive) pcre2.PCRE2_CASELESS else 0;
var err_code: c_int = 0;
var err_offset: usize = 0;
const code = pcre2.pcre2_compile_8(
pattern.ptr,
pattern.len,
options,
&err_code,
&err_offset,
context.compile_context,
) orelse {
// A failed allocation is ours, not the pattern's.
if (err_code == pcre2.PCRE2_ERROR_HEAP_FAILED) return error.OutOfMemory;
var buf: [256]u8 = undefined;
const len = pcre2.pcre2_get_error_message_8(err_code, &buf, buf.len);
const message: []const u8 = if (len < 0) "unknown error" else buf[0..@intCast(len)];
log.debug(.app, "adblock regex rejected", .{
.pattern = pattern,
.err = message,
.offset = err_offset,
});
return error.InvalidRegex;
};
return .{ .code = code, .context = context };
}
pub fn deinit(self: Regex) void {
pcre2.pcre2_code_free_8(self.code);
}
@@ -152,7 +171,6 @@ const MATCH_SCRATCH = 24 * 1024;
/// Whether the pattern matches anywhere in `text`, as `RegExp.test` would
/// answer. A match that hits the backtracking limits counts as no match.
pub fn matches(self: Regex, text: []const u8) bool {
// This runs per request; the scratch keeps the common case off the heap.
var scratch = std.heap.stackFallback(MATCH_SCRATCH, self.context.allocator);
var allocator = scratch.get();
const general = pcre2.pcre2_general_context_create_8(Context.cMalloc, Context.cFree, &allocator) orelse return false;
@@ -167,13 +185,13 @@ pub fn matches(self: Regex, text: []const u8) bool {
return rc >= 0;
}
const testing = @import("../../testing.zig");
const testing = @import("testing.zig");
test "adblock.Regex: JavaScript escapes and unanchored search" {
test "Regex: JavaScript escapes and unanchored search" {
const context: *Context = try .init(testing.allocator);
defer context.deinit();
const regex = try Regex.compile(context, "^https?:\\/\\/[0-9a-z]{5,}\\.com\\/.*", true);
const regex = try context.compile("^https?:\\/\\/[0-9a-z]{5,}\\.com\\/.*", .{ .case_insensitive = true }, null);
defer regex.deinit();
try testing.expect(regex.matches("https://abcde.com/x"));
@@ -181,44 +199,104 @@ test "adblock.Regex: JavaScript escapes and unanchored search" {
try testing.expect(!regex.matches("https://abcd.com/x"));
try testing.expect(!regex.matches("https://abcde.org/x"));
const invoke = try Regex.compile(context, "\\/[0-9a-f]{32}\\/invoke\\.js", true);
const invoke = try context.compile("\\/[0-9a-f]{32}\\/invoke\\.js", .{ .case_insensitive = true }, null);
defer invoke.deinit();
try testing.expect(invoke.matches("https://host.com/0123456789abcdef0123456789abcdef/invoke.js"));
try testing.expect(!invoke.matches("https://host.com/0123456789abcdef0123456789abcde/invoke.js"));
const dash = try Regex.compile(context, "[a-z\\-]+\\?s=", true);
const dash = try context.compile("[a-z\\-]+\\?s=", .{ .case_insensitive = true }, null);
defer dash.deinit();
try testing.expect(dash.matches("https://x.com/a-b?s=1"));
try testing.expect(!dash.matches("https://x.com/?s=1"));
}
test "adblock.Regex: $match-case keeps the case" {
test "Regex: case is kept by default" {
const context: *Context = try .init(testing.allocator);
defer context.deinit();
const exact = try Regex.compile(context, "\\/[a-z0-9]{12}\\/[a-zA-Z0-9]{20,}$", false);
const exact = try context.compile("\\/[a-z0-9]{12}\\/[a-zA-Z0-9]{20,}$", .{}, null);
defer exact.deinit();
try testing.expect(exact.matches("https://x.com/abcdef123456/aBcDeFgHiJkLmNoPqRsTuV"));
try testing.expect(!exact.matches("https://x.com/ABCDEF123456/aBcDeFgHiJkLmNoPqRsTuV"));
}
test "adblock.Regex: invalid patterns are errors, runaway ones no match" {
test "Regex: invalid patterns are errors, runaway ones no match" {
const context: *Context = try .init(testing.allocator);
defer context.deinit();
try testing.expectError(error.InvalidRegex, Regex.compile(context, "(", true));
try testing.expectError(error.InvalidRegex, Regex.compile(context, "a{2,1}", true));
try testing.expectError(error.InvalidRegex, context.compile("(", .{}, null));
try testing.expectError(error.InvalidRegex, context.compile("a{2,1}", .{}, null));
// An unknown alphanumeric escape is the literal, as in JavaScript.
const literal = try Regex.compile(context, "\\q", true);
const literal = try context.compile("\\q", .{ .case_insensitive = true }, null);
defer literal.deinit();
try testing.expect(literal.matches("https://x.com/q"));
// Exponential backtracking stops at the match limit instead of stalling
// the request.
const runaway = try Regex.compile(context, "^(a+)+$", true);
// the caller.
const runaway = try context.compile("^(a+)+$", .{ .case_insensitive = true }, null);
defer runaway.deinit();
const subject = "a" ** 64 ++ "b";
try testing.expect(!runaway.matches(subject));
try testing.expect(runaway.matches("a" ** 64));
}
test "Regex: dot_all and multiline follow the JavaScript flags" {
const context: *Context = try .init(testing.allocator);
defer context.deinit();
const dot = try context.compile("a.b", .{}, null);
defer dot.deinit();
try testing.expect(!dot.matches("a\nb"));
const dot_all = try context.compile("a.b", .{ .dot_all = true }, null);
defer dot_all.deinit();
try testing.expect(dot_all.matches("a\nb"));
const line = try context.compile("^b$", .{}, null);
defer line.deinit();
try testing.expect(!line.matches("a\nb"));
const multiline = try context.compile("^b$", .{ .multiline = true }, null);
defer multiline.deinit();
try testing.expect(multiline.matches("a\nb"));
}
test "Regex: a diagnostic names the fault and where it is" {
const context: *Context = try .init(testing.allocator);
defer context.deinit();
var diag: Diagnostic = .{};
try testing.expectError(error.InvalidRegex, context.compile("ab(", .{}, &diag));
try testing.expectString("missing closing parenthesis", diag.message());
try testing.expectEqual(3, diag.offset);
}
test "Regex: unicode folds case beyond ASCII and tolerates invalid bytes" {
const context: *Context = try .init(testing.allocator);
defer context.deinit();
const ascii = try context.compile("^реклама$", .{ .case_insensitive = true }, null);
defer ascii.deinit();
try testing.expect(ascii.matches("реклама"));
try testing.expect(!ascii.matches("Реклама"));
const unicode = try context.compile("^реклама$", .{ .case_insensitive = true, .unicode = true }, null);
defer unicode.deinit();
try testing.expect(unicode.matches("Реклама"));
try testing.expect(unicode.matches("РЕКЛАМА"));
// One code point, not one byte.
const single = try context.compile("^.$", .{ .unicode = true }, null);
defer single.deinit();
try testing.expect(single.matches("é"));
try testing.expect(!single.matches("ab"));
// Word boundaries stay ASCII, as in JavaScript.
const word = try context.compile("\\bshare\\b", .{ .unicode = true }, null);
defer word.deinit();
try testing.expect(word.matches("éshare"));
const sidebar = try context.compile("sidebar", .{ .unicode = true }, null);
defer sidebar.deinit();
try testing.expect(sidebar.matches("sidebar\xFF"));
try testing.expect(!sidebar.matches("\xFF"));
}
+22 -5
View File
@@ -1527,12 +1527,20 @@ const ScriptOutput = struct {
}
};
/// Upper bound on script source, whether read from a file or piped in.
const max_script_bytes = 10 * 1024 * 1024;
/// `lightpanda run -` reads the script from stdin.
const stdin_script_path = "-";
fn runScript(self: *Agent, path: []const u8) bool {
var script_arena: std.heap.ArenaAllocator = .init(self.allocator);
defer script_arena.deinit();
const content = std.Io.Dir.cwd().readFileAlloc(lp.io, path, script_arena.allocator(), .limited(10 * 1024 * 1024)) catch |err| {
self.terminal.printError("Failed to read script '{s}': {s}", .{ path, @errorName(err) });
const from_stdin = std.mem.eql(u8, path, stdin_script_path);
const name = if (from_stdin) "<stdin>" else path;
const content = readScriptSource(script_arena.allocator(), path, from_stdin) catch |err| {
self.terminal.printError("Failed to read script '{s}': {s}", .{ name, @errorName(err) });
return false;
};
@@ -1555,8 +1563,8 @@ fn runScript(self: *Agent, path: []const u8) bool {
var output: ScriptOutput = .{ .terminal = &self.terminal };
runtime.console_observer = .{ .context = @ptrCast(&output), .notify = ScriptOutput.observe };
self.terminal.beginTool("script", path);
const result = runtime.runSource(content, path);
self.terminal.beginTool("script", name);
const result = runtime.runSource(content, name);
self.terminal.endTool();
if (result catch |err| {
@@ -1569,10 +1577,19 @@ fn runScript(self: *Agent, path: []const u8) bool {
// A script that printed nothing leaves no trace, so freeze the spinner into
// a green bullet (like /goto); one that printed already showed its result.
if (!output.emitted) self.terminal.printScriptDone("script", path);
if (!output.emitted) self.terminal.printScriptDone("script", name);
return true;
}
fn readScriptSource(allocator: std.mem.Allocator, path: []const u8, from_stdin: bool) ![]u8 {
if (!from_stdin) {
return std.Io.Dir.cwd().readFileAlloc(lp.io, path, allocator, .limited(max_script_bytes));
}
var buf: [64 * 1024]u8 = undefined;
var stdin = std.Io.File.stdin().readerStreaming(lp.io, &buf);
return stdin.interface.allocRemaining(allocator, .limited(max_script_bytes));
}
/// Mirror a user-typed slash command into `self.conversation.messages` as if the
/// LLM had called the tool itself, so the next natural-language turn sees the
/// same conversation shape either way.
+6 -6
View File
@@ -77,7 +77,7 @@ const DispatchError = EventManagerBase.DispatchError;
pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) DispatchError!void {
event.acquireRef();
defer _ = event.releaseRef(self.frame._page);
defer _ = event.releaseRef(self.frame.page);
// Increment event count for Event Timing API
self.frame.window._performance._event_counts.increment(event._type_string.str());
@@ -101,7 +101,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat
/// called preventDefault().
pub fn dispatchCancelable(self: *EventManager, target: *EventTarget, event: *Event) DispatchError!bool {
event.acquireRef();
defer event.releaseRef(self.frame._page);
defer event.releaseRef(self.frame.page);
try self.dispatch(target, event);
return event.getDefaultPrevented();
}
@@ -140,7 +140,7 @@ pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event,
window._current_event = event;
defer window._current_event = prev_event;
try self.base.dispatchDirect(frame.call_arena, frame.js, target, event, handler, frame._page, opts);
try self.base.dispatchDirect(frame.call_arena, frame.js, target, event, handler, frame.page, opts);
}
/// Check if there are any listeners for a direct dispatch (non-DOM target).
@@ -348,7 +348,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
if (err == error.ExecutionTerminated) {
return error.ExecutionTerminated;
}
frame._page.recordJsError(err);
frame.page.recordJsError(err);
log.warn(.event, "inline handler", .{ .err = err, .caught = caught });
break :ret null;
};
@@ -409,7 +409,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
if (err == error.ExecutionTerminated) {
return error.ExecutionTerminated;
}
frame._page.recordJsError(err);
frame.page.recordJsError(err);
log.warn(.event, "inline handler", .{ .err = err, .caught = caught });
break :ret null;
};
@@ -876,7 +876,7 @@ const ActivationState = struct {
const event = try Event.initTrusted(comptime .wrap(typ), .{
.bubbles = true,
.cancelable = false,
}, frame._page);
}, frame.page);
const target = input.asElement().asEventTarget();
try frame._event_manager.dispatch(target, event);
+290 -104
View File
@@ -65,15 +65,6 @@ const popover = @import("webapi/element/popover.zig");
const slotting = @import("webapi/element/slotting.zig");
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
const PointList = @import("webapi/svg/PointList.zig");
const StringList = @import("webapi/svg/StringList.zig");
const AnimatedEnumeration = @import("webapi/svg/AnimatedEnumeration.zig");
const AnimatedLength = @import("webapi/svg/AnimatedLength.zig");
const AnimatedNumber = @import("webapi/svg/AnimatedNumber.zig");
const AnimatedString = @import("webapi/svg/AnimatedString.zig");
const AnimatedTransformList = @import("webapi/svg/AnimatedTransformList.zig");
const AnimatedPreserveAspectRatio = @import("webapi/svg/AnimatedPreserveAspectRatio.zig");
const sys_url = @import("../sys/url.zig");
const HttpClient = @import("../network/HttpClient.zig");
const GlobalScope = @import("global_scope.zig").GlobalScope;
@@ -104,7 +95,7 @@ _frame_id: u32,
// navigate.
_loader_id: u32,
_page: *Page,
page: *Page,
_session: *Session,
@@ -118,55 +109,6 @@ _parse_mode: enum { document, fragment, document_write } = .document,
// inserted into a document
_fragment_scripts_runnable: bool = false,
// See Attribute.List for what this is. TL;DR: proper DOM Attribute Nodes are
// fat yet rarely needed. We only create them on-demand, but still need proper
// identity (a given attribute should return the same *Attribute), so we do
// a look here, keyed by (list, name). We don't store this in the Element or
// Attribute.List.Entry because that would require additional space per
// element / Attribute.List.Entry even though we'll create very few (if any)
// actual *Attributes.
_attribute_lookup: Element.Attribute.List.Lookup = .empty,
// Canonical pool for attribute names that aren't in String.intern's.
// Every Attribute's entry's name is either a String intern or held here.
// This is both a memory optimization (deduping attribute names) and a performance
// optimization (since we can compare strings by just their pointer)
_attribute_names: std.StringHashMapUnmanaged(void) = .empty,
// Same as _atlribute_lookup, but instead of individual attributes, this is for
// the return of elements.attributes.
_attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute.NamedNodeMap) = .empty,
// 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, pseudo-element)
// suffices — and Chrome returns the same object for repeated calls, so
// identity is also conformance.
_element_computed_styles: Element.ComputedStyleLookup = .empty,
_element_datasets: Element.DatasetLookup = .empty,
_element_class_lists: Element.ClassListLookup = .empty,
_element_rel_lists: Element.RelListLookup = .empty,
_element_part_lists: Element.PartListLookup = .empty,
_element_token_lists: Element.TokenListLookup = .empty,
_element_shadow_roots: Element.ShadowRootLookup = .empty,
_element_scroll_positions: Element.ScrollPositionLookup = .empty,
_element_namespace_uris: Element.NamespaceUriLookup = .empty,
_svg_animated_enumerations: AnimatedEnumeration.Lookup = .empty,
_svg_animated_lengths: AnimatedLength.Lookup = .empty,
_svg_animated_numbers: AnimatedNumber.Lookup = .empty,
_svg_animated_preserve_aspect_ratios: AnimatedPreserveAspectRatio.Lookup = .empty,
_svg_animated_strings: AnimatedString.Lookup = .empty,
_svg_animated_transform_lists: AnimatedTransformList.Lookup = .empty,
_svg_point_lists: PointList.Lookup = .empty,
_svg_string_lists: StringList.Lookup = .empty,
// Same as above, but for Nodes (slot assigments apply to both Element AND
// Text nodes)
_assigned_slots: Node.AssignedSlotLookup = .empty,
_manual_slot_assignments: Node.AssignedSlotLookup = .empty,
/// Lazily-created inline event listeners (or listeners provided as attributes).
/// Avoids bloating all elements with extra function fields for rare usage.
///
@@ -398,6 +340,7 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, opts: InitOpts) !void {
self.* = .{
.js = undefined,
.page = page,
.arena = arena,
.parent = parent,
.document = document,
@@ -407,7 +350,6 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, opts: InitOpts) !void {
.call_arena = call_arena.allocator(),
.local_arena = local_arena.allocator(),
._frame_id = frame_id,
._page = page,
._session = session,
._loader_id = session.nextLoaderId(),
._factory = factory,
@@ -521,7 +463,7 @@ pub fn deinit(self: *Frame) void {
cs.detach();
}
const page = self._page;
const page = self.page;
if (self._queued_navigation) |qn| {
qn.arena.release();
@@ -544,16 +486,6 @@ pub fn deinit(self: *Frame) void {
observers.deinit(self, page);
var svg_point_lists = self._svg_point_lists.valueIterator();
while (svg_point_lists.next()) |list| {
list.*.deinit(page);
}
var svg_transform_lists = self._svg_animated_transform_lists.valueIterator();
while (svg_transform_lists.next()) |list| {
list.*.deinit(page);
}
var document = self.window._document;
document._selection.releaseRef(page);
@@ -722,7 +654,7 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo
const location = try Location.init(self.url, self);
location.acquireRef();
// We're not holding a ref to old location anymore.
self.window._location.releaseRef(self._page);
self.window._location.releaseRef(self.page);
self.window._location = location;
if (is_blob) {
@@ -754,7 +686,7 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo
// Content injection
if (is_blob) {
const blob = blk: {
if (self._page.blob_urls.get(request_url)) |entry| break :blk entry.blob;
if (self.page.blob_urls.get(request_url)) |entry| break :blk entry.blob;
log.warn(.js, "invalid blob", .{ .url = request_url });
return error.BlobNotFound;
};
@@ -903,7 +835,7 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo
// and the in-flight transfer survives the OLD page's frame.deinit which
// calls http_client.abortList() on the shared frame_id during
// commitPendingPage.
const is_pending_root = self._page.replaces != null;
const is_pending_root = self.page.replaces != null;
// We dispatch frame_navigate event before sending the request.
// It ensures the event frame_navigated is not dispatched before this one.
@@ -1031,7 +963,7 @@ fn scheduleNavigationWithArena(originator: *Frame, arena: *lp.Arena, request_url
const location = try Location.init(target.url, target);
location.acquireRef();
target.window._location.releaseRef(target._page);
target.window._location.releaseRef(target.page);
target.window._location = location;
if (target.parent == null) {
@@ -1056,6 +988,7 @@ fn scheduleNavigationWithArena(originator: *Frame, arena: *lp.Arena, request_url
// Navigation: kill in-flight HTTP transfers, but leave WebSockets
// alive — they're cross-document by spec.
target.abortDocumentLoad();
session.browser.http_client.abortRequests(&target._http_owner);
// Capture the originating frame's URL as the Referer for this
@@ -1097,7 +1030,8 @@ fn scheduleNavigationWithArena(originator: *Frame, arena: *lp.Arena, request_url
}
target._queued_navigation = qn;
return session.scheduleNavigation(target);
try session.scheduleNavigation(target);
target.abortedDocumentIsComplete();
}
// A script can have multiple competing navigation events, say it starts off
@@ -1165,13 +1099,16 @@ pub fn stopLoading(self: *Frame) void {
self.child_frames.items[i].stopLoading();
}
if (self._queued_navigation) |qn| {
const queued = self._page.queued_navigation;
if (std.mem.indexOfScalar(*Frame, queued.items, self)) |idx| {
_ = queued.swapRemove(idx);
}
qn.arena.release();
self._queued_navigation = null;
self.cancelQueuedNavigation();
// HTML's "active parser was aborted" flag. Stopping is the only thing that
// actually kills the parser: a merely *scheduled* navigation leaves it
// running until the replacement commits, and Chrome keeps honouring
// document.write until then.
if (self.parserIsRunning()) {
self.document._active_parser_aborted = true;
} else if (self.document._script_created_parser) |parser| {
if (parser.handle != null) self.document._active_parser_aborted = true;
}
const http_client = &self._session.browser.http_client;
@@ -1183,8 +1120,79 @@ pub fn stopLoading(self: *Frame) void {
http_client.cancelRequests(&self._http_owner);
}
// A cross-document navigation has been scheduled (or started) for this frame:
// its current document is superseded and must never fire DOMContentLoaded or
// load, even if the replacement is discarded or fails. Deliberately does NOT
// touch _load_state — the parser can still be on the stack, and open/write/
// maybeCheckpoint key off it.
pub fn abortDocumentLoad(self: *Frame) void {
self.document._load_aborted = true;
}
// The navigation parser is on the stack, i.e. an inline script is running from
// inside parser.parse(). Narrower than `_load_state == .parsing`, which stays
// true through deferred and async scripts.
fn parserIsRunning(self: *const Frame) bool {
return switch (self._parse_state) {
.html => true,
else => false,
};
}
// Chrome moves a superseded document's readyState to "complete" but never
// fires DOMContentLoaded or load. `_load_aborted` suppresses the events; this
// is the readyState half, run as soon as the navigation is scheduled. The
// guard makes it idempotent: a handler that renavigates lands here again.
pub fn abortedDocumentIsComplete(self: *Frame) void {
if (self.document._ready_state == .complete) {
return;
}
self.document._ready_state = .complete;
self.dispatchReadyStateChange() catch |err| switch (err) {
error.JsException => {}, // already logged
else => log.err(.frame, "aborted document is complete", .{ .err = err, .type = self._type, .url = self.url }),
};
}
fn loadEventsAborted(self: *const Frame) bool {
if (self.document._load_aborted or self.js.env.terminatePending()) return true;
const parent = self.parent orelse return false;
return parent.loadEventsAborted();
}
pub fn cancelQueuedNavigation(self: *Frame) void {
const qn = self._queued_navigation orelse return;
const queued = self.page.queued_navigation;
if (std.mem.indexOfScalar(*Frame, queued.items, self)) |idx| {
_ = queued.swapRemove(idx);
}
qn.arena.release();
self._queued_navigation = null;
// Our own load is aborted and the replacement that would have completed it
// is now gone, so _documentIsComplete will never reach the parent. Release
// the parent's load delay here or it waits forever.
if (self.document._load_aborted) {
self.releaseParentLoadDelay();
}
}
// Stop delaying the parent's load event without dispatching the iframe
// element's load event: that event belongs to a document that actually
// finished loading, and this one never will.
fn releaseParentLoadDelay(self: *Frame) void {
const parent = self.parent orelse return;
if (self._parent_notified) {
return;
}
self._parent_notified = true;
if (self._delays_parent_load) {
parent.pendingLoadCompleted();
}
}
pub fn documentIsLoaded(self: *Frame) void {
if (self._load_state != .parsing) {
if (self._load_state != .parsing or self.loadEventsAborted()) {
// Ideally, documentIsLoaded would only be called once, but if a
// script is dynamically added from an async script after
// documentIsLoaded is already called, then ScriptManager will call
@@ -1202,8 +1210,9 @@ pub fn documentIsLoaded(self: *Frame) void {
fn _documentIsLoaded(self: *Frame) !void {
try self.dispatchReadyStateChange();
if (self.loadEventsAborted()) return;
const event = try Event.initTrusted(.wrap("DOMContentLoaded"), .{ .bubbles = true }, self._page);
const event = try Event.initTrusted(.wrap("DOMContentLoaded"), .{ .bubbles = true }, self.page);
try self._event_manager.dispatch(
self.document.asEventTarget(),
event,
@@ -1222,7 +1231,7 @@ fn _documentIsLoaded(self: *Frame) !void {
// (readiness -> complete). Does not bubble.
// https://html.spec.whatwg.org/multipage/dom.html#current-document-readiness
fn dispatchReadyStateChange(self: *Frame) !void {
const event = try Event.initTrusted(.wrap("readystatechange"), .{}, self._page);
const event = try Event.initTrusted(.wrap("readystatechange"), .{}, self.page);
try self._event_manager.dispatch(
self.document.asEventTarget(),
event,
@@ -1254,7 +1263,7 @@ fn iframeCompletedLoading(self: *Frame, iframe: *IFrame, delays_load: bool) void
defer entered.exit();
blk: {
const event = Event.initTrusted(comptime .wrap("load"), .{}, self._page) catch |err| {
const event = Event.initTrusted(comptime .wrap("load"), .{}, self.page) catch |err| {
log.err(.frame, "iframe event init", .{ .err = err, .url = iframe._src });
break :blk;
};
@@ -1293,6 +1302,7 @@ pub fn documentIsComplete(self: *Frame) void {
// documentIsLoaded, if there were _only_ async scripts
if (self._load_state == .parsing) {
self.documentIsLoaded();
if (self._load_state == .complete) return;
}
self._load_state = .complete;
@@ -1301,23 +1311,28 @@ pub fn documentIsComplete(self: *Frame) void {
else => log.err(.frame, "document is complete", .{ .err = err, .type = self._type, .url = self.url }),
};
if (self._maybe_meta_refresh) {
if (self._maybe_meta_refresh and !self.loadEventsAborted()) {
self._maybe_meta_refresh = false;
self.metaRefreshOnLoad();
}
}
fn _documentIsComplete(self: *Frame) !void {
self.document._ready_state = .complete;
try self.dispatchReadyStateChange();
// abortedDocumentIsComplete may already have done this half.
if (self.document._ready_state != .complete) {
self.document._ready_state = .complete;
try self.dispatchReadyStateChange();
}
if (self.loadEventsAborted()) return;
// Run element load/error events before window.load.
try self.dispatchQueuedEvents();
if (self.loadEventsAborted()) return;
// Dispatch window.load event.
const window_target = self.window.asEventTarget();
if (self._event_manager.hasDirectListeners(window_target, "load", self.window._on_load)) {
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self._page);
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self.page);
// This event is weird, it's dispatched directly on the window, but
// with the document as the target.
event._target = self.document.asEventTarget();
@@ -1389,8 +1404,8 @@ fn frameHeaderDoneCallback(transfer: *HttpClient.Transfer) !HttpClient.Transfer.
// frame_remove (clears OLD V8 context group + CDP node_registry),
// tears down the OLD page, flips the pointer, and dispatches
// frame_created against the new (now active) frame.
if (self._page.replaces != null) {
try self._session.commitPendingPage(self._page);
if (self.page.replaces != null) {
try self._session.commitPendingPage(self.page);
}
const response_url = transfer.req.url;
@@ -1423,7 +1438,7 @@ fn frameHeaderDoneCallback(transfer: *HttpClient.Transfer) !HttpClient.Transfer.
// Init new location.
const location = try Location.init(self.url, self);
location.acquireRef();
self.window._location.releaseRef(self._page);
self.window._location.releaseRef(self.page);
self.window._location = location;
if (comptime lp.IS_DEBUG) {
@@ -1890,8 +1905,8 @@ fn frameErrorCallback(ctx: *anyopaque, err: anyerror) void {
// pending Page; the OLD active Page (and its V8 context) is untouched.
// We do NOT run frameDoneCallback against the pending frame — the frame
// is about to be freed.
if (self._page.replaces != null) {
self._session.discardPendingPage(self._page);
if (self.page.replaces != null) {
self._session.discardPendingPage(self.page);
return;
}
@@ -1993,7 +2008,7 @@ pub fn iframeAddedCallback(self: *Frame, iframe: *IFrame) !void {
const new_frame = try self.arena.create(Frame);
const frame_id = session.nextFrameId();
try Frame.init(new_frame, frame_id, self._page, .{ .parent = self });
try Frame.init(new_frame, frame_id, self.page, .{ .parent = self });
errdefer new_frame.deinit();
const delays_load = iframe.isLazyLoading() == false;
@@ -2099,7 +2114,7 @@ const OpenPopupOpts = struct {
// The popup shares the Page's arena, factory, and identity map, but has no
// parent and is not attached to the frame tree — it lives in page.popups.
pub fn openPopup(self: *Frame, opts: OpenPopupOpts) !*Frame {
const page = self._page;
const page = self.page;
const session = self._session;
const resolved_url: [:0]const u8 = blk: {
@@ -2161,7 +2176,7 @@ pub fn openPopup(self: *Frame, opts: OpenPopupOpts) !*Frame {
}
pub fn domChanged(self: *Frame) void {
self._page.dom_version += 1;
self.page.dom_version += 1;
self.styleChanged();
// A DOM change is our "rendering opportunity": re-evaluate the layout
@@ -2173,7 +2188,7 @@ pub fn domChanged(self: *Frame) void {
/// Stamps the cascade: any change that can alter a selector match or cascade
/// result, including non-tree state that live collections never see.
pub fn styleChanged(self: *Frame) void {
self._page.style_version += 1;
self.page.style_version += 1;
}
const ElementIdMaps = struct { lookup: *std.StringHashMapUnmanaged(*Element), removed_ids: *std.StringHashMapUnmanaged(void) };
@@ -2484,7 +2499,7 @@ pub fn loadExternalStylesheet(self: *Frame, link: *Element.Html.Link, href: []co
}
fn fireElementEvent(self: *Frame, el: *Element, name: String) !void {
const event = try Event.initTrusted(name, .{}, self._page);
const event = try Event.initTrusted(name, .{}, self.page);
try self._event_manager.dispatch(el.asEventTarget(), event);
}
@@ -2921,7 +2936,7 @@ fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *Node,
}
}
if (self._element_shadow_roots.count() != 0) {
if (self.page.element_shadow_roots.count() != 0) {
// html5ever wraps fragment parses in a temporary <html> element that
// gets unwrapped later; it must not take part in slot assignment.
const in_fragment_parse = from_parser and self._parse_mode == .fragment;
@@ -3636,7 +3651,7 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For
// so submit_event is still valid when we check _prevent_default
submit_event.acquireRef();
defer _ = submit_event.releaseRef(self._page);
defer _ = submit_event.releaseRef(self.page);
try self._event_manager.dispatch(form_element.asEventTarget(), submit_event);
// If the submit event was prevented, don't submit the form
@@ -3664,7 +3679,7 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For
const form_data = try FormData.initWithCharset(form, submitter_, charset, &self.js.execution);
form_data.acquireRef();
defer form_data.releaseRef(self._page);
defer form_data.releaseRef(self.page);
// Per HTML spec form-submission algorithm, when the submitter is a submit
// button, its formaction/formmethod/formenctype attributes override the
@@ -3858,6 +3873,177 @@ test "Page: isSameOrigin" {
try testing.expectEqual(false, frame.isSameOrigin("//origin.com/foo"));
}
test "Frame: superseded documents omit DOMContentLoaded and load" {
const cases = [_]struct { trigger: []const u8, expected: []const u8 }{
.{ .trigger = "location.assign('/next');", .expected = "complete" },
.{
.trigger = "document.addEventListener('readystatechange', () => { if (document.readyState === 'interactive') location.assign('/next'); });",
.expected = "interactive|complete",
},
.{
.trigger = "document.addEventListener('DOMContentLoaded', () => location.assign('/next'));",
.expected = "interactive|dcl|complete",
},
.{
.trigger = "document.addEventListener('readystatechange', () => { if (document.readyState === 'complete') location.assign('/next'); });",
.expected = "interactive|dcl|complete",
},
.{ .trigger = "location.hash = 'section';", .expected = "interactive|dcl|complete|load" },
.{ .trigger = "history.replaceState({}, '', '?same-document=1');", .expected = "interactive|dcl|complete|load" },
};
for (cases) |case| {
const page = try testing.pageTest("hi.html", .{});
defer page.close();
const frame = page.frame().?;
var ls: JS.Local.Scope = undefined;
frame.js.localScope(&ls);
defer ls.deinit();
try ls.local.eval(
\\globalThis.events = [];
\\document.addEventListener('readystatechange', () => events.push(document.readyState));
\\document.addEventListener('DOMContentLoaded', () => events.push('dcl'));
\\window.addEventListener('load', () => events.push('load'));
, null);
frame._load_state = .parsing;
frame.document._ready_state = .loading;
try ls.local.eval(case.trigger, null);
frame.documentIsComplete();
const events = try ls.local.exec("events.join('|')", null);
try testing.expectEqual(case.expected, try events.toStringSlice());
}
}
test "Frame: pending or discarded replacements do not resume old load events" {
for ([_]bool{ false, true }) |discard| {
const page = try testing.pageTest("hi.html", .{});
defer page.close();
const frame = page.frame().?;
var ls: JS.Local.Scope = undefined;
frame.js.localScope(&ls);
defer ls.deinit();
try ls.local.eval(
\\globalThis.events = [];
\\document.addEventListener('readystatechange', () => events.push(document.readyState));
\\document.addEventListener('DOMContentLoaded', () => events.push('dcl'));
\\window.addEventListener('load', () => events.push('load'));
, null);
frame._load_state = .parsing;
frame.document._ready_state = .loading;
try frame._session.initiateRootNavigation(frame._frame_id, "http://127.0.0.1:9582/src/browser/tests/hi.html?replacement", .{});
const replacement = frame.page.replacement.?;
if (discard) frame._session.discardPendingPage(replacement);
try testing.expectEqual(null, frame._queued_navigation);
frame.documentIsComplete();
const events = try ls.local.exec("events.join('|')", null);
try testing.expectEqual("complete", try events.toStringSlice());
if (!discard) frame._session.discardPendingPage(replacement);
}
}
test "Frame: readystatechange during an aborted load may renavigate or throw" {
const page = try testing.pageTest("hi.html", .{});
defer page.close();
const frame = page.frame().?;
var ls: JS.Local.Scope = undefined;
frame.js.localScope(&ls);
defer ls.deinit();
try ls.local.eval(
\\globalThis.events = [];
\\document.addEventListener('readystatechange', () => {
\\ events.push(document.readyState);
\\ if (document.readyState === 'complete') { location.assign('/second'); throw new Error('handler'); }
\\});
\\window.addEventListener('load', () => events.push('load'));
, null);
frame._load_state = .parsing;
frame.document._ready_state = .loading;
testing.silenceLog(&.{ .js, .event, .frame });
try ls.local.eval("location.assign('/first');", null);
try testing.expectEqual("complete", try (try ls.local.exec("events.join('|')", null)).toStringSlice());
try testing.expectEqual(true, std.mem.endsWith(u8, frame._queued_navigation.?.url, "/second"));
try testing.expectEqual(1, frame.page.queued_navigation.items.len);
frame.documentIsComplete();
try testing.expectEqual("complete", try (try ls.local.exec("events.join('|')", null)).toStringSlice());
}
test "Frame: document.open cancels the queued navigation without reviving load" {
const page = try testing.pageTest("hi.html", .{});
defer page.close();
const frame = page.frame().?;
var ls: JS.Local.Scope = undefined;
frame.js.localScope(&ls);
defer ls.deinit();
try ls.local.eval(
\\globalThis.events = [];
\\window.addEventListener('load', () => events.push('load'));
\\location.assign('/next');
\\document.open();
\\document.write('<title>Rewritten</title>');
\\document.close();
, null);
try testing.expectEqual(null, frame._queued_navigation);
try testing.expectEqual(0, frame.page.queued_navigation.items.len);
try testing.expectEqual("Rewritten", try (try ls.local.exec("document.title", null)).toStringSlice());
try testing.expectEqual("", try (try ls.local.exec("events.join('|')", null)).toStringSlice());
}
test "Frame: document.open after inline navigation does not restart the parser" {
const page = try testing.pageTest("fixtures/navigation_open.html", .{});
defer page.close();
try testing.expect(std.mem.endsWith(u8, page.frame().?.url, "/hi.html"));
}
test "Frame: document.open can cancel navigation once parsing has finished" {
inline for (.{ "?interactive", "?dcl" }) |query| {
const page = try testing.pageTest("fixtures/navigation_open.html" ++ query, .{});
defer page.close();
const frame = page.frame().?;
try testing.expect(std.mem.endsWith(u8, frame.url, "/navigation_open.html" ++ query));
try testing.expectEqual(false, frame.document._active_parser_aborted);
try testing.expectEqual(null, frame._queued_navigation);
try testing.expectEqual("Rewritten", (try frame.getTitle()).?);
}
}
test "Frame: a scheduled navigation does not abort the parser" {
const page = try testing.pageTest("fixtures/navigation_open.html?write", .{});
defer page.close();
const frame = page.frame().?;
try testing.expect(std.mem.endsWith(u8, frame.url, "/navigation_open.html?write"));
var ls: JS.Local.Scope = undefined;
frame.js.localScope(&ls);
defer ls.deinit();
// The write landed: only window.stop() sets the active-parser-was-aborted
// flag, scheduling the navigation doesn't.
const late = try ls.local.exec("document.getElementById('late').textContent", null);
try testing.expectEqual("late", try late.toStringSlice());
try testing.expectEqual(true, frame.document._active_parser_aborted);
}
test "Frame: cancelling navigation does not revive an aborted parser" {
const page = try testing.pageTest("fixtures/navigation_open.html?cancel", .{});
defer page.close();
const frame = page.frame().?;
try testing.expect(std.mem.endsWith(u8, frame.url, "/navigation_open.html?cancel"));
var ls: JS.Local.Scope = undefined;
frame.js.localScope(&ls);
defer ls.deinit();
// The original parse has unwound, but the active-parser-was-aborted flag
// must still prevent open/write/writeln from replacing the document.
try ls.local.eval(
\\document.open();
\\document.write('<title>Later write</title>');
\\document.writeln('<title>Later writeln</title>');
\\document.close();
, null);
try testing.expectEqual("Original", try (try ls.local.exec("document.title", null)).toStringSlice());
try testing.expectEqual(null, frame.document._script_created_parser);
}
test "Frame: static immediate meta refresh navigates" {
const page = try testing.pageTest("fixtures/meta_refresh.html", .{});
defer page.close();
+77 -1
View File
@@ -27,8 +27,17 @@ const Factory = @import("Factory.zig");
const Viewport = @import("Viewport.zig");
const Blob = @import("webapi/Blob.zig");
const Node = @import("webapi/Node.zig");
const Element = @import("webapi/Element.zig");
const SharedWorkerGlobalScope = @import("webapi/SharedWorkerGlobalScope.zig");
const PointList = @import("webapi/svg/PointList.zig");
const StringList = @import("webapi/svg/StringList.zig");
const AnimatedEnumeration = @import("webapi/svg/AnimatedEnumeration.zig");
const AnimatedLength = @import("webapi/svg/AnimatedLength.zig");
const AnimatedNumber = @import("webapi/svg/AnimatedNumber.zig");
const AnimatedString = @import("webapi/svg/AnimatedString.zig");
const AnimatedTransformList = @import("webapi/svg/AnimatedTransformList.zig");
const AnimatedPreserveAspectRatio = @import("webapi/svg/AnimatedPreserveAspectRatio.zig");
const Allocator = std.mem.Allocator;
@@ -78,6 +87,61 @@ factory: Factory,
_frame_arena: *lp.Arena,
frame_arena: Allocator,
// Lazily-created per-node state, kept out of the nodes themselves because
// few nodes ever need it. Keyed by node pointer and held by the Page, not a
// Frame or Document: the state belongs to the node whichever realm touches
// it, and follows the node across documents (adoption) with no migration.
// Nodes live until the Page does, so a key is never reused.
// See Attribute.List for what this is. TL;DR: proper DOM Attribute Nodes are
// fat yet rarely needed. We only create them on-demand, but still need proper
// identity (a given attribute should return the same *Attribute), so we do
// a look here, keyed by (list, name). We don't store this in the Element or
// Attribute.List.Entry because that would require additional space per
// element / Attribute.List.Entry even though we'll create very few (if any)
// actual *Attributes.
attribute_lookup: Element.Attribute.List.Lookup = .empty,
// Canonical pool for attribute names that aren't in String.intern's.
// Every Attribute's entry's name is either a String intern or held here.
// This is both a memory optimization (deduping attribute names) and a performance
// optimization (since we can compare strings by just their pointer)
attribute_names: std.StringHashMapUnmanaged(void) = .empty,
// Same as attribute_lookup, but instead of individual attributes, this is for
// the return of elements.attributes.
attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute.NamedNodeMap) = .empty,
// 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, pseudo-element)
// suffices — and Chrome returns the same object for repeated calls, so
// identity is also conformance.
element_computed_styles: Element.ComputedStyleLookup = .empty,
element_datasets: Element.DatasetLookup = .empty,
element_class_lists: Element.ClassListLookup = .empty,
element_rel_lists: Element.RelListLookup = .empty,
element_part_lists: Element.PartListLookup = .empty,
element_token_lists: Element.TokenListLookup = .empty,
element_shadow_roots: Element.ShadowRootLookup = .empty,
element_scroll_positions: Element.ScrollPositionLookup = .empty,
element_namespace_uris: Element.NamespaceUriLookup = .empty,
svg_animated_enumerations: AnimatedEnumeration.Lookup = .empty,
svg_animated_lengths: AnimatedLength.Lookup = .empty,
svg_animated_numbers: AnimatedNumber.Lookup = .empty,
svg_animated_preserve_aspect_ratios: AnimatedPreserveAspectRatio.Lookup = .empty,
svg_animated_strings: AnimatedString.Lookup = .empty,
svg_animated_transform_lists: AnimatedTransformList.Lookup = .empty,
_svg_point_lists: PointList.Lookup = .empty,
_svg_string_lists: StringList.Lookup = .empty,
// Same as above, but for Nodes (slot assigments apply to both Element AND
// Text nodes)
_assigned_slots: Node.AssignedSlotLookup = .empty,
_manual_slot_assignments: Node.AssignedSlotLookup = .empty,
// Origin map for same-origin context sharing. Entries live for the Page's
// lifetime.
origins: std.StringHashMapUnmanaged(*js.Origin) = .empty,
@@ -204,6 +268,18 @@ pub fn deinit(self: *Page) void {
self.frame.deinit();
{
var svg_point_lists = self._svg_point_lists.valueIterator();
while (svg_point_lists.next()) |list| {
list.*.deinit(self);
}
var svg_transform_lists = self.svg_animated_transform_lists.valueIterator();
while (svg_transform_lists.next()) |list| {
list.*.deinit(self);
}
}
for (self.shared_workers.items) |scope| {
scope.deinit();
}
@@ -439,5 +515,5 @@ test "Page: js_error_count" {
const page = try testing.pageTest("page_js_error.html", .{});
defer page.close();
try testing.expectEqual(2, page.frame().?._page.js_error_count);
try testing.expectEqual(2, page.frame().?.page.js_error_count);
}
+19
View File
@@ -559,6 +559,25 @@ test "Runner: lazy iframe does not delay the load event" {
try testing.expectEqual(true, lazy_child._parent_notified);
}
test "Runner: iframe that cancels its own navigation stops delaying the parent" {
const page = try testing.pageTest("runner/iframe_nav_cancel.html", .{ .wait_until_done = false });
defer page.close();
var runner = page.session.runner(.{});
try runner.waitForFrame(page.frame_id, 2000, .{ .until = .load });
const frame = page.frame().?;
try testing.expectEqual(true, frame._load_state == .complete);
try testing.expectEqual(0, frame._pending_loads);
// The child aborted its load for a navigation it then cancelled, so no
// replacement frame will ever notify the parent on its behalf.
const child = frame.child_frames.items[0];
try testing.expectEqual(true, child.document._load_aborted);
try testing.expectEqual(null, child._queued_navigation);
try testing.expectEqual(true, child._parent_notified);
}
test "Runner: idle notifications advance past a resolved condition" {
const page = try testing.pageTest("runner/runner1.html", .{});
defer page.close();
+1 -1
View File
@@ -955,7 +955,7 @@ pub const Script = struct {
const fe = self.extra.frame;
const frame = fe.frame;
const Event = @import("webapi/Event.zig");
const event = Event.initTrusted(typ, .{}, frame._page) catch |err| {
const event = Event.initTrusted(typ, .{}, frame.page) catch |err| {
log.warn(.js, "script internal callback", .{
.url = self.url,
.type = typ,
+1 -1
View File
@@ -170,7 +170,7 @@ fn siblingMatches(self: SelectorPath, el: *Element, sel: []const u8) bool {
fn matchCount(self: SelectorPath, candidate: []const u8) usize {
const root = self.frame.window._document.asNode();
const list = Selector.querySelectorAllUncached(root, candidate, self.frame) catch return 0;
defer list.deinit(self.frame._page);
defer list.deinit(self.frame.page);
return list.getLength();
}
+34 -3
View File
@@ -569,7 +569,7 @@ pub fn idleSlice(self: *Session) u31 {
}
pub fn scheduleNavigation(_: *Session, frame: *Frame) !void {
return frame._page.scheduleNavigation(frame);
return frame.page.scheduleNavigation(frame);
}
// Drain one page's queued navigations and return whether any page had work.
@@ -730,7 +730,7 @@ fn _processFrameNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation)
const frame_id = frame._frame_id;
const reuse_window = frame.window;
const page = frame._page;
const page = frame.page;
frame.js.detachGlobal();
frame.deinit();
frame.* = undefined;
@@ -780,7 +780,7 @@ fn processPopupNavigation(_: *Session, frame: *Frame, qn: *QueuedNavigation) !vo
const saved_name = reuse_window._name;
const saved_opener = reuse_window._opener;
const frame_id = frame._frame_id;
const page = frame._page;
const page = frame.page;
frame.js.detachGlobal();
frame.deinit();
@@ -904,6 +904,9 @@ pub fn initiateRootNavigation(self: *Session, frame_id: u32, url: [:0]const u8,
log.err(.browser, "pending navigation start", .{ .err = err, .url = url });
return err;
};
live.frame.abortDocumentLoad();
live.frame.abortedDocumentIsComplete();
}
// Promote a pending replacement Page to be the live Page.
@@ -1055,3 +1058,31 @@ test "Session: retiring a pending page destroys it once" {
// Would deinit `pending` twice if it had been queued twice.
session.processDestroyQueues();
}
test "Session: console capture runs no page JS" {
const js = @import("js/js.zig");
const session = testing.test_session;
try session.enableConsoleCapture();
defer {
session.notification.unregister(.console_message, session);
session._console_capture = false;
session._console_messages.clearRetainingCapacity();
}
const frame = try testing.createFrame();
defer session.closeAllPages();
var ls: js.Local.Scope = undefined;
frame.js.localScope(&ls);
defer ls.deinit();
_ = try ls.local.exec(
\\globalThis.probed = 0;
\\const probe = { toString() { globalThis.probed++; console.log('inner'); return 'outer'; } };
\\console.log('head', probe, 10n, Symbol('s'));
, null);
try testing.expectEqualSlices(u8, "[log] head [object Object] 10n Symbol(s)\n", session.drainConsoleMessages());
const probed = try ls.local.exec("globalThis.probed", null);
try testing.expectEqual(0, try probed.toF64());
}
+2 -2
View File
@@ -159,7 +159,7 @@ fn applyMediaAtRule(self: *StyleManager, build_arena: Allocator, text: []const u
const block = atRuleBlock(text, "@media") orelse return;
const query = std.mem.trim(u8, block.prelude, &std.ascii.whitespace);
if (MediaQuery.matches(query, self.frame._page.getViewport()) == false) {
if (MediaQuery.matches(query, self.frame.page.getViewport()) == false) {
return;
}
@@ -735,7 +735,7 @@ fn anyInChain(self: *StyleManager, el: *Element, comptime what: Probe, options:
/// The memoized own-element result. Callers must have run rebuildIfDirty,
/// which resets the memo.
fn ownProps(self: *StyleManager, el: *Element) Props {
const version = self.frame._page.style_version;
const version = self.frame.page.style_version;
if (self.memo_version != version) {
self.memo.clearRetainingCapacity();
self.memo_version = version;
+2 -2
View File
@@ -28,12 +28,12 @@ const Frame = @import("Frame.zig");
const Session = @import("Session.zig");
fn dispatchInputAndChangeEvents(el: *Element, frame: *Frame) !void {
const input_evt: *Event = try .initTrusted(comptime .wrap("input"), .{ .bubbles = true }, frame._page);
const input_evt: *Event = try .initTrusted(comptime .wrap("input"), .{ .bubbles = true }, frame.page);
frame._event_manager.dispatch(el.asEventTarget(), input_evt) catch |err| {
lp.log.err(.app, "dispatch input event failed", .{ .err = err });
};
const change_evt: *Event = try .initTrusted(comptime .wrap("change"), .{ .bubbles = true }, frame._page);
const change_evt: *Event = try .initTrusted(comptime .wrap("change"), .{ .bubbles = true }, frame.page);
frame._event_manager.dispatch(el.asEventTarget(), change_evt) catch |err| {
lp.log.err(.app, "dispatch change event failed", .{ .err = err });
};
+6 -6
View File
@@ -100,7 +100,7 @@ pub fn registerMutationObserver(frame: *Frame, observer: *MutationObserver) !voi
}
pub fn unregisterMutationObserver(frame: *Frame, observer: *MutationObserver) void {
observer.releaseRef(frame._page);
observer.releaseRef(frame.page);
frame._mutation.observers.remove(&observer.node);
}
@@ -112,7 +112,7 @@ pub fn registerIntersectionObserver(frame: *Frame, observer: *IntersectionObserv
pub fn unregisterIntersectionObserver(frame: *Frame, observer: *IntersectionObserver) void {
for (frame._intersection.observers.items, 0..) |obs, i| {
if (obs == observer) {
observer.releaseRef(frame._page);
observer.releaseRef(frame.page);
_ = frame._intersection.observers.swapRemove(i);
return;
}
@@ -127,7 +127,7 @@ pub fn registerResizeObserver(frame: *Frame, observer: *ResizeObserver) !void {
pub fn unregisterResizeObserver(frame: *Frame, observer: *ResizeObserver) void {
for (frame._resize.observers.items, 0..) |obs, i| {
if (obs == observer) {
observer.releaseRef(frame._page);
observer.releaseRef(frame.page);
_ = frame._resize.observers.swapRemove(i);
return;
}
@@ -342,8 +342,8 @@ fn disconnectRunawayIntersectionObservers(frame: *Frame) void {
}
for (frame._intersection.observers.items) |observer| {
observer.reset(frame._page);
observer.releaseRef(frame._page);
observer.reset(frame.page);
observer.releaseRef(frame.page);
}
frame._intersection.observers.clearRetainingCapacity();
}
@@ -465,7 +465,7 @@ pub fn deliverMutations(frame: *Frame) void {
// slotchange events fire after the observer callbacks (spec step order)
for (slots) |slot| {
const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, frame._page) catch |err| {
const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, frame.page) catch |err| {
log.err(.frame, "deliverSlotchange.init", .{ .err = err, .type = frame._type, .url = frame.url });
continue;
};
+1 -1
View File
@@ -49,7 +49,7 @@ pub fn fragment(frame: *Frame, node: *Node, html: []const u8, opts: FragmentPars
// The html5ever wrapper-unwrap below rebinds children without going
// through the insertion path, so recompute slot assignments for any
// shadow tree this fragment landed in (idempotent; signals only on diff).
defer if (frame._element_shadow_roots.count() != 0) {
defer if (frame.page.element_shadow_roots.count() != 0) {
const root = node.getRootNode(.{});
if (root.is(ShadowRoot) != null) {
slotting.assignSlottablesForTree(root, frame);
+4 -4
View File
@@ -66,7 +66,7 @@ const HoverContext = struct {
// bubbles up normally, but mouseleave will only fire on parents where the new
// target isn't part of.
pub fn updateHoverTarget(frame: *Frame, to: ?*Element, ctx: HoverContext) void {
const page = frame._page;
const page = frame.page;
const from = page.input_hover_target;
if (from == to) {
return;
@@ -638,7 +638,7 @@ pub fn triggerKeyboard(frame: *Frame, keyboard_event: *KeyboardEvent) !void {
// the keydown still fires and its default action — e.g. sequential focus
// navigation on Tab — can run.
const element = frame.window._document.getActiveElement() orelse {
event.deinit(frame._page);
event.deinit(frame.page);
return;
};
@@ -775,7 +775,7 @@ fn allowEdit(frame: *Frame, keydown: *Event, target: *Element, before_data: ?[]c
.inputType = input_type,
}, frame)).asEvent();
before.acquireRef(); // need to check its _prevent_default
defer _ = before.releaseRef(frame._page);
defer _ = before.releaseRef(frame.page);
try frame._event_manager.dispatch(target.asEventTarget(), before);
if (before._prevent_default) {
return false;
@@ -791,7 +791,7 @@ fn allowEdit(frame: *Frame, keydown: *Event, target: *Element, before_data: ?[]c
.data = data,
}, frame)).asEvent();
text_event.acquireRef(); // need to check its _prevent_default
defer _ = text_event.releaseRef(frame._page);
defer _ = text_event.releaseRef(frame.page);
try frame._event_manager.dispatch(target.asEventTarget(), text_event);
return text_event._prevent_default == false;
}
+1 -1
View File
@@ -139,7 +139,7 @@ pub const GlobalScope = union(enum) {
// The Page-level blob: URL store, shared by every global on the page.
pub fn blobUrls(self: GlobalScope) *const Blob.UrlMap {
return switch (self) {
inline else => |g| &g._page.blob_urls,
inline else => |g| &g.page.blob_urls,
};
}
+39 -3
View File
@@ -20,6 +20,7 @@ const std = @import("std");
const Frame = @import("Frame.zig");
const URL = @import("URL.zig");
const Regex = @import("../Regex.zig");
const TreeWalker = @import("webapi/TreeWalker.zig");
const Label = @import("webapi/element/html/Label.zig");
const AXNode = @import("../server/cdp/AXNode.zig");
@@ -149,11 +150,18 @@ pub fn collectInteractiveElements(
return walkInteractive(root, arena, frame, .{});
}
pub const Name = union(enum) {
/// Case-insensitive.
substring: []const u8,
/// Unanchored.
regex: Regex,
};
const FindFilter = struct {
/// Exact role match (case-insensitive). When null, role is not filtered.
role: ?[]const u8 = null,
/// Accessible-name substring match (case-insensitive). When null, name is not filtered.
name: ?[]const u8 = null,
/// Accessible-name match. When null, name is not filtered.
name: ?Name = null,
/// Stop walking once this many matches accumulate. When null, walks the full subtree.
max: ?usize = null,
};
@@ -227,7 +235,11 @@ fn walkInteractive(
if (role == null) try getTextContent(node, arena) else null;
if (filter.name) |nf| {
const n = name orelse continue;
if (std.ascii.indexOfIgnoreCase(n, nf) == null) continue;
const hit = switch (nf) {
.substring => |s| std.ascii.indexOfIgnoreCase(n, s) != null,
.regex => |re| re.matches(n),
};
if (!hit) continue;
}
const listener_types = getListenerTypes(el.asEventTarget(), listener_targets);
@@ -490,6 +502,30 @@ fn testInteractiveInBody(html: []const u8) ![]InteractiveElement {
return collectInteractiveElements(div.asNode(), frame.call_arena, frame);
}
test "browser.interactive: a name regex filters the walk" {
const frame = try testing.createFrame();
defer testing.test_session.closeAllPages();
const doc = frame.window._document;
const div = try doc.createElement("div", null, frame);
try Frame.parse.htmlAsChildren(frame, div.asNode(), "<button>Add to cart</button><button>Cart</button><a href=\"#\">Add item</a>");
const context = testing.test_app.regex_context;
const options: Regex.Options = .{ .case_insensitive = true, .unicode = true };
const starts_add = try context.compile("^add", options, null);
defer starts_add.deinit();
const found_add = try findInteractiveElements(div.asNode(), frame.call_arena, frame, .{ .name = .{ .regex = starts_add } });
try testing.expectEqual(2, found_add.len);
try testing.expectEqual("Add to cart", found_add[0].name.?);
try testing.expectEqual("Add item", found_add[1].name.?);
const only_cart = try context.compile("^cart$", options, null);
defer only_cart.deinit();
const found_cart = try findInteractiveElements(div.asNode(), frame.call_arena, frame, .{ .name = .{ .regex = only_cart } });
try testing.expectEqual(1, found_cart.len);
try testing.expectEqual("Cart", found_cart[0].name.?);
}
test "browser.interactive: names come from labels, like the tree" {
const elements = try testInteractiveInBody(
\\<label for="email">Email address</label><input id="email" type="text">
+1 -1
View File
@@ -658,7 +658,7 @@ fn serializeFunctionArgs(local: *const Local, info: FunctionCallbackInfo) ![]con
for (0..info.length()) |i| {
try buf.writer.print("{s}{d} - ", .{ separator, i + 1 });
const js_value = info.getArg(@intCast(i), local);
try local.debugValue(js_value, &buf.writer);
try js_value.format(&buf.writer);
}
return buf.written();
}
+1 -1
View File
@@ -338,7 +338,7 @@ fn _createContext(self: *Env, global: anytype, params: ContextParams) !*Context
const context_id = self.context_id;
self.context_id = context_id + 1;
const page = global._page;
const page = global.page;
const origin = try page.getOrCreateOrigin(null);
errdefer page.releaseOrigin(origin);
-105
View File
@@ -1535,111 +1535,6 @@ pub fn createPromiseResolver(self: *const Local) js.PromiseResolver {
return js.PromiseResolver.init(self);
}
pub fn debugValue(self: *const Local, js_val: js.Value, writer: *std.Io.Writer) !void {
// _debugValue walks arbitrary, caller-supplied object graphs (e.g. a
// rejected promise's reason) via raw property gets. A getter or Proxy
// trap encountered along the way can throw; without a TryCatch here,
// that leaves the isolate's exception flag set after we return, and the
// next unrelated JS entry point trips V8's has_exception() debug check.
var try_catch: js.TryCatch = undefined;
try_catch.init(self);
defer try_catch.deinit();
var seen: std.AutoHashMapUnmanaged(u32, void) = .empty;
return self._debugValue(js_val, &seen, 0, writer) catch error.WriteFailed;
}
fn _debugValue(self: *const Local, js_val: js.Value, seen: *std.AutoHashMapUnmanaged(u32, void), depth: usize, writer: *std.Io.Writer) !void {
if (js_val.isNull()) {
// I think null can sometimes appear as an object, so check this and
// handle it first.
return writer.writeAll("null");
}
if (!js_val.isObject()) {
// handle these explicitly, so we don't include the type (we only want to include
// it when there's some ambiguity, e.g. the string "true")
if (js_val.isUndefined()) {
return writer.writeAll("undefined");
}
if (js_val.isTrue()) {
return writer.writeAll("true");
}
if (js_val.isFalse()) {
return writer.writeAll("false");
}
if (js_val.isSymbol()) {
const symbol_handle = v8.v8__Symbol__Description(@ptrCast(js_val.handle), self.isolate.handle).?;
if (v8.v8__Value__IsUndefined(symbol_handle)) {
return writer.writeAll("undefined (symbol)");
}
return writer.print("{f} (symbol)", .{js.String{ .local = self, .handle = @ptrCast(symbol_handle) }});
}
const js_val_str = try js_val.toStringSlice();
if (js_val_str.len > 2000) {
try writer.writeAll(js_val_str[0..2000]);
try writer.writeAll(" ... (truncated)");
} else {
try writer.writeAll(js_val_str);
}
return writer.print(" ({f})", .{js_val.typeOf()});
}
const js_obj = js_val.toObject();
{
// explicit scope because gop will become invalid in recursive call
const obj_id: u32 = @bitCast(v8.v8__Object__GetIdentityHash(js_obj.handle));
const gop = try seen.getOrPut(self.call_arena, obj_id);
if (gop.found_existing) {
return writer.writeAll("<circular>\n");
}
gop.value_ptr.* = {};
}
if (depth > 20) {
return writer.writeAll("...deeply nested object...");
}
const names_arr = js_obj.getOwnPropertyNames() catch {
return writer.writeAll("...invalid object...");
};
const len = names_arr.len();
const own_len = blk: {
const own_names = js_obj.getOwnPropertyNames() catch break :blk 0;
break :blk own_names.len();
};
if (own_len == 0) {
const js_val_str = try js_val.toStringSlice();
if (js_val_str.len > 2000) {
try writer.writeAll(js_val_str[0..2000]);
return writer.writeAll(" ... (truncated)");
}
return writer.writeAll(js_val_str);
}
const all_len = js_obj.getPropertyNames().len();
try writer.print("({d}/{d})", .{ own_len, all_len });
for (0..len) |i| {
if (i == 0) {
try writer.writeByte('\n');
}
const field_name = try names_arr.get(@intCast(i));
const name = try field_name.toStringSlice();
try writer.splatByteAll(' ', depth);
try writer.writeAll(name);
try writer.writeAll(": ");
const field_val = try js_obj.get(name);
try self._debugValue(field_val, seen, depth + 1, writer);
if (i != len - 1) {
try writer.writeByte('\n');
}
}
}
// == Misc ==
pub fn parseJSON(self: *const Local, json: []const u8) !js.Value {
const string_handle = self.isolate.initStringHandle(json);
+1 -5
View File
@@ -91,11 +91,7 @@ pub fn toValue(self: Object) js.Value {
}
pub fn format(self: Object, writer: *std.Io.Writer) !void {
if (comptime lp.IS_DEBUG) {
return self.local.ctx.debugValue(self.toValue(), writer);
}
const str = self.toString() catch return error.WriteFailed;
return writer.writeAll(str);
return self.toValue().format(writer);
}
pub fn persist(self: Object) !Global {
+235 -5
View File
@@ -718,13 +718,187 @@ pub fn toBigInt(self: Value) js.BigInt {
}
pub fn format(self: Value, writer: *std.Io.Writer) !void {
if (comptime lp.IS_DEBUG) {
return self.local.debugValue(self, writer);
}
const js_str = self.toString() catch return error.WriteFailed;
return js_str.format(writer);
const inert: Inert = .{ .value = self };
return inert.format(writer);
}
// Stringify without running JS, avoiding potential side effects (e.g. getters,
// proxies, ...).
const Inert = struct {
value: Value,
const max_array_depth = 32;
const max_array_items = 1_000;
pub fn format(self: Inert, writer: *std.Io.Writer) !void {
const local = self.value.local;
// We might still end up calling an interceptor via
// GetOwnPropertyDescriptor, which can throw.
var try_catch: js.TryCatch = undefined;
try_catch.init(local);
defer try_catch.deinit();
var state: State = .{ .local = local };
return state.write(self.value.handle, writer);
}
const State = struct {
depth: u32 = 0,
local: *const js.Local,
items_left: u32 = max_array_items,
arrays: [max_array_depth]*const v8.Value = undefined,
fn write(self: *State, handle: *const v8.Value, writer: *std.Io.Writer) std.Io.Writer.Error!void {
const local = self.local;
const isolate = local.isolate.handle;
if (v8.v8__Value__IsString(handle)) {
return self.writeString(@ptrCast(handle), writer);
}
if (v8.v8__Value__IsStringObject(handle)) {
return self.writeString(v8.v8__StringObject__ValueOf(@ptrCast(handle)).?, writer);
}
if (v8.v8__Value__IsNumberObject(handle)) {
return self.write(@ptrCast(v8.v8__Number__New(isolate, v8.v8__NumberObject__ValueOf(@ptrCast(handle))).?), writer);
}
if (v8.v8__Value__IsBooleanObject(handle)) {
return writer.writeAll(if (v8.v8__BooleanObject__ValueOf(@ptrCast(handle))) "true" else "false");
}
if (v8.v8__Value__IsBigIntObject(handle)) {
return self.write(@ptrCast(v8.v8__BigIntObject__ValueOf(@ptrCast(handle)).?), writer);
}
if (v8.v8__Value__IsSymbolObject(handle)) {
return self.write(@ptrCast(v8.v8__SymbolObject__ValueOf(@ptrCast(handle)).?), writer);
}
if (v8.v8__Value__IsSymbol(handle)) {
try writer.writeAll("Symbol(");
const description = v8.v8__Symbol__Description(@ptrCast(handle), isolate).?;
if (v8.v8__Value__IsString(description)) {
try self.writeString(@ptrCast(description), writer);
}
return writer.writeByte(')');
}
if (v8.v8__Value__IsArray(handle)) {
return self.writeArray(handle, writer);
}
if (v8.v8__Value__IsProxy(handle)) {
return writer.writeAll("[object Proxy]");
}
if (v8.v8__Value__IsDate(handle)) {
return self.writeString(v8.v8__Date__ToISOString(@ptrCast(handle)).?, writer);
}
if (v8.v8__Value__IsRegExp(handle)) {
try writer.writeByte('/');
try self.writeString(v8.v8__RegExp__GetSource(@ptrCast(handle)).?, writer);
try writer.writeByte('/');
const flags: u32 = @intCast(v8.v8__RegExp__GetFlags(@ptrCast(handle)));
for (regexp_flags) |flag| {
if (flags & flag[0] != 0) {
try writer.writeByte(flag[1]);
}
}
return;
}
if (v8.v8__Value__IsFunction(handle)) {
const source = v8.v8__Function__FunctionProtoToString(@ptrCast(handle), local.handle) orelse {
return writer.writeAll("function");
};
return self.writeString(source, writer);
}
if (v8.v8__Value__IsNativeError(handle)) {
try self.writeString(v8.v8__Object__GetConstructorName(@ptrCast(handle)).?, writer);
const message = self.ownDataProperty(@ptrCast(handle), "message") orelse return;
if (v8.v8__Value__IsUndefined(message)) {
return;
}
if (v8.v8__Value__IsString(message) and v8.v8__String__Length(@ptrCast(message)) == 0) {
return;
}
try writer.writeAll(": ");
return self.write(message, writer);
}
if (v8.v8__Value__IsObject(handle)) {
try writer.writeAll("[object ");
try self.writeString(v8.v8__Object__GetConstructorName(@ptrCast(handle)).?, writer);
return writer.writeByte(']');
}
// number, bigint, boolean, null, undefined: converting a primitive runs no JS
const str = v8.v8__Value__ToString(handle, local.handle) orelse return error.WriteFailed;
try self.writeString(str, writer);
if (v8.v8__Value__IsBigInt(handle)) {
try writer.writeByte('n');
}
}
fn writeArray(self: *State, handle: *const v8.Value, writer: *std.Io.Writer) std.Io.Writer.Error!void {
for (self.arrays[0..self.depth]) |seen| {
if (v8.v8__Value__StrictEquals(seen, handle)) {
return;
}
}
const len = v8.v8__Array__Length(@ptrCast(handle));
if (len > self.items_left or self.depth == max_array_depth) {
// V8's builder drops the whole message here; a summary keeps the rest.
return writer.print("Array({d})", .{len});
}
self.items_left -= len;
self.arrays[self.depth] = handle;
self.depth += 1;
defer self.depth -= 1;
var key_buf: [10]u8 = undefined;
for (0..len) |i| {
if (i != 0) {
try writer.writeByte(',');
}
const key = std.fmt.bufPrint(&key_buf, "{d}", .{i}) catch unreachable;
const element = self.ownDataProperty(@ptrCast(handle), key) orelse continue;
if (v8.v8__Value__IsNullOrUndefined(element)) {
continue;
}
try self.write(element, writer);
}
}
fn writeString(self: *State, handle: *const v8.String, writer: *std.Io.Writer) std.Io.Writer.Error!void {
const str: js.String = .{ .local = self.local, .handle = handle };
return str.format(writer);
}
fn ownDataProperty(self: *State, object: *const v8.Object, key: []const u8) ?*const v8.Value {
const local = self.local;
const descriptor = v8.v8__Object__GetOwnPropertyDescriptor(object, local.handle, local.isolate.initStringHandle(key)) orelse return null;
if (v8.v8__Value__IsObject(descriptor) == false) {
return null;
}
// An accessor descriptor has no own `value`, and a Get would then reach Object.prototype.
const value_key = local.isolate.initStringHandle("value");
var has: v8.MaybeBool = undefined;
v8.v8__Object__HasOwnProperty(@ptrCast(descriptor), local.handle, value_key, &has);
if (has.has_value == false or has.value == false) {
return null;
}
return v8.v8__Object__Get(@ptrCast(descriptor), local.handle, value_key);
}
};
// Same order as RegExp.prototype.flags, plus V8's `l`.
const regexp_flags = [_]struct { u32, u8 }{
.{ v8.kRegExpHasIndices, 'd' },
.{ v8.kRegExpGlobal, 'g' },
.{ v8.kRegExpIgnoreCase, 'i' },
.{ v8.kRegExpLinear, 'l' },
.{ v8.kRegExpMultiline, 'm' },
.{ v8.kRegExpDotAll, 's' },
.{ v8.kRegExpUnicode, 'u' },
.{ v8.kRegExpUnicodeSets, 'v' },
.{ v8.kRegExpSticky, 'y' },
};
};
// The JS iteration protocol (@@iterator)
pub fn iterator(self: Value) !?Iterator {
if (!self.isObject()) {
@@ -791,6 +965,62 @@ pub const Global = struct {
};
const testing = @import("../../testing.zig");
test "Value: inert formatting runs no page JS" {
const frame = try testing.createFrame();
defer testing.test_session.closeAllPages();
var ls: js.Local.Scope = undefined;
frame.js.localScope(&ls);
defer ls.deinit();
_ = try ls.local.exec(
\\globalThis.probed = 0;
\\globalThis.probe = function() { globalThis.probed++; return 'probed'; };
, null);
const cases = [_]struct { expr: []const u8, expected: []const u8 }{
.{ .expr = "'str'", .expected = "str" },
.{ .expr = "1.5", .expected = "1.5" },
.{ .expr = "-0", .expected = "0" },
.{ .expr = "NaN", .expected = "NaN" },
.{ .expr = "true", .expected = "true" },
.{ .expr = "null", .expected = "null" },
.{ .expr = "undefined", .expected = "undefined" },
.{ .expr = "10n", .expected = "10n" },
.{ .expr = "Symbol('s')", .expected = "Symbol(s)" },
.{ .expr = "Symbol()", .expected = "Symbol()" },
.{ .expr = "new Number(42)", .expected = "42" },
.{ .expr = "new String('w')", .expected = "w" },
.{ .expr = "new Boolean(false)", .expected = "false" },
.{ .expr = "Object(5n)", .expected = "5n" },
.{ .expr = "Object(Symbol('q'))", .expected = "Symbol(q)" },
.{ .expr = "({ toString: probe, valueOf: probe, [Symbol.toPrimitive]: probe })", .expected = "[object Object]" },
.{ .expr = "Object.defineProperty({}, Symbol.toStringTag, { get: probe })", .expected = "[object Object]" },
.{ .expr = "(() => { const d = document.createElement('div'); Object.defineProperty(d, 'id', { get: probe }); return d; })()", .expected = "[object HTMLDivElement]" },
.{ .expr = "new (class Foo {})()", .expected = "[object Foo]" },
.{ .expr = "new Proxy({}, { get: probe, getOwnPropertyDescriptor: probe, getPrototypeOf: probe })", .expected = "[object Proxy]" },
.{ .expr = "[1, null, undefined, 'a', [2, [3]]]", .expected = "1,,,a,2,3" },
.{ .expr = "(() => { const a = [1]; a.push(a); return a; })()", .expected = "1," },
.{ .expr = "Object.defineProperty([1], 1, { get: probe })", .expected = "1," },
.{ .expr = "Object.assign(new TypeError('boom'), { toString: probe })", .expected = "TypeError: boom" },
.{ .expr = "new RangeError()", .expected = "RangeError" },
.{ .expr = "Object.defineProperty(new Error(), 'message', { get: probe })", .expected = "Error" },
.{ .expr = "Object.defineProperty(Object.assign(new Date(0), { toString: probe }), Symbol.toPrimitive, { value: probe })", .expected = "1970-01-01T00:00:00.000Z" },
.{ .expr = "new Date(NaN)", .expected = "Invalid Date" },
.{ .expr = "Object.assign(/a+/gi, { toString: probe })", .expected = "/a+/gi" },
.{ .expr = "Object.assign(function named() {}, { toString: probe })", .expected = "function named() {}" },
};
for (cases) |case| {
const value = try ls.local.exec(case.expr, null);
const out = try std.fmt.allocPrint(testing.allocator, "{f}", .{value});
defer testing.allocator.free(out);
try testing.expectEqualSlices(u8, case.expected, out);
}
const probed = try ls.local.exec("globalThis.probed", null);
try testing.expectEqual(0, try probed.toF64());
}
test "Value: persisted handle early-release swap-removes and fixes up indices" {
const frame = try testing.createFrame();
defer testing.test_session.closeAllPages();
+1 -1
View File
@@ -65,7 +65,7 @@ pub fn collectLinks(arena: Allocator, root: *Node, frame: *Frame) ![]Link {
var labels: Label.LabelByForIndex = .{};
if (Selector.querySelectorAll(root, "a[href]", frame)) |list| {
defer list.deinit(frame._page);
defer list.deinit(frame.page);
for (list._nodes) |node| {
const anchor = node.is(Element.Html.Anchor) orelse continue;
+2 -1
View File
@@ -534,7 +534,8 @@ fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName,
if (namespace == .unknown and namespace_string.len > 0) {
// Same as Document.createElementNS: keep the URI so namespaceURI and
// lookupNamespaceURI can return it.
try frame._element_namespace_uris.put(frame.arena, node.as(Element), try frame.dupeString(namespace_string));
const page = self.document._page;
try page.element_namespace_uris.put(page.frame_arena, node.as(Element), try frame.dupeString(namespace_string));
}
const pn = try self.arena.create(ParsedNode);
+20
View File
@@ -244,3 +244,23 @@
testing.expectEqual('red', impDiv.style.getPropertyValue('color'));
}
</script>
<script id="computedStyleReadOnly">
{
const div = document.createElement('div');
div.setAttribute('style', 'color: red; margin: 1px');
document.body.appendChild(div);
const cs = window.getComputedStyle(div);
testing.expectError('NoModificationAllowedError', () => cs.setProperty('width', '10px'));
testing.expectError('NoModificationAllowedError', () => cs.setProperty('width', '10px', 'bogus'));
testing.expectError('NoModificationAllowedError', () => cs.removeProperty('color'));
testing.expectError('NoModificationAllowedError', () => { cs.cssText = ''; });
testing.expectError('NoModificationAllowedError', () => { cs.cssFloat = 'left'; });
testing.expectError('NoModificationAllowedError', () => { cs.color = 'blue'; });
testing.expectEqual('color: red; margin: 1px', div.getAttribute('style'));
div.style.width = '50px';
testing.expectEqual('50px', cs.getPropertyValue('width'));
}
</script>
+28
View File
@@ -0,0 +1,28 @@
<!doctype html>
<title>Original</title>
<script>
function navigateAndOpen() {
location.assign('../hi.html');
if (location.search === '?cancel') window.stop();
document.open();
document.write('<title>Rewritten</title>');
document.close();
}
if (location.search === '?interactive') {
document.addEventListener('readystatechange', () => {
if (document.readyState === 'interactive') navigateAndOpen();
});
} else if (location.search === '?dcl') {
document.addEventListener('DOMContentLoaded', navigateAndOpen);
} else if (location.search === '?write') {
// Scheduling a navigation does not abort the parser: Chrome keeps honouring
// document.write until the replacement commits. window.stop() cancels the
// navigation so the test can see what landed.
location.assign('../hi.html');
document.write('<p id="late">late</p>');
window.stop();
} else {
navigateAndOpen();
}
</script>
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<head></head>
<body>
<script src="../testing.js"></script>
<!--
Lazily-created per-node state (dataset, classList, shadow roots, inline event
handlers, ...) belongs to the node, whichever realm touches it. The iframe
helpers run in the iframe's realm, so each pair below reads the same node from
two realms.
-->
<iframe id="idf" src="support/cross_realm_node_state.html"></iframe>
<script id="identity">
testing.onload(() => {
const idf = document.getElementById('idf');
const iwin = idf.contentWindow;
const el = idf.contentDocument.getElementById('s1');
testing.expectTrue(el.dataset === iwin.datasetOf('s1'));
testing.expectTrue(el.classList === iwin.classListOf('s1'));
});
</script>
<script id="shadow_root">
testing.onload(() => {
const idf = document.getElementById('idf');
const iwin = idf.contentWindow;
const host = idf.contentDocument.getElementById('host');
const shadow = host.attachShadow({ mode: 'open' });
testing.expectTrue(iwin.shadowRootOf('host') === shadow);
});
</script>
<script id="declarative_shadow_root">
testing.onload(() => {
const idf = document.getElementById('idf');
const iwin = idf.contentWindow;
const dhost = idf.contentDocument.getElementById('dhost');
// Attached by the iframe's parser; must be visible to JS from both realms.
const shadow = dhost.shadowRoot;
testing.expectTrue(shadow !== null);
testing.expectTrue(iwin.shadowRootOf('dhost') === shadow);
testing.expectEqual('in', shadow.getElementById('inner').textContent);
});
</script>
<script id="inline_handler">
testing.onload(() => {
const idf = document.getElementById('idf');
const iwin = idf.contentWindow;
const btn = idf.contentDocument.getElementById('btn');
let fired = 0;
const handler = () => { fired += 1; };
btn.onclick = handler;
testing.expectTrue(iwin.onclickOf('btn') === handler);
iwin.clickById('btn');
testing.expectEqual(1, fired);
});
</script>
</body>
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<head></head>
<body>
<span id="s1" data-cp="5F" class="a b">x</span>
<div id="host"></div>
<div id="dhost"><template shadowrootmode="open"><p id="inner">in</p></template></div>
<button id="btn">b</button>
<script>
// Called by the parent test: these execute in this frame's realm.
function datasetOf(id) {
return document.getElementById(id).dataset;
}
function classListOf(id) {
return document.getElementById(id).classList;
}
function shadowRootOf(id) {
return document.getElementById(id).shadowRoot;
}
function onclickOf(id) {
return document.getElementById(id).onclick;
}
function clickById(id) {
document.getElementById(id).click();
}
</script>
</body>
+22
View File
@@ -1520,4 +1520,26 @@
});
}
</script>
<!-- A popup that closes itself from inside an IndexedDB success handler. The
close detaches its context from the engine's connection gate while the
delete task is still on the stack; receiving the message proves neither
the popup nor its in-flight task was freed twice. -->
<script id=popup_self_close type=module>
{
const state = await testing.async();
window.addEventListener('message', (e) => {
if (e.data && e.data.from === 'popup_idb_self_close') {
state.resolve(e.data);
}
}, { once: true });
const w = window.open(testing.BASE_URL + 'window/support/popup_idb_self_close.html');
testing.expectTrue(w != null);
await state.done((data) => {
testing.expectEqual('popup_idb_self_close', data.from);
});
}
</script>
</body>
@@ -0,0 +1,170 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=absent_properties_do_not_mutate_style>
{
for (const initial of [null, '', 'color:red']) {
const element = document.createElement('div');
if (initial !== null) element.setAttribute('style', initial);
const observer = new MutationObserver(() => {});
observer.observe(element, { attributes: true });
testing.expectEqual('', element.style.removeProperty('display'));
element.style.display = '';
element.style.setProperty('display', '');
element.style.cssFloat = '';
element.style.removeProperty('--absent');
testing.expectEqual(0, observer.takeRecords().length);
testing.expectEqual(initial, element.getAttribute('style'));
observer.disconnect();
}
}
</script>
<script id=identical_declarations_preserve_raw_style>
{
const element = document.createElement('div');
const raw = 'color:red; margin-top:0px; float:left; --token:x';
element.setAttribute('style', raw);
const observer = new MutationObserver(() => {});
observer.observe(element, { attributes: true });
element.style.color = 'red';
element.style.setProperty('COLOR', 'red');
element.style.marginTop = '0';
element.style.cssFloat = 'left';
element.style.setProperty('--token', 'x');
testing.expectEqual(0, observer.takeRecords().length);
testing.expectEqual(raw, element.getAttribute('style'));
observer.disconnect();
}
</script>
<script id=priority_changes_and_empty_values>
{
const element = document.createElement('div');
element.style.setProperty('color', 'red', 'important');
const observer = new MutationObserver(() => {});
observer.observe(element, { attributes: true, attributeOldValue: true });
element.style.setProperty('color', 'red', 'IMPORTANT');
testing.expectEqual(0, observer.takeRecords().length);
element.style.setProperty('color', 'blue', 'invalid');
testing.expectEqual(0, observer.takeRecords().length);
testing.expectEqual('red', element.style.color);
element.style.setProperty('color', 'red');
const priorityRecords = observer.takeRecords();
testing.expectEqual(1, priorityRecords.length);
testing.expectEqual('color: red !important;', priorityRecords[0].oldValue);
testing.expectEqual('', element.style.getPropertyPriority('color'));
element.style.setProperty('color', '', 'important');
const removed = observer.takeRecords();
testing.expectEqual(1, removed.length);
testing.expectEqual('color: red;', removed[0].oldValue);
testing.expectEqual('', element.getAttribute('style'));
element.style.setProperty('color', '', 'important');
testing.expectEqual(0, observer.takeRecords().length);
observer.disconnect();
}
</script>
<script id=real_changes_and_explicit_assignments_still_notify>
{
const element = document.createElement('div');
element.style.color = 'red';
const observer = new MutationObserver(() => {});
observer.observe(element, { attributes: true, attributeOldValue: true });
element.style.color = 'blue';
let records = observer.takeRecords();
testing.expectEqual(1, records.length);
testing.expectEqual('style', records[0].attributeName);
testing.expectEqual('color: red;', records[0].oldValue);
testing.expectEqual('blue', element.style.color);
element.style.cssText = element.style.cssText;
testing.expectEqual(1, observer.takeRecords().length);
element.setAttribute('style', element.getAttribute('style'));
testing.expectEqual(1, observer.takeRecords().length);
testing.expectEqual('blue', element.style.removeProperty('COLOR'));
records = observer.takeRecords();
testing.expectEqual(1, records.length);
testing.expectEqual('color: blue;', records[0].oldValue);
testing.expectEqual('', element.style.removeProperty('color'));
testing.expectEqual(0, observer.takeRecords().length);
element.style.cssText = '';
testing.expectEqual(1, observer.takeRecords().length);
observer.disconnect();
}
</script>
<script id=css_float_priority_changes_notify>
{
const element = document.createElement('div');
element.style.setProperty('float', 'left', 'important');
const observer = new MutationObserver(() => {});
observer.observe(element, { attributes: true });
element.style.cssFloat = 'left';
testing.expectEqual(1, observer.takeRecords().length);
testing.expectEqual('', element.style.getPropertyPriority('float'));
element.style.cssFloat = 'left';
testing.expectEqual(0, observer.takeRecords().length);
observer.disconnect();
}
</script>
<script id=custom_property_case_is_significant>
{
const element = document.createElement('div');
element.style.setProperty('--Token', 'x');
const observer = new MutationObserver(() => {});
observer.observe(element, { attributes: true });
element.style.setProperty('--token', 'x');
testing.expectEqual(1, observer.takeRecords().length);
element.style.setProperty('--Token', 'y');
testing.expectEqual(1, observer.takeRecords().length);
element.style.setProperty('--Token', 'y');
testing.expectEqual(0, observer.takeRecords().length);
testing.expectEqual('x', element.style.getPropertyValue('--token'));
testing.expectEqual('y', element.style.getPropertyValue('--Token'));
observer.disconnect();
}
</script>
<script id=observer_reapplying_style_converges>
(async () => {
const state = await testing.async();
const element = document.createElement('div');
let calls = 0;
const observer = new MutationObserver(() => {
if (++calls >= 4) observer.disconnect();
element.style.color = 'red';
element.style.removeProperty('display');
element.style.cssFloat = '';
});
observer.observe(element, { attributes: true });
element.style.color = 'red';
await new Promise(resolve => setTimeout(resolve, 0));
observer.disconnect();
state.resolve();
await state.done(() => {
testing.expectEqual(1, calls);
testing.expectEqual('red', element.style.color);
});
})();
</script>
<script id=healthy_batches_are_not_disconnected>
(async () => {
const state = await testing.async();
const element = document.createElement('div');
let delivered = 0;
const observers = Array.from({ length: 64 }, () => {
const observer = new MutationObserver(() => delivered++);
observer.observe(element, { attributes: true });
return observer;
});
for (let round = 0; round < 32; round++) {
element.setAttribute('data-round', String(round));
await new Promise(resolve => setTimeout(resolve, 0));
}
observers.forEach(observer => observer.disconnect());
state.resolve();
await state.done(() => testing.expectEqual(2048, delivered));
})();
</script>
@@ -0,0 +1,3 @@
<!DOCTYPE html>
<meta charset="UTF-8">
<iframe src="iframe_nav_cancel_child.html"></iframe>
@@ -0,0 +1,7 @@
<!doctype html>
<script>
// Abort our own load for a navigation, then cancel the navigation. Nothing
// will ever complete this document, so it must stop delaying the parent.
location.assign('runner1.html');
window.stop();
</script>
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<script>
// deleteDatabase's success handler runs on the delete task's stack, and
// close() detaches this frame's context from the IndexedDB connection gate.
// Regression: the detach freed the in-flight task, which then freed itself
// again on its way out of the gate.
const req = indexedDB.deleteDatabase('popup-self-close-db');
req.onsuccess = () => {
window.opener.postMessage({ from: 'popup_idb_self_close' }, '*');
window.close();
};
</script>
+79 -15
View File
@@ -50,8 +50,8 @@ pub const driver_guidance =
\\ values are already in the tree — don't re-fetch via `nodeDetails`.
\\- `nodeDetails(backendNodeId)` → a ready-to-use CSS `selector` that
\\ resolves to one node, plus its id/class/attrs.
\\- `findElement(role, name)` → locate a candidate by role/name without
\\ parsing the whole tree.
\\- `findElement(role, name)` → locate a candidate by role and name (a
\\ substring, or `/regex/`) without parsing the whole tree.
\\- `markdown(selector | backendNodeId)` → readable text for one
\\ subtree. Use after `tree` has shown you where the interesting
\\ region is.
@@ -683,7 +683,7 @@ pub const Tool = enum {
\\ "type": "object",
\\ "properties": {
\\ "role": { "type": "string", "description": "Optional ARIA role to match (e.g. 'button', 'link', 'textbox', 'checkbox')." },
\\ "name": { "type": "string", "description": "Optional accessible name substring to match (case-insensitive)." }
\\ "name": { "type": "string", "description": "Optional accessible name to match, case-insensitive: a substring, or a JavaScript regex literal such as /sign (in|up)/ (unanchored; flags i, m, s, u accepted; case-insensitive even without i, prefix (?-i) to make it case-sensitive)." }
\\ }
\\}
),
@@ -814,8 +814,9 @@ pub fn errorMessage(err: ToolError) []const u8 {
/// Outcome of running a tool against the page. Operational failures (OOM,
/// missing page, invalid params) come out as Zig errors on the enclosing
/// `!ToolResult`; `is_error = true` is the in-band signal for a JS-level
/// failure (V8 caught a throw inside `evaluate`/`extract`) — the LLM consumes
/// `text` either way to self-correct. Non-evaluate tools always set `is_error =
/// failure (V8 caught a throw inside `evaluate`/`extract`) or any failure whose
/// message carries detail the model needs — the LLM consumes `text` either way
/// to self-correct. Non-evaluate tools always set `is_error =
/// false` on success.
pub const ToolResult = struct {
text: []const u8,
@@ -924,7 +925,7 @@ fn dispatch(
.press => .{ .text = try execPress(arena, session, registry, substituted) },
.selectOption => .{ .text = try execSelectOption(arena, session, registry, substituted) },
.setChecked => .{ .text = try execSetChecked(arena, session, registry, substituted) },
.findElement => .{ .text = try execFindElement(arena, session, registry, substituted) },
.findElement => execFindElement(arena, session, registry, substituted),
.evaluate => execEvaluate(arena, session, registry, substituted),
.extract => execExtract(arena, session, registry, substituted),
.getEnv => .{ .text = try execGetEnv(arena, substituted) },
@@ -1368,7 +1369,7 @@ fn execScreenshot(arena: std.mem.Allocator, session: *lp.Session, registry: *Nod
const page = try ensurePage(session, registry, args.url, args.timeout);
const scope = try resolveScope(session, registry, page, args.selector, args.backendNodeId);
const state = lp.RenderTree.resolve(arena, scope, args.strip, page) catch return ToolError.OutOfMemory;
const opts: lp.screenshot.Opts = .fromViewport(page._page.getViewport(), args.fullPage);
const opts: lp.screenshot.Opts = .fromViewport(page.page.getViewport(), args.fullPage);
var prepared = lp.screenshot.preparePng(arena, state, opts, page) catch
return ToolError.InternalError;
@@ -1767,7 +1768,7 @@ fn awaitQueuedNavigation(session: *lp.Session, frame: *lp.Frame) ToolError!void
// Runner waits are keyed by Page root (a popup lives on its opener's
// Page). Read it before processing: a synthetic root navigation frees
// the Page in place.
const root_frame_id = frame._page.frame._frame_id;
const root_frame_id = frame.page.frame._frame_id;
const navigated = session.processQueuedNavigation() catch return ToolError.InternalError;
if (navigated == false) {
return;
@@ -1794,7 +1795,7 @@ const ActionScope = struct {
fn beginAction(session: *lp.Session) ActionScope {
const frame = session.currentFrame();
return .{ .frame = frame, .popups = if (frame) |f| f._page.popups.items.len else 0 };
return .{ .frame = frame, .popups = if (frame) |f| f.page.popups.items.len else 0 };
}
/// Finish a state-changing action: drain any queued navigation triggered by
@@ -1813,14 +1814,14 @@ fn finalizeAction(arena: std.mem.Allocator, session: *lp.Session, registry: *Nod
if (before != null and before.? != page) registry.reset();
var note: []const u8 = "";
if (page._page.popups.items.len > scope.popups) {
if (page.page.popups.items.len > scope.popups) {
// The action opened a new window (target=_blank or window.open).
// Follow it, as a user whose click opened a tab would.
var runner = session.runner(.{});
runner.waitForFrame(page._page.frame._frame_id, 10000, .{ .until = .done }) catch |err|
runner.waitForFrame(page.page.frame._frame_id, 10000, .{ .until = .done }) catch |err|
return if (err == error.Cancelled) ToolError.Cancelled else ToolError.NavigationFailed;
page = try requireFrame(session);
const popups = page._page.popups.items;
const popups = page.page.popups.items;
if (popups.len > scope.popups) {
page = popups[popups.len - 1];
session.followPopup(page._frame_id);
@@ -2077,7 +2078,7 @@ fn execSetChecked(arena: std.mem.Allocator, session: *lp.Session, registry: *Nod
return finalizeAction(arena, session, registry, scope, body);
}
fn execFindElement(arena: std.mem.Allocator, session: *lp.Session, registry: *NodeRegistry, arguments: ?std.json.Value) ToolError![]const u8 {
fn execFindElement(arena: std.mem.Allocator, session: *lp.Session, registry: *NodeRegistry, arguments: ?std.json.Value) ToolError!ToolResult {
const Params = struct {
role: ?[]const u8 = null,
name: ?[]const u8 = null,
@@ -2088,14 +2089,77 @@ fn execFindElement(arena: std.mem.Allocator, session: *lp.Session, registry: *No
const page = try requireFrame(session);
var name_filter: ?lp.interactive.Name = null;
defer if (name_filter) |nf| switch (nf) {
.regex => |re| re.deinit(),
.substring => {},
};
if (args.name) |name| {
if (regexLiteral(name)) |lit| {
var options: lp.Regex.Options = .{ .case_insensitive = true, .unicode = true };
for (lit.flags) |flag| switch (flag) {
'i', 'u' => {},
's' => options.dot_all = true,
'm' => options.multiline = true,
else => return .{
.text = try std.fmt.allocPrint(arena, "findElement: unsupported regex flag '{c}' in '{s}'", .{ flag, name }),
.is_error = true,
},
};
var diag: lp.Regex.Diagnostic = .{};
const regex = session.browser.app.regex_context.compile(lit.body, options, &diag) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.InvalidRegex => return .{
.text = try std.fmt.allocPrint(arena, "findElement: invalid name regex '{s}': {s} at offset {d}", .{ lit.body, diag.message(), diag.offset }),
.is_error = true,
},
};
name_filter = .{ .regex = regex };
} else {
name_filter = .{ .substring = name };
}
}
const matched = lp.interactive.findInteractiveElements(page.document.asNode(), arena, page, .{
.role = args.role,
.name = args.name,
.name = name_filter,
}) catch return ToolError.InternalError;
lp.interactive.registerNodes(matched, registry) catch
return ToolError.InternalError;
return renderJson(arena, matched);
return .{ .text = try renderJson(arena, matched) };
}
const RegexLiteral = struct {
body: []const u8,
flags: []const u8,
};
/// A JavaScript `/body/flags` literal, or null for plain text. A name really
/// written as `/foo/` still matches itself, the search being unanchored.
fn regexLiteral(text: []const u8) ?RegexLiteral {
if (text.len == 0 or text[0] != '/') return null;
const close = std.mem.lastIndexOfScalar(u8, text, '/') orelse return null;
if (close < 2) return null;
const flags = text[close + 1 ..];
for (flags) |flag| {
if (std.mem.indexOfScalar(u8, "dgimsuvy", flag) == null) return null;
}
return .{ .body = text[1..close], .flags = flags };
}
test "regexLiteral" {
for ([_][]const u8{ "foo", "/", "//", "//i", "/foo", "/foo/ bar", "/usr/bin" }) |text| {
try std.testing.expectEqual(null, regexLiteral(text));
}
const plain = regexLiteral("/foo/").?;
try std.testing.expectEqualStrings("foo", plain.body);
try std.testing.expectEqualStrings("", plain.flags);
const flagged = regexLiteral("/a/b/gi").?;
try std.testing.expectEqualStrings("a/b", flagged.body);
try std.testing.expectEqualStrings("gi", flagged.flags);
}
fn execGetEnv(arena: std.mem.Allocator, arguments: ?std.json.Value) ToolError![]const u8 {
+2 -2
View File
@@ -447,8 +447,8 @@ test "Blob: a pinned arena reaches the browser's account and is given back" {
const frame = try testing.createFrame();
defer testing.test_session.closeAllPages();
const page = frame._page;
const browser = frame._session.browser;
const page = frame.page;
const browser = page.session.browser;
browser.flushArenaMemory();
try testing.expectEqual(0, browser.arena_account.pending);
+2 -2
View File
@@ -33,8 +33,8 @@ pub fn parseDimensionViewport(value: []const u8, frame: *Frame) ?f64 {
const parsed = units.parse(value) catch return null;
return switch (parsed.unit) {
.none, .px => parsed.value,
.vh => parsed.value * @as(f64, @floatFromInt(frame._page.getViewport().height)) / 100.0,
.vw => parsed.value * @as(f64, @floatFromInt(frame._page.getViewport().width)) / 100.0,
.vh => parsed.value * @as(f64, @floatFromInt(frame.page.getViewport().height)) / 100.0,
.vw => parsed.value * @as(f64, @floatFromInt(frame.page.getViewport().width)) / 100.0,
else => null,
};
}
+1 -1
View File
@@ -122,7 +122,7 @@ fn createDocument(_: *const DOMImplementation, namespace_nullable: js.Nullable([
if (namespace == .unknown) {
if (namespace_) |uri| {
const duped = try frame.dupeString(uri);
try frame._element_namespace_uris.put(frame.arena, root.as(Node.Element), duped);
try document._page.element_namespace_uris.put(document._page.frame_arena, root.as(Node.Element), duped);
}
}
+2 -1
View File
@@ -108,7 +108,8 @@ const parsererror_ns = "http://www.mozilla.org/newlayout/xml/parsererror.xml";
fn parserErrorDocument(frame: *Frame) !*Document.XMLDocument {
const doc = try frame._factory.document(Document.XMLDocument{ ._proto = undefined });
const root = try Frame.node_factory.createElementNS(doc.asDocument(), .unknown, "parsererror", null);
try frame._element_namespace_uris.put(frame.arena, root.as(Node.Element), parsererror_ns);
const page = doc.asDocument()._page;
try page.element_namespace_uris.put(page.frame_arena, root.as(Node.Element), parsererror_ns);
const text = try Frame.node_factory.createTextNode(doc.asDocument(), "error");
_ = try root.appendChild(text, frame);
_ = try doc.asNode().appendChild(root, frame);
+2 -2
View File
@@ -181,7 +181,7 @@ pub fn removeItem(self: *DataTransfer, index: u32, frame: *Frame) !void {
}
const it = self._items.orderedRemove(index);
if (it._kind == .file) {
it._payload.file._proto.releaseRef(frame._page);
it._payload.file._proto.releaseRef(frame.page);
try self.rebuildFiles(frame);
}
}
@@ -189,7 +189,7 @@ pub fn removeItem(self: *DataTransfer, index: u32, frame: *Frame) !void {
pub fn clearItems(self: *DataTransfer, frame: *Frame) !void {
for (self._items.items) |it| {
if (it._kind == .file) {
it._payload.file._proto.releaseRef(frame._page);
it._payload.file._proto.releaseRef(frame.page);
}
}
self._items.clearRetainingCapacity();
@@ -213,7 +213,7 @@ const ReceiveMessageCallback = struct {
const event = (try MessageEvent.initTrusted(comptime .wrap("messageerror"), .{
.bubbles = false,
.cancelable = false,
}, wsg._page)).asEvent();
}, wsg.page)).asEvent();
try wsg.dispatch(target, event, on_messageerror, .{});
return null;
}
@@ -230,7 +230,7 @@ const ReceiveMessageCallback = struct {
.data = .{ .value = self.data.? },
.bubbles = false,
.cancelable = false,
}, wsg._page)).asEvent();
}, wsg.page)).asEvent();
try wsg.dispatch(target, event, on_message, .{});
return null;
}
+13 -5
View File
@@ -65,6 +65,9 @@ _content_type: ?[]const u8 = null,
// createDocument) are UTF-8 regardless of the frame's encoding
_charset: ?[]const u8 = null,
_ready_state: ReadyState = .loading,
_load_aborted: bool = false,
// HTML's "active parser was aborted" flag also makes open/write no-ops.
_active_parser_aborted: bool = false,
_current_script: ?*Element.Html.Script = null,
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty,
// Track IDs that were removed from the map - they might have duplicates in the tree
@@ -398,7 +401,7 @@ pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8
if (ns == .unknown) {
if (namespace) |uri| {
const duped = try frame.dupeString(uri);
try frame._element_namespace_uris.put(frame.arena, node.as(Element), duped);
try self._page.element_namespace_uris.put(self._page.frame_arena, node.as(Element), duped);
}
}
return node.as(Element);
@@ -547,12 +550,12 @@ fn createEvent(_: *const Document, event_type: []const u8, frame: *Frame) !*@imp
const event: *Event = blk: {
if (std.mem.eql(u8, normalized, "event") or std.mem.eql(u8, normalized, "events") or std.mem.eql(u8, normalized, "htmlevents") or std.mem.eql(u8, normalized, "svgevents")) {
break :blk try Event.init("", null, frame._page);
break :blk try Event.init("", null, frame.page);
}
if (std.mem.eql(u8, normalized, "customevent")) {
const CustomEvent = @import("event/CustomEvent.zig");
break :blk (try CustomEvent.init("", null, frame._page)).asEvent();
break :blk (try CustomEvent.init("", null, frame.page)).asEvent();
}
if (std.mem.eql(u8, normalized, "keyboardevent")) {
@@ -577,7 +580,7 @@ fn createEvent(_: *const Document, event_type: []const u8, frame: *Frame) !*@imp
if (std.mem.eql(u8, normalized, "messageevent")) {
const MessageEvent = @import("event/MessageEvent.zig");
break :blk (try MessageEvent.init("", null, frame._page)).asEvent();
break :blk (try MessageEvent.init("", null, frame.page)).asEvent();
}
if (std.mem.eql(u8, normalized, "hashchangeevent")) {
@@ -996,6 +999,8 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool
return error.InvalidStateError;
}
if (self._active_parser_aborted) return;
const html = blk: {
var joined: std.ArrayList(u8) = .empty;
for (text) |str| {
@@ -1122,7 +1127,7 @@ pub fn open(self: *Document, call_frame: *Frame) !*Document {
return error.InvalidStateError;
}
if (frame._load_state == .parsing) {
if (self._active_parser_aborted or frame._load_state == .parsing) {
return self;
}
@@ -1148,6 +1153,9 @@ pub fn open(self: *Document, call_frame: *Frame) !*Document {
self._style_sheets = null;
self._implementation = null;
self._ready_state = .loading;
// open() cancels an ongoing navigation; the aborted document's load is
// gone for good, as in Chrome.
frame.cancelQueuedNavigation();
self._script_created_parser = Parser.Streaming.init(frame.arena, doc_node, frame, .{ .allow_declarative_shadow = true });
try self._script_created_parser.?.start();
+39 -32
View File
@@ -443,7 +443,7 @@ pub fn getNamespaceURI(self: *const Element) ?[]const u8 {
pub fn getNamespaceUri(self: *Element, frame: *Frame) ?[]const u8 {
if (self._namespace != .unknown) return self._namespace.toUri();
return frame._element_namespace_uris.get(self);
return frame.page.element_namespace_uris.get(self);
}
pub fn lookupNamespaceURIForElement(self: *Element, prefix: ?[]const u8, frame: *Frame) ?[]const u8 {
@@ -900,7 +900,8 @@ pub fn attachShadow(self: *Element, opts: ShadowRoot.AttachOptions, frame: *Fram
}
const shadow_root = try ShadowRoot.init(self, opts, frame);
try frame._element_shadow_roots.put(frame.arena, self, shadow_root);
const page = frame.page;
try page.element_shadow_roots.put(page.frame_arena, self, shadow_root);
self._flags.shadow_host = true;
return shadow_root;
}
@@ -912,7 +913,7 @@ pub fn hostedShadowRoot(self: *Element, frame: *const Frame) ?*ShadowRoot {
if (!self._flags.shadow_host) {
return null;
}
return frame._element_shadow_roots.get(self);
return frame.page.element_shadow_roots.get(self);
}
pub fn insertAdjacentElement(
@@ -995,19 +996,20 @@ pub fn getAttributeNames(self: *const Element, frame: *Frame) ![][]const u8 {
}
pub fn getAttributeNamedNodeMap(self: *Element, frame: *Frame) !*Attribute.NamedNodeMap {
const gop = try frame._attribute_named_node_map_lookup.getOrPut(frame.arena, @intFromPtr(self));
const page = frame.page;
const gop = try page.attribute_named_node_map_lookup.getOrPut(page.frame_arena, @intFromPtr(self));
if (!gop.found_existing) {
gop.value_ptr.* = try frame._factory.create(Attribute.NamedNodeMap{ ._element = self });
}
return gop.value_ptr.*;
}
// The materialized style lives in the map of the element's own frame, not
// the caller's: attributeChange (which resyncs it) is dispatched on the owner
// frame, and a same-origin script can reach an element in another frame.
// The materialized style is built with the element's own frame, not the
// caller's: a same-origin script can reach an element in another frame.
pub fn getOrCreateStyle(self: *Element, frame: *Frame) !*CSSStyleProperties {
const owner = self.ownerFrame(frame) orelse frame;
const gop = try owner._element_styles.getOrPut(owner.arena, self);
const page = frame.page;
const gop = try page.element_styles.getOrPut(page.frame_arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = try CSSStyleProperties.init(self, false, owner);
}
@@ -1019,7 +1021,7 @@ pub fn existingStyle(self: *Element, frame: *Frame) ?*CSSStyleProperties {
if (!self._flags.has_inline_style) {
return null;
}
return (self.ownerFrame(frame) orelse frame)._element_styles.get(self);
return frame.page.element_styles.get(self);
}
/// The inline style object, parsed from the style attribute on first use;
@@ -1055,7 +1057,8 @@ pub fn setStyle(self: *Element, value: []const u8, frame: *Frame) !void {
}
pub fn getClassList(self: *Element, frame: *Frame) !*collections.DOMTokenList {
const gop = try frame._element_class_lists.getOrPut(frame.arena, self);
const page = frame.page;
const gop = try page.element_class_lists.getOrPut(page.frame_arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = try frame._factory.create(collections.DOMTokenList{
._element = self,
@@ -1071,7 +1074,8 @@ pub fn setClassList(self: *Element, value: String, frame: *Frame) !void {
}
pub fn getPartList(self: *Element, frame: *Frame) !*collections.DOMTokenList {
const gop = try frame._element_part_lists.getOrPut(frame.arena, self);
const page = frame.page;
const gop = try page.element_part_lists.getOrPut(page.frame_arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = try frame._factory.create(collections.DOMTokenList{
._element = self,
@@ -1082,7 +1086,8 @@ pub fn getPartList(self: *Element, frame: *Frame) !*collections.DOMTokenList {
}
pub fn getRelList(self: *Element, frame: *Frame) !*collections.DOMTokenList {
const gop = try frame._element_rel_lists.getOrPut(frame.arena, self);
const page = frame.page;
const gop = try page.element_rel_lists.getOrPut(page.frame_arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = try frame._factory.create(collections.DOMTokenList{
._element = self,
@@ -1099,7 +1104,8 @@ pub const TokenListKey = struct { element: *Element, attribute: TokenListAttribu
pub const TokenListLookup = std.AutoHashMapUnmanaged(TokenListKey, *collections.DOMTokenList);
pub fn getTokenList(self: *Element, comptime attribute: TokenListAttribute, frame: *Frame) !*collections.DOMTokenList {
const gop = try frame._element_token_lists.getOrPut(frame.arena, .{ .element = self, .attribute = attribute });
const page = frame.page;
const gop = try page.element_token_lists.getOrPut(page.frame_arena, .{ .element = self, .attribute = attribute });
if (!gop.found_existing) {
gop.value_ptr.* = try frame._factory.create(collections.DOMTokenList{
._element = self,
@@ -1110,7 +1116,8 @@ pub fn getTokenList(self: *Element, comptime attribute: TokenListAttribute, fram
}
pub fn getDataset(self: *Element, frame: *Frame) !*DOMStringMap {
const gop = try frame._element_datasets.getOrPut(frame.arena, self);
const page = frame.page;
const gop = try page.element_datasets.getOrPut(page.frame_arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = try frame._factory.create(DOMStringMap{
._element = self,
@@ -1504,7 +1511,7 @@ fn viewportAxis(self: *Element, frame: *Frame, comptime axis: Axis) ?f64 {
// clientWidth and clientHeight rather than its own MASSIVE box. This
// fixes jstracker's uiContourMap which attempts to tile the clientHeight
// of the body. (https://github.com/lightpanda-io/browser/issues/3251)
const viewport = frame._page.getViewport();
const viewport = frame.page.getViewport();
return @floatFromInt(if (axis == .width) viewport.width else viewport.height);
}
@@ -1558,20 +1565,19 @@ pub fn getClientRects(self: *Element, frame: *Frame) ![]*DOMRect {
return rects;
}
// Scroll positions live in the map of the element's own frame — not the
// caller's, which differs when a same-origin script scrolls an element in
// another frame (e.g. inside an iframe). All scroll accessors resolve the
// owner frame first so the state, the fired events and the document
// comparison stay in the element's frame.
// Scroll events fire in the element's own frame — not the caller's, which
// differs when a same-origin script scrolls an element in another frame (e.g.
// inside an iframe). All scroll accessors resolve the owner frame first so the
// fired events and the document comparison stay in the element's frame.
pub fn getScrollTop(self: *Element, frame: *Frame) u32 {
const owner = self.ownerFrame(frame) orelse return 0;
const pos = owner._element_scroll_positions.get(self) orelse return 0;
const pos = owner.page.element_scroll_positions.get(self) orelse return 0;
return pos.y;
}
pub fn setScrollTop(self: *Element, value: i32, frame: *Frame) !void {
const owner = self.ownerFrame(frame) orelse return;
const gop = try owner._element_scroll_positions.getOrPut(owner.arena, self);
const gop = try owner.page.element_scroll_positions.getOrPut(owner.page.frame_arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
@@ -1584,13 +1590,13 @@ pub fn setScrollTop(self: *Element, value: i32, frame: *Frame) !void {
pub fn getScrollLeft(self: *Element, frame: *Frame) u32 {
const owner = self.ownerFrame(frame) orelse return 0;
const pos = owner._element_scroll_positions.get(self) orelse return 0;
const pos = owner.page.element_scroll_positions.get(self) orelse return 0;
return pos.x;
}
pub fn setScrollLeft(self: *Element, value: i32, frame: *Frame) !void {
const owner = self.ownerFrame(frame) orelse return;
const gop = try owner._element_scroll_positions.getOrPut(owner.arena, self);
const gop = try owner.page.element_scroll_positions.getOrPut(owner.page.frame_arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
@@ -1955,8 +1961,9 @@ pub fn clone(self: *Element, deep: bool, document: *const Node.Document, frame:
// A namespace outside the built-in set lives in a side table; the clone
// must report the same namespaceURI.
if (self._namespace == .unknown) {
if (frame._element_namespace_uris.get(self)) |uri| {
try frame._element_namespace_uris.put(frame.arena, node.as(Element), uri);
const page = document._page;
if (page.element_namespace_uris.get(self)) |uri| {
try page.element_namespace_uris.put(page.frame_arena, node.as(Element), uri);
}
}
@@ -2040,7 +2047,7 @@ pub const ScrollToOpts = union(enum) {
pub fn scrollTo(self: *Element, opts: ?ScrollToOpts, y: ?i32, frame: *Frame) !void {
const o = (opts orelse return).offsets(y);
const owner = self.ownerFrame(frame) orelse return;
const gop = try owner._element_scroll_positions.getOrPut(owner.arena, self);
const gop = try owner.page.element_scroll_positions.getOrPut(owner.page.frame_arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
@@ -2057,7 +2064,7 @@ pub fn scrollTo(self: *Element, opts: ?ScrollToOpts, y: ?i32, frame: *Frame) !vo
pub fn scrollBy(self: *Element, opts: ?ScrollToOpts, y: ?i32, frame: *Frame) !void {
const o = (opts orelse return).offsets(y);
const owner = self.ownerFrame(frame) orelse return;
const gop = try owner._element_scroll_positions.getOrPut(owner.arena, self);
const gop = try owner.page.element_scroll_positions.getOrPut(owner.page.frame_arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
@@ -2075,7 +2082,7 @@ pub fn scrollBy(self: *Element, opts: ?ScrollToOpts, y: ?i32, frame: *Frame) !vo
// scrolling element (the root) are fired at the document instead.
// `frame` is the element's owner frame (resolved by the public accessors).
fn scheduleScrollEvents(self: *Element, frame: *Frame) !void {
const gop = try frame._element_scroll_positions.getOrPut(frame.arena, self);
const gop = try frame.page.element_scroll_positions.getOrPut(frame.page.frame_arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
@@ -2116,7 +2123,7 @@ const ScrollEventTask = struct {
fn cancelled(ptr: *anyopaque) void {
const self: *ScrollEventTask = @ptrCast(@alignCast(ptr));
if (self.frame._element_scroll_positions.getPtr(self.element)) |pos| {
if (self.frame.page.element_scroll_positions.getPtr(self.element)) |pos| {
pos.state = .done;
}
self.frame._factory.destroy(self);
@@ -2125,7 +2132,7 @@ const ScrollEventTask = struct {
fn run(ptr: *anyopaque) anyerror!?u32 {
const self: *ScrollEventTask = @ptrCast(@alignCast(ptr));
const f = self.frame;
const pos = f._element_scroll_positions.getPtr(self.element) orelse {
const pos = f.page.element_scroll_positions.getPtr(self.element) orelse {
f._factory.destroy(self);
return null;
};
@@ -2150,7 +2157,7 @@ const ScrollEventTask = struct {
fn dispatchEvent(self: *ScrollEventTask, comptime event_type: String) void {
const Event = @import("Event.zig");
const event = Event.initTrusted(event_type, .{ .bubbles = self.bubbles() }, self.frame._page) catch |err| {
const event = Event.initTrusted(event_type, .{ .bubbles = self.bubbles() }, self.frame.page) catch |err| {
log.warn(.dom, "element.scroll.event", .{ .err = err });
return;
};
+1 -1
View File
@@ -179,7 +179,7 @@ pub fn dispatchEvent(self: *EventTarget, event: *Event, exec: *js.Execution) !bo
switch (exec.js.global) {
.frame => |frame| {
event.acquireRef();
defer _ = event.releaseRef(frame._page);
defer _ = event.releaseRef(frame.page);
try frame._event_manager.dispatch(self, event);
},
.worker => |wgs| try wgs.dispatch(self, event, null, .{}),
+5 -5
View File
@@ -154,7 +154,7 @@ fn unobserve(self: *IntersectionObserver, target: *Element, frame: *Frame) void
while (j < self._pending_entries.items.len) {
if (self._pending_entries.items[j]._target == target) {
const entry = self._pending_entries.swapRemove(j);
entry.releaseRef(frame._page);
entry.releaseRef(frame.page);
} else {
j += 1;
}
@@ -178,7 +178,7 @@ pub fn reset(self: *IntersectionObserver, page: *Page) void {
pub fn disconnect(self: *IntersectionObserver, frame: *Frame) void {
const registered = self._observing.items.len > 0;
self.reset(frame._page);
self.reset(frame.page);
if (registered) {
Frame.observers.unregisterIntersectionObserver(frame, self);
}
@@ -188,7 +188,7 @@ fn takeRecords(self: *IntersectionObserver, frame: *Frame) !js.Value {
const local = frame.js.local orelse return error.NotHandled;
const entries = try self.takePendingEntries(frame);
// whether we safely deliver these to v8 or not, we're done with these
defer releaseAll(entries, frame._page);
defer releaseAll(entries, frame.page);
return local.zigValueToJs(entries, .{});
}
@@ -216,7 +216,7 @@ fn calculateIntersection(
const root_rect = if (self._root) |root|
root.boundingClientRectValues(frame)
else blk: {
const viewport = frame._page.getViewport();
const viewport = frame.page.getViewport();
break :blk DOMRect.Data{
.width = @floatFromInt(viewport.width),
.height = @floatFromInt(viewport.height),
@@ -317,7 +317,7 @@ pub fn deliverEntries(self: *IntersectionObserver, frame: *Frame) !void {
const entries = try self.takePendingEntries(frame);
// whether we safely deliver these to v8 or not, we're done with these
defer releaseAll(entries, frame._page);
defer releaseAll(entries, frame.page);
var caught: js.TryCatch.Caught = .{};
+1 -1
View File
@@ -33,7 +33,7 @@ _rc: lp.RC = .{},
pub fn init(raw_url: []const u8, frame: *Frame) !*Location {
const url = try URL.init(raw_url, null, &frame.js.execution);
url.acquireRef();
errdefer url.releaseRef(frame._page);
errdefer url.releaseRef(frame.page);
return frame._factory.create(Location{
._url = url,
+3 -3
View File
@@ -171,7 +171,7 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
}
pub fn disconnect(self: *MutationObserver, frame: *Frame) void {
releaseAll(self._pending_records.items, frame._page);
releaseAll(self._pending_records.items, frame.page);
self._pending_records.clearRetainingCapacity();
if (self._observing.items.len > 0) {
@@ -184,7 +184,7 @@ fn takeRecords(self: *MutationObserver, frame: *Frame) !js.Value {
const local = frame.js.local orelse return error.NotHandled;
const records = try self.takePendingRecords(frame);
// whether we safely deliver these to v8 or not, we're done with these
defer releaseAll(records, frame._page);
defer releaseAll(records, frame.page);
return local.zigValueToJs(records, .{});
}
@@ -356,7 +356,7 @@ pub fn deliverRecords(self: *MutationObserver, frame: *Frame) !void {
// This ensures mutations triggered during the callback go into a fresh list
const records = try self.takePendingRecords(frame);
// whether we safely deliver these to v8 or not, we're done with these
defer releaseAll(records, frame._page);
defer releaseAll(records, frame.page);
var ls: js.Local.Scope = undefined;
frame.js.localScope(&ls);
+1 -1
View File
@@ -1696,7 +1696,7 @@ pub fn assignedSlot(self: *Node, frame: *const Frame) ?*Element.Html.Slot {
if (!self._flags.assigned_slot) {
return null;
}
return frame._assigned_slots.get(self);
return frame.page._assigned_slots.get(self);
}
pub const JsApi = struct {
+2 -2
View File
@@ -48,12 +48,12 @@ fn getOrientation(self: *Screen, frame: *Frame) !*Orientation {
}
pub fn getWidth(_: *const Screen, frame: *Frame) u32 {
const viewport = frame._page.getViewport();
const viewport = frame.page.getViewport();
return viewport.screen_width orelse viewport.width;
}
pub fn getHeight(_: *const Screen, frame: *Frame) u32 {
const viewport = frame._page.getViewport();
const viewport = frame.page.getViewport();
return viewport.screen_height orelse viewport.height;
}
+2 -2
View File
@@ -55,7 +55,7 @@ pub fn acquireRef(self: *Selection) void {
}
fn dispatchSelectionChangeEvent(frame: *Frame) !void {
const event = try Event.init("selectionchange", .{}, frame._page);
const event = try Event.init("selectionchange", .{}, frame.page);
try frame._event_manager.dispatch(frame.document.asEventTarget(), event);
}
@@ -719,7 +719,7 @@ pub fn toString(self: *const Selection, frame: *Frame) ![]const u8 {
fn setRange(self: *Selection, new_range: ?*Range, frame: *Frame) void {
if (self._range) |existing| {
_ = existing.asAbstractRange().releaseRef(frame._page);
_ = existing.asAbstractRange().releaseRef(frame.page);
}
if (new_range) |nr| {
nr.asAbstractRange().acquireRef();
+2 -2
View File
@@ -77,7 +77,7 @@ pub fn init(url: []const u8, name_or_options: ?NameOrOpts, frame: *Frame) !*Shar
const s = try SharedWorkerGlobalScope.init(frame, resolved_url, options.name, options.type);
errdefer s.deinit();
const page = frame._page;
const page = frame.page;
try page.shared_workers.append(page.frame_arena, s);
errdefer _ = page.shared_workers.pop();
@@ -87,7 +87,7 @@ pub fn init(url: []const u8, name_or_options: ?NameOrOpts, frame: *Frame) !*Shar
};
const port = try scope.connect(&frame.js.execution);
return frame._page.factory.eventTarget(SharedWorker{
return frame.page.factory.eventTarget(SharedWorker{
._proto = undefined,
._port = port,
});
@@ -387,7 +387,7 @@ const ConnectCallback = struct {
.ports = &.{self.port},
.bubbles = false,
.cancelable = false,
}, wgs._page)).asEvent();
}, wgs.page)).asEvent();
try wgs.dispatch(target, event, on_connect, .{ .context = "SharedWorkerGlobalScope.connect" });
return null;
+2 -2
View File
@@ -39,11 +39,11 @@ fn getPageTop(_: *const VisualViewport, frame: *Frame) u32 {
}
pub fn getWidth(_: *const VisualViewport, frame: *Frame) u32 {
return frame._page.getViewport().width;
return frame.page.getViewport().width;
}
pub fn getHeight(_: *const VisualViewport, frame: *Frame) u32 {
return frame._page.getViewport().height;
return frame.page.getViewport().height;
}
pub const JsApi = struct {
+5 -5
View File
@@ -271,7 +271,7 @@ fn performPointerSource(source: js.Object, frame: *Frame) !void {
} else {
Frame.user_input.updateHoverTarget(frame, el, .{
.buttons = pressed_mask,
.modifiers = frame._page.input_modifiers,
.modifiers = frame.page.input_modifiers,
.with_pointer = true,
});
dispatchPointer(el, "pointermove", 0, pressed_mask, frame);
@@ -421,7 +421,7 @@ fn performKeySource(source: js.Object, frame: *Frame) !void {
// A modifier's own keydown already carries its flag; its keyup no
// longer does.
setModifier(&frame._page.input_modifiers, key, is_down);
setModifier(&frame.page.input_modifiers, key, is_down);
// Key actions have no explicit target; they go to the focused element,
// or the document if nothing is focused. Resolved per action since a
@@ -517,7 +517,7 @@ fn setModifier(modifiers: *Modifiers, key: []const u8, pressed: bool) void {
}
fn dispatchKey(target: *EventTarget, typ: lp.String, key: []const u8, frame: *Frame) void {
const modifiers = frame._page.input_modifiers;
const modifiers = frame.page.input_modifiers;
const event = KeyboardEvent.initTrusted(typ, .{
.bubbles = true,
.cancelable = true,
@@ -543,7 +543,7 @@ fn readI32(obj: js.Object, key: []const u8, default: i32) i32 {
}
fn dispatchPointer(el: *Element, comptime typ: []const u8, button: i32, buttons: u16, frame: *Frame) void {
const modifiers = frame._page.input_modifiers;
const modifiers = frame.page.input_modifiers;
const event = PointerEvent.initTrusted(typ, .{
.bubbles = true,
.cancelable = true,
@@ -565,7 +565,7 @@ fn dispatchPointer(el: *Element, comptime typ: []const u8, button: i32, buttons:
}
fn dispatchMouse(el: *Element, comptime typ: []const u8, button: i32, buttons: u16, detail: u32, frame: *Frame) bool {
const modifiers = frame._page.input_modifiers;
const modifiers = frame.page.input_modifiers;
const event = MouseEvent.initTrusted(comptime .wrap(typ), .{
.bubbles = true,
.cancelable = true,
+14 -13
View File
@@ -580,7 +580,7 @@ pub fn reportError(self: *Window, err: js.Value, frame: *Frame) !void {
return;
}
frame._page.recordJsError(error.JsException);
frame.page.recordJsError(error.JsException);
const target = self.asEventTarget();
if (!frame._event_manager.hasDirectListeners(target, "error", self._on_error)) {
@@ -600,7 +600,7 @@ pub fn reportError(self: *Window, err: js.Value, frame: *Frame) !void {
.message = err.toStringSlice() catch "Unknown error",
.bubbles = false,
.cancelable = true,
}, frame._page);
}, frame.page);
// Invoke window.onerror callback if set (per WHATWG spec, this is called
// with 5 arguments: message, source, lineno, colno, error)
@@ -628,7 +628,7 @@ pub fn reportError(self: *Window, err: js.Value, frame: *Frame) !void {
const event = error_event.asEvent();
event.acquireRef();
defer event.releaseRef(frame._page);
defer event.releaseRef(frame.page);
event._prevent_default = prevent_default;
// Pass null as handler: onerror was already called above with 5 args.
@@ -659,7 +659,8 @@ fn getComputedStyle(_: *const Window, element: *Element, pseudo_element: ?[]cons
// (the element's own computed style) is a reasonable default for the
// common probes
const pseudo = Element.PseudoElement.parse(pseudo_element orelse "");
const gop = try frame._element_computed_styles.getOrPut(frame.arena, .{ .element = element, .pseudo = pseudo });
const page = frame.page;
const gop = try page.element_computed_styles.getOrPut(page.frame_arena, .{ .element = element, .pseudo = pseudo });
if (!gop.found_existing) {
if (pseudo == .other) {
log.warn(.not_implemented, "window.GetComputedStyle", .{ .pseudo_element = pseudo_element.? });
@@ -714,7 +715,7 @@ pub fn open(self: *Window, url_: ?[]const u8, target_: ?[]const u8, features_: ?
return Access.init(frame.window, nav_target.window);
}
const page = frame._page;
const page = frame.page;
// Name-based reuse: if a popup with this name already exists, reuse it.
// `_blank` is reserved and never reuses.
@@ -755,7 +756,7 @@ pub fn close(self: *Window) void {
// Per spec, close() is only honored on script-opened windows. That
// maps exactly to membership in page.popups.
const frame = self._frame;
const page = frame._page;
const page = frame.page;
var popup_index: usize = 0;
while (popup_index < page.popups.items.len) : (popup_index += 1) {
@@ -913,13 +914,13 @@ pub fn getScrollY(self: *const Window) u32 {
}
fn getInnerWidth(_: *const Window, frame: *Frame) u32 {
return frame._page.getViewport().width;
return frame.page.getViewport().width;
}
// Faux-layout viewport height, used to decide whether an element is already
// within view (e.g. scrollIntoViewIfNeeded).
pub fn getInnerHeight(_: *const Window, frame: *Frame) u32 {
return frame._page.getViewport().height;
return frame.page.getViewport().height;
}
pub fn scrollTo(self: *Window, opts: Element.ScrollToOpts, y: ?i32, frame: *Frame) !void {
@@ -949,7 +950,7 @@ pub fn scrollTo(self: *Window, opts: Element.ScrollToOpts, y: ?i32, frame: *Fram
return null;
}
const event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, f._page);
const event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, f.page);
try f._event_manager.dispatch(f.document.asEventTarget(), event);
pos.state = .end;
@@ -975,7 +976,7 @@ pub fn scrollTo(self: *Window, opts: Element.ScrollToOpts, y: ?i32, frame: *Fram
.end => {},
.done => return null,
}
const event = try Event.initTrusted(comptime .wrap("scrollend"), .{ .bubbles = true }, f._page);
const event = try Event.initTrusted(comptime .wrap("scrollend"), .{ .bubbles = true }, f.page);
try f._event_manager.dispatch(f.document.asEventTarget(), event);
pos.state = .done;
@@ -1016,7 +1017,7 @@ pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js.
};
if (no_handler) {
frame._page.recordJsError(error.JsException);
frame.page.recordJsError(error.JsException);
}
const target = self.asEventTarget();
@@ -1024,7 +1025,7 @@ pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js.
const event = (try @import("event/PromiseRejectionEvent.zig").init(event_name, .{
.reason = if (rejection.reason()) |r| try r.persist() else null,
.promise = try rejection.promise().persist(),
}, frame._page)).asEvent();
}, frame.page)).asEvent();
try frame._event_manager.dispatchDirect(target, event, attribute_callback, .{ .context = "window.unhandledrejection" });
}
}
@@ -1099,7 +1100,7 @@ const PostMessageCallback = struct {
.ports = self.ports,
.bubbles = false,
.cancelable = false,
}, frame._page)).asEvent();
}, frame.page)).asEvent();
try frame._event_manager.dispatchDirect(event_target, event, window._on_message, .{ .context = "window.postMessage" });
return null;
+4 -4
View File
@@ -75,7 +75,7 @@ pub fn init(url: []const u8, options: ?WorkerOptions, frame: *Frame) !*Worker {
errdefer arena.release();
const resolved_url = try URL.resolve(arena.allocator(), frame.base(), url, .{ .encoding = frame.charset });
const self = try frame._page.factory.eventTargetWithAllocator(arena.allocator(), Worker{
const self = try frame.page.factory.eventTargetWithAllocator(arena.allocator(), Worker{
._arena = arena,
._proto = undefined,
._frame = frame,
@@ -320,7 +320,7 @@ fn _fireErrorEvent(self: *Worker, message: []const u8, error_value: ?js.Value.Gl
.filename = self._url,
.bubbles = false,
.cancelable = true,
}, frame._page);
}, frame.page);
try frame._event_manager.dispatchDirect(target, error_event.asEvent(), on_error, .{
.context = "Worker.onerror",
@@ -441,7 +441,7 @@ const ReceiveMessageCallback = struct {
.data = .{ .string = @errorName(err) },
.bubbles = false,
.cancelable = false,
}, frame._page)).asEvent();
}, frame.page)).asEvent();
try frame._event_manager.dispatchDirect(target, event, on_messageerror, .{ .context = "Worker.messageerror" });
return null;
};
@@ -458,7 +458,7 @@ const ReceiveMessageCallback = struct {
.data = .{ .value = data },
.bubbles = false,
.cancelable = false,
}, frame._page)).asEvent();
}, frame.page)).asEvent();
try frame._event_manager.dispatchDirect(target, event, on_message, .{ .context = "Worker.receiveMessage" });
+10 -10
View File
@@ -65,7 +65,7 @@ _is_module: bool,
// Meant to follow the same field naming as Page so that an anytype of generic
// can access these the same for a Page of a WGS.
// These fields represent the "Page"-like component of the WGS
_page: *Page,
page: *Page,
_session: *Session,
_factory: *Factory,
_identity: JS.Identity = .{},
@@ -155,7 +155,7 @@ pub fn init(
.call_arena = call_arena.allocator(),
.local_arena = local_arena.allocator(),
._frame = frame,
._page = frame._page,
.page = frame.page,
._session = session,
._identity = .{},
._type = undefined,
@@ -201,7 +201,7 @@ pub fn init(
}
pub fn deinit(self: *WorkerGlobalScope) void {
const page = self._page;
const page = self.page;
const session = page.session;
const browser = session.browser;
@@ -246,7 +246,7 @@ pub fn dispatch(
target,
event,
handler,
self._page,
self.page,
opts,
);
}
@@ -378,7 +378,7 @@ pub fn unhandledPromiseRejection(self: *WorkerGlobalScope, no_handler: bool, rej
};
if (no_handler) {
self._page.recordJsError(error.JsException);
self.page.recordJsError(error.JsException);
}
const target = self.asEventTarget();
@@ -386,7 +386,7 @@ pub fn unhandledPromiseRejection(self: *WorkerGlobalScope, no_handler: bool, rej
const event = (try @import("event/PromiseRejectionEvent.zig").init(event_name, .{
.reason = if (rejection.reason()) |r| try r.persist() else null,
.promise = try rejection.promise().persist(),
}, self._page)).asEvent();
}, self.page)).asEvent();
try self.dispatch(target, event, attribute_callback, .{});
}
}
@@ -453,7 +453,7 @@ fn importScript(self: *WorkerGlobalScope, arena: Allocator, url: [:0]const u8) !
defer try_catch.deinit();
_ = ls.local.eval(response.body.items, url) catch |err| {
self._page.recordJsError(err);
self.page.recordJsError(err);
const caught = try_catch.caughtOrError(arena, err);
log.err(.browser, "importScript", .{ .url = resolved_url, .caught = caught });
return;
@@ -463,14 +463,14 @@ fn importScript(self: *WorkerGlobalScope, arena: Allocator, url: [:0]const u8) !
}
pub fn reportError(self: *WorkerGlobalScope, err: JS.Value) !void {
self._page.recordJsError(error.JsException);
self.page.recordJsError(error.JsException);
const error_event = try ErrorEvent.initTrusted(comptime .wrap("error"), .{
.@"error" = try err.persist(),
.message = err.toStringSlice() catch "Unknown error",
.bubbles = false,
.cancelable = true,
}, self._page);
}, self.page);
// Invoke onerror callback if set (per WHATWG spec, this is called
// with 5 arguments: message, source, lineno, colno, error)
@@ -499,7 +499,7 @@ pub fn reportError(self: *WorkerGlobalScope, err: JS.Value) !void {
const event = error_event.asEvent();
// Keep the event alive past dispatch so we can read _prevent_default.
event.acquireRef();
defer _ = event.releaseRef(self._page);
defer _ = event.releaseRef(self.page);
event._prevent_default = prevent_default;
// Pass null as handler: onerror was already called above with 5 args.
+3 -3
View File
@@ -84,7 +84,7 @@ pub fn play(self: *Animation, frame: *Frame) !void {
// Schedule the transition from .running => .finished in 10ms.
self.acquireRef();
errdefer self.releaseRef(frame._page);
errdefer self.releaseRef(frame.page);
try frame.js.scheduler.add(
self,
Animation.update,
@@ -97,7 +97,7 @@ pub fn play(self: *Animation, frame: *Frame) !void {
// and `cancelled` are mutually exclusive, so play()'s ref is released once.
fn cancelled(ctx: *anyopaque) void {
const self: *Animation = @ptrCast(@alignCast(ctx));
self.releaseRef(self._frame._page);
self.releaseRef(self._frame.page);
}
pub fn pause(self: *Animation) void {
@@ -217,7 +217,7 @@ fn update(ctx: *anyopaque) !?u32 {
}
// No future change scheduled, set the object weak for garbage collection.
self.releaseRef(self._frame._page);
self.releaseRef(self._frame.page);
return null;
}
@@ -51,7 +51,7 @@ pub fn init(node: *Node, frame: *Frame) !*ChildNodes {
._last_index = 0,
._last_node = null,
._last_length = null,
._cached_version = frame._page.dom_version,
._cached_version = frame.page.dom_version,
};
return self;
}
@@ -118,7 +118,7 @@ pub fn entries(self: *ChildNodes, frame: *Frame) !*EntryIterator {
}
fn versionCheck(self: *ChildNodes, frame: *const Frame) bool {
const current = frame._page.dom_version;
const current = frame.page.dom_version;
if (current == self._cached_version) {
return true;
}
@@ -248,7 +248,7 @@ pub fn forEach(self: *DOMTokenList, cb_: js.Function, js_this_: ?js.Object, fram
}
var caught: js.TryCatch.Caught = .{};
cb.tryCall(void, .{ token, i, self }, &caught) catch |err| {
frame._page.recordJsError(err);
frame.page.recordJsError(err);
log.debug(.js, "forEach callback", .{ .caught = caught, .source = "DOMTokenList" });
return;
};
+1 -1
View File
@@ -99,7 +99,7 @@ pub fn forEach(self: *NodeList, cb: js.Function, frame: *Frame) !void {
var caught: js.TryCatch.Caught = .{};
cb.tryCall(void, .{ node, i, self }, &caught) catch |err| {
frame._page.recordJsError(err);
frame.page.recordJsError(err);
log.debug(.js, "forEach callback", .{ .caught = caught, .source = "nodelist" });
return;
};
+2 -2
View File
@@ -133,7 +133,7 @@ pub fn NodeLive(comptime mode: Mode) type {
._last_length = null,
._filter = filter,
._tw = TW.init(root, .{}),
._cached_version = frame._page.dom_version,
._cached_version = frame.page.dom_version,
};
}
@@ -402,7 +402,7 @@ pub fn NodeLive(comptime mode: Mode) type {
}
fn versionCheck(self: *Self, frame: *const Frame) bool {
const current = frame._page.dom_version;
const current = frame.page.dom_version;
if (current == self._cached_version) {
return true;
}
+37 -16
View File
@@ -153,6 +153,10 @@ pub fn getPropertyPriority(self: *const CSSStyleDeclaration, property_name: []co
}
pub fn setProperty(self: *CSSStyleDeclaration, property_name: []const u8, value: []const u8, priority_: ?[]const u8, frame: *Frame) !void {
if (self._is_computed) {
return error.NoModificationAllowed;
}
// Validate priority
const priority = priority_ orelse "";
const important = if (priority.len > 0) blk: {
@@ -162,9 +166,9 @@ pub fn setProperty(self: *CSSStyleDeclaration, property_name: []const u8, value:
break :blk true;
} else false;
try self.setPropertyImpl(property_name, value, important, frame);
try self.syncStyleAttribute(frame);
if (try self.setPropertyImpl(property_name, value, important, frame)) {
try self.syncStyleAttribute(frame);
}
}
/// Apply one declaration parsed from a `style=` block. Unlike the imperative
@@ -177,7 +181,7 @@ fn applyParsedDeclaration(self: *CSSStyleDeclaration, declaration: CssParser.Dec
if (existing._important) return;
}
}
try self.setPropertyImpl(declaration.name, declaration.value, declaration.important, frame);
_ = try self.setPropertyImpl(declaration.name, declaration.value, declaration.important, frame);
}
fn initOwnedString(allocator: Allocator, value: []const u8) !String {
@@ -186,10 +190,9 @@ fn initOwnedString(allocator: Allocator, value: []const u8) !String {
return String.wrap(try allocator.dupe(u8, value));
}
fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value: []const u8, important: bool, frame: *Frame) !void {
fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value: []const u8, important: bool, frame: *Frame) !bool {
if (value.len == 0) {
_ = try self.removePropertyImpl(property_name, frame);
return;
return (try self.removePropertyImpl(property_name, frame)) != null;
}
const normalized = normalizePropertyName(property_name, &frame.buf);
@@ -199,12 +202,13 @@ fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value:
// Find existing property
if (self.findProperty(.wrap(normalized))) |existing| {
if (existing._value.eql(.wrap(normalized_value)) and existing._important == important) return false;
const allocator = frame._factory.storageAllocator();
const new_value = try initOwnedString(allocator, normalized_value);
existing._value.deinit(allocator);
existing._value = new_value;
existing._important = important;
return;
return true;
}
// Create new property
@@ -215,17 +219,21 @@ fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value:
._important = important,
});
self._properties.append(&prop._node);
return true;
}
pub fn removeProperty(self: *CSSStyleDeclaration, property_name: []const u8, frame: *Frame) ![]const u8 {
const result = try self.removePropertyImpl(property_name, frame);
if (self._is_computed) {
return error.NoModificationAllowed;
}
const result = (try self.removePropertyImpl(property_name, frame)) orelse return "";
try self.syncStyleAttribute(frame);
return result;
}
fn removePropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, frame: *Frame) ![]const u8 {
fn removePropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, frame: *Frame) !?[]const u8 {
const normalized = normalizePropertyName(property_name, &frame.buf);
const prop = self.findProperty(.wrap(normalized)) orelse return "";
const prop = self.findProperty(.wrap(normalized)) orelse return null;
// the value might not be on the heap (it could be inlined in the small string
// optimization), so we need to dupe it.
@@ -277,8 +285,12 @@ fn getFloat(self: *const CSSStyleDeclaration, frame: *Frame) []const u8 {
}
fn setFloat(self: *CSSStyleDeclaration, value_: ?[]const u8, frame: *Frame) !void {
try self.setPropertyImpl("float", value_ orelse "", false, frame);
try self.syncStyleAttribute(frame);
if (self._is_computed) {
return error.NoModificationAllowed;
}
if (try self.setPropertyImpl("float", value_ orelse "", false, frame)) {
try self.syncStyleAttribute(frame);
}
}
fn getCssText(self: *const CSSStyleDeclaration, frame: *Frame) ![]const u8 {
@@ -288,6 +300,15 @@ fn getCssText(self: *const CSSStyleDeclaration, frame: *Frame) ![]const u8 {
}
pub fn setCssText(self: *CSSStyleDeclaration, text: []const u8, frame: *Frame) !void {
if (self._is_computed) {
return error.NoModificationAllowed;
}
try self.replaceCssText(text, frame);
}
// setCssText without the read-only check, for declarations that are never
// computed (a CSSStyleRule's style).
pub fn replaceCssText(self: *CSSStyleDeclaration, text: []const u8, frame: *Frame) !void {
self.clearProperties(frame);
try self.applyDeclarations(text, frame);
@@ -971,10 +992,10 @@ test "CSS property value storage is reused" {
var style = CSSStyleDeclaration{};
defer style.clearProperties(frame);
try style.setPropertyImpl("transform", "translate3d(1px,0,0)", false, frame);
try testing.expect(try style.setPropertyImpl("transform", "translate3d(1px,0,0)", false, frame));
const first_ptr = style.findProperty(comptime .wrap("transform")).?._value.suffix.ptr;
try style.setPropertyImpl("transform", "translate3d(2px,0,0)", false, frame);
try style.setPropertyImpl("transform", "translate3d(3px,0,0)", false, frame);
try testing.expect(try style.setPropertyImpl("transform", "translate3d(2px,0,0)", false, frame));
try testing.expect(try style.setPropertyImpl("transform", "translate3d(3px,0,0)", false, frame));
const property = style.findProperty(comptime .wrap("transform")).?;
try testing.expectEqual(first_ptr, property._value.suffix.ptr);
+2 -2
View File
@@ -93,7 +93,7 @@ pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, maybe_index: ?u32, fra
const style_props = try style_rule.getStyle(frame);
const style = style_props.asCSSStyleDeclaration();
try style.setCssText(s.block, frame);
try style.replaceCssText(s.block, frame);
break :blk style_rule._proto;
},
// Opaque placeholder for at-rules. The CSS engine doesn't apply
@@ -182,7 +182,7 @@ fn parseInto(self: *CSSStyleSheet, text: []const u8, frame: *Frame) CSSError!voi
const style_props = try style_rule.getStyle(frame);
const style = style_props.asCSSStyleDeclaration();
try style.setCssText(s.block, frame);
try style.replaceCssText(s.block, frame);
break :blk style_rule._proto;
},
.at_rule => |a| try CSSRule.initAtRule(atRuleTypeFor(a.keyword), a.text, frame),
+2 -2
View File
@@ -82,13 +82,13 @@ pub fn load(self: *FontFaceSet, font: []const u8, frame: *Frame) !js.Promise {
// Dispatch loading event
const target = self.asEventTarget();
if (frame._event_manager.hasDirectListeners(target, "loading", null)) {
const event = try Event.initTrusted(comptime .wrap("loading"), .{}, frame._page);
const event = try Event.initTrusted(comptime .wrap("loading"), .{}, frame.page);
try frame._event_manager.dispatchDirect(target, event, null, .{ .context = "load font face set" });
}
// Dispatch loadingdone event
if (frame._event_manager.hasDirectListeners(target, "loadingdone", null)) {
const event = try Event.initTrusted(comptime .wrap("loadingdone"), .{}, frame._page);
const event = try Event.initTrusted(comptime .wrap("loadingdone"), .{}, frame.page);
try frame._event_manager.dispatchDirect(target, event, null, .{ .context = "load font face set" });
}
+2 -2
View File
@@ -43,7 +43,7 @@ pub fn init(query: []const u8, frame: *Frame) !*MediaQueryList {
._proto = undefined,
._media = media,
._frame = frame,
._matches = MediaQuery.matches(media, frame._page.getViewport()),
._matches = MediaQuery.matches(media, frame.page.getViewport()),
});
try frame._media_query_lists.append(frame.arena, self);
return self;
@@ -62,7 +62,7 @@ pub fn getMedia(self: *const MediaQueryList) []const u8 {
/// from the page (overridable via Emulation.setDeviceMetricsOverride),
/// matching `Window.innerWidth` / `innerHeight`.
fn getMatches(self: *const MediaQueryList) bool {
return MediaQuery.matches(self._media, self._frame._page.getViewport());
return MediaQuery.matches(self._media, self._frame.page.getViewport());
}
pub fn viewportChanged(self: *MediaQueryList) void {
+15 -16
View File
@@ -126,7 +126,7 @@ pub const JsApi = struct {
// Attribute value (the same JSValue) when called multiple time, and that gets
// more important when you look at the [hardly every used] el.removeAttributeNode
// and setAttributeNode.
// So, we maintain a lookup, frame._attribute_lookup, to serve as an identity map
// So, we maintain a lookup, page.attribute_lookup, to serve as an identity map
// from our internal Entry to a proper Attribute. This is lazily populated
// whenever an Attribute is created. Why not just have an ?*Attribute field
// in our Entry? Because that would require an extra 8 bytes for every single
@@ -141,7 +141,7 @@ pub const List = struct {
pub const Lookup = std.AutoHashMapUnmanaged(LookupKey, *Attribute);
// for Frame._attribute_lookup which is our identity map for attributes
// for Page.attribute_lookup which is our identity map for attributes
const LookupKey = struct {
list: *const List,
// canonical (see canonicalizeName), so identity is the address
@@ -210,13 +210,12 @@ pub const List = struct {
}
// Identity map access: a given (list, name) always yields the same
// *Attribute until the attribute is removed. The map must be the
// element's frame's, not the caller's frame.
// *Attribute until the attribute is removed.
pub fn getOrCreateAttribute(self: *const List, entry: *const Entry, element: *Element, frame: *Frame) !*Attribute {
const owner = element.ownerFrame(frame) orelse frame;
const gop = try owner._attribute_lookup.getOrPut(owner.arena, .{ .list = self, .name = entry._name_ptr });
const page = frame.page;
const gop = try page.attribute_lookup.getOrPut(page.frame_arena, .{ .list = self, .name = entry._name_ptr });
if (!gop.found_existing) {
gop.value_ptr.* = try entry.toAttribute(element, owner);
gop.value_ptr.* = try entry.toAttribute(element, element.ownerFrame(frame) orelse frame);
}
return gop.value_ptr.*;
}
@@ -304,8 +303,8 @@ pub const List = struct {
const name = try self.put(attribute._name, attribute._value, element, frame);
attribute._element = element;
const owner = element.ownerFrame(frame) orelse frame;
try owner._attribute_lookup.put(owner.arena, .{ .list = self, .name = name.ptr }, attribute);
const page = frame.page;
try page.attribute_lookup.put(page.frame_arena, .{ .list = self, .name = name.ptr }, attribute);
return existing_attribute;
}
@@ -352,7 +351,7 @@ pub const List = struct {
// remove this BEFORE triggering anything, incase that re-enters delete
// or some other callback.
if (owner._attribute_lookup.fetchRemove(.{ .list = self, .name = entry._name_ptr })) |kv| {
if (frame.page.attribute_lookup.fetchRemove(.{ .list = self, .name = entry._name_ptr })) |kv| {
// The attribute can still be alive
kv.value._element = null;
}
@@ -538,17 +537,17 @@ pub fn validateAttributeName(name: String) !void {
}
// Every stored entry name either comes from the static String.intern or from
// the frame._attribute_names. Beyond avoiding extra dupes/allocations, this
// gives a stable pointer for the frame's lifetime, which List.LookupKey
// relies on for identity. The pointer is NOT comparable across frames (each
// frame has its own pool), which is why lookups byte-compare.
// the page's attribute_names. Beyond avoiding extra dupes/allocations, this
// gives a stable pointer for the page's lifetime, which List.LookupKey
// relies on for identity.
fn canonicalizeName(name: []const u8, frame: *Frame) ![]const u8 {
if (String.intern(name)) |static| {
return static;
}
const gop = try frame._attribute_names.getOrPut(frame.arena, name);
const page = frame.page;
const gop = try page.attribute_names.getOrPut(page.frame_arena, name);
if (!gop.found_existing) {
gop.key_ptr.* = try frame.arena.dupe(u8, name);
gop.key_ptr.* = try page.frame_arena.dupe(u8, name);
}
return gop.key_ptr.*;
}
+1 -1
View File
@@ -423,7 +423,7 @@ pub fn click(self: *HtmlElement, frame: *Frame) !void {
// Keep the event alive past dispatch (which runs handlers/microtasks) so we
// can read _prevent_default afterwards.
event.acquireRef();
defer _ = event.releaseRef(frame._page);
defer _ = event.releaseRef(frame.page);
try frame._event_manager.dispatch(self.asEventTarget(), event);
+1 -1
View File
@@ -165,7 +165,7 @@ pub fn checkValidity(self: *Button, frame: *Frame) !bool {
if (!self.getWillValidate()) return true;
if (self._custom_validity == null) return true;
const event = try Event.initTrusted(comptime .wrap("invalid"), .{ .cancelable = true }, frame._page);
const event = try Event.initTrusted(comptime .wrap("invalid"), .{ .cancelable = true }, frame.page);
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
return false;
}
+1 -1
View File
@@ -52,7 +52,7 @@ pub fn close(self: *Dialog, return_value: ?[]const u8, frame: *Frame) !void {
if (return_value) |v| {
try self.asElement().setAttributeSafe(comptime .wrap("returnvalue"), .wrap(v), frame);
}
const event = try Event.init("close", .{ .bubbles = false, .cancelable = false }, frame._page);
const event = try Event.init("close", .{ .bubbles = false, .cancelable = false }, frame.page);
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
}
+4 -4
View File
@@ -288,9 +288,9 @@ pub fn selectFiles(self: *Input, files: []const *File, frame: *Frame) !void {
// A file input fires `input` then `change`, both as plain bubbling Events
// (not InputEvents — `inputType`/`data` only apply to editable text inputs).
const input_evt = try Event.initTrusted(comptime .wrap("input"), .{ .bubbles = true }, frame._page);
const input_evt = try Event.initTrusted(comptime .wrap("input"), .{ .bubbles = true }, frame.page);
try frame._event_manager.dispatch(self.asElement().asEventTarget(), input_evt);
const change_evt = try Event.initTrusted(comptime .wrap("change"), .{ .bubbles = true }, frame._page);
const change_evt = try Event.initTrusted(comptime .wrap("change"), .{ .bubbles = true }, frame.page);
try frame._event_manager.dispatch(self.asElement().asEventTarget(), change_evt);
}
@@ -310,7 +310,7 @@ fn replaceFiles(self: *Input, files: []const *File, frame: *Frame) !void {
}
for (fl._files) |old| {
old._proto.releaseRef(frame._page);
old._proto.releaseRef(frame.page);
}
fl._files = dupe;
@@ -365,7 +365,7 @@ pub fn checkValidity(self: *Input, frame: *Frame) !bool {
const v = ValidityState{ ._owner = self.asElement() };
if (v.getValid(frame)) return true;
const event = try Event.initTrusted(comptime .wrap("invalid"), .{ .cancelable = true }, frame._page);
const event = try Event.initTrusted(comptime .wrap("invalid"), .{ .cancelable = true }, frame.page);
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
return false;
}
+1 -1
View File
@@ -184,7 +184,7 @@ pub fn load(self: *Media, frame: *Frame) !void {
}
fn dispatchEvent(self: *Media, name: []const u8, frame: *Frame) !void {
const event = try Event.init(name, .{ .bubbles = false, .cancelable = false }, frame._page);
const event = try Event.init(name, .{ .bubbles = false, .cancelable = false }, frame.page);
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
}
+1 -1
View File
@@ -430,7 +430,7 @@ pub fn checkValidity(self: *Select, frame: *Frame) !bool {
const v = ValidityState{ ._owner = self.asElement() };
if (v.getValid(frame)) return true;
const event = try Event.initTrusted(comptime .wrap("invalid"), .{ .cancelable = true }, frame._page);
const event = try Event.initTrusted(comptime .wrap("invalid"), .{ .cancelable = true }, frame.page);
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
return false;
}
+3 -2
View File
@@ -119,13 +119,14 @@ pub fn assign(self: *Slot, values: []const js.Value, frame: *Frame) !void {
entry.* = node;
}
const page = frame.page;
for (self._manually_assigned.items) |node| {
_ = frame._manual_slot_assignments.remove(node);
_ = page._manual_slot_assignments.remove(node);
}
self._manually_assigned.clearRetainingCapacity();
for (nodes) |node| {
const gop = try frame._manual_slot_assignments.getOrPut(frame.arena, node);
const gop = try page._manual_slot_assignments.getOrPut(page.frame_arena, node);
if (gop.found_existing) {
const other = gop.value_ptr.*;
if (other == self) {
+1 -1
View File
@@ -218,7 +218,7 @@ pub fn checkValidity(self: *TextArea, frame: *Frame) !bool {
const v = ValidityState{ ._owner = self.asElement() };
if (v.getValid(frame)) return true;
const event = try Event.initTrusted(comptime .wrap("invalid"), .{ .cancelable = true }, frame._page);
const event = try Event.initTrusted(comptime .wrap("invalid"), .{ .cancelable = true }, frame.page);
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
return false;
}
+1 -1
View File
@@ -252,7 +252,7 @@ fn fireToggle(
// Keep the event alive while dispatching so we can read _prevent_default.
event.acquireRef();
defer _ = event.releaseRef(frame._page);
defer _ = event.releaseRef(frame.page);
try frame._event_manager.dispatch(el.asEventTarget(), event);
return event._prevent_default;
+7 -6
View File
@@ -50,7 +50,7 @@ pub fn findSlot(slottable: *Node, comptime open_only: bool, frame: *Frame) ?*Slo
const shadow_node = shadow_root.asNode();
if (shadow_root._slot_assignment == .manual) {
const slot = frame._manual_slot_assignments.get(slottable) orelse return null;
const slot = frame.page._manual_slot_assignments.get(slottable) orelse return null;
if (slot.asNode().getRootNode(.{}) != shadow_node) {
return null;
}
@@ -130,16 +130,17 @@ fn _assignSlottables(slot: *Slot, frame: *Frame) !void {
frame.signalSlotChange(slot);
const page = frame.page;
for (old) |node| {
if (frame._assigned_slots.get(node) == slot) {
_ = frame._assigned_slots.remove(node);
if (page._assigned_slots.get(node) == slot) {
_ = page._assigned_slots.remove(node);
node._flags.assigned_slot = false;
}
}
slot._assigned.clearRetainingCapacity();
try slot._assigned.appendSlice(frame.arena, slottables.items);
for (slottables.items) |node| {
try frame._assigned_slots.put(frame.arena, node, slot);
try page._assigned_slots.put(page.frame_arena, node, slot);
node._flags.assigned_slot = true;
}
}
@@ -202,7 +203,7 @@ pub fn insertionSteps(parent: *Node, child: *Node, in_fragment_parse: bool, fram
// DOM spec removing steps that affect slot assignment. Runs after child has
// been unlinked from parent.
pub fn removalSteps(parent: *Node, child: *Node, frame: *Frame) void {
if (frame._element_shadow_roots.count() == 0) {
if (frame.page.element_shadow_roots.count() == 0) {
// shortcut
return;
}
@@ -233,7 +234,7 @@ pub fn slotAttributeChanged(slottable: *Node, old_value: []const u8, value: []co
if (std.mem.eql(u8, old_value, value)) {
return;
}
if (frame._element_shadow_roots.count() == 0) {
if (frame.page.element_shadow_roots.count() == 0) {
return;
}
if (slottable.assignedSlot(frame)) |old_slot| {
+1 -1
View File
@@ -126,7 +126,7 @@ fn getPointAtLength(self: *Geometry, distance: f64, frame: *Frame) !*DOMPoint {
var path = try self.buildPath(frame);
defer path.deinit(frame.local_arena);
const point = try path.pointAtLength(distance, frame.local_arena);
return DOMPoint.create(point.x, point.y, 0, 1, frame._page);
return DOMPoint.create(point.x, point.y, 0, 1, frame.page);
}
pub fn buildPath(self: *Geometry, frame: *Frame) !PathData.Path {
+1 -1
View File
@@ -264,7 +264,7 @@ fn currentTransformMatrix(self: *Graphics, space: enum { viewport, screen }, fra
matrix.c, matrix.d, 0, 0,
0, 0, 1, 0,
matrix.e, matrix.f, 0, 1,
}, true, frame._page);
}, true, frame.page);
}
fn transformMatrix(element: *Element) PathData.Matrix {
+2 -2
View File
@@ -87,7 +87,7 @@ fn getPreserveAspectRatio(self: *Svg, frame: *Frame) !*AnimatedPreserveAspectRat
}
fn createSVGPoint(_: *Svg, frame: *Frame) !*DOMPoint {
const point = try DOMPoint.create(0, 0, 0, 1, frame._page);
const point = try DOMPoint.create(0, 0, 0, 1, frame.page);
point._proto.restrict();
return point;
}
@@ -99,7 +99,7 @@ fn createSVGMatrix(_: *Svg, frame: *Frame) !*DOMMatrix {
0, 0, 1, 0,
0, 0, 0, 1,
};
return DOMMatrix.create(identity, true, frame._page);
return DOMMatrix.create(identity, true, frame.page);
}
fn createSVGRect(_: *Svg, frame: *Frame) !*DOMRect {
+2 -2
View File
@@ -32,7 +32,7 @@ pub fn TextEntry(comptime T: type) type {
pub fn select(self: *T, frame: *Frame) !void {
const len: u32 = @intCast(self.getValue().len);
try setSelectionRange(self, 0, len, null, frame);
const event = try Event.init("select", .{ .bubbles = true }, frame._page);
const event = try Event.init("select", .{ .bubbles = true }, frame.page);
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
}
@@ -290,7 +290,7 @@ pub fn TextEntry(comptime T: type) type {
}
fn dispatchSelectionChangeEvent(self: *T, frame: *Frame) !void {
const event = try Event.init("selectionchange", .{ .bubbles = true }, frame._page);
const event = try Event.init("selectionchange", .{ .bubbles = true }, frame.page);
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
}
@@ -85,7 +85,7 @@ pub fn getState(self: *const NavigationHistoryEntry, frame: *Frame) !StateReturn
pub fn fireDispose(self: *NavigationHistoryEntry, frame: *Frame) !void {
if (!frame.hasDirectListeners(self.asEventTarget(), "dispose", self._on_dispose)) return;
const event = try Event.initTrusted(comptime .wrap("dispose"), .{}, frame._page);
const event = try Event.initTrusted(comptime .wrap("dispose"), .{}, frame.page);
try frame.dispatch(self.asEventTarget(), event, self._on_dispose, .{ .context = "NavigationHistoryEntry" });
}
+9 -9
View File
@@ -1024,8 +1024,8 @@ test "FormData: multipart with file" {
const frame = try testing.createFrame();
defer testing.test_session.closeAllPages();
const file = try buildTestFile(allocator, frame._page, "hello.txt", "text/plain", "hello");
defer file._proto.releaseRef(frame._page);
const file = try buildTestFile(allocator, frame.page, "hello.txt", "text/plain", "hello");
defer file._proto.releaseRef(frame.page);
var fd = FormData{
._rc = .{},
@@ -1061,8 +1061,8 @@ test "FormData: multipart with empty file defaults to octet-stream" {
const frame = try testing.createFrame();
defer testing.test_session.closeAllPages();
const file = try buildTestFile(allocator, frame._page, "", "", "");
defer file._proto.releaseRef(frame._page);
const file = try buildTestFile(allocator, frame.page, "", "", "");
defer file._proto.releaseRef(frame.page);
var fd = FormData{
._rc = .{},
@@ -1094,8 +1094,8 @@ test "FormData: multipart escapes file name and filename" {
const frame = try testing.createFrame();
defer testing.test_session.closeAllPages();
const file = try buildTestFile(allocator, frame._page, "a\"b\r\nc.txt", "text/plain", "x");
defer file._proto.releaseRef(frame._page);
const file = try buildTestFile(allocator, frame.page, "a\"b\r\nc.txt", "text/plain", "x");
defer file._proto.releaseRef(frame.page);
var fd = FormData{
._rc = .{},
@@ -1127,8 +1127,8 @@ test "FormData: file entry collapses to filename in urlencode" {
const frame = try testing.createFrame();
defer testing.test_session.closeAllPages();
const file = try buildTestFile(allocator, frame._page, "hello.txt", "text/plain", "hello");
defer file._proto.releaseRef(frame._page);
const file = try buildTestFile(allocator, frame.page, "hello.txt", "text/plain", "hello");
defer file._proto.releaseRef(frame.page);
var fd = FormData{
._rc = .{},
@@ -1330,7 +1330,7 @@ test "FormData: multipart parse with file" {
"bytes\r\n" ++
"--B--\r\n", "B", &frame.js.execution);
defer for (fd._entries.items) |entry| switch (entry.value) {
.file => |file| file.releaseRef(frame._page),
.file => |file| file.releaseRef(frame.page),
else => {},
};
+23 -18
View File
@@ -62,6 +62,12 @@ pub fn open(_: *IDBFactory, name: []const u8, version: ?u64, exec: *Execution) !
return request;
}
const State = enum {
scheduled, // The scheduler's finalizer
parked, // cancelParked
running, // on the stack, will finalize when done
};
const OpenContext = struct {
request: *IDBRequest,
name: []const u8,
@@ -69,9 +75,7 @@ const OpenContext = struct {
exec: *Execution,
// Our node in the engine's connection gate wait-list. See Engine.acquireGate.
_gate_waiter: Engine.GateWaiter,
// Whether a scheduler task currently points at us; its finalizer owns our
// destruction then. When parked on the gate instead, cancelParked owns it.
_scheduled: bool = true,
_state: State = .scheduled,
// If an callback queued more requests, we need to process those requests
// on the next tick, and thus need to hold onto the transaction (which pins
@@ -90,9 +94,8 @@ const OpenContext = struct {
self.exec._factory.destroy(self);
}
// Engine.detach cancel: our context is going away while we sit on the
// gate. When parked there's no scheduler task, so we own our destruction;
// in the wake->run window the task finalizer does.
// Engine.detach cancel: our context is going away. Only a parked context
// owns its destruction here; otherwise a task finalizer or `run` does.
fn cancelParked(waiter: *Engine.GateWaiter) void {
const self: *OpenContext = @fieldParentPtr("_gate_waiter", waiter);
if (self._upgrade) |txn| {
@@ -103,14 +106,14 @@ const OpenContext = struct {
txn._settled = true;
return;
}
if (!self._scheduled) {
if (self._state == .parked) {
self.exec._factory.destroy(self);
}
}
fn run(ctx: *anyopaque) !?u32 {
const self: *OpenContext = @ptrCast(@alignCast(ctx));
self._scheduled = false;
self._state = .running;
if (self._upgrade != null) {
return self.drainUpgrade();
@@ -127,7 +130,8 @@ const OpenContext = struct {
// connection, so it must serialize with other transactions/opens. Park
// on the gate if it's held; wakeUp re-runs us when it's handed over.
if (!engine.acquireGate(&self._gate_waiter)) {
return null; // parked; not destroyed
self._state = .parked;
return null; // not destroyed
}
const upgrading = self.runOpen(engine) catch |err| blk: {
@@ -137,7 +141,7 @@ const OpenContext = struct {
break :blk false;
};
if (upgrading) {
self._scheduled = true;
self._state = .scheduled;
return 1; // the versionchange drain continues next turn; keep the gate
}
@@ -155,7 +159,7 @@ const OpenContext = struct {
if (txn.settleStep(self.exec) or txn.abortDeliveryPending()) {
// either settle succeeded, and we have more batches to deliver, or
// abortDeliveryPending succeeded and we have an abort event.
self._scheduled = true;
self._state = .scheduled;
return 1;
}
@@ -191,6 +195,7 @@ const OpenContext = struct {
// Scheduler wake-up: the connection gate was handed to us, so re-run.
fn wakeUp(waiter: *Engine.GateWaiter) void {
const self: *OpenContext = @fieldParentPtr("_gate_waiter", waiter);
self._state = .scheduled;
self.exec.js.scheduler.add(self, run, 0, .{
.name = "IDBFactory.open",
.finalizer = cancelled,
@@ -201,7 +206,6 @@ const OpenContext = struct {
if (self.resolveEngine()) |engine| _ = engine.releaseGate(&self._gate_waiter) else |_| {}
self.exec._factory.destroy(self);
};
self._scheduled = true;
}
fn resolveEngine(self: *OpenContext) !*Engine {
@@ -319,8 +323,8 @@ const DeleteContext = struct {
name: []const u8,
exec: *Execution,
_gate_waiter: Engine.GateWaiter,
// See OpenContext._scheduled.
_scheduled: bool = true,
// See OpenContext._state.
_state: State = .scheduled,
fn cancelled(ctx: *anyopaque) void {
// What if we're gated? Well, A scheduled task is only canceled on
@@ -332,14 +336,14 @@ const DeleteContext = struct {
// See OpenContext.cancelParked.
fn cancelParked(waiter: *Engine.GateWaiter) void {
const self: *DeleteContext = @fieldParentPtr("_gate_waiter", waiter);
if (!self._scheduled) {
if (self._state == .parked) {
self.exec._factory.destroy(self);
}
}
fn run(ctx: *anyopaque) !?u32 {
const self: *DeleteContext = @ptrCast(@alignCast(ctx));
self._scheduled = false;
self._state = .running;
const engine = self.resolveEngine() catch |err| {
self.exec._factory.destroy(self);
@@ -349,7 +353,8 @@ const DeleteContext = struct {
};
if (!engine.acquireGate(&self._gate_waiter)) {
return null; // parked; not destroyed
self._state = .parked;
return null; // not destroyed
}
defer self.exec._factory.destroy(self);
defer _ = engine.releaseGate(&self._gate_waiter);
@@ -365,6 +370,7 @@ const DeleteContext = struct {
// Scheduler wake-up: the connection gate was handed to us, so re-run.
fn wakeUp(waiter: *Engine.GateWaiter) void {
const self: *DeleteContext = @fieldParentPtr("_gate_waiter", waiter);
self._state = .scheduled;
self.exec.js.scheduler.add(self, run, 0, .{
.name = "IDBFactory.deleteDatabase",
.finalizer = cancelled,
@@ -375,7 +381,6 @@ const DeleteContext = struct {
if (self.resolveEngine()) |engine| _ = engine.releaseGate(&self._gate_waiter) else |_| {}
self.exec._factory.destroy(self);
};
self._scheduled = true;
}
fn resolveEngine(self: *DeleteContext) !*Engine {
+1 -1
View File
@@ -46,7 +46,7 @@ const Unit = enum(u16) {
};
pub fn detached(frame: *Frame) !*Angle {
const arena = try frame._page.getArena(.tiny, "SVGAngle");
const arena = try frame.page.getArena(.tiny, "SVGAngle");
errdefer arena.release();
const self = try arena.create(Angle);
self.* = .{ ._arena = arena };
@@ -124,9 +124,10 @@ pub const Lookup = std.AutoHashMapUnmanaged(Key, *AnimatedEnumeration);
pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*AnimatedEnumeration {
const key: Key = .{ .element = element, .kind = kind };
const gop = try frame._svg_animated_enumerations.getOrPut(frame.arena, key);
const page = frame.page;
const gop = try page.svg_animated_enumerations.getOrPut(page.frame_arena, key);
if (!gop.found_existing) {
errdefer _ = frame._svg_animated_enumerations.remove(key);
errdefer _ = page.svg_animated_enumerations.remove(key);
gop.value_ptr.* = try create(
element,
kind.attributeName(),
+3 -2
View File
@@ -193,9 +193,10 @@ pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*AnimatedLengt
.element = element,
.kind = kind,
};
const gop = try frame._svg_animated_lengths.getOrPut(frame.arena, key);
const page = frame.page;
const gop = try page.svg_animated_lengths.getOrPut(page.frame_arena, key);
if (!gop.found_existing) {
errdefer _ = frame._svg_animated_lengths.remove(key);
errdefer _ = page.svg_animated_lengths.remove(key);
gop.value_ptr.* = try createConfigured(
element,
kind.attributeName(),
+3 -2
View File
@@ -57,9 +57,10 @@ pub const Lookup = std.AutoHashMapUnmanaged(Key, *AnimatedNumber);
pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*AnimatedNumber {
const key: Key = .{ .element = element, .kind = kind };
const gop = try frame._svg_animated_numbers.getOrPut(frame.arena, key);
const page = frame.page;
const gop = try page.svg_animated_numbers.getOrPut(page.frame_arena, key);
if (!gop.found_existing) {
errdefer _ = frame._svg_animated_numbers.remove(key);
errdefer _ = page.svg_animated_numbers.remove(key);
gop.value_ptr.* = try frame._factory.create(AnimatedNumber{
._element = element,
._attr_name = kind.attributeName(),
Loaded 100 of 127 files, more files were not shown because too many files have changed in this diff. Show more