mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-09-17 08:27:11 -04:00
Merge branch 'main' into stylesheet-scroll-container
This commit is contained in:
commit
2cc537fc8c
127 files changed
+6163
-4557
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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
@@ -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 && \
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
+743
-863
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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).
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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>
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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, .{}),
|
||||
|
||||
@@ -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 = .{};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" });
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.*;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
|
||||
@@ -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 => {},
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
Reference in new issue
Block a user