mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-29 16:10:04 +00:00
Compare commits
221 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77aa2241dc | ||
|
|
0766d08479 | ||
|
|
f6ed0d43a2 | ||
|
|
c8413cb029 | ||
|
|
97d53b81a7 | ||
|
|
ab888f5cd0 | ||
|
|
f54246eac1 | ||
|
|
7de9422b75 | ||
|
|
f02a37d3f0 | ||
|
|
28815a0ae6 | ||
|
|
70c7dfd0f4 | ||
|
|
9c2ebd308b | ||
|
|
11d8412591 | ||
|
|
32ca170c4d | ||
|
|
388ed08b0e | ||
|
|
b408f88b8c | ||
|
|
09087401b4 | ||
|
|
c68692d78e | ||
|
|
ee2a4d0a5d | ||
|
|
a15885fe80 | ||
|
|
24111570cf | ||
|
|
ded203b1c1 | ||
|
|
e43fc98c0d | ||
|
|
1efd13545e | ||
|
|
1193ee1ab9 | ||
|
|
a6ba801738 | ||
|
|
e7958f2910 | ||
|
|
cbac9a7703 | ||
|
|
60d8f2323e | ||
|
|
70ae6b8d72 | ||
|
|
e1850440b0 | ||
|
|
d5c2aaeea3 | ||
|
|
a06b7acc85 | ||
|
|
615168423a | ||
|
|
73abf7d20e | ||
|
|
fcea42e91e | ||
|
|
14f7574c41 | ||
|
|
8f15ded650 | ||
|
|
4aec4ef80a | ||
|
|
ecb8f1de30 | ||
|
|
4c28180125 | ||
|
|
4138180f43 | ||
|
|
0d508a88f6 | ||
|
|
7c8fcf73f6 | ||
|
|
5904d72776 | ||
|
|
5e32ccbf12 | ||
|
|
6ce136bede | ||
|
|
b9f8ce5729 | ||
|
|
115530a104 | ||
|
|
65c9b2a5f7 | ||
|
|
46c73a05a9 | ||
|
|
c5f7e72ca8 | ||
|
|
e6fb63ddba | ||
|
|
e2645e4126 | ||
|
|
36d267ca40 | ||
|
|
2e5d04389b | ||
|
|
6130ed17a6 | ||
|
|
c4e82407ec | ||
|
|
2e28e68c48 | ||
|
|
a7882fa32b | ||
|
|
82f9e70406 | ||
|
|
3fa27c7ffa | ||
|
|
1aa50dee20 | ||
|
|
f58f7257be | ||
|
|
e17f1269a2 | ||
|
|
82f48b84b3 | ||
|
|
926bd20281 | ||
|
|
a6cd019118 | ||
|
|
bbfc476d7e | ||
|
|
8d49515a3c | ||
|
|
0a410a5544 | ||
|
|
eef203633b | ||
|
|
122255058e | ||
|
|
b1681b2213 | ||
|
|
73d79f55d8 | ||
|
|
a02fcd97d6 | ||
|
|
016708b338 | ||
|
|
9f26fc28c4 | ||
|
|
7c1b354fc3 | ||
|
|
abeda1935d | ||
|
|
403ee9ff9e | ||
|
|
cccb45fe13 | ||
|
|
8d43becb27 | ||
|
|
ee22e07fff | ||
|
|
37464e2d95 | ||
|
|
5abcecbc9b | ||
|
|
cecdf0d511 | ||
|
|
a451fe4248 | ||
|
|
d6f801f764 | ||
|
|
a35e772a6b | ||
|
|
aca3fae6b1 | ||
|
|
17891f0209 | ||
|
|
aea2b3c8e5 | ||
|
|
57fb167a9c | ||
|
|
0406bba384 | ||
|
|
7c4c80fe4a | ||
|
|
bfb267e164 | ||
|
|
a0720948a1 | ||
|
|
9f00159a84 | ||
|
|
34067a1d70 | ||
|
|
3f6917fdcb | ||
|
|
c04a6e501e | ||
|
|
661b564399 | ||
|
|
761c103373 | ||
|
|
f4bd9e3d24 | ||
|
|
b9ddac878c | ||
|
|
f304ce5ccf | ||
|
|
828401f057 | ||
|
|
445d77a220 | ||
|
|
4d768bb5eb | ||
|
|
4e3b87d338 | ||
|
|
00740b6117 | ||
|
|
7775f203fc | ||
|
|
945af879ec | ||
|
|
b2506f0afe | ||
|
|
2eab4b84c9 | ||
|
|
7746d9968d | ||
|
|
da49d918d6 | ||
|
|
804ed758c9 | ||
|
|
17aac58e08 | ||
|
|
a7095d7dec | ||
|
|
3afbb6fcc2 | ||
|
|
8ecbd8e71c | ||
|
|
988f499723 | ||
|
|
50aeb9ff21 | ||
|
|
e620c28a1c | ||
|
|
29ee7d41f5 | ||
|
|
f9104c71f6 | ||
|
|
b6af5884b1 | ||
|
|
e4f250435d | ||
|
|
1a246f2e38 | ||
|
|
48ebc46c5f | ||
|
|
e27803038c | ||
|
|
babf8ba3e7 | ||
|
|
6ccd3f277b | ||
|
|
9d6f9aae9a | ||
|
|
95a000c279 | ||
|
|
b19debff14 | ||
|
|
39c9024747 | ||
|
|
3c660f2cb0 | ||
|
|
13dbdc7dc7 | ||
|
|
f903e4b2de | ||
|
|
b96cb2142b | ||
|
|
cc51cd4476 | ||
|
|
8a995fc515 | ||
|
|
078eccea2d | ||
|
|
190119bcd4 | ||
|
|
7672b42fbc | ||
|
|
c590658f16 | ||
|
|
017d4e792b | ||
|
|
0671be870d | ||
|
|
2f9ed37db2 | ||
|
|
2cf2db3eef | ||
|
|
11ad025e5d | ||
|
|
630cf05b2f | ||
|
|
a72782f91e | ||
|
|
fbd554a15f | ||
|
|
f71aa1cad2 | ||
|
|
6d9517f6ea | ||
|
|
fd8c488dbd | ||
|
|
dbf18b90a7 | ||
|
|
d318fe24b8 | ||
|
|
1352315441 | ||
|
|
3c635532c4 | ||
|
|
f8703bf884 | ||
|
|
eea3aa7a27 | ||
|
|
6eff448508 | ||
|
|
eb8cac5980 | ||
|
|
1a4086c98c | ||
|
|
5c91076660 | ||
|
|
5467b8dd0d | ||
|
|
d46a9d6286 | ||
|
|
2fa7810128 | ||
|
|
8249725ae7 | ||
|
|
c07b83335b | ||
|
|
7e575c501a | ||
|
|
933e2fb0ef | ||
|
|
8d51383fb2 | ||
|
|
80f4c83b83 | ||
|
|
0d739e4af7 | ||
|
|
58f9027002 | ||
|
|
990f2e2892 | ||
|
|
ce7989c171 | ||
|
|
4efb0229d4 | ||
|
|
5dd6dc2d69 | ||
|
|
20931eb9d6 | ||
|
|
c11fa122af | ||
|
|
e9141c8300 | ||
|
|
1d03b688d9 | ||
|
|
176d42f625 | ||
|
|
7c98a27c53 | ||
|
|
020b30783e | ||
|
|
fafbdb0714 | ||
|
|
466cdb4ee7 | ||
|
|
fa66f0b509 | ||
|
|
12a566c07e | ||
|
|
bf7a1c6b1f | ||
|
|
55891aa5f8 | ||
|
|
7c0acd9fcb | ||
|
|
333f1e2c47 | ||
|
|
9d30cdfefc | ||
|
|
324f6fe16e | ||
|
|
5d96304332 | ||
|
|
e6e32b5fd2 | ||
|
|
181f265de5 | ||
|
|
e5fc8bb27c | ||
|
|
34dda780d9 | ||
|
|
c7cf4eeb7a | ||
|
|
a6e5d9f6dc | ||
|
|
ea1017584e | ||
|
|
6aef32d7a8 | ||
|
|
4a1d71b6b8 | ||
|
|
a18b61cb1d | ||
|
|
e31e19aeba | ||
|
|
ef6d8a6554 | ||
|
|
100764d79e | ||
|
|
75abe7da1b | ||
|
|
a19a125aec | ||
|
|
c84106570f | ||
|
|
1a05da9e55 | ||
|
|
8e8ffd21d5 |
12
.github/actions/install/action.yml
vendored
12
.github/actions/install/action.yml
vendored
@@ -13,7 +13,7 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig v8 version to install'
|
||||
required: false
|
||||
default: 'v0.2.6'
|
||||
default: 'v0.2.8'
|
||||
v8:
|
||||
description: 'v8 version to install'
|
||||
required: false
|
||||
@@ -22,6 +22,10 @@ inputs:
|
||||
description: 'cache dir to use'
|
||||
required: false
|
||||
default: '~/.cache'
|
||||
debug:
|
||||
description: 'enable v8 pre-built debug version, only available for linux x86_64'
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -47,17 +51,17 @@ runs:
|
||||
cache-name: cache-v8
|
||||
with:
|
||||
path: ${{ inputs.cache-dir }}/v8
|
||||
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}_${{ inputs.zig-v8 }}.a
|
||||
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}_${{ inputs.zig-v8 }}${{inputs.debug == 'true' && '_debug' || '' }}.a
|
||||
|
||||
- if: ${{ steps.cache-v8.outputs.cache-hit != 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p ${{ inputs.cache-dir }}/v8
|
||||
|
||||
wget -O ${{ inputs.cache-dir }}/v8/libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${{ inputs.zig-v8 }}/libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}.a
|
||||
wget -O ${{ inputs.cache-dir }}/v8/libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${{ inputs.zig-v8 }}/libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}${{inputs.debug == 'true' && '_debug' || '' }}.a
|
||||
|
||||
- name: install v8
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p v8
|
||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/libc_v8.a
|
||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/libc_v8${{inputs.debug == 'true' && '_debug' || '' }}.a
|
||||
|
||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -40,7 +40,6 @@ jobs:
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
mode: 'release'
|
||||
|
||||
- name: v8 snapshot
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||
@@ -83,7 +82,6 @@ jobs:
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
mode: 'release'
|
||||
|
||||
- name: v8 snapshot
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||
@@ -128,7 +126,6 @@ jobs:
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
mode: 'release'
|
||||
|
||||
- name: v8 snapshot
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||
@@ -171,7 +168,6 @@ jobs:
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
mode: 'release'
|
||||
|
||||
- name: v8 snapshot
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||
|
||||
2
.github/workflows/e2e-test.yml
vendored
2
.github/workflows/e2e-test.yml
vendored
@@ -56,8 +56,6 @@ jobs:
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/install
|
||||
with:
|
||||
mode: 'release'
|
||||
|
||||
- name: zig build release
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
|
||||
23
.github/workflows/zig-test.yml
vendored
23
.github/workflows/zig-test.yml
vendored
@@ -12,8 +12,7 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- "build.zig"
|
||||
- "src/**/*.zig"
|
||||
- "src/*.zig"
|
||||
- "src/**"
|
||||
- "vendor/zig-js-runtime"
|
||||
- ".github/**"
|
||||
- "vendor/**"
|
||||
@@ -38,6 +37,26 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
zig-test-debug:
|
||||
name: zig test using v8 in debug mode
|
||||
timeout-minutes: 15
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/install
|
||||
with:
|
||||
debug: true
|
||||
|
||||
- name: zig build test
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8_debug.a -Dtsan=true test
|
||||
|
||||
zig-test:
|
||||
name: zig test
|
||||
timeout-minutes: 15
|
||||
|
||||
@@ -3,7 +3,7 @@ FROM debian:stable-slim
|
||||
ARG MINISIG=0.12
|
||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||
ARG V8=14.0.365.4
|
||||
ARG ZIG_V8=v0.2.6
|
||||
ARG ZIG_V8=v0.2.8
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
RUN apt-get update -yq && \
|
||||
|
||||
43
README.md
43
README.md
@@ -78,23 +78,49 @@ docker run -d --name lightpanda -p 9222:9222 lightpanda/browser:nightly
|
||||
### Dump a URL
|
||||
|
||||
```console
|
||||
./lightpanda fetch --dump https://lightpanda.io
|
||||
./lightpanda fetch --obey_robots --log_format pretty --log_level info https://demo-browser.lightpanda.io/campfire-commerce/
|
||||
```
|
||||
```console
|
||||
info(browser): GET https://lightpanda.io/ http.Status.ok
|
||||
info(browser): fetch script https://api.website.lightpanda.io/js/script.js: http.Status.ok
|
||||
info(browser): eval remote https://api.website.lightpanda.io/js/script.js: TypeError: Cannot read properties of undefined (reading 'pushState')
|
||||
INFO telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
||||
disabled = false
|
||||
|
||||
INFO page : navigate . . . . . . . . . . . . . . . . . . . . [+6ms]
|
||||
url = https://demo-browser.lightpanda.io/campfire-commerce/
|
||||
method = GET
|
||||
reason = address_bar
|
||||
body = false
|
||||
req_id = 1
|
||||
|
||||
INFO browser : executing script . . . . . . . . . . . . . . [+118ms]
|
||||
src = https://demo-browser.lightpanda.io/campfire-commerce/script.js
|
||||
kind = javascript
|
||||
cacheable = true
|
||||
|
||||
INFO http : request complete . . . . . . . . . . . . . . . . [+140ms]
|
||||
source = xhr
|
||||
url = https://demo-browser.lightpanda.io/campfire-commerce/json/product.json
|
||||
status = 200
|
||||
len = 4770
|
||||
|
||||
INFO http : request complete . . . . . . . . . . . . . . . . [+141ms]
|
||||
source = fetch
|
||||
url = https://demo-browser.lightpanda.io/campfire-commerce/json/reviews.json
|
||||
status = 200
|
||||
len = 1615
|
||||
<!DOCTYPE html>
|
||||
```
|
||||
|
||||
### Start a CDP server
|
||||
|
||||
```console
|
||||
./lightpanda serve --host 127.0.0.1 --port 9222
|
||||
./lightpanda serve --obey_robots --log_format pretty --log_level info --host 127.0.0.1 --port 9222
|
||||
```
|
||||
```console
|
||||
info(websocket): starting blocking worker to listen on 127.0.0.1:9222
|
||||
info(server): accepting new conn...
|
||||
INFO telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
||||
disabled = false
|
||||
|
||||
INFO app : server running . . . . . . . . . . . . . . . . . [+0ms]
|
||||
address = 127.0.0.1:9222
|
||||
```
|
||||
|
||||
Once the CDP server started, you can run a Puppeteer script by configuring the
|
||||
@@ -115,7 +141,7 @@ const context = await browser.createBrowserContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
// Dump all the links from the page.
|
||||
await page.goto('https://wikipedia.com/');
|
||||
await page.goto('https://demo-browser.lightpanda.io/amiibo/', {waitUntil: "networkidle0"});
|
||||
|
||||
const links = await page.evaluate(() => {
|
||||
return Array.from(document.querySelectorAll('a')).map(row => {
|
||||
@@ -156,6 +182,7 @@ Here are the key features we have implemented:
|
||||
- [x] Custom HTTP headers
|
||||
- [x] Proxy support
|
||||
- [x] Network interception
|
||||
- [x] Respect `robots.txt` with option `--obey_robots`
|
||||
|
||||
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
||||
|
||||
|
||||
19
build.zig
19
build.zig
@@ -35,7 +35,8 @@ pub fn build(b: *Build) !void {
|
||||
opts.addOption([]const u8, "git_commit", git_commit orelse "dev");
|
||||
opts.addOption(?[]const u8, "snapshot_path", snapshot_path);
|
||||
|
||||
const enable_tsan = b.option(bool, "tsan", "Enable Thread Sanitizer");
|
||||
const enable_tsan = b.option(bool, "tsan", "Enable Thread Sanitizer") orelse false;
|
||||
const enable_asan = b.option(bool, "asan", "Enable Address Sanitizer") orelse false;
|
||||
const enable_csan = b.option(std.zig.SanitizeC, "csan", "Enable C Sanitizers");
|
||||
|
||||
const lightpanda_module = blk: {
|
||||
@@ -50,7 +51,7 @@ pub fn build(b: *Build) !void {
|
||||
});
|
||||
mod.addImport("lightpanda", mod); // allow circular "lightpanda" import
|
||||
|
||||
try addDependencies(b, mod, opts, prebuilt_v8_path);
|
||||
try addDependencies(b, mod, opts, enable_asan, enable_tsan, prebuilt_v8_path);
|
||||
|
||||
break :blk mod;
|
||||
};
|
||||
@@ -170,15 +171,25 @@ pub fn build(b: *Build) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, prebuilt_v8_path: ?[]const u8) !void {
|
||||
fn addDependencies(
|
||||
b: *Build,
|
||||
mod: *Build.Module,
|
||||
opts: *Build.Step.Options,
|
||||
is_asan: bool,
|
||||
is_tsan: bool,
|
||||
prebuilt_v8_path: ?[]const u8,
|
||||
) !void {
|
||||
mod.addImport("build_config", opts.createModule());
|
||||
|
||||
const target = mod.resolved_target.?;
|
||||
const dep_opts = .{
|
||||
.target = target,
|
||||
.optimize = mod.optimize.?,
|
||||
.prebuilt_v8_path = prebuilt_v8_path,
|
||||
.cache_root = b.pathFromRoot(".lp-cache"),
|
||||
.prebuilt_v8_path = prebuilt_v8_path,
|
||||
.is_asan = is_asan,
|
||||
.is_tsan = is_tsan,
|
||||
.v8_enable_sandbox = is_tsan,
|
||||
};
|
||||
|
||||
mod.addIncludePath(b.path("vendor/lightpanda"));
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.dependencies = .{
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/v0.2.6.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH60NRBAAWmpZq9nWdfFAEqVJ9zqJnvr1Nl9m2AbcY",
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.2.8.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH63lfBADJ7UE2zAJ8nJIBnxoSiimXSaX6Q_M_7DS3",
|
||||
},
|
||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||
.@"boringssl-zig" = .{
|
||||
|
||||
24
flake.lock
generated
24
flake.lock
generated
@@ -8,11 +8,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1763016383,
|
||||
"narHash": "sha256-eYmo7FNvm3q08iROzwIi8i9dWuUbJJl3uLR3OLnSmdI=",
|
||||
"lastModified": 1770708269,
|
||||
"narHash": "sha256-OnZW86app7hHJJoB5lC9GNXY5QBBIESJB+sIdwEyld0=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "0fad5c0e5c531358e7174cd666af4608f08bc3ba",
|
||||
"rev": "6b5325a017a9a9fe7e6252ccac3680cc7181cd63",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -96,11 +96,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1763043403,
|
||||
"narHash": "sha256-DgCTbHdIpzbXSlQlOZEWj8oPt2lrRMlSk03oIstvkVQ=",
|
||||
"lastModified": 1768649915,
|
||||
"narHash": "sha256-jc21hKogFnxU7KXSVTRmxC7u5D4RHwm9BAvDf5/Z1Uo=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "75e04ecd084f93d4105ce68c07dac7656291fe2e",
|
||||
"rev": "3e3f3c7f9977dc123c23ee21e8085ed63daf8c37",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -122,11 +122,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1762860488,
|
||||
"narHash": "sha256-rMfWMCOo/pPefM2We0iMBLi2kLBAnYoB9thi4qS7uk4=",
|
||||
"lastModified": 1770668050,
|
||||
"narHash": "sha256-Q05yaIZtQrBKHpyWaPmyJmDRj0lojnVf8nUFE0vydcY=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "2efc80078029894eec0699f62ec8d5c1a56af763",
|
||||
"rev": "9efc1f709f3c8134c3acac5d3592a8e4c184a0c6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -175,11 +175,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1762907712,
|
||||
"narHash": "sha256-VNW/+VYIg6N4b9Iq+F0YZmm22n74IdFS7hsPLblWuOY=",
|
||||
"lastModified": 1770598090,
|
||||
"narHash": "sha256-k+82IDgTd9o5sxHIqGlvfwseKln3Ejx1edGtDltuPXo=",
|
||||
"owner": "mitchellh",
|
||||
"repo": "zig-overlay",
|
||||
"rev": "d16453ee78765e49527c56d23386cead799b6b53",
|
||||
"rev": "142495696982c88edddc8e17e4da90d8164acadf",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
52
src/App.zig
52
src/App.zig
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -21,68 +21,38 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("log.zig");
|
||||
const Config = @import("Config.zig");
|
||||
const Snapshot = @import("browser/js/Snapshot.zig");
|
||||
const Platform = @import("browser/js/Platform.zig");
|
||||
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
||||
const RobotStore = @import("browser/Robots.zig").RobotStore;
|
||||
|
||||
pub const Http = @import("http/Http.zig");
|
||||
pub const ArenaPool = @import("ArenaPool.zig");
|
||||
pub const Notification = @import("Notification.zig");
|
||||
|
||||
// Container for global state / objects that various parts of the system
|
||||
// might need.
|
||||
const App = @This();
|
||||
|
||||
http: Http,
|
||||
config: Config,
|
||||
config: *const Config,
|
||||
platform: Platform,
|
||||
snapshot: Snapshot,
|
||||
telemetry: Telemetry,
|
||||
allocator: Allocator,
|
||||
arena_pool: ArenaPool,
|
||||
robots: RobotStore,
|
||||
app_dir_path: ?[]const u8,
|
||||
notification: *Notification,
|
||||
shutdown: bool = false,
|
||||
|
||||
pub const RunMode = enum {
|
||||
help,
|
||||
fetch,
|
||||
serve,
|
||||
version,
|
||||
};
|
||||
|
||||
pub const Config = struct {
|
||||
run_mode: RunMode,
|
||||
tls_verify_host: bool = true,
|
||||
http_proxy: ?[:0]const u8 = null,
|
||||
proxy_bearer_token: ?[:0]const u8 = null,
|
||||
http_timeout_ms: ?u31 = null,
|
||||
http_connect_timeout_ms: ?u31 = null,
|
||||
http_max_host_open: ?u8 = null,
|
||||
http_max_concurrent: ?u8 = null,
|
||||
user_agent: [:0]const u8,
|
||||
};
|
||||
|
||||
pub fn init(allocator: Allocator, config: Config) !*App {
|
||||
pub fn init(allocator: Allocator, config: *const Config) !*App {
|
||||
const app = try allocator.create(App);
|
||||
errdefer allocator.destroy(app);
|
||||
|
||||
app.config = config;
|
||||
app.allocator = allocator;
|
||||
|
||||
app.notification = try Notification.init(allocator, null);
|
||||
errdefer app.notification.deinit();
|
||||
app.robots = RobotStore.init(allocator);
|
||||
|
||||
app.http = try Http.init(allocator, .{
|
||||
.max_host_open = config.http_max_host_open orelse 4,
|
||||
.max_concurrent = config.http_max_concurrent orelse 10,
|
||||
.timeout_ms = config.http_timeout_ms orelse 5000,
|
||||
.connect_timeout_ms = config.http_connect_timeout_ms orelse 0,
|
||||
.http_proxy = config.http_proxy,
|
||||
.tls_verify_host = config.tls_verify_host,
|
||||
.proxy_bearer_token = config.proxy_bearer_token,
|
||||
.user_agent = config.user_agent,
|
||||
});
|
||||
app.http = try Http.init(allocator, &app.robots, config);
|
||||
errdefer app.http.deinit();
|
||||
|
||||
app.platform = try Platform.init();
|
||||
@@ -93,11 +63,9 @@ pub fn init(allocator: Allocator, config: Config) !*App {
|
||||
|
||||
app.app_dir_path = getAndMakeAppDir(allocator);
|
||||
|
||||
app.telemetry = try Telemetry.init(app, config.run_mode);
|
||||
app.telemetry = try Telemetry.init(app, config.mode);
|
||||
errdefer app.telemetry.deinit();
|
||||
|
||||
try app.telemetry.register(app.notification);
|
||||
|
||||
app.arena_pool = ArenaPool.init(allocator);
|
||||
errdefer app.arena_pool.deinit();
|
||||
|
||||
@@ -115,7 +83,7 @@ pub fn deinit(self: *App) void {
|
||||
self.app_dir_path = null;
|
||||
}
|
||||
self.telemetry.deinit();
|
||||
self.notification.deinit();
|
||||
self.robots.deinit();
|
||||
self.http.deinit();
|
||||
self.snapshot.deinit();
|
||||
self.platform.deinit();
|
||||
|
||||
@@ -56,6 +56,7 @@ pub fn deinit(self: *ArenaPool) void {
|
||||
pub fn acquire(self: *ArenaPool) !Allocator {
|
||||
if (self.free_list) |entry| {
|
||||
self.free_list = entry.next;
|
||||
self.free_list_len -= 1;
|
||||
return entry.arena.allocator();
|
||||
}
|
||||
|
||||
@@ -72,7 +73,8 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void {
|
||||
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
||||
const entry: *Entry = @fieldParentPtr("arena", arena);
|
||||
|
||||
if (self.free_list_len == self.free_list_max) {
|
||||
const free_list_len = self.free_list_len;
|
||||
if (free_list_len == self.free_list_max) {
|
||||
arena.deinit();
|
||||
self.entry_pool.destroy(entry);
|
||||
return;
|
||||
@@ -80,5 +82,6 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void {
|
||||
|
||||
_ = arena.reset(.{ .retain_with_limit = self.retain_bytes });
|
||||
entry.next = self.free_list;
|
||||
self.free_list_len = free_list_len + 1;
|
||||
self.free_list = entry;
|
||||
}
|
||||
|
||||
800
src/Config.zig
Normal file
800
src/Config.zig
Normal file
@@ -0,0 +1,800 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("log.zig");
|
||||
const dump = @import("browser/dump.zig");
|
||||
|
||||
pub const RunMode = enum {
|
||||
help,
|
||||
fetch,
|
||||
serve,
|
||||
version,
|
||||
};
|
||||
|
||||
mode: Mode,
|
||||
exec_name: []const u8,
|
||||
http_headers: HttpHeaders,
|
||||
|
||||
const Config = @This();
|
||||
|
||||
pub fn init(allocator: Allocator, exec_name: []const u8, mode: Mode) !Config {
|
||||
var config = Config{
|
||||
.mode = mode,
|
||||
.exec_name = exec_name,
|
||||
.http_headers = undefined,
|
||||
};
|
||||
config.http_headers = try HttpHeaders.init(allocator, &config);
|
||||
return config;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Config, allocator: Allocator) void {
|
||||
self.http_headers.deinit(allocator);
|
||||
}
|
||||
|
||||
pub fn tlsVerifyHost(self: *const Config) bool {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.tls_verify_host,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn obeyRobots(self: *const Config) bool {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.obey_robots,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn httpProxy(self: *const Config) ?[:0]const u8 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.http_proxy,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn proxyBearerToken(self: *const Config) ?[:0]const u8 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.proxy_bearer_token,
|
||||
.help, .version => null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn httpMaxConcurrent(self: *const Config) u8 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.http_max_concurrent orelse 10,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn httpMaxHostOpen(self: *const Config) u8 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.http_max_host_open orelse 4,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn httpConnectTimeout(self: *const Config) u31 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.http_connect_timeout orelse 0,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn httpTimeout(self: *const Config) u31 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.http_timeout orelse 5000,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn httpMaxRedirects(_: *const Config) u8 {
|
||||
return 10;
|
||||
}
|
||||
|
||||
pub fn httpMaxResponseSize(self: *const Config) ?usize {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.http_max_response_size,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn logLevel(self: *const Config) ?log.Level {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.log_level,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn logFormat(self: *const Config) ?log.Format {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.log_format,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn logFilterScopes(self: *const Config) ?[]const log.Scope {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.log_filter_scopes,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn userAgentSuffix(self: *const Config) ?[]const u8 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.user_agent_suffix,
|
||||
.help, .version => null,
|
||||
};
|
||||
}
|
||||
|
||||
pub const Mode = union(RunMode) {
|
||||
help: bool, // false when being printed because of an error
|
||||
fetch: Fetch,
|
||||
serve: Serve,
|
||||
version: void,
|
||||
};
|
||||
|
||||
pub const Serve = struct {
|
||||
host: []const u8 = "127.0.0.1",
|
||||
port: u16 = 9222,
|
||||
timeout: u31 = 10,
|
||||
max_connections: u16 = 16,
|
||||
max_tabs_per_connection: u16 = 8,
|
||||
max_memory_per_tab: u64 = 512 * 1024 * 1024,
|
||||
max_pending_connections: u16 = 128,
|
||||
common: Common = .{},
|
||||
};
|
||||
|
||||
pub const Fetch = struct {
|
||||
url: [:0]const u8,
|
||||
dump: bool = false,
|
||||
common: Common = .{},
|
||||
withbase: bool = false,
|
||||
strip: dump.Opts.Strip = .{},
|
||||
};
|
||||
|
||||
pub const Common = struct {
|
||||
obey_robots: bool = false,
|
||||
proxy_bearer_token: ?[:0]const u8 = null,
|
||||
http_proxy: ?[:0]const u8 = null,
|
||||
http_max_concurrent: ?u8 = null,
|
||||
http_max_host_open: ?u8 = null,
|
||||
http_timeout: ?u31 = null,
|
||||
http_connect_timeout: ?u31 = null,
|
||||
http_max_response_size: ?usize = null,
|
||||
tls_verify_host: bool = true,
|
||||
log_level: ?log.Level = null,
|
||||
log_format: ?log.Format = null,
|
||||
log_filter_scopes: ?[]log.Scope = null,
|
||||
user_agent_suffix: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
/// Pre-formatted HTTP headers for reuse across Http and Client.
|
||||
/// Must be initialized with an allocator that outlives all HTTP connections.
|
||||
pub const HttpHeaders = struct {
|
||||
const user_agent_base: [:0]const u8 = "Lightpanda/1.0";
|
||||
|
||||
user_agent: [:0]const u8, // User agent value (e.g. "Lightpanda/1.0")
|
||||
user_agent_header: [:0]const u8,
|
||||
|
||||
proxy_bearer_header: ?[:0]const u8,
|
||||
|
||||
pub fn init(allocator: Allocator, config: *const Config) !HttpHeaders {
|
||||
const user_agent: [:0]const u8 = if (config.userAgentSuffix()) |suffix|
|
||||
try std.fmt.allocPrintSentinel(allocator, "{s} {s}", .{ user_agent_base, suffix }, 0)
|
||||
else
|
||||
user_agent_base;
|
||||
errdefer if (config.userAgentSuffix() != null) allocator.free(user_agent);
|
||||
|
||||
const user_agent_header = try std.fmt.allocPrintSentinel(allocator, "User-Agent: {s}", .{user_agent}, 0);
|
||||
errdefer allocator.free(user_agent_header);
|
||||
|
||||
const proxy_bearer_header: ?[:0]const u8 = if (config.proxyBearerToken()) |token|
|
||||
try std.fmt.allocPrintSentinel(allocator, "Proxy-Authorization: Bearer {s}", .{token}, 0)
|
||||
else
|
||||
null;
|
||||
|
||||
return .{
|
||||
.user_agent = user_agent,
|
||||
.user_agent_header = user_agent_header,
|
||||
.proxy_bearer_header = proxy_bearer_header,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const HttpHeaders, allocator: Allocator) void {
|
||||
if (self.proxy_bearer_header) |hdr| {
|
||||
allocator.free(hdr);
|
||||
}
|
||||
allocator.free(self.user_agent_header);
|
||||
if (self.user_agent.ptr != user_agent_base.ptr) {
|
||||
allocator.free(self.user_agent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
||||
// MAX_HELP_LEN|
|
||||
const common_options =
|
||||
\\
|
||||
\\--insecure_disable_tls_host_verification
|
||||
\\ Disables host verification on all HTTP requests. This is an
|
||||
\\ advanced option which should only be set if you understand
|
||||
\\ and accept the risk of disabling host verification.
|
||||
\\
|
||||
\\--obey_robots
|
||||
\\ Fetches and obeys the robots.txt (if available) of the web pages
|
||||
\\ we make requests towards.
|
||||
\\ Defaults to false.
|
||||
\\
|
||||
\\--http_proxy The HTTP proxy to use for all HTTP requests.
|
||||
\\ A username:password can be included for basic authentication.
|
||||
\\ Defaults to none.
|
||||
\\
|
||||
\\--proxy_bearer_token
|
||||
\\ The <token> to send for bearer authentication with the proxy
|
||||
\\ Proxy-Authorization: Bearer <token>
|
||||
\\
|
||||
\\--http_max_concurrent
|
||||
\\ The maximum number of concurrent HTTP requests.
|
||||
\\ Defaults to 10.
|
||||
\\
|
||||
\\--http_max_host_open
|
||||
\\ The maximum number of open connection to a given host:port.
|
||||
\\ Defaults to 4.
|
||||
\\
|
||||
\\--http_connect_timeout
|
||||
\\ The time, in milliseconds, for establishing an HTTP connection
|
||||
\\ before timing out. 0 means it never times out.
|
||||
\\ Defaults to 0.
|
||||
\\
|
||||
\\--http_timeout
|
||||
\\ The maximum time, in milliseconds, the transfer is allowed
|
||||
\\ to complete. 0 means it never times out.
|
||||
\\ Defaults to 10000.
|
||||
\\
|
||||
\\--http_max_response_size
|
||||
\\ Limits the acceptable response size for any request
|
||||
\\ (e.g. XHR, fetch, script loading, ...).
|
||||
\\ Defaults to no limit.
|
||||
\\
|
||||
\\--log_level The log level: debug, info, warn, error or fatal.
|
||||
\\ Defaults to
|
||||
++ (if (builtin.mode == .Debug) " info." else "warn.") ++
|
||||
\\
|
||||
\\
|
||||
\\--log_format The log format: pretty or logfmt.
|
||||
\\ Defaults to
|
||||
++ (if (builtin.mode == .Debug) " pretty." else " logfmt.") ++
|
||||
\\
|
||||
\\
|
||||
\\--log_filter_scopes
|
||||
\\ Filter out too verbose logs per scope:
|
||||
\\ http, unknown_prop, event, ...
|
||||
\\
|
||||
\\--user_agent_suffix
|
||||
\\ Suffix to append to the Lightpanda/X.Y User-Agent
|
||||
\\
|
||||
;
|
||||
|
||||
// MAX_HELP_LEN|
|
||||
const usage =
|
||||
\\usage: {s} command [options] [URL]
|
||||
\\
|
||||
\\Command can be either 'fetch', 'serve' or 'help'
|
||||
\\
|
||||
\\fetch command
|
||||
\\Fetches the specified URL
|
||||
\\Example: {s} fetch --dump https://lightpanda.io/
|
||||
\\
|
||||
\\Options:
|
||||
\\--dump Dumps document to stdout.
|
||||
\\ Defaults to false.
|
||||
\\
|
||||
\\--strip_mode Comma separated list of tag groups to remove from dump
|
||||
\\ the dump. e.g. --strip_mode js,css
|
||||
\\ - "js" script and link[as=script, rel=preload]
|
||||
\\ - "ui" includes img, picture, video, css and svg
|
||||
\\ - "css" includes style and link[rel=stylesheet]
|
||||
\\ - "full" includes js, ui and css
|
||||
\\
|
||||
\\--with_base Add a <base> tag in dump. Defaults to false.
|
||||
\\
|
||||
++ common_options ++
|
||||
\\
|
||||
\\serve command
|
||||
\\Starts a websocket CDP server
|
||||
\\Example: {s} serve --host 127.0.0.1 --port 9222
|
||||
\\
|
||||
\\Options:
|
||||
\\--host Host of the CDP server
|
||||
\\ Defaults to "127.0.0.1"
|
||||
\\
|
||||
\\--port Port of the CDP server
|
||||
\\ Defaults to 9222
|
||||
\\
|
||||
\\--timeout Inactivity timeout in seconds before disconnecting clients
|
||||
\\ Defaults to 10 (seconds). Limited to 604800 (1 week).
|
||||
\\
|
||||
\\--max_connections
|
||||
\\ Maximum number of simultaneous CDP connections.
|
||||
\\ Defaults to 16.
|
||||
\\
|
||||
\\--max_tabs Maximum number of tabs per CDP connection.
|
||||
\\ Defaults to 8.
|
||||
\\
|
||||
\\--max_tab_memory
|
||||
\\ Maximum memory per tab in bytes.
|
||||
\\ Defaults to 536870912 (512 MB).
|
||||
\\
|
||||
\\--max_pending_connections
|
||||
\\ Maximum pending connections in the accept queue.
|
||||
\\ Defaults to 128.
|
||||
\\
|
||||
++ common_options ++
|
||||
\\
|
||||
\\version command
|
||||
\\Displays the version of {s}
|
||||
\\
|
||||
\\help command
|
||||
\\Displays this message
|
||||
\\
|
||||
;
|
||||
std.debug.print(usage, .{ self.exec_name, self.exec_name, self.exec_name, self.exec_name });
|
||||
if (success) {
|
||||
return std.process.cleanExit();
|
||||
}
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
pub fn parseArgs(allocator: Allocator) !Config {
|
||||
var args = try std.process.argsWithAllocator(allocator);
|
||||
defer args.deinit();
|
||||
|
||||
const exec_name = try allocator.dupe(u8, std.fs.path.basename(args.next().?));
|
||||
|
||||
const mode_string = args.next() orelse "";
|
||||
const run_mode = std.meta.stringToEnum(RunMode, mode_string) orelse blk: {
|
||||
const inferred_mode = inferMode(mode_string) orelse
|
||||
return init(allocator, exec_name, .{ .help = false });
|
||||
// "command" wasn't a command but an option. We can't reset args, but
|
||||
// we can create a new one. Not great, but this fallback is temporary
|
||||
// as we transition to this command mode approach.
|
||||
args.deinit();
|
||||
|
||||
args = try std.process.argsWithAllocator(allocator);
|
||||
// skip the exec_name
|
||||
_ = args.skip();
|
||||
|
||||
break :blk inferred_mode;
|
||||
};
|
||||
|
||||
const mode: Mode = switch (run_mode) {
|
||||
.help => .{ .help = true },
|
||||
.serve => .{ .serve = parseServeArgs(allocator, &args) catch
|
||||
return init(allocator, exec_name, .{ .help = false }) },
|
||||
.fetch => .{ .fetch = parseFetchArgs(allocator, &args) catch
|
||||
return init(allocator, exec_name, .{ .help = false }) },
|
||||
.version => .{ .version = {} },
|
||||
};
|
||||
return init(allocator, exec_name, mode);
|
||||
}
|
||||
|
||||
fn inferMode(opt: []const u8) ?RunMode {
|
||||
if (opt.len == 0) {
|
||||
return .serve;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, opt, "--") == false) {
|
||||
return .fetch;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, opt, "--dump")) {
|
||||
return .fetch;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, opt, "--noscript")) {
|
||||
return .fetch;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, opt, "--strip_mode")) {
|
||||
return .fetch;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, opt, "--with_base")) {
|
||||
return .fetch;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, opt, "--host")) {
|
||||
return .serve;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, opt, "--port")) {
|
||||
return .serve;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, opt, "--timeout")) {
|
||||
return .serve;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn parseServeArgs(
|
||||
allocator: Allocator,
|
||||
args: *std.process.ArgIterator,
|
||||
) !Serve {
|
||||
var serve: Serve = .{};
|
||||
|
||||
while (args.next()) |opt| {
|
||||
if (std.mem.eql(u8, "--host", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--host" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
serve.host = try allocator.dupe(u8, str);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--port", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--port" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
serve.port = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--port", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--timeout", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--timeout" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
serve.timeout = std.fmt.parseInt(u31, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--timeout", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--max_connections", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--max_connections" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
serve.max_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--max_connections", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--max_tabs", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--max_tabs" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
serve.max_tabs_per_connection = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--max_tabs", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--max_tab_memory", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--max_tab_memory" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
serve.max_memory_per_tab = std.fmt.parseInt(u64, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--max_tab_memory", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--max_pending_connections", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--max_pending_connections" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
serve.max_pending_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--max_pending_connections", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (try parseCommonArg(allocator, opt, args, &serve.common)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log.fatal(.app, "unknown argument", .{ .mode = "serve", .arg = opt });
|
||||
return error.UnkownOption;
|
||||
}
|
||||
|
||||
return serve;
|
||||
}
|
||||
|
||||
fn parseFetchArgs(
|
||||
allocator: Allocator,
|
||||
args: *std.process.ArgIterator,
|
||||
) !Fetch {
|
||||
var fetch_dump: bool = false;
|
||||
var withbase: bool = false;
|
||||
var url: ?[:0]const u8 = null;
|
||||
var common: Common = .{};
|
||||
var strip: dump.Opts.Strip = .{};
|
||||
|
||||
while (args.next()) |opt| {
|
||||
if (std.mem.eql(u8, "--dump", opt)) {
|
||||
fetch_dump = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--noscript", opt)) {
|
||||
log.warn(.app, "deprecation warning", .{
|
||||
.feature = "--noscript argument",
|
||||
.hint = "use '--strip_mode js' instead",
|
||||
});
|
||||
strip.js = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--with_base", opt)) {
|
||||
withbase = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--strip_mode", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--strip_mode" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
var it = std.mem.splitScalar(u8, str, ',');
|
||||
while (it.next()) |part| {
|
||||
const trimmed = std.mem.trim(u8, part, &std.ascii.whitespace);
|
||||
if (std.mem.eql(u8, trimmed, "js")) {
|
||||
strip.js = true;
|
||||
} else if (std.mem.eql(u8, trimmed, "ui")) {
|
||||
strip.ui = true;
|
||||
} else if (std.mem.eql(u8, trimmed, "css")) {
|
||||
strip.css = true;
|
||||
} else if (std.mem.eql(u8, trimmed, "full")) {
|
||||
strip.js = true;
|
||||
strip.ui = true;
|
||||
strip.css = true;
|
||||
} else {
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = "--strip_mode", .value = trimmed });
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (try parseCommonArg(allocator, opt, args, &common)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, opt, "--")) {
|
||||
log.fatal(.app, "unknown argument", .{ .mode = "fetch", .arg = opt });
|
||||
return error.UnkownOption;
|
||||
}
|
||||
|
||||
if (url != null) {
|
||||
log.fatal(.app, "duplicate fetch url", .{ .help = "only 1 URL can be specified" });
|
||||
return error.TooManyURLs;
|
||||
}
|
||||
url = try allocator.dupeZ(u8, opt);
|
||||
}
|
||||
|
||||
if (url == null) {
|
||||
log.fatal(.app, "missing fetch url", .{ .help = "URL to fetch must be provided" });
|
||||
return error.MissingURL;
|
||||
}
|
||||
|
||||
return .{
|
||||
.url = url.?,
|
||||
.dump = fetch_dump,
|
||||
.strip = strip,
|
||||
.common = common,
|
||||
.withbase = withbase,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseCommonArg(
|
||||
allocator: Allocator,
|
||||
opt: []const u8,
|
||||
args: *std.process.ArgIterator,
|
||||
common: *Common,
|
||||
) !bool {
|
||||
if (std.mem.eql(u8, "--insecure_disable_tls_host_verification", opt)) {
|
||||
common.tls_verify_host = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--obey_robots", opt)) {
|
||||
common.obey_robots = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--http_proxy", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_proxy" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
common.http_proxy = try allocator.dupeZ(u8, str);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--proxy_bearer_token", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--proxy_bearer_token" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
common.proxy_bearer_token = try allocator.dupeZ(u8, str);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--http_max_concurrent", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_max_concurrent" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
common.http_max_concurrent = std.fmt.parseInt(u8, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--http_max_concurrent", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--http_max_host_open", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_max_host_open" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
common.http_max_host_open = std.fmt.parseInt(u8, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--http_max_host_open", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--http_connect_timeout", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_connect_timeout" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
common.http_connect_timeout = std.fmt.parseInt(u31, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--http_connect_timeout", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--http_timeout", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_timeout" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
common.http_timeout = std.fmt.parseInt(u31, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--http_timeout", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--http_max_response_size", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_max_response_size" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
common.http_max_response_size = std.fmt.parseInt(usize, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--http_max_response_size", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--log_level", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--log_level" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
common.log_level = std.meta.stringToEnum(log.Level, str) orelse blk: {
|
||||
if (std.mem.eql(u8, str, "error")) {
|
||||
break :blk .err;
|
||||
}
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = "--log_level", .value = str });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--log_format", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--log_format" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
common.log_format = std.meta.stringToEnum(log.Format, str) orelse {
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = "--log_format", .value = str });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--log_filter_scopes", opt)) {
|
||||
if (builtin.mode != .Debug) {
|
||||
log.fatal(.app, "experimental", .{ .help = "log scope filtering is only available in debug builds" });
|
||||
return false;
|
||||
}
|
||||
|
||||
const str = args.next() orelse {
|
||||
// disables the default filters
|
||||
common.log_filter_scopes = &.{};
|
||||
return true;
|
||||
};
|
||||
|
||||
var arr: std.ArrayList(log.Scope) = .empty;
|
||||
|
||||
var it = std.mem.splitScalar(u8, str, ',');
|
||||
while (it.next()) |part| {
|
||||
try arr.append(allocator, std.meta.stringToEnum(log.Scope, part) orelse {
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = "--log_filter_scopes", .value = part });
|
||||
return false;
|
||||
});
|
||||
}
|
||||
common.log_filter_scopes = arr.items;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--user_agent_suffix", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--user_agent_suffix" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
for (str) |c| {
|
||||
if (!std.ascii.isPrint(c)) {
|
||||
log.fatal(.app, "not printable character", .{ .arg = "--user_agent_suffix" });
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
}
|
||||
common.user_agent_suffix = try allocator.dupe(u8, str);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -39,10 +39,9 @@ const List = std.DoublyLinkedList;
|
||||
// CDP code registers for the "network_bytes_sent" event, because it needs to
|
||||
// send messages to the client when this happens. Our HTTP client could then
|
||||
// emit a "network_bytes_sent" message. It would be easy, and it would work.
|
||||
// That is, it would work until the Telemetry code makes an HTTP request, and
|
||||
// because everything's just one big global, that gets picked up by the
|
||||
// registered CDP listener, and the telemetry network activity gets sent to the
|
||||
// CDP client.
|
||||
// That is, it would work until multiple CDP clients connect, and because
|
||||
// everything's just one big global, events from one CDP session would be sent
|
||||
// to all CDP clients.
|
||||
//
|
||||
// To avoid this, one way or another, we need scoping. We could still have
|
||||
// a global registry but every "register" and every "emit" has some type of
|
||||
@@ -50,14 +49,10 @@ const List = std.DoublyLinkedList;
|
||||
// between components to share a common scope.
|
||||
//
|
||||
// Instead, the approach that we take is to have a notification instance per
|
||||
// scope. This makes some things harder, but we only plan on having 2
|
||||
// notification instances at a given time: one in a Browser and one in the App.
|
||||
// What about something like Telemetry, which lives outside of a Browser but
|
||||
// still cares about Browser-events (like .page_navigate)? When the Browser
|
||||
// notification is created, a `notification_created` event is raised in the
|
||||
// App's notification, which Telemetry is registered for. This allows Telemetry
|
||||
// to register for events in the Browser notification. See the Telemetry's
|
||||
// register function.
|
||||
// CDP connection (BrowserContext). Each CDP connection has its own notification
|
||||
// that is shared across all Sessions (tabs) within that connection. This ensures
|
||||
// proper isolation between different CDP clients while allowing a single client
|
||||
// to receive events from all its tabs.
|
||||
const Notification = @This();
|
||||
// Every event type (which are hard-coded), has a list of Listeners.
|
||||
// When the event happens, we dispatch to those listener.
|
||||
@@ -66,7 +61,7 @@ event_listeners: EventListeners,
|
||||
// list of listeners for a specified receiver
|
||||
// @intFromPtr(receiver) -> [listener1, listener2, ...]
|
||||
// Used when `unregisterAll` is called.
|
||||
listeners: std.AutoHashMapUnmanaged(usize, std.ArrayListUnmanaged(*Listener)),
|
||||
listeners: std.AutoHashMapUnmanaged(usize, std.ArrayList(*Listener)),
|
||||
|
||||
allocator: Allocator,
|
||||
mem_pool: std.heap.MemoryPool(Listener),
|
||||
@@ -85,7 +80,6 @@ const EventListeners = struct {
|
||||
http_request_auth_required: List = .{},
|
||||
http_response_data: List = .{},
|
||||
http_response_header_done: List = .{},
|
||||
notification_created: List = .{},
|
||||
};
|
||||
|
||||
const Events = union(enum) {
|
||||
@@ -102,7 +96,6 @@ const Events = union(enum) {
|
||||
http_request_done: *const RequestDone,
|
||||
http_response_data: *const ResponseData,
|
||||
http_response_header_done: *const ResponseHeaderDone,
|
||||
notification_created: *Notification,
|
||||
};
|
||||
const EventType = std.meta.FieldEnum(Events);
|
||||
|
||||
@@ -162,12 +155,7 @@ pub const RequestFail = struct {
|
||||
err: anyerror,
|
||||
};
|
||||
|
||||
pub fn init(allocator: Allocator, parent: ?*Notification) !*Notification {
|
||||
|
||||
// This is put on the heap because we want to raise a .notification_created
|
||||
// event, so that, something like Telemetry, can receive the
|
||||
// .page_navigate event on all notification instances. That can only work
|
||||
// if we dispatch .notification_created with a *Notification.
|
||||
pub fn init(allocator: Allocator) !*Notification {
|
||||
const notification = try allocator.create(Notification);
|
||||
errdefer allocator.destroy(notification);
|
||||
|
||||
@@ -178,10 +166,6 @@ pub fn init(allocator: Allocator, parent: ?*Notification) !*Notification {
|
||||
.mem_pool = std.heap.MemoryPool(Listener).init(allocator),
|
||||
};
|
||||
|
||||
if (parent) |pn| {
|
||||
pn.dispatch(.notification_created, notification);
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
@@ -256,6 +240,9 @@ pub fn unregisterAll(self: *Notification, receiver: *anyopaque) void {
|
||||
}
|
||||
|
||||
pub fn dispatch(self: *Notification, comptime event: EventType, data: ArgType(event)) void {
|
||||
if (self.listeners.count() == 0) {
|
||||
return;
|
||||
}
|
||||
const list = &@field(self.event_listeners, @tagName(event));
|
||||
|
||||
var node = list.first;
|
||||
@@ -313,7 +300,7 @@ const Listener = struct {
|
||||
|
||||
const testing = std.testing;
|
||||
test "Notification" {
|
||||
var notifier = try Notification.init(testing.allocator, null);
|
||||
var notifier = try Notification.init(testing.allocator);
|
||||
defer notifier.deinit();
|
||||
|
||||
// noop
|
||||
|
||||
@@ -205,7 +205,6 @@ fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: u32) !void {
|
||||
}
|
||||
ms_remaining -= @intCast(elapsed);
|
||||
},
|
||||
.navigate => unreachable, // must have been handled by the session
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -561,7 +560,7 @@ pub const Client = struct {
|
||||
|
||||
pub fn sendJSONRaw(
|
||||
self: *Client,
|
||||
buf: std.ArrayListUnmanaged(u8),
|
||||
buf: std.ArrayList(u8),
|
||||
) !void {
|
||||
// Dangerous API!. We assume the caller has reserved the first 10
|
||||
// bytes in `buf`.
|
||||
@@ -883,7 +882,7 @@ fn growBuffer(allocator: Allocator, buf: []u8, required_capacity: usize) ![]u8 {
|
||||
|
||||
const Fragments = struct {
|
||||
type: Message.Type,
|
||||
message: std.ArrayListUnmanaged(u8),
|
||||
message: std.ArrayList(u8),
|
||||
};
|
||||
|
||||
const Message = struct {
|
||||
@@ -907,7 +906,7 @@ const OpCode = enum(u8) {
|
||||
pong = 128 | 10,
|
||||
};
|
||||
|
||||
fn fillWebsocketHeader(buf: std.ArrayListUnmanaged(u8)) []const u8 {
|
||||
fn fillWebsocketHeader(buf: std.ArrayList(u8)) []const u8 {
|
||||
// can't use buf[0..10] here, because the header length
|
||||
// is variable. If it's just 2 bytes, for example, we need the
|
||||
// framed message to be:
|
||||
@@ -1342,7 +1341,7 @@ fn assertWebSocketMessage(expected: []const u8, input: []const u8) !void {
|
||||
}
|
||||
|
||||
const MockCDP = struct {
|
||||
messages: std.ArrayListUnmanaged([]const u8) = .{},
|
||||
messages: std.ArrayList([]const u8) = .{},
|
||||
|
||||
allocator: Allocator = testing.allocator,
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ const std = @import("std");
|
||||
|
||||
const TestHTTPServer = @This();
|
||||
|
||||
shutdown: bool,
|
||||
shutdown: std.atomic.Value(bool),
|
||||
listener: ?std.net.Server,
|
||||
handler: Handler,
|
||||
|
||||
@@ -28,16 +28,23 @@ const Handler = *const fn (req: *std.http.Server.Request) anyerror!void;
|
||||
|
||||
pub fn init(handler: Handler) TestHTTPServer {
|
||||
return .{
|
||||
.shutdown = true,
|
||||
.shutdown = .init(true),
|
||||
.listener = null,
|
||||
.handler = handler,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TestHTTPServer) void {
|
||||
self.shutdown = true;
|
||||
self.listener = null;
|
||||
}
|
||||
|
||||
pub fn stop(self: *TestHTTPServer) void {
|
||||
self.shutdown.store(true, .release);
|
||||
if (self.listener) |*listener| {
|
||||
listener.deinit();
|
||||
switch (@import("builtin").target.os.tag) {
|
||||
.linux => std.posix.shutdown(listener.stream.handle, .recv) catch {},
|
||||
else => std.posix.close(listener.stream.handle),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,12 +53,13 @@ pub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void {
|
||||
|
||||
self.listener = try address.listen(.{ .reuse_address = true });
|
||||
var listener = &self.listener.?;
|
||||
self.shutdown.store(false, .release);
|
||||
|
||||
wg.finish();
|
||||
|
||||
while (true) {
|
||||
const conn = listener.accept() catch |err| {
|
||||
if (self.shutdown) {
|
||||
if (self.shutdown.load(.acquire) or err == error.SocketNotListening) {
|
||||
return;
|
||||
}
|
||||
return err;
|
||||
|
||||
@@ -27,11 +27,11 @@ const App = @import("../App.zig");
|
||||
|
||||
const ArenaPool = App.ArenaPool;
|
||||
const HttpClient = App.Http.Client;
|
||||
const Notification = App.Notification;
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const Session = @import("Session.zig");
|
||||
const Notification = @import("../Notification.zig");
|
||||
|
||||
// Browser is an instance of the browser.
|
||||
// You can create multiple browser instances.
|
||||
@@ -48,25 +48,22 @@ call_arena: ArenaAllocator,
|
||||
page_arena: ArenaAllocator,
|
||||
session_arena: ArenaAllocator,
|
||||
transfer_arena: ArenaAllocator,
|
||||
notification: *Notification,
|
||||
|
||||
pub fn init(app: *App) !Browser {
|
||||
const InitOpts = struct {
|
||||
env: js.Env.InitOpts = .{},
|
||||
};
|
||||
|
||||
pub fn init(app: *App, opts: InitOpts) !Browser {
|
||||
const allocator = app.allocator;
|
||||
|
||||
var env = try js.Env.init(allocator, &app.platform, &app.snapshot);
|
||||
var env = try js.Env.init(app, opts.env);
|
||||
errdefer env.deinit();
|
||||
|
||||
const notification = try Notification.init(allocator, app.notification);
|
||||
app.http.client.notification = notification;
|
||||
app.http.client.next_request_id = 0; // Should we track ids in CDP only?
|
||||
errdefer notification.deinit();
|
||||
|
||||
return .{
|
||||
.app = app,
|
||||
.env = env,
|
||||
.session = null,
|
||||
.allocator = allocator,
|
||||
.notification = notification,
|
||||
.arena_pool = &app.arena_pool,
|
||||
.http_client = app.http.client,
|
||||
.call_arena = ArenaAllocator.init(allocator),
|
||||
@@ -83,15 +80,13 @@ pub fn deinit(self: *Browser) void {
|
||||
self.page_arena.deinit();
|
||||
self.session_arena.deinit();
|
||||
self.transfer_arena.deinit();
|
||||
self.http_client.notification = null;
|
||||
self.notification.deinit();
|
||||
}
|
||||
|
||||
pub fn newSession(self: *Browser) !*Session {
|
||||
pub fn newSession(self: *Browser, notification: *Notification) !*Session {
|
||||
self.closeSession();
|
||||
self.session = @as(Session, undefined);
|
||||
const session = &self.session.?;
|
||||
try Session.init(session, self);
|
||||
try Session.init(session, self, notification);
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -108,6 +103,10 @@ pub fn runMicrotasks(self: *const Browser) void {
|
||||
self.env.runMicrotasks();
|
||||
}
|
||||
|
||||
pub fn runMacrotasks(self: *Browser) !?u64 {
|
||||
return try self.env.runMacrotasks();
|
||||
}
|
||||
|
||||
pub fn runMessageLoop(self: *const Browser) void {
|
||||
while (self.env.pumpMessageLoop()) {
|
||||
if (comptime IS_DEBUG) {
|
||||
|
||||
@@ -33,13 +33,36 @@ const Allocator = std.mem.Allocator;
|
||||
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
const EventKey = struct {
|
||||
event_target: usize,
|
||||
type_string: String,
|
||||
};
|
||||
|
||||
const EventKeyContext = struct {
|
||||
pub fn hash(_: @This(), key: EventKey) u64 {
|
||||
var hasher = std.hash.Wyhash.init(0);
|
||||
hasher.update(std.mem.asBytes(&key.event_target));
|
||||
hasher.update(key.type_string.str());
|
||||
return hasher.final();
|
||||
}
|
||||
|
||||
pub fn eql(_: @This(), a: EventKey, b: EventKey) bool {
|
||||
return a.event_target == b.event_target and a.type_string.eql(b.type_string);
|
||||
}
|
||||
};
|
||||
|
||||
pub const EventManager = @This();
|
||||
|
||||
page: *Page,
|
||||
arena: Allocator,
|
||||
listener_pool: std.heap.MemoryPool(Listener),
|
||||
list_pool: std.heap.MemoryPool(std.DoublyLinkedList),
|
||||
lookup: std.AutoHashMapUnmanaged(usize, *std.DoublyLinkedList),
|
||||
lookup: std.HashMapUnmanaged(
|
||||
EventKey,
|
||||
*std.DoublyLinkedList,
|
||||
EventKeyContext,
|
||||
std.hash_map.default_max_load_percentage,
|
||||
),
|
||||
dispatch_depth: usize,
|
||||
deferred_removals: std.ArrayList(struct { list: *std.DoublyLinkedList, listener: *Listener }),
|
||||
|
||||
@@ -69,7 +92,7 @@ pub const Callback = union(enum) {
|
||||
|
||||
pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, opts: RegisterOptions) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.event, "eventManager.register", .{ .type = typ, .capture = opts.capture, .once = opts.once, .target = target });
|
||||
log.debug(.event, "eventManager.register", .{ .type = typ, .capture = opts.capture, .once = opts.once, .target = target.toString() });
|
||||
}
|
||||
|
||||
// If a signal is provided and already aborted, don't register the listener
|
||||
@@ -79,20 +102,24 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
||||
}
|
||||
}
|
||||
|
||||
const gop = try self.lookup.getOrPut(self.arena, @intFromPtr(target));
|
||||
// Allocate the type string we'll use in both listener and key
|
||||
const type_string = try String.init(self.arena, typ, .{});
|
||||
|
||||
const gop = try self.lookup.getOrPut(self.arena, .{
|
||||
.type_string = type_string,
|
||||
.event_target = @intFromPtr(target),
|
||||
});
|
||||
if (gop.found_existing) {
|
||||
// check for duplicate callbacks already registered
|
||||
var node = gop.value_ptr.*.first;
|
||||
while (node) |n| {
|
||||
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||
if (listener.typ.eqlSlice(typ)) {
|
||||
const is_duplicate = switch (callback) {
|
||||
.object => |obj| listener.function.eqlObject(obj),
|
||||
.function => |func| listener.function.eqlFunction(func),
|
||||
};
|
||||
if (is_duplicate and listener.capture == opts.capture) {
|
||||
return;
|
||||
}
|
||||
const is_duplicate = switch (callback) {
|
||||
.object => |obj| listener.function.eqlObject(obj),
|
||||
.function => |func| listener.function.eqlFunction(func),
|
||||
};
|
||||
if (is_duplicate and listener.capture == opts.capture) {
|
||||
return;
|
||||
}
|
||||
node = n.next;
|
||||
}
|
||||
@@ -114,20 +141,34 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
||||
.passive = opts.passive,
|
||||
.function = func,
|
||||
.signal = opts.signal,
|
||||
.typ = try String.init(self.arena, typ, .{}),
|
||||
.typ = type_string,
|
||||
};
|
||||
// append the listener to the list of listeners for this target
|
||||
gop.value_ptr.*.append(&listener.node);
|
||||
}
|
||||
|
||||
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void {
|
||||
const list = self.lookup.get(@intFromPtr(target)) orelse return;
|
||||
if (findListener(list, typ, callback, use_capture)) |listener| {
|
||||
const list = self.lookup.get(.{
|
||||
.type_string = .wrap(typ),
|
||||
.event_target = @intFromPtr(target),
|
||||
}) orelse return;
|
||||
if (findListener(list, callback, use_capture)) |listener| {
|
||||
self.removeListener(list, listener);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void {
|
||||
// Dispatching can be recursive from the compiler's point of view, so we need to
|
||||
// give it an explicit error set so that other parts of the code can use and
|
||||
// inferred error.
|
||||
const DispatchError = error{
|
||||
OutOfMemory,
|
||||
StringTooLarge,
|
||||
JSExecCallback,
|
||||
CompilationError,
|
||||
ExecutionError,
|
||||
JsException,
|
||||
};
|
||||
pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) DispatchError!void {
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
|
||||
}
|
||||
@@ -154,9 +195,13 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void
|
||||
.navigation,
|
||||
.screen,
|
||||
.screen_orientation,
|
||||
.visual_viewport,
|
||||
.generic,
|
||||
=> {
|
||||
const list = self.lookup.get(@intFromPtr(target)) orelse return;
|
||||
const list = self.lookup.get(.{
|
||||
.event_target = @intFromPtr(target),
|
||||
.type_string = event._type_string,
|
||||
}) orelse return;
|
||||
try self.dispatchAll(list, target, event, &was_handled);
|
||||
},
|
||||
}
|
||||
@@ -199,7 +244,10 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
|
||||
}
|
||||
}
|
||||
|
||||
const list = self.lookup.get(@intFromPtr(target)) orelse return;
|
||||
const list = self.lookup.get(.{
|
||||
.event_target = @intFromPtr(target),
|
||||
.type_string = event._type_string,
|
||||
}) orelse return;
|
||||
try self.dispatchAll(list, target, event, &was_dispatched);
|
||||
}
|
||||
|
||||
@@ -267,7 +315,10 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
||||
while (i > 1) {
|
||||
i -= 1;
|
||||
const current_target = path[i];
|
||||
if (self.lookup.get(@intFromPtr(current_target))) |list| {
|
||||
if (self.lookup.get(.{
|
||||
.event_target = @intFromPtr(current_target),
|
||||
.type_string = event._type_string,
|
||||
})) |list| {
|
||||
try self.dispatchPhase(list, current_target, event, was_handled, true);
|
||||
if (event._stop_propagation) {
|
||||
return;
|
||||
@@ -278,7 +329,10 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
||||
// Phase 2: At target
|
||||
event._event_phase = .at_target;
|
||||
const target_et = target.asEventTarget();
|
||||
if (self.lookup.get(@intFromPtr(target_et))) |list| {
|
||||
if (self.lookup.get(.{
|
||||
.type_string = event._type_string,
|
||||
.event_target = @intFromPtr(target_et),
|
||||
})) |list| {
|
||||
try self.dispatchPhase(list, target_et, event, was_handled, null);
|
||||
if (event._stop_propagation) {
|
||||
return;
|
||||
@@ -290,7 +344,10 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
||||
if (event._bubbles) {
|
||||
event._event_phase = .bubbling_phase;
|
||||
for (path[1..]) |current_target| {
|
||||
if (self.lookup.get(@intFromPtr(current_target))) |list| {
|
||||
if (self.lookup.get(.{
|
||||
.type_string = event._type_string,
|
||||
.event_target = @intFromPtr(current_target),
|
||||
})) |list| {
|
||||
try self.dispatchPhase(list, current_target, event, was_handled, false);
|
||||
if (event._stop_propagation) {
|
||||
break;
|
||||
@@ -302,7 +359,6 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
||||
|
||||
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime capture_only: ?bool) !void {
|
||||
const page = self.page;
|
||||
const typ = event._type_string;
|
||||
|
||||
// Track dispatch depth for deferred removal
|
||||
self.dispatch_depth += 1;
|
||||
@@ -337,9 +393,6 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
||||
node = n.next;
|
||||
|
||||
// Skip non-matching listeners
|
||||
if (!listener.typ.eql(typ)) {
|
||||
continue;
|
||||
}
|
||||
if (comptime capture_only) |capture| {
|
||||
if (listener.capture != capture) {
|
||||
continue;
|
||||
@@ -419,7 +472,7 @@ fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *L
|
||||
}
|
||||
}
|
||||
|
||||
fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Callback, capture: bool) ?*Listener {
|
||||
fn findListener(list: *const std.DoublyLinkedList, callback: Callback, capture: bool) ?*Listener {
|
||||
var node = list.first;
|
||||
while (node) |n| {
|
||||
node = n.next;
|
||||
@@ -434,9 +487,6 @@ fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Ca
|
||||
if (listener.capture != capture) {
|
||||
continue;
|
||||
}
|
||||
if (!listener.typ.eqlSlice(typ)) {
|
||||
continue;
|
||||
}
|
||||
return listener;
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -172,60 +172,42 @@ pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||
return chain.get(1);
|
||||
}
|
||||
|
||||
fn eventInit(typ: []const u8, value: anytype, page: *Page) !Event {
|
||||
// Round to 2ms for privacy (browsers do this)
|
||||
const raw_timestamp = @import("../datetime.zig").milliTimestamp(.monotonic);
|
||||
const time_stamp = (raw_timestamp / 2) * 2;
|
||||
|
||||
return .{
|
||||
._type = unionInit(Event.Type, value),
|
||||
._type_string = try String.init(page.arena, typ, .{}),
|
||||
._time_stamp = time_stamp,
|
||||
};
|
||||
}
|
||||
|
||||
// this is a root object
|
||||
pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
|
||||
const allocator = self._slab.allocator();
|
||||
|
||||
pub fn event(self: *Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
||||
const chain = try PrototypeChain(
|
||||
&.{ Event, @TypeOf(child) },
|
||||
).allocate(allocator);
|
||||
).allocate(arena);
|
||||
|
||||
// Special case: Event has a _type_string field, so we need manual setup
|
||||
const event_ptr = chain.get(0);
|
||||
event_ptr.* = try eventInit(typ, chain.get(1), self._page);
|
||||
event_ptr.* = try self.eventInit(arena, typ, chain.get(1));
|
||||
chain.setLeaf(1, child);
|
||||
|
||||
return chain.get(1);
|
||||
}
|
||||
|
||||
pub fn uiEvent(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
|
||||
const allocator = self._slab.allocator();
|
||||
|
||||
pub fn uiEvent(self: *Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
||||
const chain = try PrototypeChain(
|
||||
&.{ Event, UIEvent, @TypeOf(child) },
|
||||
).allocate(allocator);
|
||||
).allocate(arena);
|
||||
|
||||
// Special case: Event has a _type_string field, so we need manual setup
|
||||
const event_ptr = chain.get(0);
|
||||
event_ptr.* = try eventInit(typ, chain.get(1), self._page);
|
||||
event_ptr.* = try self.eventInit(arena, typ, chain.get(1));
|
||||
chain.setMiddle(1, UIEvent.Type);
|
||||
chain.setLeaf(2, child);
|
||||
|
||||
return chain.get(2);
|
||||
}
|
||||
|
||||
pub fn mouseEvent(self: *Factory, typ: []const u8, mouse: MouseEvent, child: anytype) !*@TypeOf(child) {
|
||||
const allocator = self._slab.allocator();
|
||||
|
||||
pub fn mouseEvent(self: *Factory, arena: Allocator, typ: String, mouse: MouseEvent, child: anytype) !*@TypeOf(child) {
|
||||
const chain = try PrototypeChain(
|
||||
&.{ Event, UIEvent, MouseEvent, @TypeOf(child) },
|
||||
).allocate(allocator);
|
||||
).allocate(arena);
|
||||
|
||||
// Special case: Event has a _type_string field, so we need manual setup
|
||||
const event_ptr = chain.get(0);
|
||||
event_ptr.* = try eventInit(typ, chain.get(1), self._page);
|
||||
event_ptr.* = try self.eventInit(arena, typ, chain.get(1));
|
||||
chain.setMiddle(1, UIEvent.Type);
|
||||
|
||||
// Set MouseEvent with all its fields
|
||||
@@ -239,6 +221,20 @@ pub fn mouseEvent(self: *Factory, typ: []const u8, mouse: MouseEvent, child: any
|
||||
return chain.get(3);
|
||||
}
|
||||
|
||||
fn eventInit(self: *const Factory, arena: Allocator, typ: String, value: anytype) !Event {
|
||||
// Round to 2ms for privacy (browsers do this)
|
||||
const raw_timestamp = @import("../datetime.zig").milliTimestamp(.monotonic);
|
||||
const time_stamp = (raw_timestamp / 2) * 2;
|
||||
|
||||
return .{
|
||||
._arena = arena,
|
||||
._page = self._page,
|
||||
._type = unionInit(Event.Type, value),
|
||||
._type_string = typ,
|
||||
._time_stamp = time_stamp,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||
const allocator = self._slab.allocator();
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ const String = @import("../string.zig").String;
|
||||
const Mime = @import("Mime.zig");
|
||||
const Factory = @import("Factory.zig");
|
||||
const Session = @import("Session.zig");
|
||||
const Scheduler = @import("Scheduler.zig");
|
||||
const EventManager = @import("EventManager.zig");
|
||||
const ScriptManager = @import("ScriptManager.zig");
|
||||
|
||||
@@ -42,14 +41,17 @@ const Parser = @import("parser/Parser.zig");
|
||||
const URL = @import("URL.zig");
|
||||
const Node = @import("webapi/Node.zig");
|
||||
const Event = @import("webapi/Event.zig");
|
||||
const EventTarget = @import("webapi/EventTarget.zig");
|
||||
const CData = @import("webapi/CData.zig");
|
||||
const Element = @import("webapi/Element.zig");
|
||||
const HtmlElement = @import("webapi/element/Html.zig");
|
||||
const Window = @import("webapi/Window.zig");
|
||||
const Location = @import("webapi/Location.zig");
|
||||
const Document = @import("webapi/Document.zig");
|
||||
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
||||
const Performance = @import("webapi/Performance.zig");
|
||||
const Screen = @import("webapi/Screen.zig");
|
||||
const VisualViewport = @import("webapi/VisualViewport.zig");
|
||||
const PerformanceObserver = @import("webapi/PerformanceObserver.zig");
|
||||
const MutationObserver = @import("webapi/MutationObserver.zig");
|
||||
const IntersectionObserver = @import("webapi/IntersectionObserver.zig");
|
||||
@@ -66,6 +68,9 @@ const timestamp = @import("../datetime.zig").timestamp;
|
||||
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
||||
|
||||
const WebApiURL = @import("webapi/URL.zig");
|
||||
const global_event_handlers = @import("webapi/global_event_handlers.zig");
|
||||
const GlobalEventHandlersLookup = global_event_handlers.Lookup;
|
||||
const GlobalEventHandler = global_event_handlers.Handler;
|
||||
|
||||
var default_url = WebApiURL{ ._raw = "about:blank" };
|
||||
pub var default_location: Location = Location{ ._url = &default_url };
|
||||
@@ -105,16 +110,26 @@ _element_assigned_slots: Element.AssignedSlotLookup = .{},
|
||||
/// Lazily-created inline event listeners (or listeners provided as attributes).
|
||||
/// Avoids bloating all elements with extra function fields for rare usage.
|
||||
///
|
||||
/// Use this when a listener provided like these:
|
||||
/// Use this when a listener provided like this:
|
||||
///
|
||||
/// ```js
|
||||
/// img.onload = () => { ... };
|
||||
/// ```
|
||||
///
|
||||
/// Its also used as cache for such cases after lazy evaluation:
|
||||
///
|
||||
/// ```html
|
||||
/// <img onload="(() => { ... })()" />
|
||||
/// ```
|
||||
///
|
||||
/// ```js
|
||||
/// img.onload = () => { ... };
|
||||
/// img.setAttribute("onload", "(() => { ... })()");
|
||||
/// ```
|
||||
_element_attr_listeners: Element.AttrListenerLookup = .{},
|
||||
_element_attr_listeners: GlobalEventHandlersLookup = .{},
|
||||
|
||||
/// `load` events that'll be fired before window's `load` event.
|
||||
/// A call to `documentIsComplete` (which calls `_documentIsComplete`) resets it.
|
||||
_to_load: std.ArrayList(*Element) = .{},
|
||||
|
||||
_script_manager: ScriptManager,
|
||||
|
||||
@@ -171,6 +186,9 @@ url: [:0]const u8,
|
||||
// If null the url must be used.
|
||||
base_url: ?[:0]const u8,
|
||||
|
||||
// referer header cache.
|
||||
referer_header: ?[:0]const u8,
|
||||
|
||||
// Arbitrary buffer. Need to temporarily lowercase a value? Use this. No lifetime
|
||||
// guarantee - it's valid until someone else uses it.
|
||||
buf: [BUF_SIZE]u8,
|
||||
@@ -199,28 +217,26 @@ document: *Document,
|
||||
// DOM version used to invalidate cached state of "live" collections
|
||||
version: usize,
|
||||
|
||||
scheduler: Scheduler,
|
||||
|
||||
_req_id: ?usize = null,
|
||||
_navigated_options: ?NavigatedOpts = null,
|
||||
|
||||
pub fn init(arena: Allocator, call_arena: Allocator, session: *Session) !*Page {
|
||||
pub fn init(self: *Page, session: *Session) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.page, "page.init", .{});
|
||||
}
|
||||
|
||||
const page = try session.browser.allocator.create(Page);
|
||||
page._session = session;
|
||||
const browser = session.browser;
|
||||
self._session = session;
|
||||
|
||||
self.arena_pool = browser.arena_pool;
|
||||
self.arena = browser.page_arena.allocator();
|
||||
self.call_arena = browser.call_arena.allocator();
|
||||
|
||||
page.arena = arena;
|
||||
page.call_arena = call_arena;
|
||||
page.arena_pool = session.browser.arena_pool;
|
||||
if (comptime IS_DEBUG) {
|
||||
page._arena_pool_leak_track = .empty;
|
||||
self._arena_pool_leak_track = .empty;
|
||||
}
|
||||
|
||||
try page.reset(true);
|
||||
return page;
|
||||
try self.reset(true);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Page) void {
|
||||
@@ -234,17 +250,8 @@ pub fn deinit(self: *Page) void {
|
||||
// stats.print(&stream) catch unreachable;
|
||||
}
|
||||
|
||||
{
|
||||
// some MicroTasks might be referencing the page, we need to drain it while
|
||||
// the page still exists
|
||||
var ls: JS.Local.Scope = undefined;
|
||||
self.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
ls.local.runMicrotasks();
|
||||
}
|
||||
|
||||
const session = self._session;
|
||||
session.executor.removeContext();
|
||||
session.browser.env.destroyContext(self.js);
|
||||
|
||||
self._script_manager.shutdown = true;
|
||||
session.browser.http_client.abort();
|
||||
@@ -258,46 +265,49 @@ pub fn deinit(self: *Page) void {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
session.browser.allocator.destroy(self);
|
||||
}
|
||||
|
||||
fn reset(self: *Page, comptime initializing: bool) !void {
|
||||
const browser = self._session.browser;
|
||||
|
||||
if (comptime initializing == false) {
|
||||
self._session.executor.removeContext();
|
||||
|
||||
// removing a context can trigger finalizers, so we can only check for
|
||||
// a leak after the above.
|
||||
if (comptime IS_DEBUG) {
|
||||
var it = self._arena_pool_leak_track.valueIterator();
|
||||
while (it.next()) |value_ptr| {
|
||||
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.* });
|
||||
}
|
||||
self._arena_pool_leak_track.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
browser.env.destroyContext(self.js);
|
||||
|
||||
// We force a garbage collection between page navigations to keep v8
|
||||
// memory usage as low as possible.
|
||||
self._session.browser.env.memoryPressureNotification(.moderate);
|
||||
|
||||
browser.env.memoryPressureNotification(.moderate);
|
||||
self._script_manager.shutdown = true;
|
||||
self._session.browser.http_client.abort();
|
||||
browser.http_client.abort();
|
||||
self._script_manager.deinit();
|
||||
_ = self._session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
|
||||
// destroying the context, and aborting the http_client can both cause
|
||||
// resources to be freed. We need to check for a leak after we've finished
|
||||
// all of our cleanup.
|
||||
if (comptime IS_DEBUG) {
|
||||
var it = self._arena_pool_leak_track.valueIterator();
|
||||
while (it.next()) |value_ptr| {
|
||||
if (value_ptr.count > 0) {
|
||||
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner });
|
||||
}
|
||||
}
|
||||
self._arena_pool_leak_track = .empty;
|
||||
}
|
||||
|
||||
_ = browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
}
|
||||
|
||||
self._factory = Factory.init(self);
|
||||
self.scheduler = Scheduler.init(self.arena);
|
||||
|
||||
self.version = 0;
|
||||
self.url = "about:blank";
|
||||
self.base_url = null;
|
||||
self.referer_header = null;
|
||||
|
||||
self.document = (try self._factory.document(Node.Document.HTMLDocument{ ._proto = undefined })).asDocument();
|
||||
|
||||
const storage_bucket = try self._factory.create(storage.Bucket{});
|
||||
const screen = try Screen.init(self);
|
||||
const visual_viewport = try VisualViewport.init(self);
|
||||
self.window = try self._factory.eventTarget(Window{
|
||||
._document = self.document,
|
||||
._storage_bucket = storage_bucket,
|
||||
@@ -305,6 +315,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
|
||||
._proto = undefined,
|
||||
._location = &default_location,
|
||||
._screen = screen,
|
||||
._visual_viewport = visual_viewport,
|
||||
});
|
||||
self.window._document = self.document;
|
||||
self.window._location = &default_location;
|
||||
@@ -320,7 +331,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
|
||||
self._script_manager = ScriptManager.init(self);
|
||||
errdefer self._script_manager.deinit();
|
||||
|
||||
self.js = try self._session.executor.createContext(self, true);
|
||||
self.js = try browser.env.createContext(self, true);
|
||||
errdefer self.js.deinit();
|
||||
|
||||
self._element_styles = .{};
|
||||
@@ -333,6 +344,8 @@ fn reset(self: *Page, comptime initializing: bool) !void {
|
||||
|
||||
self._element_attr_listeners = .{};
|
||||
|
||||
self._to_load = .{};
|
||||
|
||||
self._notified_network_idle = .init;
|
||||
self._notified_network_almost_idle = .init;
|
||||
|
||||
@@ -369,7 +382,7 @@ fn registerBackgroundTasks(self: *Page) !void {
|
||||
|
||||
const Browser = @import("Browser.zig");
|
||||
|
||||
try self.scheduler.add(self._session.browser, struct {
|
||||
try self.js.scheduler.add(self._session.browser, struct {
|
||||
fn runMessageLoop(ctx: *anyopaque) !?u32 {
|
||||
const b: *Browser = @ptrCast(@alignCast(ctx));
|
||||
b.runMessageLoop();
|
||||
@@ -389,6 +402,32 @@ pub fn getOrigin(self: *Page, allocator: Allocator) !?[]const u8 {
|
||||
return try URL.getOrigin(allocator, self.url);
|
||||
}
|
||||
|
||||
// Add comon headers for a request:
|
||||
// * cookies
|
||||
// * referer
|
||||
pub fn headersForRequest(self: *Page, temp: Allocator, url: [:0]const u8, headers: *Http.Headers) !void {
|
||||
try self.requestCookie(.{}).headersForRequest(temp, url, headers);
|
||||
|
||||
// Build the referer
|
||||
const referer = blk: {
|
||||
if (self.referer_header == null) {
|
||||
// build the cache
|
||||
if (std.mem.startsWith(u8, self.url, "http")) {
|
||||
self.referer_header = try std.mem.concatWithSentinel(self.arena, u8, &.{ "Referer: ", self.url }, 0);
|
||||
} else {
|
||||
self.referer_header = "";
|
||||
}
|
||||
}
|
||||
|
||||
break :blk self.referer_header.?;
|
||||
};
|
||||
|
||||
// If the referer is empty, ignore the header.
|
||||
if (referer.len > 0) {
|
||||
try headers.add(referer);
|
||||
}
|
||||
}
|
||||
|
||||
const GetArenaOpts = struct {
|
||||
debug: []const u8,
|
||||
};
|
||||
@@ -450,14 +489,22 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
||||
// This assumption may be false when CDP Page.addScriptToEvaluateOnNewDocument is implemented
|
||||
self.documentIsComplete();
|
||||
|
||||
self._session.browser.notification.dispatch(.page_navigate, &.{
|
||||
self._session.notification.dispatch(.page_navigate, &.{
|
||||
.req_id = req_id,
|
||||
.opts = opts,
|
||||
.url = request_url,
|
||||
.timestamp = timestamp(.monotonic),
|
||||
});
|
||||
|
||||
self._session.browser.notification.dispatch(.page_navigated, &.{
|
||||
// Record telemetry for navigation
|
||||
self._session.browser.app.telemetry.record(.{
|
||||
.navigate = .{
|
||||
.tls = false, // about:blank is not TLS
|
||||
.proxy = self._session.browser.app.config.httpProxy() != null,
|
||||
},
|
||||
});
|
||||
|
||||
self._session.notification.dispatch(.page_navigated, &.{
|
||||
.req_id = req_id,
|
||||
.opts = .{
|
||||
.cdp_id = opts.cdp_id,
|
||||
@@ -492,13 +539,19 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
||||
|
||||
// We dispatch page_navigate event before sending the request.
|
||||
// It ensures the event page_navigated is not dispatched before this one.
|
||||
self._session.browser.notification.dispatch(.page_navigate, &.{
|
||||
self._session.notification.dispatch(.page_navigate, &.{
|
||||
.req_id = req_id,
|
||||
.opts = opts,
|
||||
.url = self.url,
|
||||
.timestamp = timestamp(.monotonic),
|
||||
});
|
||||
|
||||
// Record telemetry for navigation
|
||||
self._session.browser.app.telemetry.record(.{ .navigate = .{
|
||||
.tls = std.ascii.startsWithIgnoreCase(self.url, "https://"),
|
||||
.proxy = self._session.browser.app.config.httpProxy() != null,
|
||||
} });
|
||||
|
||||
session.navigation._current_navigation_kind = opts.kind;
|
||||
|
||||
http_client.request(.{
|
||||
@@ -509,6 +562,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
||||
.body = opts.body,
|
||||
.cookie_jar = &self._session.cookie_jar,
|
||||
.resource_type = .document,
|
||||
.notification = self._session.notification,
|
||||
.header_callback = pageHeaderDoneCallback,
|
||||
.data_callback = pageDataCallback,
|
||||
.done_callback = pageDoneCallback,
|
||||
@@ -526,12 +580,6 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
||||
// specifically for this type of lifetime.
|
||||
pub fn scheduleNavigation(self: *Page, request_url: []const u8, opts: NavigateOpts, priority: NavigationPriority) !void {
|
||||
if (self.canScheduleNavigation(priority) == false) {
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.browser, "ignored navigation", .{
|
||||
.target = request_url,
|
||||
.reason = opts.reason,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -608,7 +656,8 @@ pub fn documentIsLoaded(self: *Page) void {
|
||||
}
|
||||
|
||||
pub fn _documentIsLoaded(self: *Page) !void {
|
||||
const event = try Event.initTrusted("DOMContentLoaded", .{ .bubbles = true }, self);
|
||||
const event = try Event.initTrusted(.wrap("DOMContentLoaded"), .{ .bubbles = true }, self);
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
try self._event_manager.dispatch(
|
||||
self.document.asEventTarget(),
|
||||
event,
|
||||
@@ -642,7 +691,7 @@ pub fn documentIsComplete(self: *Page) void {
|
||||
std.debug.assert(self._navigated_options != null);
|
||||
}
|
||||
|
||||
self._session.browser.notification.dispatch(.page_navigated, &.{
|
||||
self._session.notification.dispatch(.page_navigated, &.{
|
||||
.req_id = self._req_id.?,
|
||||
.opts = self._navigated_options.?,
|
||||
.url = self.url,
|
||||
@@ -653,15 +702,37 @@ pub fn documentIsComplete(self: *Page) void {
|
||||
fn _documentIsComplete(self: *Page) !void {
|
||||
self.document._ready_state = .complete;
|
||||
|
||||
// dispatch window.load event
|
||||
const event = try Event.initTrusted("load", .{}, self);
|
||||
// this event is weird, it's dispatched directly on the window, but
|
||||
// with the document as the target
|
||||
|
||||
var ls: JS.Local.Scope = undefined;
|
||||
self.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
// Dispatch `_to_load` events before window.load.
|
||||
for (self._to_load.items) |element| {
|
||||
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self);
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
|
||||
// Dispatch inline event.
|
||||
blk: {
|
||||
const html_element = element.is(HtmlElement) orelse break :blk;
|
||||
|
||||
const listener = (try html_element.getOnLoad(self)) orelse break :blk;
|
||||
ls.toLocal(listener).call(void, .{}) catch |err| {
|
||||
log.warn(.event, "inline load event", .{ .element = element, .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
// Dispatch events registered to event manager.
|
||||
try self._event_manager.dispatch(element.asEventTarget(), event);
|
||||
}
|
||||
|
||||
// `_to_load` can be cleaned here.
|
||||
self._to_load.clearAndFree(self.arena);
|
||||
|
||||
// Dispatch window.load event.
|
||||
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self);
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
// This event is weird, it's dispatched directly on the window, but
|
||||
// with the document as the target.
|
||||
event._target = self.document.asEventTarget();
|
||||
try self._event_manager.dispatchWithFunction(
|
||||
self.window.asEventTarget(),
|
||||
@@ -670,10 +741,11 @@ fn _documentIsComplete(self: *Page) !void {
|
||||
.{ .inject_target = false, .context = "page load" },
|
||||
);
|
||||
|
||||
const pageshow_event = try PageTransitionEvent.initTrusted("pageshow", .{}, self);
|
||||
const pageshow_event = (try PageTransitionEvent.initTrusted(comptime .wrap("pageshow"), .{}, self)).asEvent();
|
||||
defer if (!pageshow_event._v8_handoff) pageshow_event.deinit(false);
|
||||
try self._event_manager.dispatchWithFunction(
|
||||
self.window.asEventTarget(),
|
||||
pageshow_event.asEvent(),
|
||||
pageshow_event,
|
||||
ls.toLocal(self.window._on_pageshow),
|
||||
.{ .context = "page show" },
|
||||
);
|
||||
@@ -720,7 +792,7 @@ fn pageDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||
switch (mime.content_type) {
|
||||
.text_html => self._parse_state = .{ .html = .{} },
|
||||
.application_json, .text_javascript, .text_css, .text_plain => {
|
||||
var arr: std.ArrayListUnmanaged(u8) = .empty;
|
||||
var arr: std.ArrayList(u8) = .empty;
|
||||
try arr.appendSlice(self.arena, "<html><head><meta charset=\"utf-8\"></head><body><pre>");
|
||||
self._parse_state = .{ .text = arr };
|
||||
},
|
||||
@@ -855,8 +927,8 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
|
||||
var timer = try std.time.Timer.start();
|
||||
var ms_remaining = wait_ms;
|
||||
|
||||
var scheduler = &self.scheduler;
|
||||
var http_client = self._session.browser.http_client;
|
||||
const browser = self._session.browser;
|
||||
var http_client = browser.http_client;
|
||||
|
||||
// I'd like the page to know NOTHING about cdp_socket / CDP, but the
|
||||
// fact is that the behavior of wait changes depending on whether or
|
||||
@@ -900,7 +972,7 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
|
||||
},
|
||||
.html, .complete => {
|
||||
if (self._queued_navigation != null) {
|
||||
return .navigate;
|
||||
return .done;
|
||||
}
|
||||
|
||||
// The HTML page was parsed. We now either have JS scripts to
|
||||
@@ -909,7 +981,7 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
|
||||
// scheduler.run could trigger new http transfers, so do not
|
||||
// store http_client.active BEFORE this call and then use
|
||||
// it AFTER.
|
||||
const ms_to_next_task = try scheduler.run();
|
||||
const ms_to_next_task = try browser.runMacrotasks();
|
||||
|
||||
const http_active = http_client.active;
|
||||
const total_network_activity = http_active + http_client.intercepted;
|
||||
@@ -1045,16 +1117,16 @@ fn printWaitAnalysis(self: *Page) void {
|
||||
|
||||
const now = milliTimestamp(.monotonic);
|
||||
{
|
||||
std.debug.print("\nhigh_priority schedule: {d}\n", .{self.scheduler.high_priority.count()});
|
||||
var it = self.scheduler.high_priority.iterator();
|
||||
std.debug.print("\nhigh_priority schedule: {d}\n", .{self.js.scheduler.high_priority.count()});
|
||||
var it = self.js.scheduler.high_priority.iterator();
|
||||
while (it.next()) |task| {
|
||||
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.run_at - now });
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std.debug.print("\nlow_priority schedule: {d}\n", .{self.scheduler.low_priority.count()});
|
||||
var it = self.scheduler.low_priority.iterator();
|
||||
std.debug.print("\nlow_priority schedule: {d}\n", .{self.js.scheduler.low_priority.count()});
|
||||
var it = self.js.scheduler.low_priority.iterator();
|
||||
while (it.next()) |task| {
|
||||
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.run_at - now });
|
||||
}
|
||||
@@ -1172,7 +1244,7 @@ pub fn getElementByIdFromNode(self: *Page, node: *Node, id: []const u8) ?*Elemen
|
||||
pub fn setAttrListener(
|
||||
self: *Page,
|
||||
element: *Element,
|
||||
listener_type: Element.KnownListener,
|
||||
listener_type: GlobalEventHandler,
|
||||
listener_callback: JS.Function.Global,
|
||||
) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
@@ -1182,7 +1254,7 @@ pub fn setAttrListener(
|
||||
});
|
||||
}
|
||||
|
||||
const key = element.calcAttrListenerKey(listener_type);
|
||||
const key = global_event_handlers.calculateKey(element.asEventTarget(), listener_type);
|
||||
const gop = try self._element_attr_listeners.getOrPut(self.arena, key);
|
||||
gop.value_ptr.* = listener_callback;
|
||||
}
|
||||
@@ -1191,9 +1263,10 @@ pub fn setAttrListener(
|
||||
pub fn getAttrListener(
|
||||
self: *const Page,
|
||||
element: *Element,
|
||||
listener_type: Element.KnownListener,
|
||||
listener_type: GlobalEventHandler,
|
||||
) ?JS.Function.Global {
|
||||
return self._element_attr_listeners.get(element.calcAttrListenerKey(listener_type));
|
||||
const key = global_event_handlers.calculateKey(element.asEventTarget(), listener_type);
|
||||
return self._element_attr_listeners.get(key);
|
||||
}
|
||||
|
||||
pub fn registerPerformanceObserver(self: *Page, observer: *PerformanceObserver) !void {
|
||||
@@ -1226,7 +1299,7 @@ pub fn notifyPerformanceObservers(self: *Page, entry: *Performance.Entry) !void
|
||||
}
|
||||
self._performance_delivery_scheduled = true;
|
||||
|
||||
return self.scheduler.add(
|
||||
return self.js.scheduler.add(
|
||||
self,
|
||||
struct {
|
||||
fn run(_page: *anyopaque) anyerror!?u32 {
|
||||
@@ -1378,10 +1451,12 @@ pub fn deliverSlotchangeEvents(self: *Page) void {
|
||||
self._slots_pending_slotchange.clearRetainingCapacity();
|
||||
|
||||
for (slots) |slot| {
|
||||
const event = Event.initTrusted("slotchange", .{ .bubbles = true }, self) catch |err| {
|
||||
const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, self) catch |err| {
|
||||
log.err(.page, "deliverSlotchange.init", .{ .err = err });
|
||||
continue;
|
||||
};
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
|
||||
const target = slot.asNode().asEventTarget();
|
||||
_ = target.dispatchEvent(event, self) catch |err| {
|
||||
log.err(.page, "deliverSlotchange.dispatch", .{ .err = err });
|
||||
@@ -1391,14 +1466,14 @@ pub fn deliverSlotchangeEvents(self: *Page) void {
|
||||
|
||||
fn notifyNetworkIdle(self: *Page) void {
|
||||
lp.assert(self._notified_network_idle == .done, "Page.notifyNetworkIdle", .{});
|
||||
self._session.browser.notification.dispatch(.page_network_idle, &.{
|
||||
self._session.notification.dispatch(.page_network_idle, &.{
|
||||
.timestamp = timestamp(.monotonic),
|
||||
});
|
||||
}
|
||||
|
||||
fn notifyNetworkAlmostIdle(self: *Page) void {
|
||||
lp.assert(self._notified_network_almost_idle == .done, "Page.notifyNetworkAlmostIdle", .{});
|
||||
self._session.browser.notification.dispatch(.page_network_almost_idle, &.{
|
||||
self._session.notification.dispatch(.page_network_almost_idle, &.{
|
||||
.timestamp = timestamp(.monotonic),
|
||||
});
|
||||
}
|
||||
@@ -2034,6 +2109,12 @@ pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const
|
||||
attribute_iterator,
|
||||
.{ ._proto = undefined, ._tag_name = String.init(undefined, "address", .{}) catch unreachable, ._tag = .address },
|
||||
),
|
||||
asUint("picture") => return self.createHtmlElementT(
|
||||
Element.Html.Picture,
|
||||
namespace,
|
||||
attribute_iterator,
|
||||
.{ ._proto = undefined },
|
||||
),
|
||||
else => {},
|
||||
},
|
||||
8 => switch (@as(u64, @bitCast(name[0..8].*))) {
|
||||
@@ -2235,236 +2316,6 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi
|
||||
}
|
||||
var attributes = try element.createAttributeList(self);
|
||||
while (list.next()) |attr| {
|
||||
// Event handlers can be provided like attributes; here we check if there's such.
|
||||
const name = attr.name.local;
|
||||
lp.assert(name.len != 0, "populateElementAttributes: 0-length attr name", .{ .attr = attr });
|
||||
// Idea here is to make this check as cheap as possible.
|
||||
const has_on_prefix = @as(u16, @bitCast([2]u8{ name.ptr[0], name.ptr[1 % name.len] })) == asUint("on");
|
||||
// We may have found an event handler.
|
||||
if (has_on_prefix) {
|
||||
// Must be usable as function.
|
||||
const func = self.js.stringToPersistedFunction(attr.value.slice()) catch continue;
|
||||
|
||||
// Longest known listener kind is 32 bytes long.
|
||||
const remaining: u6 = @truncate(name.len -| 2);
|
||||
const unsafe = name.ptr + 2;
|
||||
const Vec16x8 = @Vector(16, u8);
|
||||
const Vec32x8 = @Vector(32, u8);
|
||||
|
||||
switch (remaining) {
|
||||
3 => if (@as(u24, @bitCast(unsafe[0..3].*)) == asUint("cut")) {
|
||||
try self.setAttrListener(element, .cut, func);
|
||||
},
|
||||
4 => switch (@as(u32, @bitCast(unsafe[0..4].*))) {
|
||||
asUint("blur") => try self.setAttrListener(element, .blur, func),
|
||||
asUint("copy") => try self.setAttrListener(element, .copy, func),
|
||||
asUint("drag") => try self.setAttrListener(element, .drag, func),
|
||||
asUint("drop") => try self.setAttrListener(element, .drop, func),
|
||||
asUint("load") => try self.setAttrListener(element, .load, func),
|
||||
asUint("play") => try self.setAttrListener(element, .play, func),
|
||||
else => {},
|
||||
},
|
||||
5 => switch (@as(u40, @bitCast(unsafe[0..5].*))) {
|
||||
asUint("abort") => try self.setAttrListener(element, .abort, func),
|
||||
asUint("click") => try self.setAttrListener(element, .click, func),
|
||||
asUint("close") => try self.setAttrListener(element, .close, func),
|
||||
asUint("ended") => try self.setAttrListener(element, .ended, func),
|
||||
asUint("error") => try self.setAttrListener(element, .@"error", func),
|
||||
asUint("focus") => try self.setAttrListener(element, .focus, func),
|
||||
asUint("input") => try self.setAttrListener(element, .input, func),
|
||||
asUint("keyup") => try self.setAttrListener(element, .keyup, func),
|
||||
asUint("paste") => try self.setAttrListener(element, .paste, func),
|
||||
asUint("pause") => try self.setAttrListener(element, .pause, func),
|
||||
asUint("reset") => try self.setAttrListener(element, .reset, func),
|
||||
asUint("wheel") => try self.setAttrListener(element, .wheel, func),
|
||||
else => {},
|
||||
},
|
||||
6 => switch (@as(u48, @bitCast(unsafe[0..6].*))) {
|
||||
asUint("cancel") => try self.setAttrListener(element, .cancel, func),
|
||||
asUint("change") => try self.setAttrListener(element, .change, func),
|
||||
asUint("resize") => try self.setAttrListener(element, .resize, func),
|
||||
asUint("scroll") => try self.setAttrListener(element, .scroll, func),
|
||||
asUint("seeked") => try self.setAttrListener(element, .seeked, func),
|
||||
asUint("select") => try self.setAttrListener(element, .select, func),
|
||||
asUint("submit") => try self.setAttrListener(element, .submit, func),
|
||||
asUint("toggle") => try self.setAttrListener(element, .toggle, func),
|
||||
else => {},
|
||||
},
|
||||
7 => switch (@as(u56, @bitCast(unsafe[0..7].*))) {
|
||||
asUint("canplay") => try self.setAttrListener(element, .canplay, func),
|
||||
asUint("command") => try self.setAttrListener(element, .command, func),
|
||||
asUint("dragend") => try self.setAttrListener(element, .dragend, func),
|
||||
asUint("emptied") => try self.setAttrListener(element, .emptied, func),
|
||||
asUint("invalid") => try self.setAttrListener(element, .invalid, func),
|
||||
asUint("keydown") => try self.setAttrListener(element, .keydown, func),
|
||||
asUint("mouseup") => try self.setAttrListener(element, .mouseup, func),
|
||||
asUint("playing") => try self.setAttrListener(element, .playing, func),
|
||||
asUint("seeking") => try self.setAttrListener(element, .seeking, func),
|
||||
asUint("stalled") => try self.setAttrListener(element, .stalled, func),
|
||||
asUint("suspend") => try self.setAttrListener(element, .@"suspend", func),
|
||||
asUint("waiting") => try self.setAttrListener(element, .waiting, func),
|
||||
else => {},
|
||||
},
|
||||
8 => switch (@as(u64, @bitCast(unsafe[0..8].*))) {
|
||||
asUint("auxclick") => try self.setAttrListener(element, .auxclick, func),
|
||||
asUint("dblclick") => try self.setAttrListener(element, .dblclick, func),
|
||||
asUint("dragexit") => try self.setAttrListener(element, .dragexit, func),
|
||||
asUint("dragover") => try self.setAttrListener(element, .dragover, func),
|
||||
asUint("formdata") => try self.setAttrListener(element, .formdata, func),
|
||||
asUint("keypress") => try self.setAttrListener(element, .keypress, func),
|
||||
asUint("mouseout") => try self.setAttrListener(element, .mouseout, func),
|
||||
asUint("progress") => try self.setAttrListener(element, .progress, func),
|
||||
else => {},
|
||||
},
|
||||
// Won't fit to 64-bit integer; we do 2 checks.
|
||||
9 => switch (@as(u64, @bitCast(unsafe[0..8].*))) {
|
||||
asUint("cuechang") => if (unsafe[8] == 'e') try self.setAttrListener(element, .cuechange, func),
|
||||
asUint("dragente") => if (unsafe[8] == 'r') try self.setAttrListener(element, .dragenter, func),
|
||||
asUint("dragleav") => if (unsafe[8] == 'e') try self.setAttrListener(element, .dragleave, func),
|
||||
asUint("dragstar") => if (unsafe[8] == 't') try self.setAttrListener(element, .dragstart, func),
|
||||
asUint("loadstar") => if (unsafe[8] == 't') try self.setAttrListener(element, .loadstart, func),
|
||||
asUint("mousedow") => if (unsafe[8] == 'n') try self.setAttrListener(element, .mousedown, func),
|
||||
asUint("mousemov") => if (unsafe[8] == 'e') try self.setAttrListener(element, .mousemove, func),
|
||||
asUint("mouseove") => if (unsafe[8] == 'r') try self.setAttrListener(element, .mouseover, func),
|
||||
asUint("pointeru") => if (unsafe[8] == 'p') try self.setAttrListener(element, .pointerup, func),
|
||||
asUint("scrollen") => if (unsafe[8] == 'd') try self.setAttrListener(element, .scrollend, func),
|
||||
else => {},
|
||||
},
|
||||
10 => switch (@as(u64, @bitCast(unsafe[0..8].*))) {
|
||||
asUint("loadedda") => if (asUint("ta") == @as(u16, @bitCast(unsafe[8..10].*)))
|
||||
try self.setAttrListener(element, .loadeddata, func),
|
||||
asUint("pointero") => if (asUint("ut") == @as(u16, @bitCast(unsafe[8..10].*)))
|
||||
try self.setAttrListener(element, .pointerout, func),
|
||||
asUint("ratechan") => if (asUint("ge") == @as(u16, @bitCast(unsafe[8..10].*)))
|
||||
try self.setAttrListener(element, .ratechange, func),
|
||||
asUint("slotchan") => if (asUint("ge") == @as(u16, @bitCast(unsafe[8..10].*)))
|
||||
try self.setAttrListener(element, .slotchange, func),
|
||||
asUint("timeupda") => if (asUint("te") == @as(u16, @bitCast(unsafe[8..10].*)))
|
||||
try self.setAttrListener(element, .timeupdate, func),
|
||||
else => {},
|
||||
},
|
||||
11 => switch (@as(u64, @bitCast(unsafe[0..8].*))) {
|
||||
asUint("beforein") => if (asUint("put") == @as(u24, @bitCast(unsafe[8..11].*)))
|
||||
try self.setAttrListener(element, .beforeinput, func),
|
||||
asUint("beforema") => if (asUint("tch") == @as(u24, @bitCast(unsafe[8..11].*)))
|
||||
try self.setAttrListener(element, .beforematch, func),
|
||||
asUint("contextl") => if (asUint("ost") == @as(u24, @bitCast(unsafe[8..11].*)))
|
||||
try self.setAttrListener(element, .contextlost, func),
|
||||
asUint("contextm") => if (asUint("enu") == @as(u24, @bitCast(unsafe[8..11].*)))
|
||||
try self.setAttrListener(element, .contextmenu, func),
|
||||
asUint("pointerd") => if (asUint("own") == @as(u24, @bitCast(unsafe[8..11].*)))
|
||||
try self.setAttrListener(element, .pointerdown, func),
|
||||
asUint("pointerm") => if (asUint("ove") == @as(u24, @bitCast(unsafe[8..11].*)))
|
||||
try self.setAttrListener(element, .pointermove, func),
|
||||
asUint("pointero") => if (asUint("ver") == @as(u24, @bitCast(unsafe[8..11].*)))
|
||||
try self.setAttrListener(element, .pointerover, func),
|
||||
asUint("selectst") => if (asUint("art") == @as(u24, @bitCast(unsafe[8..11].*)))
|
||||
try self.setAttrListener(element, .selectstart, func),
|
||||
else => {},
|
||||
},
|
||||
12 => switch (@as(u64, @bitCast(unsafe[0..8].*))) {
|
||||
asUint("animatio") => if (asUint("nend") == @as(u32, @bitCast(unsafe[8..12].*)))
|
||||
try self.setAttrListener(element, .animationend, func),
|
||||
asUint("beforeto") => if (asUint("ggle") == @as(u32, @bitCast(unsafe[8..12].*)))
|
||||
try self.setAttrListener(element, .beforetoggle, func),
|
||||
asUint("pointere") => if (asUint("nter") == @as(u32, @bitCast(unsafe[8..12].*)))
|
||||
try self.setAttrListener(element, .pointerenter, func),
|
||||
asUint("pointerl") => if (asUint("eave") == @as(u32, @bitCast(unsafe[8..12].*)))
|
||||
try self.setAttrListener(element, .pointerleave, func),
|
||||
asUint("volumech") => if (asUint("ange") == @as(u32, @bitCast(unsafe[8..12].*)))
|
||||
try self.setAttrListener(element, .volumechange, func),
|
||||
else => {},
|
||||
},
|
||||
13 => switch (@as(u64, @bitCast(unsafe[0..8].*))) {
|
||||
asUint("pointerc") => if (asUint("ancel") == @as(u40, @bitCast(unsafe[8..13].*)))
|
||||
try self.setAttrListener(element, .pointercancel, func),
|
||||
asUint("transiti") => switch (@as(u40, @bitCast(unsafe[8..13].*))) {
|
||||
asUint("onend") => try self.setAttrListener(element, .transitionend, func),
|
||||
asUint("onrun") => try self.setAttrListener(element, .transitionrun, func),
|
||||
else => {},
|
||||
},
|
||||
else => {},
|
||||
},
|
||||
14 => switch (@as(u64, @bitCast(unsafe[0..8].*))) {
|
||||
asUint("animatio") => if (asUint("nstart") == @as(u48, @bitCast(unsafe[8..14].*)))
|
||||
try self.setAttrListener(element, .animationstart, func),
|
||||
asUint("canplayt") => if (asUint("hrough") == @as(u48, @bitCast(unsafe[8..14].*)))
|
||||
try self.setAttrListener(element, .canplaythrough, func),
|
||||
asUint("duration") => if (asUint("change") == @as(u48, @bitCast(unsafe[8..14].*)))
|
||||
try self.setAttrListener(element, .durationchange, func),
|
||||
asUint("loadedme") => if (asUint("tadata") == @as(u48, @bitCast(unsafe[8..14].*)))
|
||||
try self.setAttrListener(element, .loadedmetadata, func),
|
||||
else => {},
|
||||
},
|
||||
15 => switch (@as(u64, @bitCast(unsafe[0..8].*))) {
|
||||
asUint("animatio") => if (asUint("ncancel") == @as(u56, @bitCast(unsafe[8..15].*)))
|
||||
try self.setAttrListener(element, .animationcancel, func),
|
||||
asUint("contextr") => if (asUint("estored") == @as(u56, @bitCast(unsafe[8..15].*)))
|
||||
try self.setAttrListener(element, .contextrestored, func),
|
||||
asUint("fullscre") => if (asUint("enerror") == @as(u56, @bitCast(unsafe[8..15].*)))
|
||||
try self.setAttrListener(element, .fullscreenerror, func),
|
||||
asUint("selectio") => if (asUint("nchange") == @as(u56, @bitCast(unsafe[8..15].*)))
|
||||
try self.setAttrListener(element, .selectionchange, func),
|
||||
asUint("transiti") => if (asUint("onstart") == @as(u56, @bitCast(unsafe[8..15].*)))
|
||||
try self.setAttrListener(element, .transitionstart, func),
|
||||
else => {},
|
||||
},
|
||||
// Can't switch on vector types.
|
||||
16 => {
|
||||
const as_vector: Vec16x8 = unsafe[0..16].*;
|
||||
|
||||
if (@reduce(.And, as_vector == @as(Vec16x8, "fullscreenchange".*))) {
|
||||
try self.setAttrListener(element, .fullscreenchange, func);
|
||||
} else if (@reduce(.And, as_vector == @as(Vec16x8, "pointerrawupdate".*))) {
|
||||
try self.setAttrListener(element, .pointerrawupdate, func);
|
||||
} else if (@reduce(.And, as_vector == @as(Vec16x8, "transitioncancel".*))) {
|
||||
try self.setAttrListener(element, .transitioncancel, func);
|
||||
}
|
||||
},
|
||||
17 => {
|
||||
const as_vector: Vec16x8 = unsafe[0..16].*;
|
||||
|
||||
const dirty = @reduce(.And, as_vector == @as(Vec16x8, "gotpointercaptur".*)) and
|
||||
unsafe[16] == 'e';
|
||||
if (dirty) {
|
||||
try self.setAttrListener(element, .gotpointercapture, func);
|
||||
}
|
||||
},
|
||||
18 => {
|
||||
const as_vector: Vec16x8 = unsafe[0..16].*;
|
||||
|
||||
const is_animationiteration = @reduce(.And, as_vector == @as(Vec16x8, "animationiterati".*)) and
|
||||
asUint("on") == @as(u16, @bitCast(unsafe[16..18].*));
|
||||
if (is_animationiteration) {
|
||||
try self.setAttrListener(element, .animationiteration, func);
|
||||
} else {
|
||||
const is_lostpointercapture = @reduce(.And, as_vector == @as(Vec16x8, "lostpointercaptu".*)) and
|
||||
asUint("re") == @as(u16, @bitCast(unsafe[16..18].*));
|
||||
if (is_lostpointercapture) {
|
||||
try self.setAttrListener(element, .lostpointercapture, func);
|
||||
}
|
||||
}
|
||||
},
|
||||
23 => {
|
||||
const as_vector: Vec16x8 = unsafe[0..16].*;
|
||||
|
||||
const dirty = @reduce(.And, as_vector == @as(Vec16x8, "securitypolicyvi".*)) and
|
||||
asUint("olation") == @as(u56, @bitCast(unsafe[16..23].*));
|
||||
if (dirty) {
|
||||
try self.setAttrListener(element, .securitypolicyviolation, func);
|
||||
}
|
||||
},
|
||||
32 => {
|
||||
const as_vector: Vec32x8 = unsafe[0..32].*;
|
||||
|
||||
if (@reduce(.And, as_vector == @as(Vec32x8, "contentvisibilityautostatechange".*))) {
|
||||
try self.setAttrListener(element, .contentvisibilityautostatechange, func);
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
try attributes.putNew(attr.name.local.slice(), attr.value.slice(), self);
|
||||
}
|
||||
}
|
||||
@@ -3058,9 +2909,9 @@ const ParseState = union(enum) {
|
||||
pre,
|
||||
complete,
|
||||
err: anyerror,
|
||||
html: std.ArrayListUnmanaged(u8),
|
||||
text: std.ArrayListUnmanaged(u8),
|
||||
raw: std.ArrayListUnmanaged(u8),
|
||||
html: std.ArrayList(u8),
|
||||
text: std.ArrayList(u8),
|
||||
raw: std.ArrayList(u8),
|
||||
raw_done: []const u8,
|
||||
};
|
||||
|
||||
@@ -3197,14 +3048,16 @@ pub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void {
|
||||
.y = y,
|
||||
});
|
||||
}
|
||||
const event = try @import("webapi/event/MouseEvent.zig").init("click", .{
|
||||
const event = (try @import("webapi/event/MouseEvent.zig").init("click", .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.composed = true,
|
||||
.clientX = x,
|
||||
.clientY = y,
|
||||
}, self);
|
||||
try self._event_manager.dispatch(target.asEventTarget(), event.asEvent());
|
||||
}, self)).asEvent();
|
||||
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
try self._event_manager.dispatch(target.asEventTarget(), event);
|
||||
}
|
||||
|
||||
// callback when the "click" event reaches the pages.
|
||||
@@ -3242,12 +3095,12 @@ pub fn handleClick(self: *Page, target: *Node) !void {
|
||||
}, .anchor);
|
||||
},
|
||||
.input => |input| switch (input._input_type) {
|
||||
.submit => return self.submitForm(element, input.getForm(self)),
|
||||
.submit => return self.submitForm(element, input.getForm(self), .{}),
|
||||
else => self.window._document._active_element = element,
|
||||
},
|
||||
.button => |button| {
|
||||
if (std.mem.eql(u8, button.getType(), "submit")) {
|
||||
return self.submitForm(element, button.getForm(self));
|
||||
return self.submitForm(element, button.getForm(self), .{});
|
||||
}
|
||||
},
|
||||
.select, .textarea => self.window._document._active_element = element,
|
||||
@@ -3256,6 +3109,9 @@ pub fn handleClick(self: *Page, target: *Node) !void {
|
||||
}
|
||||
|
||||
pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
|
||||
const event = keyboard_event.asEvent();
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
|
||||
const element = self.window._document._active_element orelse return;
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.page, "page keydown", .{
|
||||
@@ -3264,7 +3120,7 @@ pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
|
||||
.key = keyboard_event._key,
|
||||
});
|
||||
}
|
||||
try self._event_manager.dispatch(element.asEventTarget(), keyboard_event.asEvent());
|
||||
try self._event_manager.dispatch(element.asEventTarget(), event);
|
||||
}
|
||||
|
||||
pub fn handleKeydown(self: *Page, target: *Node, event: *Event) !void {
|
||||
@@ -3277,7 +3133,7 @@ pub fn handleKeydown(self: *Page, target: *Node, event: *Event) !void {
|
||||
|
||||
if (target.is(Element.Html.Input)) |input| {
|
||||
if (key == .Enter) {
|
||||
return self.submitForm(input.asElement(), input.getForm(self));
|
||||
return self.submitForm(input.asElement(), input.getForm(self), .{});
|
||||
}
|
||||
|
||||
// Don't handle text input for radio/checkbox
|
||||
@@ -3307,7 +3163,10 @@ pub fn handleKeydown(self: *Page, target: *Node, event: *Event) !void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form) !void {
|
||||
const SubmitFormOpts = struct {
|
||||
fire_event: bool = true,
|
||||
};
|
||||
pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form, submit_opts: SubmitFormOpts) !void {
|
||||
const form = form_ orelse return;
|
||||
|
||||
if (submitter_) |submitter| {
|
||||
@@ -3315,8 +3174,36 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.canScheduleNavigation(.form) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const form_element = form.asElement();
|
||||
|
||||
if (submit_opts.fire_event) {
|
||||
const submit_event = try Event.initTrusted(comptime .wrap("submit"), .{ .bubbles = true, .cancelable = true }, self);
|
||||
defer if (!submit_event._v8_handoff) submit_event.deinit(false);
|
||||
|
||||
const onsubmit_handler = try form.asHtmlElement().getOnSubmit(self);
|
||||
|
||||
var ls: JS.Local.Scope = undefined;
|
||||
self.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
try self._event_manager.dispatchWithFunction(
|
||||
form_element.asEventTarget(),
|
||||
submit_event,
|
||||
ls.toLocal(onsubmit_handler),
|
||||
.{ .context = "form submit" },
|
||||
);
|
||||
|
||||
// If the submit event was prevented, don't submit the form
|
||||
if (submit_event._prevent_default) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const FormData = @import("webapi/net/FormData.zig");
|
||||
// The submitter can be an input box (if enter was entered on the box)
|
||||
// I don't think this is technically correct, but FormData handles it ok
|
||||
|
||||
884
src/browser/Robots.zig
Normal file
884
src/browser/Robots.zig
Normal file
@@ -0,0 +1,884 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const log = @import("../log.zig");
|
||||
|
||||
pub const Rule = union(enum) {
|
||||
allow: []const u8,
|
||||
disallow: []const u8,
|
||||
};
|
||||
|
||||
pub const Key = enum {
|
||||
@"user-agent",
|
||||
allow,
|
||||
disallow,
|
||||
};
|
||||
|
||||
/// https://www.rfc-editor.org/rfc/rfc9309.html
|
||||
pub const Robots = @This();
|
||||
pub const empty: Robots = .{ .rules = &.{} };
|
||||
|
||||
pub const RobotStore = struct {
|
||||
const RobotsEntry = union(enum) {
|
||||
present: Robots,
|
||||
absent,
|
||||
};
|
||||
|
||||
pub const RobotsMap = std.HashMapUnmanaged([]const u8, RobotsEntry, struct {
|
||||
const Context = @This();
|
||||
|
||||
pub fn hash(_: Context, value: []const u8) u32 {
|
||||
var hasher = std.hash.Wyhash.init(value.len);
|
||||
for (value) |c| {
|
||||
std.hash.autoHash(&hasher, std.ascii.toLower(c));
|
||||
}
|
||||
return @truncate(hasher.final());
|
||||
}
|
||||
|
||||
pub fn eql(_: Context, a: []const u8, b: []const u8) bool {
|
||||
return std.ascii.eqlIgnoreCase(a, b);
|
||||
}
|
||||
}, 80);
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
map: RobotsMap,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) RobotStore {
|
||||
return .{ .allocator = allocator, .map = .empty };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *RobotStore) void {
|
||||
var iter = self.map.iterator();
|
||||
|
||||
while (iter.next()) |entry| {
|
||||
self.allocator.free(entry.key_ptr.*);
|
||||
|
||||
switch (entry.value_ptr.*) {
|
||||
.present => |*robots| robots.deinit(self.allocator),
|
||||
.absent => {},
|
||||
}
|
||||
}
|
||||
|
||||
self.map.deinit(self.allocator);
|
||||
}
|
||||
|
||||
pub fn get(self: *RobotStore, url: []const u8) ?RobotsEntry {
|
||||
return self.map.get(url);
|
||||
}
|
||||
|
||||
pub fn robotsFromBytes(self: *RobotStore, user_agent: []const u8, bytes: []const u8) !Robots {
|
||||
return try Robots.fromBytes(self.allocator, user_agent, bytes);
|
||||
}
|
||||
|
||||
pub fn put(self: *RobotStore, url: []const u8, robots: Robots) !void {
|
||||
const duped = try self.allocator.dupe(u8, url);
|
||||
try self.map.put(self.allocator, duped, .{ .present = robots });
|
||||
}
|
||||
|
||||
pub fn putAbsent(self: *RobotStore, url: []const u8) !void {
|
||||
const duped = try self.allocator.dupe(u8, url);
|
||||
try self.map.put(self.allocator, duped, .absent);
|
||||
}
|
||||
};
|
||||
|
||||
rules: []const Rule,
|
||||
|
||||
const State = struct {
|
||||
entry: enum {
|
||||
not_in_entry,
|
||||
in_other_entry,
|
||||
in_our_entry,
|
||||
in_wildcard_entry,
|
||||
},
|
||||
has_rules: bool = false,
|
||||
};
|
||||
|
||||
fn freeRulesInList(allocator: std.mem.Allocator, rules: []const Rule) void {
|
||||
for (rules) |rule| {
|
||||
switch (rule) {
|
||||
.allow => |value| allocator.free(value),
|
||||
.disallow => |value| allocator.free(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parseRulesWithUserAgent(
|
||||
allocator: std.mem.Allocator,
|
||||
user_agent: []const u8,
|
||||
raw_bytes: []const u8,
|
||||
) ![]const Rule {
|
||||
var rules: std.ArrayList(Rule) = .empty;
|
||||
defer rules.deinit(allocator);
|
||||
|
||||
var wildcard_rules: std.ArrayList(Rule) = .empty;
|
||||
defer wildcard_rules.deinit(allocator);
|
||||
|
||||
var state: State = .{ .entry = .not_in_entry, .has_rules = false };
|
||||
|
||||
// https://en.wikipedia.org/wiki/Byte_order_mark
|
||||
const UTF8_BOM: []const u8 = &.{ 0xEF, 0xBB, 0xBF };
|
||||
|
||||
// Strip UTF8 BOM
|
||||
const bytes = if (std.mem.startsWith(u8, raw_bytes, UTF8_BOM))
|
||||
raw_bytes[3..]
|
||||
else
|
||||
raw_bytes;
|
||||
|
||||
var iter = std.mem.splitScalar(u8, bytes, '\n');
|
||||
while (iter.next()) |line| {
|
||||
const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace);
|
||||
|
||||
// Skip all comment lines.
|
||||
if (std.mem.startsWith(u8, trimmed, "#")) continue;
|
||||
|
||||
// Remove end of line comment.
|
||||
const true_line = if (std.mem.indexOfScalar(u8, trimmed, '#')) |pos|
|
||||
std.mem.trimRight(u8, trimmed[0..pos], &std.ascii.whitespace)
|
||||
else
|
||||
trimmed;
|
||||
|
||||
if (true_line.len == 0) continue;
|
||||
|
||||
const colon_idx = std.mem.indexOfScalar(u8, true_line, ':') orelse {
|
||||
log.warn(.browser, "robots line missing colon", .{ .line = line });
|
||||
continue;
|
||||
};
|
||||
const key_str = try std.ascii.allocLowerString(allocator, true_line[0..colon_idx]);
|
||||
defer allocator.free(key_str);
|
||||
|
||||
const key = std.meta.stringToEnum(Key, key_str) orelse continue;
|
||||
const value = std.mem.trim(u8, true_line[colon_idx + 1 ..], &std.ascii.whitespace);
|
||||
|
||||
switch (key) {
|
||||
.@"user-agent" => {
|
||||
if (state.has_rules) {
|
||||
state = .{ .entry = .not_in_entry, .has_rules = false };
|
||||
}
|
||||
|
||||
switch (state.entry) {
|
||||
.in_other_entry => {
|
||||
if (std.ascii.eqlIgnoreCase(user_agent, value)) {
|
||||
state.entry = .in_our_entry;
|
||||
}
|
||||
},
|
||||
.in_our_entry => {},
|
||||
.in_wildcard_entry => {
|
||||
if (std.ascii.eqlIgnoreCase(user_agent, value)) {
|
||||
state.entry = .in_our_entry;
|
||||
}
|
||||
},
|
||||
.not_in_entry => {
|
||||
if (std.ascii.eqlIgnoreCase(user_agent, value)) {
|
||||
state.entry = .in_our_entry;
|
||||
} else if (std.mem.eql(u8, "*", value)) {
|
||||
state.entry = .in_wildcard_entry;
|
||||
} else {
|
||||
state.entry = .in_other_entry;
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
.allow => {
|
||||
defer state.has_rules = true;
|
||||
|
||||
switch (state.entry) {
|
||||
.in_our_entry => {
|
||||
const duped_value = try allocator.dupe(u8, value);
|
||||
errdefer allocator.free(duped_value);
|
||||
try rules.append(allocator, .{ .allow = duped_value });
|
||||
},
|
||||
.in_other_entry => {},
|
||||
.in_wildcard_entry => {
|
||||
const duped_value = try allocator.dupe(u8, value);
|
||||
errdefer allocator.free(duped_value);
|
||||
try wildcard_rules.append(allocator, .{ .allow = duped_value });
|
||||
},
|
||||
.not_in_entry => {
|
||||
log.warn(.browser, "robots unexpected rule", .{ .rule = "allow" });
|
||||
continue;
|
||||
},
|
||||
}
|
||||
},
|
||||
.disallow => {
|
||||
defer state.has_rules = true;
|
||||
|
||||
switch (state.entry) {
|
||||
.in_our_entry => {
|
||||
const duped_value = try allocator.dupe(u8, value);
|
||||
errdefer allocator.free(duped_value);
|
||||
try rules.append(allocator, .{ .disallow = duped_value });
|
||||
},
|
||||
.in_other_entry => {},
|
||||
.in_wildcard_entry => {
|
||||
const duped_value = try allocator.dupe(u8, value);
|
||||
errdefer allocator.free(duped_value);
|
||||
try wildcard_rules.append(allocator, .{ .disallow = duped_value });
|
||||
},
|
||||
.not_in_entry => {
|
||||
log.warn(.browser, "robots unexpected rule", .{ .rule = "disallow" });
|
||||
continue;
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// If we have rules for our specific User-Agent, we will use those rules.
|
||||
// If we don't have any rules, we fallback to using the wildcard ("*") rules.
|
||||
if (rules.items.len > 0) {
|
||||
freeRulesInList(allocator, wildcard_rules.items);
|
||||
return try rules.toOwnedSlice(allocator);
|
||||
} else {
|
||||
freeRulesInList(allocator, rules.items);
|
||||
return try wildcard_rules.toOwnedSlice(allocator);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fromBytes(allocator: std.mem.Allocator, user_agent: []const u8, bytes: []const u8) !Robots {
|
||||
const rules = try parseRulesWithUserAgent(allocator, user_agent, bytes);
|
||||
return .{ .rules = rules };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Robots, allocator: std.mem.Allocator) void {
|
||||
freeRulesInList(allocator, self.rules);
|
||||
allocator.free(self.rules);
|
||||
}
|
||||
|
||||
fn matchPatternRecursive(pattern: []const u8, path: []const u8, exact_match: bool) bool {
|
||||
if (pattern.len == 0) return true;
|
||||
|
||||
const star_pos = std.mem.indexOfScalar(u8, pattern, '*') orelse {
|
||||
if (exact_match) {
|
||||
// If we end in '$', we must be exactly equal.
|
||||
return std.mem.eql(u8, path, pattern);
|
||||
} else {
|
||||
// Otherwise, we are just a prefix.
|
||||
return std.mem.startsWith(u8, path, pattern);
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure the prefix before the '*' matches.
|
||||
if (!std.mem.startsWith(u8, path, pattern[0..star_pos])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const suffix_pattern = pattern[star_pos + 1 ..];
|
||||
if (suffix_pattern.len == 0) return true;
|
||||
|
||||
var i: usize = star_pos;
|
||||
while (i <= path.len) : (i += 1) {
|
||||
if (matchPatternRecursive(suffix_pattern, path[i..], exact_match)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// There are rules for how the pattern in robots.txt should be matched.
|
||||
///
|
||||
/// * should match 0 or more of any character.
|
||||
/// $ should signify the end of a path, making it exact.
|
||||
/// otherwise, it is a prefix path.
|
||||
fn matchPattern(pattern: []const u8, path: []const u8) ?usize {
|
||||
if (pattern.len == 0) return 0;
|
||||
const exact_match = pattern[pattern.len - 1] == '$';
|
||||
const inner_pattern = if (exact_match) pattern[0 .. pattern.len - 1] else pattern;
|
||||
|
||||
if (matchPatternRecursive(
|
||||
inner_pattern,
|
||||
path,
|
||||
exact_match,
|
||||
)) return pattern.len else return null;
|
||||
}
|
||||
|
||||
pub fn isAllowed(self: *const Robots, path: []const u8) bool {
|
||||
const rules = self.rules;
|
||||
|
||||
var longest_match_len: usize = 0;
|
||||
var is_allowed_result = true;
|
||||
|
||||
for (rules) |rule| {
|
||||
switch (rule) {
|
||||
.allow => |pattern| {
|
||||
if (matchPattern(pattern, path)) |len| {
|
||||
// Longest or Last Wins.
|
||||
if (len >= longest_match_len) {
|
||||
longest_match_len = len;
|
||||
is_allowed_result = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
.disallow => |pattern| {
|
||||
if (pattern.len == 0) continue;
|
||||
|
||||
if (matchPattern(pattern, path)) |len| {
|
||||
// Longest or Last Wins.
|
||||
if (len >= longest_match_len) {
|
||||
longest_match_len = len;
|
||||
is_allowed_result = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return is_allowed_result;
|
||||
}
|
||||
|
||||
test "Robots: simple robots.txt" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const file =
|
||||
\\User-agent: *
|
||||
\\Disallow: /private/
|
||||
\\Allow: /public/
|
||||
\\
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /admin/
|
||||
\\
|
||||
;
|
||||
|
||||
const rules = try parseRulesWithUserAgent(allocator, "GoogleBot", file);
|
||||
defer {
|
||||
freeRulesInList(allocator, rules);
|
||||
allocator.free(rules);
|
||||
}
|
||||
|
||||
try std.testing.expectEqual(1, rules.len);
|
||||
try std.testing.expectEqualStrings("/admin/", rules[0].disallow);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - simple prefix" {
|
||||
try std.testing.expect(matchPattern("/admin", "/admin/page") != null);
|
||||
try std.testing.expect(matchPattern("/admin", "/admin") != null);
|
||||
try std.testing.expect(matchPattern("/admin", "/other") == null);
|
||||
try std.testing.expect(matchPattern("/admin/page", "/admin") == null);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - single wildcard" {
|
||||
try std.testing.expect(matchPattern("/admin/*", "/admin/") != null);
|
||||
try std.testing.expect(matchPattern("/admin/*", "/admin/page") != null);
|
||||
try std.testing.expect(matchPattern("/admin/*", "/admin/page/subpage") != null);
|
||||
try std.testing.expect(matchPattern("/admin/*", "/other/page") == null);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - wildcard in middle" {
|
||||
try std.testing.expect(matchPattern("/abc/*/xyz", "/abc/def/xyz") != null);
|
||||
try std.testing.expect(matchPattern("/abc/*/xyz", "/abc/def/ghi/xyz") != null);
|
||||
try std.testing.expect(matchPattern("/abc/*/xyz", "/abc/def") == null);
|
||||
try std.testing.expect(matchPattern("/abc/*/xyz", "/other/def/xyz") == null);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - complex wildcard case" {
|
||||
try std.testing.expect(matchPattern("/abc/*/def/xyz", "/abc/def/def/xyz") != null);
|
||||
try std.testing.expect(matchPattern("/abc/*/def/xyz", "/abc/ANYTHING/def/xyz") != null);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - multiple wildcards" {
|
||||
try std.testing.expect(matchPattern("/a/*/b/*/c", "/a/x/b/y/c") != null);
|
||||
try std.testing.expect(matchPattern("/a/*/b/*/c", "/a/x/y/b/z/w/c") != null);
|
||||
try std.testing.expect(matchPattern("/*.php", "/index.php") != null);
|
||||
try std.testing.expect(matchPattern("/*.php", "/admin/index.php") != null);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - end anchor" {
|
||||
try std.testing.expect(matchPattern("/*.php$", "/index.php") != null);
|
||||
try std.testing.expect(matchPattern("/*.php$", "/index.php?param=value") == null);
|
||||
try std.testing.expect(matchPattern("/admin$", "/admin") != null);
|
||||
try std.testing.expect(matchPattern("/admin$", "/admin/") == null);
|
||||
try std.testing.expect(matchPattern("/fish$", "/fish") != null);
|
||||
try std.testing.expect(matchPattern("/fish$", "/fishheads") == null);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - wildcard with extension" {
|
||||
try std.testing.expect(matchPattern("/fish*.php", "/fish.php") != null);
|
||||
try std.testing.expect(matchPattern("/fish*.php", "/fishheads.php") != null);
|
||||
try std.testing.expect(matchPattern("/fish*.php", "/fish/salmon.php") != null);
|
||||
try std.testing.expect(matchPattern("/fish*.php", "/fish.asp") == null);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - empty and edge cases" {
|
||||
try std.testing.expect(matchPattern("", "/anything") != null);
|
||||
try std.testing.expect(matchPattern("/", "/") != null);
|
||||
try std.testing.expect(matchPattern("*", "/anything") != null);
|
||||
try std.testing.expect(matchPattern("/*", "/anything") != null);
|
||||
try std.testing.expect(matchPattern("$", "") != null);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - real world examples" {
|
||||
try std.testing.expect(matchPattern("/", "/anything") != null);
|
||||
|
||||
try std.testing.expect(matchPattern("/admin/", "/admin/page") != null);
|
||||
try std.testing.expect(matchPattern("/admin/", "/public/page") == null);
|
||||
|
||||
try std.testing.expect(matchPattern("/*.pdf$", "/document.pdf") != null);
|
||||
try std.testing.expect(matchPattern("/*.pdf$", "/document.pdf.bak") == null);
|
||||
|
||||
try std.testing.expect(matchPattern("/*?", "/page?param=value") != null);
|
||||
try std.testing.expect(matchPattern("/*?", "/page") == null);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - basic allow/disallow" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "MyBot",
|
||||
\\User-agent: MyBot
|
||||
\\Disallow: /admin/
|
||||
\\Allow: /public/
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/") == true);
|
||||
try std.testing.expect(robots.isAllowed("/public/page") == true);
|
||||
try std.testing.expect(robots.isAllowed("/admin/secret") == false);
|
||||
try std.testing.expect(robots.isAllowed("/other/page") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - longest match wins" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "TestBot",
|
||||
\\User-agent: TestBot
|
||||
\\Disallow: /admin/
|
||||
\\Allow: /admin/public/
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/admin/secret") == false);
|
||||
try std.testing.expect(robots.isAllowed("/admin/public/page") == true);
|
||||
try std.testing.expect(robots.isAllowed("/admin/public/") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - specific user-agent vs wildcard" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots1 = try Robots.fromBytes(allocator, "Googlebot",
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
\\User-agent: *
|
||||
\\Disallow: /admin/
|
||||
\\
|
||||
);
|
||||
defer robots1.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots1.isAllowed("/private/page") == false);
|
||||
try std.testing.expect(robots1.isAllowed("/admin/page") == true);
|
||||
|
||||
// Test with other bot (should use wildcard)
|
||||
var robots2 = try Robots.fromBytes(allocator, "OtherBot",
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
\\User-agent: *
|
||||
\\Disallow: /admin/
|
||||
\\
|
||||
);
|
||||
defer robots2.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots2.isAllowed("/private/page") == true);
|
||||
try std.testing.expect(robots2.isAllowed("/admin/page") == false);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - case insensitive user-agent" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots1 = try Robots.fromBytes(allocator, "googlebot",
|
||||
\\User-agent: GoogleBot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
);
|
||||
defer robots1.deinit(allocator);
|
||||
try std.testing.expect(robots1.isAllowed("/private/") == false);
|
||||
|
||||
var robots2 = try Robots.fromBytes(allocator, "GOOGLEBOT",
|
||||
\\User-agent: GoogleBot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
);
|
||||
defer robots2.deinit(allocator);
|
||||
try std.testing.expect(robots2.isAllowed("/private/") == false);
|
||||
|
||||
var robots3 = try Robots.fromBytes(allocator, "GoOgLeBoT",
|
||||
\\User-agent: GoogleBot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
);
|
||||
defer robots3.deinit(allocator);
|
||||
try std.testing.expect(robots3.isAllowed("/private/") == false);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - merged rules for same agent" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "Googlebot",
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /admin/
|
||||
\\
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/admin/page") == false);
|
||||
try std.testing.expect(robots.isAllowed("/private/page") == false);
|
||||
try std.testing.expect(robots.isAllowed("/public/page") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - wildcards in patterns" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "Bot",
|
||||
\\User-agent: Bot
|
||||
\\Disallow: /*.php$
|
||||
\\Allow: /index.php$
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/page.php") == false);
|
||||
try std.testing.expect(robots.isAllowed("/index.php") == true);
|
||||
try std.testing.expect(robots.isAllowed("/page.php?param=1") == true);
|
||||
try std.testing.expect(robots.isAllowed("/page.html") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - empty disallow allows everything" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "Bot",
|
||||
\\User-agent: Bot
|
||||
\\Disallow:
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/anything") == true);
|
||||
try std.testing.expect(robots.isAllowed("/") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - no rules" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "Bot", "");
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/anything") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - disallow all" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "Bot",
|
||||
\\User-agent: Bot
|
||||
\\Disallow: /
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/") == false);
|
||||
try std.testing.expect(robots.isAllowed("/anything") == false);
|
||||
try std.testing.expect(robots.isAllowed("/admin/page") == false);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - multiple user-agents in same entry" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots1 = try Robots.fromBytes(allocator, "Googlebot",
|
||||
\\User-agent: Googlebot
|
||||
\\User-agent: Bingbot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
);
|
||||
defer robots1.deinit(allocator);
|
||||
try std.testing.expect(robots1.isAllowed("/private/") == false);
|
||||
|
||||
var robots2 = try Robots.fromBytes(allocator, "Bingbot",
|
||||
\\User-agent: Googlebot
|
||||
\\User-agent: Bingbot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
);
|
||||
defer robots2.deinit(allocator);
|
||||
try std.testing.expect(robots2.isAllowed("/private/") == false);
|
||||
|
||||
var robots3 = try Robots.fromBytes(allocator, "OtherBot",
|
||||
\\User-agent: Googlebot
|
||||
\\User-agent: Bingbot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
);
|
||||
defer robots3.deinit(allocator);
|
||||
try std.testing.expect(robots3.isAllowed("/private/") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - wildcard fallback" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "UnknownBot",
|
||||
\\User-agent: *
|
||||
\\Disallow: /admin/
|
||||
\\Allow: /admin/public/
|
||||
\\
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/admin/secret") == false);
|
||||
try std.testing.expect(robots.isAllowed("/admin/public/page") == true);
|
||||
try std.testing.expect(robots.isAllowed("/private/") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - complex real-world example" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "MyBot",
|
||||
\\User-agent: *
|
||||
\\Disallow: /cgi-bin/
|
||||
\\Disallow: /tmp/
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
\\User-agent: MyBot
|
||||
\\Disallow: /admin/
|
||||
\\Disallow: /*.pdf$
|
||||
\\Allow: /public/*.pdf$
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/") == true);
|
||||
try std.testing.expect(robots.isAllowed("/admin/dashboard") == false);
|
||||
try std.testing.expect(robots.isAllowed("/docs/guide.pdf") == false);
|
||||
try std.testing.expect(robots.isAllowed("/public/manual.pdf") == true);
|
||||
try std.testing.expect(robots.isAllowed("/page.html") == true);
|
||||
try std.testing.expect(robots.isAllowed("/cgi-bin/script.sh") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - order doesn't matter for same length" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "Bot",
|
||||
\\User-agent: Bot
|
||||
\\ # WOW!!
|
||||
\\Allow: /page
|
||||
\\Disallow: /page
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/page") == false);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - empty file uses wildcard defaults" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "MyBot",
|
||||
\\User-agent: * # ABCDEF!!!
|
||||
\\Disallow: /admin/
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/admin/") == false);
|
||||
try std.testing.expect(robots.isAllowed("/public/") == true);
|
||||
}
|
||||
test "Robots: isAllowed - wildcard entry with multiple user-agents including specific" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "Googlebot",
|
||||
\\User-agent: *
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /shared/
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/shared/") == false);
|
||||
try std.testing.expect(robots.isAllowed("/other/") == true);
|
||||
|
||||
var robots2 = try Robots.fromBytes(allocator, "Bingbot",
|
||||
\\User-agent: *
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /shared/
|
||||
\\
|
||||
);
|
||||
defer robots2.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots2.isAllowed("/shared/") == false);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - specific agent appears after wildcard in entry" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "MyBot",
|
||||
\\User-agent: *
|
||||
\\User-agent: MyBot
|
||||
\\User-agent: Bingbot
|
||||
\\Disallow: /admin/
|
||||
\\Allow: /admin/public/
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/admin/secret") == false);
|
||||
try std.testing.expect(robots.isAllowed("/admin/public/page") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - wildcard should not override specific entry" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "Googlebot",
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
\\User-agent: *
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /admin/
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/private/") == false);
|
||||
try std.testing.expect(robots.isAllowed("/admin/") == false);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - Google's real robots.txt" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// Simplified version of google.com/robots.txt
|
||||
const google_robots =
|
||||
\\User-agent: *
|
||||
\\User-agent: Yandex
|
||||
\\Disallow: /search
|
||||
\\Allow: /search/about
|
||||
\\Allow: /search/howsearchworks
|
||||
\\Disallow: /imgres
|
||||
\\Disallow: /m?
|
||||
\\Disallow: /m/
|
||||
\\Allow: /m/finance
|
||||
\\Disallow: /maps/
|
||||
\\Allow: /maps/$
|
||||
\\Allow: /maps/@
|
||||
\\Allow: /maps/dir/
|
||||
\\Disallow: /shopping?
|
||||
\\Allow: /shopping?udm=28$
|
||||
\\
|
||||
\\User-agent: AdsBot-Google
|
||||
\\Disallow: /maps/api/js/
|
||||
\\Allow: /maps/api/js
|
||||
\\Disallow: /maps/api/staticmap
|
||||
\\
|
||||
\\User-agent: Yandex
|
||||
\\Disallow: /about/careers/applications/jobs/results
|
||||
\\
|
||||
\\User-agent: facebookexternalhit
|
||||
\\User-agent: Twitterbot
|
||||
\\Allow: /imgres
|
||||
\\Allow: /search
|
||||
\\Disallow: /groups
|
||||
\\Disallow: /m/
|
||||
\\
|
||||
;
|
||||
|
||||
var regular_bot = try Robots.fromBytes(allocator, "Googlebot", google_robots);
|
||||
defer regular_bot.deinit(allocator);
|
||||
|
||||
try std.testing.expect(regular_bot.isAllowed("/") == true);
|
||||
try std.testing.expect(regular_bot.isAllowed("/search") == false);
|
||||
try std.testing.expect(regular_bot.isAllowed("/search/about") == true);
|
||||
try std.testing.expect(regular_bot.isAllowed("/search/howsearchworks") == true);
|
||||
try std.testing.expect(regular_bot.isAllowed("/imgres") == false);
|
||||
try std.testing.expect(regular_bot.isAllowed("/m/finance") == true);
|
||||
try std.testing.expect(regular_bot.isAllowed("/m/other") == false);
|
||||
try std.testing.expect(regular_bot.isAllowed("/maps/") == true);
|
||||
try std.testing.expect(regular_bot.isAllowed("/maps/@") == true);
|
||||
try std.testing.expect(regular_bot.isAllowed("/shopping?udm=28") == true);
|
||||
try std.testing.expect(regular_bot.isAllowed("/shopping?udm=28&extra") == false);
|
||||
|
||||
var adsbot = try Robots.fromBytes(allocator, "AdsBot-Google", google_robots);
|
||||
defer adsbot.deinit(allocator);
|
||||
|
||||
try std.testing.expect(adsbot.isAllowed("/maps/api/js") == true);
|
||||
try std.testing.expect(adsbot.isAllowed("/maps/api/js/") == false);
|
||||
try std.testing.expect(adsbot.isAllowed("/maps/api/staticmap") == false);
|
||||
|
||||
var twitterbot = try Robots.fromBytes(allocator, "Twitterbot", google_robots);
|
||||
defer twitterbot.deinit(allocator);
|
||||
|
||||
try std.testing.expect(twitterbot.isAllowed("/imgres") == true);
|
||||
try std.testing.expect(twitterbot.isAllowed("/search") == true);
|
||||
try std.testing.expect(twitterbot.isAllowed("/groups") == false);
|
||||
try std.testing.expect(twitterbot.isAllowed("/m/") == false);
|
||||
}
|
||||
|
||||
test "Robots: user-agent after rules starts new entry" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const file =
|
||||
\\User-agent: Bot1
|
||||
\\User-agent: Bot2
|
||||
\\Disallow: /admin/
|
||||
\\Allow: /public/
|
||||
\\User-agent: Bot3
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
;
|
||||
|
||||
var robots1 = try Robots.fromBytes(allocator, "Bot1", file);
|
||||
defer robots1.deinit(allocator);
|
||||
try std.testing.expect(robots1.isAllowed("/admin/") == false);
|
||||
try std.testing.expect(robots1.isAllowed("/public/") == true);
|
||||
try std.testing.expect(robots1.isAllowed("/private/") == true);
|
||||
|
||||
var robots2 = try Robots.fromBytes(allocator, "Bot2", file);
|
||||
defer robots2.deinit(allocator);
|
||||
try std.testing.expect(robots2.isAllowed("/admin/") == false);
|
||||
try std.testing.expect(robots2.isAllowed("/public/") == true);
|
||||
try std.testing.expect(robots2.isAllowed("/private/") == true);
|
||||
|
||||
var robots3 = try Robots.fromBytes(allocator, "Bot3", file);
|
||||
defer robots3.deinit(allocator);
|
||||
try std.testing.expect(robots3.isAllowed("/admin/") == true);
|
||||
try std.testing.expect(robots3.isAllowed("/public/") == true);
|
||||
try std.testing.expect(robots3.isAllowed("/private/") == false);
|
||||
}
|
||||
|
||||
test "Robots: blank lines don't end entries" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const file =
|
||||
\\User-agent: MyBot
|
||||
\\Disallow: /admin/
|
||||
\\
|
||||
\\
|
||||
\\Allow: /public/
|
||||
\\
|
||||
;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "MyBot", file);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/admin/") == false);
|
||||
try std.testing.expect(robots.isAllowed("/public/") == true);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ const Http = @import("../http/Http.zig");
|
||||
const Element = @import("webapi/Element.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArrayListUnmanaged = std.ArrayListUnmanaged;
|
||||
const ArrayList = std.ArrayList;
|
||||
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
@@ -138,6 +138,12 @@ fn clearList(list: *std.DoublyLinkedList) void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getHeaders(self: *ScriptManager, url: [:0]const u8) !Http.Headers {
|
||||
var headers = try self.client.newHeaders();
|
||||
try self.page.headersForRequest(self.page.arena, url, &headers);
|
||||
return headers;
|
||||
}
|
||||
|
||||
pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_element: *Element.Html.Script, comptime ctx: []const u8) !void {
|
||||
if (script_element._executed) {
|
||||
// If a script tag gets dynamically created and added to the dom:
|
||||
@@ -252,17 +258,15 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
||||
script.deinit(true);
|
||||
}
|
||||
|
||||
var headers = try self.client.newHeaders();
|
||||
try page.requestCookie(.{}).headersForRequest(page.arena, url, &headers);
|
||||
|
||||
try self.client.request(.{
|
||||
.url = url,
|
||||
.ctx = script,
|
||||
.method = .GET,
|
||||
.headers = headers,
|
||||
.headers = try self.getHeaders(url),
|
||||
.blocking = is_blocking,
|
||||
.cookie_jar = &page._session.cookie_jar,
|
||||
.resource_type = .script,
|
||||
.notification = page._session.notification,
|
||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||
.header_callback = Script.headerCallback,
|
||||
.data_callback = Script.dataCallback,
|
||||
@@ -357,9 +361,6 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
||||
.manager = self,
|
||||
};
|
||||
|
||||
var headers = try self.client.newHeaders();
|
||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
self.page.js.localScope(&ls);
|
||||
@@ -377,9 +378,10 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
||||
.url = url,
|
||||
.ctx = script,
|
||||
.method = .GET,
|
||||
.headers = headers,
|
||||
.headers = try self.getHeaders(url),
|
||||
.cookie_jar = &self.page._session.cookie_jar,
|
||||
.resource_type = .script,
|
||||
.notification = self.page._session.notification,
|
||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||
.header_callback = Script.headerCallback,
|
||||
.data_callback = Script.dataCallback,
|
||||
@@ -452,9 +454,6 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
||||
} },
|
||||
};
|
||||
|
||||
var headers = try self.client.newHeaders();
|
||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
self.page.js.localScope(&ls);
|
||||
@@ -480,10 +479,11 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
||||
try self.client.request(.{
|
||||
.url = url,
|
||||
.method = .GET,
|
||||
.headers = headers,
|
||||
.headers = try self.getHeaders(url),
|
||||
.ctx = script,
|
||||
.resource_type = .script,
|
||||
.cookie_jar = &self.page._session.cookie_jar,
|
||||
.notification = self.page._session.notification,
|
||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||
.header_callback = Script.headerCallback,
|
||||
.data_callback = Script.dataCallback,
|
||||
@@ -634,7 +634,7 @@ pub const Script = struct {
|
||||
|
||||
const Source = union(enum) {
|
||||
@"inline": []const u8,
|
||||
remote: std.ArrayListUnmanaged(u8),
|
||||
remote: std.ArrayList(u8),
|
||||
|
||||
fn content(self: Source) []const u8 {
|
||||
return switch (self) {
|
||||
@@ -684,10 +684,6 @@ pub const Script = struct {
|
||||
});
|
||||
}
|
||||
|
||||
// If this isn't true, then we'll likely leak memory. If you don't
|
||||
// set `CURLOPT_SUPPRESS_CONNECT_HEADERS` and CONNECT to a proxy, this
|
||||
// will fail. This assertion exists to catch incorrect assumptions about
|
||||
// how libcurl works, or about how we've configured it.
|
||||
lp.assert(self.source.remote.capacity == 0, "ScriptManager.HeaderCallback", .{ .capacity = self.source.remote.capacity });
|
||||
var buffer = self.manager.buffer_pool.get();
|
||||
if (transfer.getContentLength()) |cl| {
|
||||
@@ -849,7 +845,7 @@ pub const Script = struct {
|
||||
defer {
|
||||
// We should run microtasks even if script execution fails.
|
||||
local.runMicrotasks();
|
||||
_ = page.scheduler.run() catch |err| {
|
||||
_ = page.js.scheduler.run() catch |err| {
|
||||
log.err(.page, "scheduler", .{ .err = err });
|
||||
};
|
||||
}
|
||||
@@ -873,7 +869,7 @@ pub const Script = struct {
|
||||
const cb = cb_ orelse return;
|
||||
|
||||
const Event = @import("webapi/Event.zig");
|
||||
const event = Event.initTrusted(typ, .{}, page) catch |err| {
|
||||
const event = Event.initTrusted(comptime .wrap(typ), .{}, page) catch |err| {
|
||||
log.warn(.js, "script internal callback", .{
|
||||
.url = self.url,
|
||||
.type = typ,
|
||||
@@ -881,6 +877,7 @@ pub const Script = struct {
|
||||
});
|
||||
return;
|
||||
};
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
cb.tryCall(void, .{event}, &caught) catch {
|
||||
@@ -904,7 +901,7 @@ const BufferPool = struct {
|
||||
|
||||
const Container = struct {
|
||||
node: List.Node,
|
||||
buf: std.ArrayListUnmanaged(u8),
|
||||
buf: std.ArrayList(u8),
|
||||
};
|
||||
|
||||
fn init(allocator: Allocator, max_concurrent_transfers: u8) BufferPool {
|
||||
@@ -929,7 +926,7 @@ const BufferPool = struct {
|
||||
self.mem_pool.deinit();
|
||||
}
|
||||
|
||||
fn get(self: *BufferPool) std.ArrayListUnmanaged(u8) {
|
||||
fn get(self: *BufferPool) std.ArrayList(u8) {
|
||||
const node = self.available.popFirst() orelse {
|
||||
// return a new buffer
|
||||
return .{};
|
||||
@@ -941,7 +938,7 @@ const BufferPool = struct {
|
||||
return container.buf;
|
||||
}
|
||||
|
||||
fn release(self: *BufferPool, buffer: ArrayListUnmanaged(u8)) void {
|
||||
fn release(self: *BufferPool, buffer: ArrayList(u8)) void {
|
||||
// create mutable copy
|
||||
var b = buffer;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ const History = @import("webapi/History.zig");
|
||||
|
||||
const Page = @import("Page.zig");
|
||||
const Browser = @import("Browser.zig");
|
||||
const Notification = @import("../Notification.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
@@ -39,6 +40,7 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
const Session = @This();
|
||||
|
||||
browser: *Browser,
|
||||
notification: *Notification,
|
||||
|
||||
// Used to create our Inspector and in the BrowserContext.
|
||||
arena: Allocator,
|
||||
@@ -53,30 +55,27 @@ arena: Allocator,
|
||||
// page and start another.
|
||||
transfer_arena: Allocator,
|
||||
|
||||
executor: js.ExecutionWorld,
|
||||
cookie_jar: storage.Cookie.Jar,
|
||||
storage_shed: storage.Shed,
|
||||
|
||||
history: History,
|
||||
navigation: Navigation,
|
||||
|
||||
page: ?*Page = null,
|
||||
|
||||
pub fn init(self: *Session, browser: *Browser) !void {
|
||||
var executor = try browser.env.newExecutionWorld();
|
||||
errdefer executor.deinit();
|
||||
page: ?Page,
|
||||
|
||||
pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void {
|
||||
const allocator = browser.app.allocator;
|
||||
const session_allocator = browser.session_arena.allocator();
|
||||
|
||||
self.* = .{
|
||||
.browser = browser,
|
||||
.executor = executor,
|
||||
.page = null,
|
||||
.history = .{},
|
||||
.navigation = .{},
|
||||
.storage_shed = .{},
|
||||
.browser = browser,
|
||||
.notification = notification,
|
||||
.arena = session_allocator,
|
||||
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
||||
.navigation = .{},
|
||||
.history = .{},
|
||||
.transfer_arena = browser.transfer_arena.allocator(),
|
||||
};
|
||||
}
|
||||
@@ -87,7 +86,6 @@ pub fn deinit(self: *Session) void {
|
||||
}
|
||||
self.cookie_jar.deinit();
|
||||
self.storage_shed.deinit(self.browser.app.allocator);
|
||||
self.executor.deinit();
|
||||
}
|
||||
|
||||
// NOTE: the caller is not the owner of the returned value,
|
||||
@@ -95,11 +93,11 @@ pub fn deinit(self: *Session) void {
|
||||
pub fn createPage(self: *Session) !*Page {
|
||||
lp.assert(self.page == null, "Session.createPage - page not null", .{});
|
||||
|
||||
const page_arena = &self.browser.page_arena;
|
||||
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
_ = self.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
|
||||
self.page = try Page.init(page_arena.allocator(), self.browser.call_arena.allocator(), self);
|
||||
const page = self.page.?;
|
||||
self.page = @as(Page, undefined);
|
||||
const page = &self.page.?;
|
||||
try Page.init(page, self);
|
||||
|
||||
// Creates a new NavigationEventTarget for this page.
|
||||
try self.navigation.onNewPage(page);
|
||||
@@ -109,14 +107,14 @@ pub fn createPage(self: *Session) !*Page {
|
||||
}
|
||||
// start JS env
|
||||
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
||||
self.browser.notification.dispatch(.page_created, page);
|
||||
self.notification.dispatch(.page_created, page);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
pub fn removePage(self: *Session) void {
|
||||
// Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one
|
||||
self.browser.notification.dispatch(.page_remove, .{});
|
||||
self.notification.dispatch(.page_remove, .{});
|
||||
lp.assert(self.page != null, "Session.removePage - page is null", .{});
|
||||
|
||||
self.page.?.deinit();
|
||||
@@ -130,22 +128,29 @@ pub fn removePage(self: *Session) void {
|
||||
}
|
||||
|
||||
pub fn currentPage(self: *Session) ?*Page {
|
||||
return self.page orelse return null;
|
||||
return &(self.page orelse return null);
|
||||
}
|
||||
|
||||
pub const WaitResult = enum {
|
||||
done,
|
||||
no_page,
|
||||
cdp_socket,
|
||||
navigate,
|
||||
};
|
||||
|
||||
pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
||||
while (true) {
|
||||
const page = self.page orelse return .no_page;
|
||||
switch (page.wait(wait_ms)) {
|
||||
.navigate => self.processScheduledNavigation() catch return .done,
|
||||
else => |result| return result,
|
||||
if (self.page) |*page| {
|
||||
switch (page.wait(wait_ms)) {
|
||||
.done => {
|
||||
if (page._queued_navigation == null) {
|
||||
return .done;
|
||||
}
|
||||
self.processScheduledNavigation() catch return .done;
|
||||
},
|
||||
else => |result| return result,
|
||||
}
|
||||
} else {
|
||||
return .no_page;
|
||||
}
|
||||
// if we've successfull navigated, we'll give the new page another
|
||||
// page.wait(wait_ms)
|
||||
@@ -153,24 +158,32 @@ pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
||||
}
|
||||
|
||||
fn processScheduledNavigation(self: *Session) !void {
|
||||
const qn = self.page.?._queued_navigation.?;
|
||||
defer _ = self.browser.transfer_arena.reset(.{ .retain_with_limit = 8 * 1024 });
|
||||
const url, const opts = blk: {
|
||||
const qn = self.page.?._queued_navigation.?;
|
||||
// qn might not be safe to use after self.removePage is called, hence
|
||||
// this block;
|
||||
const url = qn.url;
|
||||
const opts = qn.opts;
|
||||
|
||||
// This was already aborted on the page, but it would be pretty
|
||||
// bad if old requests went to the new page, so let's make double sure
|
||||
self.browser.http_client.abort();
|
||||
self.removePage();
|
||||
// This was already aborted on the page, but it would be pretty
|
||||
// bad if old requests went to the new page, so let's make double sure
|
||||
self.browser.http_client.abort();
|
||||
self.removePage();
|
||||
|
||||
break :blk .{ url, opts };
|
||||
};
|
||||
|
||||
const page = self.createPage() catch |err| {
|
||||
log.err(.browser, "queued navigation page error", .{
|
||||
.err = err,
|
||||
.url = qn.url,
|
||||
.url = url,
|
||||
});
|
||||
return err;
|
||||
};
|
||||
|
||||
page.navigate(qn.url, qn.opts) catch |err| {
|
||||
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
|
||||
page.navigate(url, opts) catch |err| {
|
||||
log.err(.browser, "queued navigation error", .{ .err = err, .url = url });
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const ResolveOpts = struct {
|
||||
@@ -503,6 +502,16 @@ pub fn concatQueryString(arena: Allocator, url: []const u8, query_string: []cons
|
||||
return buf.items[0 .. buf.items.len - 1 :0];
|
||||
}
|
||||
|
||||
pub fn getRobotsUrl(arena: Allocator, url: [:0]const u8) ![:0]const u8 {
|
||||
const origin = try getOrigin(arena, url) orelse return error.NoOrigin;
|
||||
return try std.fmt.allocPrintSentinel(
|
||||
arena,
|
||||
"{s}/robots.txt",
|
||||
.{origin},
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "URL: isCompleteHTTPUrl" {
|
||||
try testing.expectEqual(true, isCompleteHTTPUrl("http://example.com/about"));
|
||||
@@ -779,3 +788,31 @@ test "URL: concatQueryString" {
|
||||
try testing.expectEqual("https://www.lightpanda.io/index?1=2&a=b", url);
|
||||
}
|
||||
}
|
||||
|
||||
test "URL: getRobotsUrl" {
|
||||
defer testing.reset();
|
||||
const arena = testing.arena_allocator;
|
||||
|
||||
{
|
||||
const url = try getRobotsUrl(arena, "https://www.lightpanda.io");
|
||||
try testing.expectEqual("https://www.lightpanda.io/robots.txt", url);
|
||||
}
|
||||
|
||||
{
|
||||
const url = try getRobotsUrl(arena, "https://www.lightpanda.io/some/path");
|
||||
try testing.expectString("https://www.lightpanda.io/robots.txt", url);
|
||||
}
|
||||
|
||||
{
|
||||
const url = try getRobotsUrl(arena, "https://www.lightpanda.io:8080/page");
|
||||
try testing.expectString("https://www.lightpanda.io:8080/robots.txt", url);
|
||||
}
|
||||
{
|
||||
const url = try getRobotsUrl(arena, "http://example.com/deep/nested/path?query=value#fragment");
|
||||
try testing.expectString("http://example.com/robots.txt", url);
|
||||
}
|
||||
{
|
||||
const url = try getRobotsUrl(arena, "https://user:pass@example.com/page");
|
||||
try testing.expectString("https://example.com/robots.txt", url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,10 @@ const string = @import("../../string.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
|
||||
const js = @import("js.zig");
|
||||
const bridge = @import("bridge.zig");
|
||||
const Context = @import("Context.zig");
|
||||
const TaggedOpaque = @import("TaggedOpaque.zig");
|
||||
|
||||
const v8 = js.v8;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const CALL_ARENA_RETAIN = 1024 * 16;
|
||||
@@ -37,6 +35,7 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
const Caller = @This();
|
||||
local: js.Local,
|
||||
prev_local: ?*const js.Local,
|
||||
prev_context: *Context,
|
||||
|
||||
// Takes the raw v8 isolate and extracts the context from it.
|
||||
pub fn init(self: *Caller, v8_isolate: *v8.Isolate) void {
|
||||
@@ -55,7 +54,9 @@ pub fn init(self: *Caller, v8_isolate: *v8.Isolate) void {
|
||||
.isolate = .{ .handle = v8_isolate },
|
||||
},
|
||||
.prev_local = ctx.local,
|
||||
.prev_context = ctx.page.js,
|
||||
};
|
||||
ctx.page.js = ctx;
|
||||
ctx.local = &self.local;
|
||||
}
|
||||
|
||||
@@ -81,10 +82,10 @@ pub fn deinit(self: *Caller) void {
|
||||
|
||||
ctx.call_depth = call_depth;
|
||||
ctx.local = self.prev_local;
|
||||
ctx.page.js = self.prev_context;
|
||||
}
|
||||
|
||||
pub const CallOpts = struct {
|
||||
cache: ?[]const u8 = null,
|
||||
dom_exception: bool = false,
|
||||
null_as_undefined: bool = false,
|
||||
as_typed_array: bool = false,
|
||||
@@ -314,14 +315,14 @@ fn isInErrorSet(err: anyerror, comptime T: type) bool {
|
||||
}
|
||||
|
||||
fn nameToString(self: *const Caller, comptime T: type, name: *const v8.Name) !T {
|
||||
const v8_string = @as(*const v8.String, @ptrCast(name));
|
||||
const handle = @as(*const v8.String, @ptrCast(name));
|
||||
if (T == string.String) {
|
||||
return self.local.jsStringToStringSSO(v8_string, .{});
|
||||
return js.String.toSSO(.{ .local = &self.local, .handle = handle }, false);
|
||||
}
|
||||
if (T == string.Global) {
|
||||
return self.local.jsStringToStringSSO(v8_string, .{ .allocator = self.local.ctx.allocator });
|
||||
return js.String.toSSO(.{ .local = &self.local, .handle = handle }, true);
|
||||
}
|
||||
return try self.local.valueHandleToString(v8_string, .{});
|
||||
return try js.String.toSlice(.{ .local = &self.local, .handle = handle });
|
||||
}
|
||||
|
||||
fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror, info: anytype, comptime opts: CallOpts) void {
|
||||
@@ -334,6 +335,7 @@ fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror,
|
||||
}
|
||||
|
||||
const js_err: *const v8.Value = switch (err) {
|
||||
error.TryCatchRethrow => return,
|
||||
error.InvalidArgument => isolate.createTypeError("invalid argument"),
|
||||
error.OutOfMemory => isolate.createError("out of memory"),
|
||||
error.IllegalConstructor => isolate.createError("Illegal Contructor"),
|
||||
|
||||
@@ -21,8 +21,9 @@ const lp = @import("lightpanda");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const js = @import("js.zig");
|
||||
const Env = @import("Env.zig");
|
||||
const bridge = @import("bridge.zig");
|
||||
const TaggedOpaque = @import("TaggedOpaque.zig");
|
||||
const Scheduler = @import("Scheduler.zig");
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
const ScriptManager = @import("../ScriptManager.zig");
|
||||
@@ -38,6 +39,7 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
const Context = @This();
|
||||
|
||||
id: usize,
|
||||
env: *Env,
|
||||
page: *Page,
|
||||
isolate: js.Isolate,
|
||||
|
||||
@@ -82,7 +84,8 @@ identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
||||
// Any type that is stored in the identity_map which has a finalizer declared
|
||||
// will have its finalizer stored here. This is only used when shutting down
|
||||
// if v8 hasn't called the finalizer directly itself.
|
||||
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, FinalizerCallback) = .empty,
|
||||
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
|
||||
finalizer_callback_pool: std.heap.MemoryPool(FinalizerCallback),
|
||||
|
||||
// Some web APIs have to manage opaque values. Ideally, they use an
|
||||
// js.Object, but the js.Object has no lifetime guarantee beyond the
|
||||
@@ -102,6 +105,7 @@ global_promise_resolvers: std.ArrayList(v8.Global) = .empty,
|
||||
// Temp variants stored in HashMaps for O(1) early cleanup.
|
||||
// Key is global.data_ptr.
|
||||
global_values_temp: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
||||
global_promises_temp: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
||||
global_functions_temp: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
||||
|
||||
// Our module cache: normalized module specifier => module.
|
||||
@@ -117,6 +121,15 @@ module_identifier: std.AutoHashMapUnmanaged(u32, [:0]const u8) = .empty,
|
||||
// the page's script manager
|
||||
script_manager: ?*ScriptManager,
|
||||
|
||||
// Our macrotasks
|
||||
scheduler: Scheduler,
|
||||
|
||||
// Prevents us from enqueuing a microtask for this context while we're shutting
|
||||
// down.
|
||||
shutting_down: bool = false,
|
||||
|
||||
unknown_properties: (if (IS_DEBUG) std.StringHashMapUnmanaged(UnknownPropertyStat) else void) = if (IS_DEBUG) .{} else {},
|
||||
|
||||
const ModuleEntry = struct {
|
||||
// Can be null if we're asynchrously loading the module, in
|
||||
// which case resolver_promise cannot be null.
|
||||
@@ -149,6 +162,33 @@ pub fn fromIsolate(isolate: js.Isolate) *Context {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Context) void {
|
||||
if (comptime IS_DEBUG) {
|
||||
var it = self.unknown_properties.iterator();
|
||||
while (it.next()) |kv| {
|
||||
log.debug(.unknown_prop, "unknown property", .{
|
||||
.property = kv.key_ptr.*,
|
||||
.occurrences = kv.value_ptr.count,
|
||||
.first_stack = kv.value_ptr.first_stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
defer self.env.app.arena_pool.release(self.arena);
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
const entered = self.enter(&hs);
|
||||
defer entered.exit();
|
||||
|
||||
// We might have microtasks in the isolate that refence this context. The
|
||||
// only option we have is to run them. But a microtask could queue another
|
||||
// microtask, so we set the shutting_down flag, so that any such microtask
|
||||
// will be a noop (this isn't automatic, when v8 calls our microtask callback
|
||||
// the first thing we'll check is if self.shutting_down == true).
|
||||
self.shutting_down = true;
|
||||
self.env.runMicrotasks();
|
||||
|
||||
// can release objects
|
||||
self.scheduler.deinit();
|
||||
|
||||
{
|
||||
var it = self.identity_map.valueIterator();
|
||||
while (it.next()) |global| {
|
||||
@@ -158,8 +198,9 @@ pub fn deinit(self: *Context) void {
|
||||
{
|
||||
var it = self.finalizer_callbacks.valueIterator();
|
||||
while (it.next()) |finalizer| {
|
||||
finalizer.deinit();
|
||||
finalizer.*.deinit();
|
||||
}
|
||||
self.finalizer_callback_pool.deinit();
|
||||
}
|
||||
|
||||
for (self.global_values.items) |*global| {
|
||||
@@ -193,6 +234,13 @@ pub fn deinit(self: *Context) void {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var it = self.global_promises_temp.valueIterator();
|
||||
while (it.next()) |global| {
|
||||
v8.v8__Global__Reset(global);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var it = self.global_functions_temp.valueIterator();
|
||||
while (it.next()) |global| {
|
||||
@@ -201,34 +249,44 @@ pub fn deinit(self: *Context) void {
|
||||
}
|
||||
|
||||
if (self.entered) {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
self.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
v8.v8__Context__Exit(ls.local.handle);
|
||||
v8.v8__Context__Exit(@ptrCast(v8.v8__Global__Get(&self.handle, self.isolate.handle)));
|
||||
}
|
||||
|
||||
v8.v8__Global__Reset(&self.handle);
|
||||
}
|
||||
|
||||
pub fn weakRef(self: *Context, obj: anytype) void {
|
||||
const global = self.identity_map.getPtr(@intFromPtr(obj)) orelse {
|
||||
const fc = self.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
|
||||
if (comptime IS_DEBUG) {
|
||||
// should not be possible
|
||||
std.debug.assert(false);
|
||||
}
|
||||
return;
|
||||
};
|
||||
v8.v8__Global__SetWeakFinalizer(global, obj, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter);
|
||||
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter);
|
||||
}
|
||||
|
||||
pub fn safeWeakRef(self: *Context, obj: anytype) void {
|
||||
const fc = self.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
|
||||
if (comptime IS_DEBUG) {
|
||||
// should not be possible
|
||||
std.debug.assert(false);
|
||||
}
|
||||
return;
|
||||
};
|
||||
v8.v8__Global__ClearWeak(&fc.global);
|
||||
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter);
|
||||
}
|
||||
|
||||
pub fn strongRef(self: *Context, obj: anytype) void {
|
||||
const global = self.identity_map.getPtr(@intFromPtr(obj)) orelse {
|
||||
const fc = self.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
|
||||
if (comptime IS_DEBUG) {
|
||||
// should not be possible
|
||||
std.debug.assert(false);
|
||||
}
|
||||
return;
|
||||
};
|
||||
v8.v8__Global__ClearWeak(global);
|
||||
v8.v8__Global__ClearWeak(&fc.global);
|
||||
}
|
||||
|
||||
pub fn release(self: *Context, item: anytype) void {
|
||||
@@ -246,17 +304,20 @@ pub fn release(self: *Context, item: anytype) void {
|
||||
|
||||
// The item has been fianalized, remove it for the finalizer callback so that
|
||||
// we don't try to call it again on shutdown.
|
||||
_ = self.finalizer_callbacks.fetchRemove(@intFromPtr(item)) orelse {
|
||||
const fc = self.finalizer_callbacks.fetchRemove(@intFromPtr(item)) orelse {
|
||||
if (comptime IS_DEBUG) {
|
||||
// should not be possible
|
||||
std.debug.assert(false);
|
||||
}
|
||||
return;
|
||||
};
|
||||
self.finalizer_callback_pool.destroy(fc.value);
|
||||
return;
|
||||
}
|
||||
|
||||
var map = switch (@TypeOf(item)) {
|
||||
js.Value.Temp => &self.global_values_temp,
|
||||
js.Promise.Temp => &self.global_promises_temp,
|
||||
js.Function.Temp => &self.global_functions_temp,
|
||||
else => |T| @compileError("Context.release cannot be called with a " ++ @typeName(T)),
|
||||
};
|
||||
@@ -320,7 +381,25 @@ pub fn module(self: *Context, comptime want_result: bool, local: *const js.Local
|
||||
if (cacheable) {
|
||||
gop = try self.module_cache.getOrPut(arena, url);
|
||||
if (gop.found_existing) {
|
||||
if (gop.value_ptr.module != null) {
|
||||
if (gop.value_ptr.module) |cache_mod| {
|
||||
if (gop.value_ptr.module_promise == null) {
|
||||
// This an usual case, but it can happen if a module is
|
||||
// first asynchronously requested and then synchronously
|
||||
// requested as a child of some root import. In that case,
|
||||
// the module may not be instantiated yet (so we have to
|
||||
// do that). It might not be evaluated yet. So we have
|
||||
// to do that too. Evaluation is particularly important
|
||||
// as it sets up our cache entry's module_promise.
|
||||
// It appears that v8 handles potential double-instantiated
|
||||
// and double-evaluated modules safely. The 2nd instantiation
|
||||
// is a no-op, and the second evaluation returns the same
|
||||
// promise.
|
||||
const mod = local.toLocal(cache_mod);
|
||||
if (mod.getStatus() == .kUninstantiated and try mod.instantiate(resolveModuleCallback) == false) {
|
||||
return error.ModuleInstantiationError;
|
||||
}
|
||||
return self.evaluateModule(want_result, mod, url, true);
|
||||
}
|
||||
return if (comptime want_result) gop.value_ptr.* else {};
|
||||
}
|
||||
} else {
|
||||
@@ -350,6 +429,10 @@ pub fn module(self: *Context, comptime want_result: bool, local: *const js.Local
|
||||
return error.ModuleInstantiationError;
|
||||
}
|
||||
|
||||
return self.evaluateModule(want_result, mod, owned_url, cacheable);
|
||||
}
|
||||
|
||||
fn evaluateModule(self: *Context, comptime want_result: bool, mod: js.Module, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) {
|
||||
const evaluated = mod.evaluate() catch {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(mod.getStatus() == .kErrored);
|
||||
@@ -357,9 +440,13 @@ pub fn module(self: *Context, comptime want_result: bool, local: *const js.Local
|
||||
|
||||
// Some module-loading errors aren't handled by TryCatch. We need to
|
||||
// get the error from the module itself.
|
||||
const message = blk: {
|
||||
const e = mod.getException().toString() catch break :blk "???";
|
||||
break :blk e.toSlice() catch "???";
|
||||
};
|
||||
log.warn(.js, "evaluate module", .{
|
||||
.specifier = owned_url,
|
||||
.message = mod.getException().toString(.{}) catch "???",
|
||||
.message = message,
|
||||
.specifier = url,
|
||||
});
|
||||
return error.EvaluationError;
|
||||
};
|
||||
@@ -368,24 +455,15 @@ pub fn module(self: *Context, comptime want_result: bool, local: *const js.Local
|
||||
// Must be a promise that gets returned here.
|
||||
lp.assert(evaluated.isPromise(), "Context.module non-promise", .{});
|
||||
|
||||
if (comptime !want_result) {
|
||||
// avoid creating a bunch of persisted objects if it isn't
|
||||
// cacheable and the caller doesn't care about results.
|
||||
// This is pretty common, i.e. every <script type=module>
|
||||
// within the html page.
|
||||
if (!cacheable) {
|
||||
return;
|
||||
if (!cacheable) {
|
||||
switch (comptime want_result) {
|
||||
false => return,
|
||||
true => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
// anyone who cares about the result, should also want it to
|
||||
// be cached
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(cacheable);
|
||||
}
|
||||
|
||||
// entry has to have been created atop this function
|
||||
const entry = self.module_cache.getPtr(owned_url).?;
|
||||
const entry = self.module_cache.getPtr(url).?;
|
||||
|
||||
// and the module must have been set after we compiled it
|
||||
lp.assert(entry.module != null, "Context.module with module", .{});
|
||||
@@ -457,11 +535,11 @@ fn postCompileModule(self: *Context, mod: js.Module, url: [:0]const u8, local: *
|
||||
const request_len = requests.len();
|
||||
const script_manager = self.script_manager.?;
|
||||
for (0..request_len) |i| {
|
||||
const specifier = try local.jsStringToZigZ(requests.get(i).specifier(), .{});
|
||||
const specifier = requests.get(i).specifier(local);
|
||||
const normalized_specifier = try script_manager.resolveSpecifier(
|
||||
self.call_arena,
|
||||
url,
|
||||
specifier,
|
||||
try specifier.toSliceZ(),
|
||||
);
|
||||
const nested_gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
|
||||
if (!nested_gop.found_existing) {
|
||||
@@ -494,14 +572,14 @@ fn resolveModuleCallback(
|
||||
_ = import_attributes;
|
||||
|
||||
const self = fromC(c_context.?);
|
||||
var local = js.Local{
|
||||
const local = js.Local{
|
||||
.ctx = self,
|
||||
.handle = c_context.?,
|
||||
.isolate = self.isolate,
|
||||
.call_arena = self.call_arena,
|
||||
};
|
||||
|
||||
const specifier = local.jsStringToZigZ(c_specifier.?, .{}) catch |err| {
|
||||
const specifier = js.String.toSliceZ(.{ .local = &local, .handle = c_specifier.? }) catch |err| {
|
||||
log.err(.js, "resolve module", .{ .err = err });
|
||||
return null;
|
||||
};
|
||||
@@ -527,19 +605,19 @@ pub fn dynamicModuleCallback(
|
||||
_ = import_attrs;
|
||||
|
||||
const self = fromC(c_context.?);
|
||||
var local = js.Local{
|
||||
const local = js.Local{
|
||||
.ctx = self,
|
||||
.handle = c_context.?,
|
||||
.call_arena = self.call_arena,
|
||||
.isolate = self.isolate,
|
||||
};
|
||||
|
||||
const resource = local.jsStringToZigZ(resource_name.?, .{}) catch |err| {
|
||||
const resource = js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| {
|
||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" });
|
||||
return @constCast((local.rejectPromise("Out of memory") catch return null).handle);
|
||||
};
|
||||
|
||||
const specifier = local.jsStringToZigZ(v8_specifier.?, .{}) catch |err| {
|
||||
const specifier = js.String.toSliceZ(.{ .local = &local, .handle = v8_specifier.? }) catch |err| {
|
||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" });
|
||||
return @constCast((local.rejectPromise("Out of memory") catch return null).handle);
|
||||
};
|
||||
@@ -684,6 +762,9 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
|
||||
return promise;
|
||||
}
|
||||
|
||||
// we might update the map, so we might need to re-fetch this.
|
||||
var entry = gop.value_ptr;
|
||||
|
||||
// So we have a module, but no async resolver. This can only
|
||||
// happen if the module was first synchronously loaded (Does that
|
||||
// ever even happen?!) You'd think we can just return the module
|
||||
@@ -696,7 +777,7 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
|
||||
|
||||
// If the module hasn't been evaluated yet (it was only instantiated
|
||||
// as a static import dependency), we need to evaluate it now.
|
||||
if (gop.value_ptr.module_promise == null) {
|
||||
if (entry.module_promise == null) {
|
||||
const mod = local.toLocal(gop.value_ptr.module.?);
|
||||
const status = mod.getStatus();
|
||||
if (status == .kEvaluated or status == .kEvaluating) {
|
||||
@@ -705,7 +786,7 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
|
||||
const module_resolver = local.createPromiseResolver();
|
||||
module_resolver.resolve("resolve module", mod.getModuleNamespace());
|
||||
_ = try module_resolver.persist();
|
||||
gop.value_ptr.module_promise = try module_resolver.promise().persist();
|
||||
entry.module_promise = try module_resolver.promise().persist();
|
||||
} else {
|
||||
// the module was loaded, but not evaluated, we _have_ to evaluate it now
|
||||
const evaluated = mod.evaluate() catch {
|
||||
@@ -716,18 +797,20 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
|
||||
return promise;
|
||||
};
|
||||
lp.assert(evaluated.isPromise(), "Context._dynamicModuleCallback non-promise", .{});
|
||||
gop.value_ptr.module_promise = try evaluated.toPromise().persist();
|
||||
// mod.evaluate can invalidate or gop
|
||||
entry = self.module_cache.getPtr(specifier).?;
|
||||
entry.module_promise = try evaluated.toPromise().persist();
|
||||
}
|
||||
}
|
||||
|
||||
// like before, we want to set this up so that if anything else
|
||||
// tries to load this module, it can just return our promise
|
||||
// since we're going to be doing all the work.
|
||||
gop.value_ptr.resolver_promise = try promise.persist();
|
||||
entry.resolver_promise = try promise.persist();
|
||||
|
||||
// But we can skip direclty to `resolveDynamicModule` which is
|
||||
// what the above callback will eventually do.
|
||||
self.resolveDynamicModule(state, gop.value_ptr.*, local);
|
||||
self.resolveDynamicModule(state, entry.*, local);
|
||||
return promise;
|
||||
}
|
||||
|
||||
@@ -844,70 +927,121 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul
|
||||
};
|
||||
}
|
||||
|
||||
// Microtasks
|
||||
// Used to make temporarily enter and exit a context, updating and restoring
|
||||
// page.js:
|
||||
// var hs: js.HandleScope = undefined;
|
||||
// const entered = ctx.enter(&hs);
|
||||
// defer entered.exit();
|
||||
pub fn enter(self: *Context, hs: *js.HandleScope) Entered {
|
||||
const isolate = self.isolate;
|
||||
js.HandleScope.init(hs, isolate);
|
||||
|
||||
const page = self.page;
|
||||
const original = page.js;
|
||||
page.js = self;
|
||||
|
||||
const handle: *const v8.Context = @ptrCast(v8.v8__Global__Get(&self.handle, isolate.handle));
|
||||
v8.v8__Context__Enter(handle);
|
||||
return .{ .original = original, .handle = handle, .handle_scope = hs };
|
||||
}
|
||||
|
||||
const Entered = struct {
|
||||
// the context we should restore on the page
|
||||
original: *Context,
|
||||
|
||||
// the handle of the entered context
|
||||
handle: *const v8.Context,
|
||||
|
||||
handle_scope: *js.HandleScope,
|
||||
|
||||
pub fn exit(self: Entered) void {
|
||||
self.original.page.js = self.original;
|
||||
v8.v8__Context__Exit(self.handle);
|
||||
self.handle_scope.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn queueMutationDelivery(self: *Context) !void {
|
||||
self.isolate.enqueueMicrotask(struct {
|
||||
fn run(data: ?*anyopaque) callconv(.c) void {
|
||||
const page: *Page = @ptrCast(@alignCast(data.?));
|
||||
page.deliverMutations();
|
||||
self.enqueueMicrotask(struct {
|
||||
fn run(ctx: *Context) void {
|
||||
ctx.page.deliverMutations();
|
||||
}
|
||||
}.run, self.page);
|
||||
}.run);
|
||||
}
|
||||
|
||||
pub fn queueIntersectionChecks(self: *Context) !void {
|
||||
self.isolate.enqueueMicrotask(struct {
|
||||
fn run(data: ?*anyopaque) callconv(.c) void {
|
||||
const page: *Page = @ptrCast(@alignCast(data.?));
|
||||
page.performScheduledIntersectionChecks();
|
||||
self.enqueueMicrotask(struct {
|
||||
fn run(ctx: *Context) void {
|
||||
ctx.page.performScheduledIntersectionChecks();
|
||||
}
|
||||
}.run, self.page);
|
||||
}.run);
|
||||
}
|
||||
|
||||
pub fn queueIntersectionDelivery(self: *Context) !void {
|
||||
self.isolate.enqueueMicrotask(struct {
|
||||
fn run(data: ?*anyopaque) callconv(.c) void {
|
||||
const page: *Page = @ptrCast(@alignCast(data.?));
|
||||
page.deliverIntersections();
|
||||
self.enqueueMicrotask(struct {
|
||||
fn run(ctx: *Context) void {
|
||||
ctx.page.deliverIntersections();
|
||||
}
|
||||
}.run, self.page);
|
||||
}.run);
|
||||
}
|
||||
|
||||
pub fn queueSlotchangeDelivery(self: *Context) !void {
|
||||
self.enqueueMicrotask(struct {
|
||||
fn run(ctx: *Context) void {
|
||||
ctx.page.deliverSlotchangeEvents();
|
||||
}
|
||||
}.run);
|
||||
}
|
||||
|
||||
// Helper for executing a Microtask on this Context. In V8, microtasks aren't
|
||||
// associated to a Context - they are just functions to execute in an Isolate.
|
||||
// But for these Context microtasks, we want to (a) make sure the context isn't
|
||||
// being shut down and (b) that it's entered.
|
||||
fn enqueueMicrotask(self: *Context, callback: anytype) void {
|
||||
self.isolate.enqueueMicrotask(struct {
|
||||
fn run(data: ?*anyopaque) callconv(.c) void {
|
||||
const page: *Page = @ptrCast(@alignCast(data.?));
|
||||
page.deliverSlotchangeEvents();
|
||||
const ctx: *Context = @ptrCast(@alignCast(data.?));
|
||||
if (ctx.shutting_down) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
const entered = ctx.enter(&hs);
|
||||
defer entered.exit();
|
||||
callback(ctx);
|
||||
}
|
||||
}.run, self.page);
|
||||
}.run, self);
|
||||
}
|
||||
|
||||
pub fn queueMicrotaskFunc(self: *Context, cb: js.Function) void {
|
||||
self.isolate.enqueueMicrotaskFunc(cb);
|
||||
}
|
||||
|
||||
pub fn createFinalizerCallback(self: *Context, global: v8.Global, ptr: *anyopaque, finalizerFn: *const fn (ptr: *anyopaque) void) !*FinalizerCallback {
|
||||
const fc = try self.finalizer_callback_pool.create();
|
||||
fc.* = .{
|
||||
.ctx = self,
|
||||
.ptr = ptr,
|
||||
.global = global,
|
||||
.finalizerFn = finalizerFn,
|
||||
};
|
||||
return fc;
|
||||
}
|
||||
|
||||
// == Misc ==
|
||||
// A type that has a finalizer can have its finalizer called one of two ways.
|
||||
// The first is from V8 via the WeakCallback we give to weakRef. But that isn't
|
||||
// guaranteed to fire, so we track this in ctx._finalizers and call them on
|
||||
// context shutdown.
|
||||
const FinalizerCallback = struct {
|
||||
pub const FinalizerCallback = struct {
|
||||
ctx: *Context,
|
||||
ptr: *anyopaque,
|
||||
global: v8.Global,
|
||||
finalizerFn: *const fn (ptr: *anyopaque) void,
|
||||
|
||||
pub fn init(ptr: anytype) FinalizerCallback {
|
||||
const T = bridge.Struct(@TypeOf(ptr));
|
||||
return .{
|
||||
.ptr = ptr,
|
||||
.finalizerFn = struct {
|
||||
pub fn wrap(self: *anyopaque) void {
|
||||
T.JsApi.Meta.finalizer.from_zig(self);
|
||||
}
|
||||
}.wrap,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: FinalizerCallback) void {
|
||||
pub fn deinit(self: *FinalizerCallback) void {
|
||||
self.finalizerFn(self.ptr);
|
||||
self.ctx.finalizer_callback_pool.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -940,7 +1074,7 @@ pub fn stopCpuProfiler(self: *Context) ![]const u8 {
|
||||
const title = self.isolate.initStringHandle("v8_cpu_profile");
|
||||
const handle = v8.v8__CpuProfiler__StopProfiling(self.cpu_profiler.?, title) orelse return error.NoProfiles;
|
||||
const string_handle = v8.v8__CpuProfile__Serialize(handle, self.isolate.handle) orelse return error.NoProfile;
|
||||
return ls.local.jsStringToZig(string_handle, .{});
|
||||
return (js.String{ .local = &ls.local, .handle = string_handle }).toSlice();
|
||||
}
|
||||
|
||||
pub fn startHeapProfiler(self: *Context) void {
|
||||
@@ -972,7 +1106,7 @@ pub fn stopHeapProfiler(self: *Context) !struct { []const u8, []const u8 } {
|
||||
const string_handle = v8.v8__AllocationProfile__Serialize(profile, self.isolate.handle);
|
||||
v8.v8__HeapProfiler__StopSamplingHeapProfiler(self.heap_profiler.?);
|
||||
v8.v8__AllocationProfile__Delete(profile);
|
||||
break :blk try ls.local.jsStringToZig(string_handle, .{});
|
||||
break :blk try (js.String{ .local = &ls.local, .handle = string_handle.? }).toSlice();
|
||||
};
|
||||
|
||||
const snapshot = blk: {
|
||||
@@ -980,8 +1114,13 @@ pub fn stopHeapProfiler(self: *Context) !struct { []const u8, []const u8 } {
|
||||
const string_handle = v8.v8__HeapSnapshot__Serialize(snapshot, self.isolate.handle);
|
||||
v8.v8__HeapProfiler__StopTrackingHeapObjects(self.heap_profiler.?);
|
||||
v8.v8__HeapSnapshot__Delete(snapshot);
|
||||
break :blk try ls.local.jsStringToZig(string_handle, .{});
|
||||
break :blk try (js.String{ .local = &ls.local, .handle = string_handle.? }).toSlice();
|
||||
};
|
||||
|
||||
return .{ allocating, snapshot };
|
||||
}
|
||||
|
||||
const UnknownPropertyStat = struct {
|
||||
count: usize,
|
||||
first_stack: []const u8,
|
||||
};
|
||||
|
||||
@@ -18,8 +18,11 @@
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const v8 = js.v8;
|
||||
|
||||
const App = @import("../../App.zig");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const bridge = @import("bridge.zig");
|
||||
@@ -28,13 +31,13 @@ const Isolate = @import("Isolate.zig");
|
||||
const Platform = @import("Platform.zig");
|
||||
const Snapshot = @import("Snapshot.zig");
|
||||
const Inspector = @import("Inspector.zig");
|
||||
const ExecutionWorld = @import("ExecutionWorld.zig");
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
const Window = @import("../webapi/Window.zig");
|
||||
|
||||
const JsApis = bridge.JsApis;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
// The Env maps to a V8 isolate, which represents a isolated sandbox for
|
||||
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
|
||||
@@ -44,13 +47,15 @@ const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
// The `types` parameter is a tuple of Zig structures we want to bind to V8.
|
||||
const Env = @This();
|
||||
|
||||
allocator: Allocator,
|
||||
app: *App,
|
||||
|
||||
platform: *const Platform,
|
||||
|
||||
// the global isolate
|
||||
isolate: js.Isolate,
|
||||
|
||||
contexts: std.ArrayList(*js.Context),
|
||||
|
||||
// just kept around because we need to free it on deinit
|
||||
isolate_params: *v8.CreateParams,
|
||||
|
||||
@@ -65,7 +70,17 @@ templates: []*const v8.FunctionTemplate,
|
||||
// Global template created once per isolate and reused across all contexts
|
||||
global_template: v8.Eternal,
|
||||
|
||||
pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot) !Env {
|
||||
// Inspector associated with the Isolate. Exists when CDP is being used.
|
||||
inspector: ?*Inspector,
|
||||
|
||||
pub const InitOpts = struct {
|
||||
with_inspector: bool = false,
|
||||
};
|
||||
|
||||
pub fn init(app: *App, opts: InitOpts) !Env {
|
||||
const allocator = app.allocator;
|
||||
const snapshot = &app.snapshot;
|
||||
|
||||
var params = try allocator.create(v8.CreateParams);
|
||||
errdefer allocator.destroy(params);
|
||||
v8.v8__Isolate__CreateParams__CONSTRUCT(params);
|
||||
@@ -78,17 +93,18 @@ pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot
|
||||
|
||||
var isolate = js.Isolate.init(params);
|
||||
errdefer isolate.deinit();
|
||||
const isolate_handle = isolate.handle;
|
||||
|
||||
v8.v8__Isolate__SetHostImportModuleDynamicallyCallback(isolate.handle, Context.dynamicModuleCallback);
|
||||
v8.v8__Isolate__SetPromiseRejectCallback(isolate.handle, promiseRejectCallback);
|
||||
v8.v8__Isolate__SetMicrotasksPolicy(isolate.handle, v8.kExplicit);
|
||||
v8.v8__Isolate__SetFatalErrorHandler(isolate.handle, fatalCallback);
|
||||
v8.v8__Isolate__SetOOMErrorHandler(isolate.handle, oomCallback);
|
||||
v8.v8__Isolate__SetHostImportModuleDynamicallyCallback(isolate_handle, Context.dynamicModuleCallback);
|
||||
v8.v8__Isolate__SetPromiseRejectCallback(isolate_handle, promiseRejectCallback);
|
||||
v8.v8__Isolate__SetMicrotasksPolicy(isolate_handle, v8.kExplicit);
|
||||
v8.v8__Isolate__SetFatalErrorHandler(isolate_handle, fatalCallback);
|
||||
v8.v8__Isolate__SetOOMErrorHandler(isolate_handle, oomCallback);
|
||||
|
||||
isolate.enter();
|
||||
errdefer isolate.exit();
|
||||
|
||||
v8.v8__Isolate__SetHostInitializeImportMetaObjectCallback(isolate.handle, Context.metaObjectCallback);
|
||||
v8.v8__Isolate__SetHostInitializeImportMetaObjectCallback(isolate_handle, Context.metaObjectCallback);
|
||||
|
||||
// Allocate arrays dynamically to avoid comptime dependency on JsApis.len
|
||||
const eternal_function_templates = try allocator.alloc(v8.Eternal, JsApis.len);
|
||||
@@ -105,19 +121,19 @@ pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot
|
||||
|
||||
inline for (JsApis, 0..) |JsApi, i| {
|
||||
JsApi.Meta.class_id = i;
|
||||
const data = v8.v8__Isolate__GetDataFromSnapshotOnce(isolate.handle, snapshot.data_start + i);
|
||||
const data = v8.v8__Isolate__GetDataFromSnapshotOnce(isolate_handle, snapshot.data_start + i);
|
||||
const function_handle: *const v8.FunctionTemplate = @ptrCast(data);
|
||||
// Make function template eternal
|
||||
v8.v8__Eternal__New(isolate.handle, @ptrCast(function_handle), &eternal_function_templates[i]);
|
||||
v8.v8__Eternal__New(isolate_handle, @ptrCast(function_handle), &eternal_function_templates[i]);
|
||||
|
||||
// Extract the local handle from the global for easy access
|
||||
const eternal_ptr = v8.v8__Eternal__Get(&eternal_function_templates[i], isolate.handle);
|
||||
const eternal_ptr = v8.v8__Eternal__Get(&eternal_function_templates[i], isolate_handle);
|
||||
templates[i] = @ptrCast(@alignCast(eternal_ptr.?));
|
||||
}
|
||||
|
||||
// Create global template once per isolate
|
||||
const js_global = v8.v8__FunctionTemplate__New__DEFAULT(isolate.handle);
|
||||
const window_name = v8.v8__String__NewFromUtf8(isolate.handle, "Window", v8.kNormal, 6);
|
||||
const js_global = v8.v8__FunctionTemplate__New__DEFAULT(isolate_handle);
|
||||
const window_name = v8.v8__String__NewFromUtf8(isolate_handle, "Window", v8.kNormal, 6);
|
||||
v8.v8__FunctionTemplate__SetClassName(js_global, window_name);
|
||||
|
||||
// Find Window in JsApis by name (avoids circular import)
|
||||
@@ -126,7 +142,7 @@ pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot
|
||||
|
||||
const global_template_local = v8.v8__FunctionTemplate__InstanceTemplate(js_global).?;
|
||||
v8.v8__ObjectTemplate__SetNamedHandler(global_template_local, &.{
|
||||
.getter = bridge.unknownPropertyCallback,
|
||||
.getter = bridge.unknownWindowPropertyCallback,
|
||||
.setter = null,
|
||||
.query = null,
|
||||
.deleter = null,
|
||||
@@ -136,41 +152,163 @@ pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot
|
||||
.data = null,
|
||||
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
||||
});
|
||||
v8.v8__Eternal__New(isolate.handle, @ptrCast(global_template_local), &global_eternal);
|
||||
v8.v8__Eternal__New(isolate_handle, @ptrCast(global_template_local), &global_eternal);
|
||||
}
|
||||
|
||||
var inspector: ?*js.Inspector = null;
|
||||
if (opts.with_inspector) {
|
||||
inspector = try Inspector.init(allocator, isolate_handle);
|
||||
}
|
||||
|
||||
return .{
|
||||
.app = app,
|
||||
.context_id = 0,
|
||||
.contexts = .empty,
|
||||
.isolate = isolate,
|
||||
.platform = platform,
|
||||
.allocator = allocator,
|
||||
.platform = &app.platform,
|
||||
.templates = templates,
|
||||
.isolate_params = params,
|
||||
.inspector = inspector,
|
||||
.eternal_function_templates = eternal_function_templates,
|
||||
.global_template = global_eternal,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Env) void {
|
||||
self.allocator.free(self.templates);
|
||||
self.allocator.free(self.eternal_function_templates);
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(self.contexts.items.len == 0);
|
||||
}
|
||||
for (self.contexts.items) |ctx| {
|
||||
ctx.deinit();
|
||||
}
|
||||
|
||||
const allocator = self.app.allocator;
|
||||
if (self.inspector) |i| {
|
||||
i.deinit(allocator);
|
||||
}
|
||||
|
||||
self.contexts.deinit(allocator);
|
||||
|
||||
allocator.free(self.templates);
|
||||
allocator.free(self.eternal_function_templates);
|
||||
|
||||
self.isolate.exit();
|
||||
self.isolate.deinit();
|
||||
v8.v8__ArrayBuffer__Allocator__DELETE(self.isolate_params.array_buffer_allocator.?);
|
||||
self.allocator.destroy(self.isolate_params);
|
||||
allocator.destroy(self.isolate_params);
|
||||
}
|
||||
|
||||
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !*Inspector {
|
||||
const inspector = try arena.create(Inspector);
|
||||
try Inspector.init(inspector, self.isolate.handle, ctx);
|
||||
return inspector;
|
||||
pub fn createContext(self: *Env, page: *Page, enter: bool) !*Context {
|
||||
const context_arena = try self.app.arena_pool.acquire();
|
||||
errdefer self.app.arena_pool.release(context_arena);
|
||||
|
||||
const isolate = self.isolate;
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
// Get the global template that was created once per isolate
|
||||
const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&self.global_template, isolate.handle).?));
|
||||
const v8_context = v8.v8__Context__New(isolate.handle, global_template, null).?;
|
||||
|
||||
// Create the v8::Context and wrap it in a v8::Global
|
||||
var context_global: v8.Global = undefined;
|
||||
v8.v8__Global__New(isolate.handle, v8_context, &context_global);
|
||||
|
||||
// our window wrapped in a v8::Global
|
||||
const global_obj = v8.v8__Context__Global(v8_context).?;
|
||||
var global_global: v8.Global = undefined;
|
||||
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
|
||||
|
||||
if (enter) {
|
||||
v8.v8__Context__Enter(v8_context);
|
||||
}
|
||||
errdefer if (enter) {
|
||||
v8.v8__Context__Exit(v8_context);
|
||||
};
|
||||
|
||||
const context_id = self.context_id;
|
||||
self.context_id = context_id + 1;
|
||||
|
||||
const context = try context_arena.create(Context);
|
||||
context.* = .{
|
||||
.env = self,
|
||||
.page = page,
|
||||
.id = context_id,
|
||||
.entered = enter,
|
||||
.isolate = isolate,
|
||||
.arena = context_arena,
|
||||
.handle = context_global,
|
||||
.templates = self.templates,
|
||||
.call_arena = page.call_arena,
|
||||
.script_manager = &page._script_manager,
|
||||
.scheduler = .init(context_arena),
|
||||
.finalizer_callback_pool = std.heap.MemoryPool(Context.FinalizerCallback).init(self.app.allocator),
|
||||
};
|
||||
try context.identity_map.putNoClobber(context_arena, @intFromPtr(page.window), global_global);
|
||||
|
||||
// Store a pointer to our context inside the v8 context so that, given
|
||||
// a v8 context, we can get our context out
|
||||
const data = isolate.initBigInt(@intFromPtr(context));
|
||||
v8.v8__Context__SetEmbedderData(v8_context, 1, @ptrCast(data.handle));
|
||||
|
||||
try self.contexts.append(self.app.allocator, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
pub fn destroyContext(self: *Env, context: *Context) void {
|
||||
for (self.contexts.items, 0..) |ctx, i| {
|
||||
if (ctx == context) {
|
||||
_ = self.contexts.swapRemove(i);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (comptime IS_DEBUG) {
|
||||
@panic("Tried to remove unknown context");
|
||||
}
|
||||
}
|
||||
|
||||
const isolate = self.isolate;
|
||||
if (self.inspector) |inspector| {
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(isolate);
|
||||
defer hs.deinit();
|
||||
inspector.contextDestroyed(@ptrCast(v8.v8__Global__Get(&context.handle, isolate.handle)));
|
||||
}
|
||||
|
||||
context.deinit();
|
||||
isolate.notifyContextDisposed();
|
||||
}
|
||||
|
||||
pub fn runMicrotasks(self: *const Env) void {
|
||||
self.isolate.performMicrotasksCheckpoint();
|
||||
}
|
||||
|
||||
pub fn runMacrotasks(self: *Env) !?u64 {
|
||||
var ms_to_next_task: ?u64 = null;
|
||||
for (self.contexts.items) |ctx| {
|
||||
if (comptime builtin.is_test == false) {
|
||||
// I hate this comptime check as much as you do. But we have tests
|
||||
// which rely on short execution before shutdown. In real world, it's
|
||||
// underterministic whether a timer will or won't run before the
|
||||
// page shutsdown. But for tests, we need to run them to their end.
|
||||
if (ctx.scheduler.hasReadyTasks() == false) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
const entered = ctx.enter(&hs);
|
||||
defer entered.exit();
|
||||
|
||||
const ms = (try ctx.scheduler.run()) orelse continue;
|
||||
if (ms_to_next_task == null or ms < ms_to_next_task.?) {
|
||||
ms_to_next_task = ms;
|
||||
}
|
||||
}
|
||||
return ms_to_next_task;
|
||||
}
|
||||
|
||||
pub fn pumpMessageLoop(self: *const Env) bool {
|
||||
var hs: v8.HandleScope = undefined;
|
||||
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate.handle);
|
||||
@@ -182,13 +320,6 @@ pub fn pumpMessageLoop(self: *const Env) bool {
|
||||
pub fn runIdleTasks(self: *const Env) void {
|
||||
v8.v8__Platform__RunIdleTasks(self.platform.handle, self.isolate.handle, 1);
|
||||
}
|
||||
pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
|
||||
return .{
|
||||
.env = self,
|
||||
.context = null,
|
||||
.context_arena = ArenaAllocator.init(self.allocator),
|
||||
};
|
||||
}
|
||||
|
||||
// V8 doesn't immediately free memory associated with
|
||||
// a Context, it's managed by the garbage collector. We use the
|
||||
@@ -252,18 +383,13 @@ fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) v
|
||||
.call_arena = ctx.call_arena,
|
||||
};
|
||||
|
||||
const value =
|
||||
if (v8.v8__PromiseRejectMessage__GetValue(&message_handle)) |v8_value|
|
||||
// @HandleScope - no reason to create a js.Context here
|
||||
local.valueHandleToString(v8_value, .{}) catch |err| @errorName(err)
|
||||
else
|
||||
"no value";
|
||||
|
||||
log.debug(.js, "unhandled rejection", .{
|
||||
.value = value,
|
||||
.stack = local.stackTrace() catch |err| @errorName(err) orelse "???",
|
||||
.note = "This should be updated to call window.unhandledrejection",
|
||||
});
|
||||
const page = ctx.page;
|
||||
page.window.unhandledPromiseRejection(.{
|
||||
.local = &local,
|
||||
.handle = &message_handle,
|
||||
}, page) catch |err| {
|
||||
log.warn(.browser, "unhandled rejection handler", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
fn fatalCallback(c_location: [*c]const u8, c_message: [*c]const u8) callconv(.c) void {
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const Env = @import("Env.zig");
|
||||
const bridge = @import("bridge.zig");
|
||||
const Context = @import("Context.zig");
|
||||
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const CONTEXT_ARENA_RETAIN = 1024 * 64;
|
||||
|
||||
// ExecutionWorld closely models a JS World.
|
||||
// https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#World
|
||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld
|
||||
const ExecutionWorld = @This();
|
||||
|
||||
env: *Env,
|
||||
|
||||
// Arena whose lifetime is for a single page load. Where
|
||||
// the call_arena lives for a single function call, the context_arena
|
||||
// lives for the lifetime of the entire page. The allocator will be
|
||||
// owned by the Context, but the arena itself is owned by the ExecutionWorld
|
||||
// so that we can re-use it from context to context.
|
||||
context_arena: ArenaAllocator,
|
||||
|
||||
// Currently a context maps to a Browser's Page. Here though, it's only a
|
||||
// mechanism to organization page-specific memory. The ExecutionWorld
|
||||
// does all the work, but having all page-specific data structures
|
||||
// grouped together helps keep things clean.
|
||||
context: ?Context = null,
|
||||
|
||||
// no init, must be initialized via env.newExecutionWorld()
|
||||
|
||||
pub fn deinit(self: *ExecutionWorld) void {
|
||||
if (self.context != null) {
|
||||
self.removeContext();
|
||||
}
|
||||
self.context_arena.deinit();
|
||||
}
|
||||
|
||||
// Only the top Context in the Main ExecutionWorld should hold a handle_scope.
|
||||
// A js.HandleScope is like an arena. Once created, any "Local" that
|
||||
// v8 creates will be released (or at least, releasable by the v8 GC)
|
||||
// when the handle_scope is freed.
|
||||
// We also maintain our own "context_arena" which allows us to have
|
||||
// all page related memory easily managed.
|
||||
pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context {
|
||||
lp.assert(self.context == null, "ExecptionWorld.createContext has context", .{});
|
||||
|
||||
const env = self.env;
|
||||
const isolate = env.isolate;
|
||||
const arena = self.context_arena.allocator();
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
// Get the global template that was created once per isolate
|
||||
const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&env.global_template, isolate.handle).?));
|
||||
const v8_context = v8.v8__Context__New(isolate.handle, global_template, null).?;
|
||||
|
||||
// Create the v8::Context and wrap it in a v8::Global
|
||||
var context_global: v8.Global = undefined;
|
||||
v8.v8__Global__New(isolate.handle, v8_context, &context_global);
|
||||
|
||||
// our window wrapped in a v8::Global
|
||||
const global_obj = v8.v8__Context__Global(v8_context).?;
|
||||
var global_global: v8.Global = undefined;
|
||||
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
|
||||
|
||||
if (enter) {
|
||||
v8.v8__Context__Enter(v8_context);
|
||||
}
|
||||
errdefer if (enter) {
|
||||
v8.v8__Context__Exit(v8_context);
|
||||
};
|
||||
|
||||
const context_id = env.context_id;
|
||||
env.context_id = context_id + 1;
|
||||
|
||||
self.context = Context{
|
||||
.page = page,
|
||||
.id = context_id,
|
||||
.entered = enter,
|
||||
.isolate = isolate,
|
||||
.handle = context_global,
|
||||
.templates = env.templates,
|
||||
.script_manager = &page._script_manager,
|
||||
.call_arena = page.call_arena,
|
||||
.arena = arena,
|
||||
};
|
||||
|
||||
var context = &self.context.?;
|
||||
try context.identity_map.putNoClobber(arena, @intFromPtr(page.window), global_global);
|
||||
|
||||
// Store a pointer to our context inside the v8 context so that, given
|
||||
// a v8 context, we can get our context out
|
||||
const data = isolate.initBigInt(@intFromPtr(&self.context.?));
|
||||
v8.v8__Context__SetEmbedderData(v8_context, 1, @ptrCast(data.handle));
|
||||
|
||||
return &self.context.?;
|
||||
}
|
||||
|
||||
pub fn removeContext(self: *ExecutionWorld) void {
|
||||
var context = &(self.context orelse return);
|
||||
context.deinit();
|
||||
self.context = null;
|
||||
|
||||
self.env.isolate.notifyContextDisposed();
|
||||
_ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN });
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -22,8 +22,6 @@ const v8 = js.v8;
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Function = @This();
|
||||
|
||||
local: *const js.Local,
|
||||
@@ -71,7 +69,15 @@ pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Objec
|
||||
|
||||
pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
return self._tryCallWithThis(T, self.getThis(), args, &caught) catch |err| {
|
||||
return self._tryCallWithThis(T, self.getThis(), args, &caught, .{}) catch |err| {
|
||||
log.warn(.js, "call caught", .{ .err = err, .caught = caught });
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn callRethrow(self: *const Function, comptime T: type, args: anytype) !T {
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
return self._tryCallWithThis(T, self.getThis(), args, &caught, .{ .rethrow = true }) catch |err| {
|
||||
log.warn(.js, "call caught", .{ .err = err, .caught = caught });
|
||||
return err;
|
||||
};
|
||||
@@ -79,21 +85,24 @@ pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
|
||||
|
||||
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
return self._tryCallWithThis(T, this, args, &caught) catch |err| {
|
||||
return self._tryCallWithThis(T, this, args, &caught, .{}) catch |err| {
|
||||
log.warn(.js, "callWithThis caught", .{ .err = err, .caught = caught });
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, caught: *js.TryCatch.Caught) !T {
|
||||
return self._tryCallWithThis(T, self.getThis(), args, caught);
|
||||
return self._tryCallWithThis(T, self.getThis(), args, caught, .{});
|
||||
}
|
||||
|
||||
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T {
|
||||
return self._tryCallWithThis(T, this, args, caught);
|
||||
return self._tryCallWithThis(T, this, args, caught, .{});
|
||||
}
|
||||
|
||||
pub fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T {
|
||||
const CallOpts = struct {
|
||||
rethrow: bool = false,
|
||||
};
|
||||
fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught, comptime opts: CallOpts) !T {
|
||||
caught.* = .{};
|
||||
const local = self.local;
|
||||
|
||||
@@ -147,6 +156,10 @@ pub fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype,
|
||||
defer try_catch.deinit();
|
||||
|
||||
const handle = v8.v8__Function__Call(self.handle, local.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse {
|
||||
if ((comptime opts.rethrow) and try_catch.hasCaught()) {
|
||||
try_catch.rethrow();
|
||||
return error.TryCatchRethrow;
|
||||
}
|
||||
caught.* = try_catch.caughtOrError(local.call_arena, error.JSExecCallback);
|
||||
return error.JSExecCallback;
|
||||
};
|
||||
|
||||
@@ -23,99 +23,76 @@ const v8 = js.v8;
|
||||
const TaggedOpaque = @import("TaggedOpaque.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const RndGen = std.Random.DefaultPrng;
|
||||
|
||||
const CONTEXT_GROUP_ID = 1;
|
||||
const CLIENT_TRUST_LEVEL = 1;
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
// Inspector exists for the lifetime of the Isolate/Env. 1 Isolate = 1 Inspector.
|
||||
// It combines the v8.Inspector and the v8.InspectorClientImpl. The v8.InspectorClientImpl
|
||||
// is our own implementation that fulfills the InspectorClient API, i.e. it's the
|
||||
// mechanism v8 provides to let us tweak how the inspector works. For example, it
|
||||
// Below, you'll find a few pub export fn v8_inspector__Client__IMPL__XYZ functions
|
||||
// which is our implementation of what the v8::Inspector requires of our Client
|
||||
// (not much at all)
|
||||
const Inspector = @This();
|
||||
|
||||
handle: *v8.Inspector,
|
||||
unique_id: i64,
|
||||
isolate: *v8.Isolate,
|
||||
client: Client,
|
||||
channel: Channel,
|
||||
session: Session,
|
||||
rnd: RndGen = RndGen.init(0),
|
||||
handle: *v8.Inspector,
|
||||
client: *v8.InspectorClientImpl,
|
||||
default_context: ?v8.Global,
|
||||
session: ?Session,
|
||||
|
||||
// We expect allocator to be an arena
|
||||
// Note: This initializes the pre-allocated inspector in-place
|
||||
pub fn init(self: *Inspector, isolate: *v8.Isolate, ctx: anytype) !void {
|
||||
const ContextT = @TypeOf(ctx);
|
||||
pub fn init(allocator: Allocator, isolate: *v8.Isolate) !*Inspector {
|
||||
const self = try allocator.create(Inspector);
|
||||
errdefer allocator.destroy(self);
|
||||
|
||||
const Container = switch (@typeInfo(ContextT)) {
|
||||
.@"struct" => ContextT,
|
||||
.pointer => |ptr| ptr.child,
|
||||
.void => NoopInspector,
|
||||
else => @compileError("invalid context type"),
|
||||
};
|
||||
// If necessary, turn a void context into something we can safely ptrCast
|
||||
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
|
||||
|
||||
// Initialize the fields that callbacks need first
|
||||
self.* = .{
|
||||
.handle = undefined,
|
||||
.unique_id = 1,
|
||||
.session = null,
|
||||
.isolate = isolate,
|
||||
.client = undefined,
|
||||
.channel = undefined,
|
||||
.rnd = RndGen.init(0),
|
||||
.handle = undefined,
|
||||
.default_context = null,
|
||||
.session = undefined,
|
||||
};
|
||||
|
||||
// Create client and set inspector data BEFORE creating the inspector
|
||||
// because V8 will call generateUniqueId during inspector creation
|
||||
const client = Client.init();
|
||||
self.client = client;
|
||||
client.setInspector(self);
|
||||
self.client = v8.v8_inspector__Client__IMPL__CREATE();
|
||||
errdefer v8.v8_inspector__Client__IMPL__DELETE(self.client);
|
||||
v8.v8_inspector__Client__IMPL__SET_DATA(self.client, self);
|
||||
|
||||
// Now create the inspector - generateUniqueId will work because data is set
|
||||
const handle = v8.v8_inspector__Inspector__Create(isolate, client.handle).?;
|
||||
self.handle = handle;
|
||||
self.handle = v8.v8_inspector__Inspector__Create(isolate, self.client).?;
|
||||
errdefer v8.v8_inspector__Inspector__DELETE(self.handle);
|
||||
|
||||
// Create the channel
|
||||
const channel = Channel.init(
|
||||
safe_context,
|
||||
Container.onInspectorResponse,
|
||||
Container.onInspectorEvent,
|
||||
Container.onRunMessageLoopOnPause,
|
||||
Container.onQuitMessageLoopOnPause,
|
||||
isolate,
|
||||
);
|
||||
self.channel = channel;
|
||||
channel.setInspector(self);
|
||||
|
||||
// Create the session
|
||||
const session_handle = v8.v8_inspector__Inspector__Connect(
|
||||
handle,
|
||||
CONTEXT_GROUP_ID,
|
||||
channel.handle,
|
||||
CLIENT_TRUST_LEVEL,
|
||||
).?;
|
||||
self.session = .{ .handle = session_handle };
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Inspector) void {
|
||||
pub fn deinit(self: *const Inspector, allocator: Allocator) void {
|
||||
var hs: v8.HandleScope = undefined;
|
||||
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate);
|
||||
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||
|
||||
self.session.deinit();
|
||||
self.client.deinit();
|
||||
self.channel.deinit();
|
||||
if (self.session) |*s| {
|
||||
s.deinit();
|
||||
}
|
||||
v8.v8_inspector__Client__IMPL__DELETE(self.client);
|
||||
v8.v8_inspector__Inspector__DELETE(self.handle);
|
||||
allocator.destroy(self);
|
||||
}
|
||||
|
||||
pub fn send(self: *const Inspector, msg: []const u8) void {
|
||||
// Can't assume the main Context exists (with its HandleScope)
|
||||
// available when doing this. Pages (and thus the HandleScope)
|
||||
// comes and goes, but CDP can keep sending messages.
|
||||
const isolate = self.isolate;
|
||||
var temp_scope: v8.HandleScope = undefined;
|
||||
v8.v8__HandleScope__CONSTRUCT(&temp_scope, isolate);
|
||||
defer v8.v8__HandleScope__DESTRUCT(&temp_scope);
|
||||
pub fn startSession(self: *Inspector, ctx: anytype) *Session {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(self.session == null);
|
||||
}
|
||||
|
||||
self.session.dispatchProtocolMessage(isolate, msg);
|
||||
self.session = @as(Session, undefined);
|
||||
Session.init(&self.session.?, self, ctx);
|
||||
return &self.session.?;
|
||||
}
|
||||
|
||||
pub fn stopSession(self: *Inspector) void {
|
||||
self.session.?.deinit();
|
||||
self.session = null;
|
||||
}
|
||||
|
||||
// From CDP docs
|
||||
@@ -151,8 +128,8 @@ pub fn contextCreated(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contextDestroyed(self: *Inspector, local: *const js.Local) void {
|
||||
v8.v8_inspector__Inspector__ContextDestroyed(self.handle, local.handle);
|
||||
pub fn contextDestroyed(self: *Inspector, context: *const v8.Context) void {
|
||||
v8.v8_inspector__Inspector__ContextDestroyed(self.handle, context);
|
||||
}
|
||||
|
||||
pub fn resetContextGroup(self: *const Inspector) void {
|
||||
@@ -163,51 +140,6 @@ pub fn resetContextGroup(self: *const Inspector) void {
|
||||
v8.v8_inspector__Inspector__ResetContextGroup(self.handle, CONTEXT_GROUP_ID);
|
||||
}
|
||||
|
||||
// Retrieves the RemoteObject for a given value.
|
||||
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
|
||||
// just like a method return value. Therefore, if we've mapped this
|
||||
// value before, we'll get the existing js.Global(js.Object) and if not
|
||||
// we'll create it and track it for cleanup when the context ends.
|
||||
pub fn getRemoteObject(
|
||||
self: *const Inspector,
|
||||
local: *const js.Local,
|
||||
group: []const u8,
|
||||
value: anytype,
|
||||
) !RemoteObject {
|
||||
const js_val = try local.zigValueToJs(value, .{});
|
||||
|
||||
// We do not want to expose this as a parameter for now
|
||||
const generate_preview = false;
|
||||
return self.session.wrapObject(
|
||||
local.isolate.handle,
|
||||
local.handle,
|
||||
js_val.handle,
|
||||
group,
|
||||
generate_preview,
|
||||
);
|
||||
}
|
||||
|
||||
// Gets a value by object ID regardless of which context it is in.
|
||||
// Our TaggedOpaque stores the "resolved" ptr value (the most specific _type,
|
||||
// e.g. we store the ptr to the Div not the EventTarget). But, this is asking for
|
||||
// the pointer to the Node, so we need to use the same resolution mechanism which
|
||||
// is used when we're calling a function to turn the Div into a Node, which is
|
||||
// what TaggedOpaque.fromJS does.
|
||||
pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8, local: *js.Local) !*anyopaque {
|
||||
// just to indicate that the caller is responsible for ensure there's a local environment
|
||||
_ = local;
|
||||
const unwrapped = try self.session.unwrapObject(allocator, object_id);
|
||||
// The values context and groupId are not used here
|
||||
const js_val = unwrapped.value;
|
||||
if (!v8.v8__Value__IsObject(js_val)) {
|
||||
return error.ObjectIdIsNotANode;
|
||||
}
|
||||
|
||||
const Node = @import("../webapi/Node.zig");
|
||||
// Cast to *const v8.Object for typeTaggedAnyOpaque
|
||||
return TaggedOpaque.fromJS(*Node, @ptrCast(js_val)) catch return error.ObjectIdIsNotANode;
|
||||
}
|
||||
|
||||
pub const RemoteObject = struct {
|
||||
handle: *v8.RemoteObject,
|
||||
|
||||
@@ -254,20 +186,109 @@ pub const RemoteObject = struct {
|
||||
}
|
||||
};
|
||||
|
||||
const Session = struct {
|
||||
// Combines a v8::InspectorSession and a v8::InspectorChannelImpl. The
|
||||
// InspectorSession is for zig -> v8 (sending messages to the inspector). The
|
||||
// Channel is for v8 -> zig, getting events from the Inspector (that we'll pass
|
||||
// back ot some opaque context, i.e the CDP BrowserContext).
|
||||
// The channel callbacks are defined below, as:
|
||||
// pub export fn v8_inspector__Channel__IMPL__XYZ
|
||||
pub const Session = struct {
|
||||
inspector: *Inspector,
|
||||
handle: *v8.InspectorSession,
|
||||
channel: *v8.InspectorChannelImpl,
|
||||
|
||||
fn deinit(self: Session) void {
|
||||
v8.v8_inspector__Session__DELETE(self.handle);
|
||||
// callbacks
|
||||
ctx: *anyopaque,
|
||||
onNotif: *const fn (ctx: *anyopaque, msg: []const u8) void,
|
||||
onResp: *const fn (ctx: *anyopaque, call_id: u32, msg: []const u8) void,
|
||||
|
||||
fn init(self: *Session, inspector: *Inspector, ctx: anytype) void {
|
||||
const Container = @typeInfo(@TypeOf(ctx)).pointer.child;
|
||||
|
||||
const channel = v8.v8_inspector__Channel__IMPL__CREATE(inspector.isolate);
|
||||
const handle = v8.v8_inspector__Inspector__Connect(
|
||||
inspector.handle,
|
||||
CONTEXT_GROUP_ID,
|
||||
channel,
|
||||
CLIENT_TRUST_LEVEL,
|
||||
).?;
|
||||
v8.v8_inspector__Channel__IMPL__SET_DATA(channel, self);
|
||||
|
||||
self.* = .{
|
||||
.ctx = ctx,
|
||||
.handle = handle,
|
||||
.channel = channel,
|
||||
.inspector = inspector,
|
||||
.onResp = Container.onInspectorResponse,
|
||||
.onNotif = Container.onInspectorEvent,
|
||||
};
|
||||
}
|
||||
|
||||
fn dispatchProtocolMessage(self: Session, isolate: *v8.Isolate, msg: []const u8) void {
|
||||
fn deinit(self: *const Session) void {
|
||||
v8.v8_inspector__Session__DELETE(self.handle);
|
||||
v8.v8_inspector__Channel__IMPL__DELETE(self.channel);
|
||||
}
|
||||
|
||||
pub fn send(self: *const Session, msg: []const u8) void {
|
||||
const isolate = self.inspector.isolate;
|
||||
var hs: v8.HandleScope = undefined;
|
||||
v8.v8__HandleScope__CONSTRUCT(&hs, isolate);
|
||||
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||
|
||||
v8.v8_inspector__Session__dispatchProtocolMessage(
|
||||
self.handle,
|
||||
isolate,
|
||||
msg.ptr,
|
||||
msg.len,
|
||||
);
|
||||
|
||||
v8.v8__Isolate__PerformMicrotaskCheckpoint(isolate);
|
||||
}
|
||||
|
||||
// Gets a value by object ID regardless of which context it is in.
|
||||
// Our TaggedOpaque stores the "resolved" ptr value (the most specific _type,
|
||||
// e.g. we store the ptr to the Div not the EventTarget). But, this is asking for
|
||||
// the pointer to the Node, so we need to use the same resolution mechanism which
|
||||
// is used when we're calling a function to turn the Div into a Node, which is
|
||||
// what TaggedOpaque.fromJS does.
|
||||
pub fn getNodePtr(self: *const Session, allocator: Allocator, object_id: []const u8, local: *js.Local) !*anyopaque {
|
||||
// just to indicate that the caller is responsible for ensuring there's a local environment
|
||||
_ = local;
|
||||
|
||||
const unwrapped = try self.unwrapObject(allocator, object_id);
|
||||
// The values context and groupId are not used here
|
||||
const js_val = unwrapped.value;
|
||||
if (!v8.v8__Value__IsObject(js_val)) {
|
||||
return error.ObjectIdIsNotANode;
|
||||
}
|
||||
|
||||
const Node = @import("../webapi/Node.zig");
|
||||
// Cast to *const v8.Object for typeTaggedAnyOpaque
|
||||
return TaggedOpaque.fromJS(*Node, @ptrCast(js_val)) catch return error.ObjectIdIsNotANode;
|
||||
}
|
||||
|
||||
// Retrieves the RemoteObject for a given value.
|
||||
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
|
||||
// just like a method return value. Therefore, if we've mapped this
|
||||
// value before, we'll get the existing js.Global(js.Object) and if not
|
||||
// we'll create it and track it for cleanup when the context ends.
|
||||
pub fn getRemoteObject(
|
||||
self: *const Session,
|
||||
local: *const js.Local,
|
||||
group: []const u8,
|
||||
value: anytype,
|
||||
) !RemoteObject {
|
||||
const js_val = try local.zigValueToJs(value, .{});
|
||||
|
||||
// We do not want to expose this as a parameter for now
|
||||
const generate_preview = false;
|
||||
return self.wrapObject(
|
||||
local.isolate.handle,
|
||||
local.handle,
|
||||
js_val.handle,
|
||||
group,
|
||||
generate_preview,
|
||||
);
|
||||
}
|
||||
|
||||
fn wrapObject(
|
||||
@@ -334,84 +355,6 @@ const UnwrappedObject = struct {
|
||||
object_group: ?[]const u8,
|
||||
};
|
||||
|
||||
const Channel = struct {
|
||||
handle: *v8.InspectorChannelImpl,
|
||||
|
||||
// callbacks
|
||||
ctx: *anyopaque,
|
||||
onNotif: onNotifFn = undefined,
|
||||
onResp: onRespFn = undefined,
|
||||
onRunMessageLoopOnPause: onRunMessageLoopOnPauseFn = undefined,
|
||||
onQuitMessageLoopOnPause: onQuitMessageLoopOnPauseFn = undefined,
|
||||
|
||||
pub const onNotifFn = *const fn (ctx: *anyopaque, msg: []const u8) void;
|
||||
pub const onRespFn = *const fn (ctx: *anyopaque, call_id: u32, msg: []const u8) void;
|
||||
pub const onRunMessageLoopOnPauseFn = *const fn (ctx: *anyopaque, context_group_id: u32) void;
|
||||
pub const onQuitMessageLoopOnPauseFn = *const fn (ctx: *anyopaque) void;
|
||||
|
||||
fn init(
|
||||
ctx: *anyopaque,
|
||||
onResp: onRespFn,
|
||||
onNotif: onNotifFn,
|
||||
onRunMessageLoopOnPause: onRunMessageLoopOnPauseFn,
|
||||
onQuitMessageLoopOnPause: onQuitMessageLoopOnPauseFn,
|
||||
isolate: *v8.Isolate,
|
||||
) Channel {
|
||||
const handle = v8.v8_inspector__Channel__IMPL__CREATE(isolate);
|
||||
return .{
|
||||
.handle = handle,
|
||||
.ctx = ctx,
|
||||
.onResp = onResp,
|
||||
.onNotif = onNotif,
|
||||
.onRunMessageLoopOnPause = onRunMessageLoopOnPause,
|
||||
.onQuitMessageLoopOnPause = onQuitMessageLoopOnPause,
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(self: Channel) void {
|
||||
v8.v8_inspector__Channel__IMPL__DELETE(self.handle);
|
||||
}
|
||||
|
||||
fn setInspector(self: Channel, inspector: *anyopaque) void {
|
||||
v8.v8_inspector__Channel__IMPL__SET_DATA(self.handle, inspector);
|
||||
}
|
||||
|
||||
fn resp(self: Channel, call_id: u32, msg: []const u8) void {
|
||||
self.onResp(self.ctx, call_id, msg);
|
||||
}
|
||||
|
||||
fn notif(self: Channel, msg: []const u8) void {
|
||||
self.onNotif(self.ctx, msg);
|
||||
}
|
||||
};
|
||||
|
||||
const Client = struct {
|
||||
handle: *v8.InspectorClientImpl,
|
||||
|
||||
fn init() Client {
|
||||
return .{ .handle = v8.v8_inspector__Client__IMPL__CREATE() };
|
||||
}
|
||||
|
||||
fn deinit(self: Client) void {
|
||||
v8.v8_inspector__Client__IMPL__DELETE(self.handle);
|
||||
}
|
||||
|
||||
fn setInspector(self: Client, inspector: *anyopaque) void {
|
||||
v8.v8_inspector__Client__IMPL__SET_DATA(self.handle, inspector);
|
||||
}
|
||||
};
|
||||
|
||||
const NoopInspector = struct {
|
||||
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
|
||||
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
|
||||
pub fn onRunMessageLoopOnPause(_: *anyopaque, _: u32) void {}
|
||||
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {}
|
||||
};
|
||||
|
||||
fn fromData(data: *anyopaque) *Inspector {
|
||||
return @ptrCast(@alignCast(data));
|
||||
}
|
||||
|
||||
pub fn getTaggedOpaque(value: *const v8.Value) ?*TaggedOpaque {
|
||||
if (!v8.v8__Value__IsObject(value)) {
|
||||
return null;
|
||||
@@ -437,24 +380,25 @@ pub export fn v8_inspector__Client__IMPL__generateUniqueId(
|
||||
data: *anyopaque,
|
||||
) callconv(.c) i64 {
|
||||
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
||||
return inspector.rnd.random().int(i64);
|
||||
const unique_id = inspector.unique_id + 1;
|
||||
inspector.unique_id = unique_id;
|
||||
return unique_id;
|
||||
}
|
||||
|
||||
pub export fn v8_inspector__Client__IMPL__runMessageLoopOnPause(
|
||||
_: *v8.InspectorClientImpl,
|
||||
data: *anyopaque,
|
||||
ctx_group_id: c_int,
|
||||
context_group_id: c_int,
|
||||
) callconv(.c) void {
|
||||
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
||||
inspector.channel.onRunMessageLoopOnPause(inspector.channel.ctx, @intCast(ctx_group_id));
|
||||
_ = data;
|
||||
_ = context_group_id;
|
||||
}
|
||||
|
||||
pub export fn v8_inspector__Client__IMPL__quitMessageLoopOnPause(
|
||||
_: *v8.InspectorClientImpl,
|
||||
data: *anyopaque,
|
||||
) callconv(.c) void {
|
||||
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
||||
inspector.channel.onQuitMessageLoopOnPause(inspector.channel.ctx);
|
||||
_ = data;
|
||||
}
|
||||
|
||||
pub export fn v8_inspector__Client__IMPL__runIfWaitingForDebugger(
|
||||
@@ -493,8 +437,8 @@ pub export fn v8_inspector__Channel__IMPL__sendResponse(
|
||||
msg: [*c]u8,
|
||||
length: usize,
|
||||
) callconv(.c) void {
|
||||
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
||||
inspector.channel.resp(@as(u32, @intCast(call_id)), msg[0..length]);
|
||||
const session: *Session = @ptrCast(@alignCast(data));
|
||||
session.onResp(session.ctx, @intCast(call_id), msg[0..length]);
|
||||
}
|
||||
|
||||
pub export fn v8_inspector__Channel__IMPL__sendNotification(
|
||||
@@ -503,8 +447,8 @@ pub export fn v8_inspector__Channel__IMPL__sendNotification(
|
||||
msg: [*c]u8,
|
||||
length: usize,
|
||||
) callconv(.c) void {
|
||||
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
||||
inspector.channel.notif(msg[0..length]);
|
||||
const session: *Session = @ptrCast(@alignCast(data));
|
||||
session.onNotif(session.ctx, msg[0..length]);
|
||||
}
|
||||
|
||||
pub export fn v8_inspector__Channel__IMPL__flushProtocolNotifications(
|
||||
|
||||
@@ -198,21 +198,28 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
|
||||
// context.global_objects, we want to track it in context.identity_map.
|
||||
v8.v8__Global__New(isolate.handle, js_obj.handle, gop.value_ptr);
|
||||
if (@hasDecl(JsApi.Meta, "finalizer")) {
|
||||
if (comptime IS_DEBUG) {
|
||||
// You can normally return a "*Node" and we'll correctly
|
||||
// handle it as what it really is, e.g. an HTMLScriptElement.
|
||||
// But for finalizers, we can't do that. I think this
|
||||
// limitation will be OK - this auto-resolution is largely
|
||||
// limited to Node -> HtmlElement, none of which has finalizers
|
||||
std.debug.assert(resolved.class_id == JsApi.Meta.class_id);
|
||||
// It would be great if resolved knew the resolved type, but I
|
||||
// can't figure out how to make that work, since it depends on
|
||||
// the [runtime] `value`.
|
||||
// We need the resolved finalizer, which we have in resolved.
|
||||
// The above if statement would be more clear as:
|
||||
// if (resolved.finalizer_from_v8) |finalizer| {
|
||||
// But that's a runtime check.
|
||||
// Instead, we check if the base has finalizer. The assumption
|
||||
// here is that if a resolve type has a finalizer, then the base
|
||||
// should have a finalizer too.
|
||||
const fc = try ctx.createFinalizerCallback(gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?);
|
||||
{
|
||||
errdefer fc.deinit();
|
||||
try ctx.finalizer_callbacks.put(ctx.arena, @intFromPtr(resolved.ptr), fc);
|
||||
}
|
||||
|
||||
try ctx.finalizer_callbacks.put(ctx.arena, @intFromPtr(resolved.ptr), .init(value));
|
||||
conditionallyFlagHandoff(value);
|
||||
if (@hasDecl(JsApi.Meta, "weak")) {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(JsApi.Meta.weak == true);
|
||||
}
|
||||
v8.v8__Global__SetWeakFinalizer(gop.value_ptr, resolved.ptr, JsApi.Meta.finalizer.from_v8, v8.kParameter);
|
||||
v8.v8__Global__SetWeakFinalizer(gop.value_ptr, fc, resolved.finalizer_from_v8, v8.kParameter);
|
||||
}
|
||||
}
|
||||
return js_obj;
|
||||
@@ -316,6 +323,7 @@ pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts)
|
||||
js.Value.Temp,
|
||||
js.Object.Global,
|
||||
js.Promise.Global,
|
||||
js.Promise.Temp,
|
||||
js.PromiseResolver.Global,
|
||||
js.Module.Global => return .{ .local = self, .handle = @ptrCast(value.local(self).handle) },
|
||||
else => {}
|
||||
@@ -473,10 +481,10 @@ pub fn jsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !T {
|
||||
if (ptr.child == u8) {
|
||||
if (ptr.sentinel()) |s| {
|
||||
if (comptime s == 0) {
|
||||
return self.valueToStringZ(js_val, .{});
|
||||
return try js_val.toStringSliceZ();
|
||||
}
|
||||
} else {
|
||||
return self.valueToString(js_val, .{});
|
||||
return try js_val.toStringSlice();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,10 +557,8 @@ pub fn jsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !T {
|
||||
},
|
||||
.@"enum" => |e| {
|
||||
if (@hasDecl(T, "js_enum_from_string")) {
|
||||
if (!js_val.isString()) {
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
return std.meta.stringToEnum(T, try self.valueToString(js_val, .{})) orelse return error.InvalidArgument;
|
||||
const js_str = js_val.isString() orelse return error.InvalidArgument;
|
||||
return std.meta.stringToEnum(T, try js_str.toSlice()) orelse return error.InvalidArgument;
|
||||
}
|
||||
switch (@typeInfo(e.tag_type)) {
|
||||
.int => return std.meta.intToEnum(T, try jsIntToZig(e.tag_type, js_val)),
|
||||
@@ -614,28 +620,27 @@ fn jsValueToStruct(self: *const Local, comptime T: type, js_val: js.Value) !?T {
|
||||
return try obj.persist();
|
||||
},
|
||||
|
||||
js.Promise.Global => {
|
||||
js.Promise.Global, js.Promise.Temp => {
|
||||
if (!js_val.isPromise()) {
|
||||
return null;
|
||||
}
|
||||
const promise = js.Promise{
|
||||
.ctx = self,
|
||||
const js_promise = js.Promise{
|
||||
.local = self,
|
||||
.handle = @ptrCast(js_val.handle),
|
||||
};
|
||||
return try promise.persist();
|
||||
return switch (T) {
|
||||
js.Promise.Temp => try js_promise.temp(),
|
||||
js.Promise.Global => try js_promise.persist(),
|
||||
else => unreachable,
|
||||
};
|
||||
},
|
||||
string.String => {
|
||||
if (!js_val.isString()) {
|
||||
return null;
|
||||
}
|
||||
return try self.valueToStringSSO(js_val, .{ .allocator = self.ctx.call_arena });
|
||||
const js_str = js_val.isString() orelse return null;
|
||||
return try js_str.toSSO(false);
|
||||
},
|
||||
string.Global => {
|
||||
if (!js_val.isString()) {
|
||||
return null;
|
||||
}
|
||||
// Use arena for persistent strings
|
||||
return .{ .str = try self.valueToStringSSO(js_val, .{ .allocator = self.ctx.arena }) };
|
||||
const js_str = js_val.isString() orelse return null;
|
||||
return try js_str.toSSO(true);
|
||||
},
|
||||
else => {
|
||||
if (!js_val.isObject()) {
|
||||
@@ -883,7 +888,7 @@ fn probeJsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !Pr
|
||||
}
|
||||
|
||||
if (ptr.child == u8) {
|
||||
if (js_val.isString()) {
|
||||
if (v8.v8__Value__IsString(js_val.handle)) {
|
||||
return .{ .ok = {} };
|
||||
}
|
||||
// anything can be coerced into a string
|
||||
@@ -931,10 +936,11 @@ fn probeJsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !Pr
|
||||
if (js_arr.len() == arr.len) {
|
||||
return .{ .ok = {} };
|
||||
}
|
||||
} else if (js_val.isString() and arr.child == u8) {
|
||||
const str = try js_val.toString(self.local);
|
||||
if (str.lenUtf8(self.isolate) == arr.len) {
|
||||
return .{ .ok = {} };
|
||||
} else if (arr.child == u8) {
|
||||
if (js_val.isString()) |js_str| {
|
||||
if (js_str.lenUtf8(self.isolate) == arr.len) {
|
||||
return .{ .ok = {} };
|
||||
}
|
||||
}
|
||||
}
|
||||
return .{ .invalid = {} };
|
||||
@@ -947,7 +953,7 @@ fn probeJsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !Pr
|
||||
.@"struct" => {
|
||||
// Handle string.String and string.Global specially
|
||||
if (T == string.String or T == string.Global) {
|
||||
if (js_val.isString()) {
|
||||
if (v8.v8__Value__IsString(js_val.handle)) {
|
||||
return .{ .ok = {} };
|
||||
}
|
||||
// Anything can be coerced to a string
|
||||
@@ -1032,9 +1038,12 @@ fn jsUnsignedIntToZig(comptime T: type, max: comptime_int, maybe: u32) !T {
|
||||
// This function recursively walks the _type union field (if there is one) to
|
||||
// get the most specific class_id possible.
|
||||
const Resolved = struct {
|
||||
weak: bool,
|
||||
ptr: *anyopaque,
|
||||
class_id: u16,
|
||||
prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry,
|
||||
finalizer_from_v8: ?*const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void = null,
|
||||
finalizer_from_zig: ?*const fn (ptr: *anyopaque) void = null,
|
||||
};
|
||||
pub fn resolveValue(value: anytype) Resolved {
|
||||
const T = bridge.Struct(@TypeOf(value));
|
||||
@@ -1062,13 +1071,28 @@ pub fn resolveValue(value: anytype) Resolved {
|
||||
}
|
||||
|
||||
fn resolveT(comptime T: type, value: *anyopaque) Resolved {
|
||||
const Meta = T.JsApi.Meta;
|
||||
return .{
|
||||
.ptr = value,
|
||||
.class_id = T.JsApi.Meta.class_id,
|
||||
.prototype_chain = &T.JsApi.Meta.prototype_chain,
|
||||
.class_id = Meta.class_id,
|
||||
.prototype_chain = &Meta.prototype_chain,
|
||||
.weak = if (@hasDecl(Meta, "weak")) Meta.weak else false,
|
||||
.finalizer_from_v8 = if (@hasDecl(Meta, "finalizer")) Meta.finalizer.from_v8 else null,
|
||||
.finalizer_from_zig = if (@hasDecl(Meta, "finalizer")) Meta.finalizer.from_zig else null,
|
||||
};
|
||||
}
|
||||
|
||||
fn conditionallyFlagHandoff(value: anytype) void {
|
||||
const T = bridge.Struct(@TypeOf(value));
|
||||
if (@hasField(T, "_v8_handoff")) {
|
||||
value._v8_handoff = true;
|
||||
return;
|
||||
}
|
||||
if (@hasField(T, "_proto")) {
|
||||
conditionallyFlagHandoff(value._proto);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stackTrace(self: *const Local) !?[]const u8 {
|
||||
const isolate = self.isolate;
|
||||
const separator = log.separator();
|
||||
@@ -1080,14 +1104,15 @@ pub fn stackTrace(self: *const Local) !?[]const u8 {
|
||||
const frame_count = v8.v8__StackTrace__GetFrameCount(stack_trace_handle);
|
||||
|
||||
if (v8.v8__StackTrace__CurrentScriptNameOrSourceURL__STATIC(isolate.handle)) |script| {
|
||||
try writer.print("{s}<{s}>", .{ separator, try self.jsStringToZig(script, .{}) });
|
||||
const stack = js.String{ .local = self, .handle = script };
|
||||
try writer.print("{s}<{f}>", .{ separator, stack });
|
||||
}
|
||||
|
||||
for (0..@intCast(frame_count)) |i| {
|
||||
const frame_handle = v8.v8__StackTrace__GetFrame(stack_trace_handle, isolate.handle, @intCast(i)).?;
|
||||
if (v8.v8__StackFrame__GetFunctionName(frame_handle)) |name| {
|
||||
const script = try self.jsStringToZig(name, .{});
|
||||
try writer.print("{s}{s}:{d}", .{ separator, script, v8.v8__StackFrame__GetLineNumber(frame_handle) });
|
||||
const script = js.String{ .local = self, .handle = name };
|
||||
try writer.print("{s}{f}:{d}", .{ separator, script, v8.v8__StackFrame__GetLineNumber(frame_handle) });
|
||||
} else {
|
||||
try writer.print("{s}<anonymous>:{d}", .{ separator, v8.v8__StackFrame__GetLineNumber(frame_handle) });
|
||||
}
|
||||
@@ -1095,100 +1120,6 @@ pub fn stackTrace(self: *const Local) !?[]const u8 {
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
// == Stringifiers ==
|
||||
const ToStringOpts = struct {
|
||||
allocator: ?Allocator = null,
|
||||
};
|
||||
pub fn valueToString(self: *const Local, js_val: js.Value, opts: ToStringOpts) ![]u8 {
|
||||
return self.valueHandleToString(js_val.handle, opts);
|
||||
}
|
||||
pub fn valueToStringZ(self: *const Local, js_val: js.Value, opts: ToStringOpts) ![:0]u8 {
|
||||
return self.valueHandleToStringZ(js_val.handle, opts);
|
||||
}
|
||||
|
||||
pub fn valueHandleToString(self: *const Local, js_val: *const v8.Value, opts: ToStringOpts) ![]u8 {
|
||||
return self._valueToString(false, js_val, opts);
|
||||
}
|
||||
pub fn valueHandleToStringZ(self: *const Local, js_val: *const v8.Value, opts: ToStringOpts) ![:0]u8 {
|
||||
return self._valueToString(true, js_val, opts);
|
||||
}
|
||||
|
||||
fn _valueToString(self: *const Local, comptime null_terminate: bool, value_handle: *const v8.Value, opts: ToStringOpts) !(if (null_terminate) [:0]u8 else []u8) {
|
||||
var resolved_value_handle = value_handle;
|
||||
if (v8.v8__Value__IsSymbol(value_handle)) {
|
||||
const symbol_handle = v8.v8__Symbol__Description(@ptrCast(value_handle), self.isolate.handle).?;
|
||||
resolved_value_handle = @ptrCast(symbol_handle);
|
||||
}
|
||||
|
||||
const string_handle = v8.v8__Value__ToString(resolved_value_handle, self.handle) orelse {
|
||||
return error.JsException;
|
||||
};
|
||||
|
||||
return self._jsStringToZig(null_terminate, string_handle, opts);
|
||||
}
|
||||
|
||||
pub fn jsStringToZig(self: *const Local, str: anytype, opts: ToStringOpts) ![]u8 {
|
||||
return self._jsStringToZig(false, str, opts);
|
||||
}
|
||||
pub fn jsStringToZigZ(self: *const Local, str: anytype, opts: ToStringOpts) ![:0]u8 {
|
||||
return self._jsStringToZig(true, str, opts);
|
||||
}
|
||||
fn _jsStringToZig(self: *const Local, comptime null_terminate: bool, str: anytype, opts: ToStringOpts) !(if (null_terminate) [:0]u8 else []u8) {
|
||||
const handle = if (@TypeOf(str) == js.String) str.handle else str;
|
||||
|
||||
const len = v8.v8__String__Utf8Length(handle, self.isolate.handle);
|
||||
const allocator = opts.allocator orelse self.call_arena;
|
||||
const buf = try (if (comptime null_terminate) allocator.allocSentinel(u8, @intCast(len), 0) else allocator.alloc(u8, @intCast(len)));
|
||||
const n = v8.v8__String__WriteUtf8(handle, self.isolate.handle, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||
std.debug.assert(n == len);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Convert JS string to string.String with SSO
|
||||
pub fn valueToStringSSO(self: *const Local, js_val: js.Value, opts: ToStringOpts) !string.String {
|
||||
const string_handle = v8.v8__Value__ToString(js_val.handle, self.handle) orelse {
|
||||
return error.JsException;
|
||||
};
|
||||
return self.jsStringToStringSSO(string_handle, opts);
|
||||
}
|
||||
|
||||
pub fn jsStringToStringSSO(self: *const Local, str: anytype, opts: ToStringOpts) !string.String {
|
||||
const handle = if (@TypeOf(str) == js.String) str.handle else str;
|
||||
const len: usize = @intCast(v8.v8__String__Utf8Length(handle, self.isolate.handle));
|
||||
|
||||
if (len <= 12) {
|
||||
var content: [12]u8 = undefined;
|
||||
const n = v8.v8__String__WriteUtf8(handle, self.isolate.handle, &content[0], content.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(n == len);
|
||||
}
|
||||
// Weird that we do this _after_, but we have to..I've seen weird issues
|
||||
// in ReleaseMode where v8 won't write to content if it starts off zero
|
||||
// initiated
|
||||
@memset(content[len..], 0);
|
||||
return .{ .len = @intCast(len), .payload = .{ .content = content } };
|
||||
}
|
||||
|
||||
const allocator = opts.allocator orelse self.call_arena;
|
||||
const buf = try allocator.alloc(u8, len);
|
||||
const n = v8.v8__String__WriteUtf8(handle, self.isolate.handle, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(n == len);
|
||||
}
|
||||
|
||||
var prefix: [4]u8 = @splat(0);
|
||||
@memcpy(&prefix, buf[0..4]);
|
||||
|
||||
return .{
|
||||
.len = @intCast(len),
|
||||
.payload = .{ .heap = .{
|
||||
.prefix = prefix,
|
||||
.ptr = buf.ptr,
|
||||
} },
|
||||
};
|
||||
}
|
||||
|
||||
// == Promise Helpers ==
|
||||
pub fn rejectPromise(self: *const Local, value: anytype) !js.Promise {
|
||||
var resolver = js.PromiseResolver.init(self);
|
||||
@@ -1233,18 +1164,16 @@ fn _debugValue(self: *const Local, js_val: js.Value, seen: *std.AutoHashMapUnman
|
||||
|
||||
if (js_val.isSymbol()) {
|
||||
const symbol_handle = v8.v8__Symbol__Description(@ptrCast(js_val.handle), self.isolate.handle).?;
|
||||
const js_sym_str = try self.valueToString(.{ .local = self, .handle = symbol_handle }, .{});
|
||||
return writer.print("{s} (symbol)", .{js_sym_str});
|
||||
return writer.print("{f} (symbol)", .{js.String{ .local = self, .handle = @ptrCast(symbol_handle) }});
|
||||
}
|
||||
const js_type = try self.jsStringToZig(js_val.typeOf(), .{});
|
||||
const js_val_str = try self.valueToString(js_val, .{});
|
||||
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(" ({s})", .{js_type});
|
||||
return writer.print(" ({f})", .{js_val.typeOf()});
|
||||
}
|
||||
|
||||
const js_obj = js_val.toObject();
|
||||
@@ -1266,7 +1195,7 @@ fn _debugValue(self: *const Local, js_val: js.Value, seen: *std.AutoHashMapUnman
|
||||
}
|
||||
const own_len = js_obj.getOwnPropertyNames().len();
|
||||
if (own_len == 0) {
|
||||
const js_val_str = try self.valueToString(js_val, .{});
|
||||
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)");
|
||||
@@ -1281,10 +1210,11 @@ fn _debugValue(self: *const Local, js_val: js.Value, seen: *std.AutoHashMapUnman
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
const field_name = try names_arr.get(@intCast(i));
|
||||
const name = try self.valueToString(field_name, .{});
|
||||
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) {
|
||||
|
||||
@@ -131,7 +131,7 @@ const Requests = struct {
|
||||
const Request = struct {
|
||||
handle: *const v8.ModuleRequest,
|
||||
|
||||
pub fn specifier(self: Request) *const v8.String {
|
||||
return v8.v8__ModuleRequest__GetSpecifier(self.handle).?;
|
||||
pub fn specifier(self: Request, local: *const js.Local) js.String {
|
||||
return .{ .local = local, .handle = v8.v8__ModuleRequest__GetSpecifier(self.handle).? };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -22,10 +22,6 @@ const v8 = js.v8;
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const Context = @import("Context.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Object = @This();
|
||||
|
||||
local: *const js.Local,
|
||||
@@ -80,10 +76,6 @@ pub fn defineOwnProperty(self: Object, name: []const u8, value: js.Value, attr:
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toString(self: Object) ![]const u8 {
|
||||
return self.local.ctx.valueToString(self.toValue(), .{});
|
||||
}
|
||||
|
||||
pub fn toValue(self: Object) js.Value {
|
||||
return .{
|
||||
.local = self.local,
|
||||
@@ -201,8 +193,8 @@ pub const NameIterator = struct {
|
||||
}
|
||||
self.idx += 1;
|
||||
|
||||
const js_val_handle = v8.v8__Object__GetIndex(@ptrCast(self.handle), self.local.handle, idx) orelse return error.JsException;
|
||||
const js_val = js.Value{ .local = self.local, .handle = js_val_handle };
|
||||
return try self.local.valueToString(js_val, .{});
|
||||
const local = self.local;
|
||||
const js_val_handle = v8.v8__Object__GetIndex(@ptrCast(self.handle), local.handle, idx) orelse return error.JsException;
|
||||
return try js.Value.toStringSlice(.{ .local = local, .handle = js_val_handle });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -47,25 +47,49 @@ pub fn thenAndCatch(self: Promise, on_fulfilled: js.Function, on_rejected: js.Fu
|
||||
}
|
||||
return error.PromiseChainFailed;
|
||||
}
|
||||
|
||||
pub fn persist(self: Promise) !Global {
|
||||
return self._persist(true);
|
||||
}
|
||||
|
||||
pub fn temp(self: Promise) !Temp {
|
||||
return self._persist(false);
|
||||
}
|
||||
|
||||
fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Global else Temp) {
|
||||
var ctx = self.local.ctx;
|
||||
|
||||
var global: v8.Global = undefined;
|
||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||
try ctx.global_promises.append(ctx.arena, global);
|
||||
if (comptime is_global) {
|
||||
try ctx.global_promises.append(ctx.arena, global);
|
||||
} else {
|
||||
try ctx.global_promises_temp.put(ctx.arena, global.data_ptr, global);
|
||||
}
|
||||
return .{ .handle = global };
|
||||
}
|
||||
|
||||
pub const Global = struct {
|
||||
handle: v8.Global,
|
||||
pub const Temp = G(0);
|
||||
pub const Global = G(1);
|
||||
|
||||
pub fn deinit(self: *Global) void {
|
||||
v8.v8__Global__Reset(&self.handle);
|
||||
}
|
||||
fn G(comptime discriminator: u8) type {
|
||||
return struct {
|
||||
handle: v8.Global,
|
||||
|
||||
pub fn local(self: *const Global, l: *const js.Local) Promise {
|
||||
return .{
|
||||
.local = l,
|
||||
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||
};
|
||||
}
|
||||
};
|
||||
// makes the types different (G(0) != G(1)), without taking up space
|
||||
comptime _: u8 = discriminator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
v8.v8__Global__Reset(&self.handle);
|
||||
}
|
||||
|
||||
pub fn local(self: *const Self, l: *const js.Local) Promise {
|
||||
return .{
|
||||
.local = l,
|
||||
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -19,6 +19,23 @@
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const Name = @This();
|
||||
const PromiseRejection = @This();
|
||||
|
||||
handle: *const v8.Name,
|
||||
local: *const js.Local,
|
||||
handle: *const v8.PromiseRejectMessage,
|
||||
|
||||
pub fn promise(self: PromiseRejection) js.Promise {
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = v8.v8__PromiseRejectMessage__GetPromise(self.handle).?,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reason(self: PromiseRejection) ?js.Value {
|
||||
const value_handle = v8.v8__PromiseRejectMessage__GetValue(self.handle) orelse return null;
|
||||
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = value_handle,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -19,9 +19,8 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const js = @import("js/js.zig");
|
||||
const log = @import("../log.zig");
|
||||
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
||||
const log = @import("../../log.zig");
|
||||
const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
|
||||
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
@@ -48,9 +47,15 @@ pub fn init(allocator: std.mem.Allocator) Scheduler {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Scheduler) void {
|
||||
finalizeTasks(&self.low_priority);
|
||||
finalizeTasks(&self.high_priority);
|
||||
}
|
||||
|
||||
const AddOpts = struct {
|
||||
name: []const u8 = "",
|
||||
low_priority: bool = false,
|
||||
finalizer: ?Finalizer = null,
|
||||
};
|
||||
pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
@@ -64,6 +69,7 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts
|
||||
.callback = cb,
|
||||
.sequence = seq,
|
||||
.name = opts.name,
|
||||
.finalizer = opts.finalizer,
|
||||
.run_at = milliTimestamp(.monotonic) + run_in_ms,
|
||||
});
|
||||
}
|
||||
@@ -73,6 +79,11 @@ pub fn run(self: *Scheduler) !?u64 {
|
||||
return self.runQueue(&self.high_priority);
|
||||
}
|
||||
|
||||
pub fn hasReadyTasks(self: *Scheduler) bool {
|
||||
const now = milliTimestamp(.monotonic);
|
||||
return queueuHasReadyTask(&self.low_priority, now) or queueuHasReadyTask(&self.high_priority, now);
|
||||
}
|
||||
|
||||
fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
||||
if (queue.count() == 0) {
|
||||
return null;
|
||||
@@ -106,12 +117,28 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
||||
return null;
|
||||
}
|
||||
|
||||
fn queueuHasReadyTask(queue: *Queue, now: u64) bool {
|
||||
const task = queue.peek() orelse return false;
|
||||
return task.run_at <= now;
|
||||
}
|
||||
|
||||
fn finalizeTasks(queue: *Queue) void {
|
||||
var it = queue.iterator();
|
||||
while (it.next()) |t| {
|
||||
if (t.finalizer) |func| {
|
||||
func(t.ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Task = struct {
|
||||
run_at: u64,
|
||||
sequence: u64,
|
||||
ctx: *anyopaque,
|
||||
name: []const u8,
|
||||
callback: Callback,
|
||||
finalizer: ?Finalizer,
|
||||
};
|
||||
|
||||
const Callback = *const fn (ctx: *anyopaque) anyerror!?u32;
|
||||
const Finalizer = *const fn (ctx: *anyopaque) void;
|
||||
@@ -261,12 +261,26 @@ pub fn create() !Snapshot {
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to check if a JsApi has a NamedIndexed handler
|
||||
fn hasNamedIndexedGetter(comptime JsApi: type) bool {
|
||||
const declarations = @typeInfo(JsApi).@"struct".decls;
|
||||
inline for (declarations) |d| {
|
||||
const value = @field(JsApi, d.name);
|
||||
const T = @TypeOf(value);
|
||||
if (T == bridge.NamedIndexed) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Count total callbacks needed for external_references array
|
||||
fn countExternalReferences() comptime_int {
|
||||
@setEvalBranchQuota(100_000);
|
||||
|
||||
// +1 for the illegal constructor callback
|
||||
var count: comptime_int = 1;
|
||||
var has_non_template_property: bool = false;
|
||||
|
||||
inline for (JsApis) |JsApi| {
|
||||
// Constructor (only if explicit)
|
||||
@@ -289,6 +303,10 @@ fn countExternalReferences() comptime_int {
|
||||
if (value.setter != null) count += 1; // setter
|
||||
} else if (T == bridge.Function) {
|
||||
count += 1;
|
||||
} else if (T == bridge.Property) {
|
||||
if (value.template == false) {
|
||||
has_non_template_property = true;
|
||||
}
|
||||
} else if (T == bridge.Iterator) {
|
||||
count += 1;
|
||||
} else if (T == bridge.Indexed) {
|
||||
@@ -301,6 +319,19 @@ fn countExternalReferences() comptime_int {
|
||||
}
|
||||
}
|
||||
|
||||
if (has_non_template_property) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// In debug mode, add unknown property callbacks for types without NamedIndexed
|
||||
if (comptime IS_DEBUG) {
|
||||
inline for (JsApis) |JsApi| {
|
||||
if (!hasNamedIndexedGetter(JsApi)) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count + 1; // +1 for null terminator
|
||||
}
|
||||
|
||||
@@ -311,6 +342,8 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
||||
references[idx] = @bitCast(@intFromPtr(&illegalConstructorCallback));
|
||||
idx += 1;
|
||||
|
||||
var has_non_template_property = false;
|
||||
|
||||
inline for (JsApis) |JsApi| {
|
||||
if (@hasDecl(JsApi, "constructor")) {
|
||||
references[idx] = @bitCast(@intFromPtr(JsApi.constructor.func));
|
||||
@@ -336,6 +369,10 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
||||
} else if (T == bridge.Function) {
|
||||
references[idx] = @bitCast(@intFromPtr(value.func));
|
||||
idx += 1;
|
||||
} else if (T == bridge.Property) {
|
||||
if (value.template == false) {
|
||||
has_non_template_property = true;
|
||||
}
|
||||
} else if (T == bridge.Iterator) {
|
||||
references[idx] = @bitCast(@intFromPtr(value.func));
|
||||
idx += 1;
|
||||
@@ -357,6 +394,21 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
||||
}
|
||||
}
|
||||
|
||||
if (has_non_template_property) {
|
||||
references[idx] = @bitCast(@intFromPtr(&bridge.Property.getter));
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
// In debug mode, collect unknown property callbacks for types without NamedIndexed
|
||||
if (comptime IS_DEBUG) {
|
||||
inline for (JsApis) |JsApi| {
|
||||
if (!hasNamedIndexedGetter(JsApi)) {
|
||||
references[idx] = @bitCast(@intFromPtr(bridge.unknownObjectPropertyCallback(JsApi)));
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
@@ -393,6 +445,7 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
||||
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
||||
|
||||
const declarations = @typeInfo(JsApi).@"struct".decls;
|
||||
var has_named_index_getter = false;
|
||||
|
||||
inline for (declarations) |d| {
|
||||
const name: [:0]const u8 = d.name;
|
||||
@@ -402,7 +455,7 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
||||
switch (definition) {
|
||||
bridge.Accessor => {
|
||||
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||
const getter_callback = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, value.getter).?);
|
||||
const getter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.getter }).?);
|
||||
if (value.setter == null) {
|
||||
if (value.static) {
|
||||
v8.v8__Template__SetAccessorProperty__DEFAULT(@ptrCast(template), js_name, getter_callback);
|
||||
@@ -413,12 +466,12 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(value.static == false);
|
||||
}
|
||||
const setter_callback = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, value.setter.?).?);
|
||||
const setter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.setter.? }).?);
|
||||
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT2(target, js_name, getter_callback, setter_callback);
|
||||
}
|
||||
},
|
||||
bridge.Function => {
|
||||
const function_template = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, value.func).?);
|
||||
const function_template = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func, .length = value.arity }).?);
|
||||
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||
if (value.static) {
|
||||
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
|
||||
@@ -453,9 +506,10 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
||||
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
||||
};
|
||||
v8.v8__ObjectTemplate__SetNamedHandler(instance, &configuration);
|
||||
has_named_index_getter = true;
|
||||
},
|
||||
bridge.Iterator => {
|
||||
const function_template = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, value.func).?);
|
||||
const function_template = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func }).?);
|
||||
const js_name = if (value.async)
|
||||
v8.v8__Symbol__GetAsyncIterator(isolate)
|
||||
else
|
||||
@@ -463,17 +517,29 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
||||
v8.v8__Template__Set(@ptrCast(target), js_name, @ptrCast(function_template), v8.None);
|
||||
},
|
||||
bridge.Property => {
|
||||
// simpleZigValueToJs now returns raw handle directly
|
||||
const js_value = switch (value) {
|
||||
.int => |v| js.simpleZigValueToJs(.{ .handle = isolate }, v, true, false),
|
||||
const js_value = switch (value.value) {
|
||||
.null => js.simpleZigValueToJs(.{ .handle = isolate }, null, true, false),
|
||||
inline .bool, .int, .float, .string => |v| js.simpleZigValueToJs(.{ .handle = isolate }, v, true, false),
|
||||
};
|
||||
|
||||
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||
// apply it both to the type itself
|
||||
v8.v8__Template__Set(@ptrCast(template), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
||||
|
||||
// and to instances of the type
|
||||
v8.v8__Template__Set(@ptrCast(target), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
||||
if (value.template == false) {
|
||||
// not defined on the template, only on the instance. This
|
||||
// is like an Accessor, but because the value is known at
|
||||
// compile time, we skip _a lot_ of code and quickly return
|
||||
// the hard-coded value
|
||||
const getter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{
|
||||
.callback = bridge.Property.getter,
|
||||
.data = js_value,
|
||||
.side_effect_type = v8.kSideEffectType_HasSideEffectToReceiver,
|
||||
}));
|
||||
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(target, js_name, getter_callback);
|
||||
} else {
|
||||
// apply it both to the type itself
|
||||
v8.v8__Template__Set(@ptrCast(template), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
||||
// and to instances of the type
|
||||
v8.v8__Template__Set(@ptrCast(target), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
||||
}
|
||||
},
|
||||
bridge.Constructor => {}, // already handled in generateConstructor
|
||||
else => {},
|
||||
@@ -490,6 +556,23 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
||||
const js_value = v8.v8__String__NewFromUtf8(isolate, JsApi.Meta.name.ptr, v8.kNormal, @intCast(JsApi.Meta.name.len));
|
||||
v8.v8__Template__Set(@ptrCast(instance), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
||||
}
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
if (!has_named_index_getter) {
|
||||
var configuration: v8.NamedPropertyHandlerConfiguration = .{
|
||||
.getter = bridge.unknownObjectPropertyCallback(JsApi),
|
||||
.setter = null,
|
||||
.query = null,
|
||||
.deleter = null,
|
||||
.enumerator = null,
|
||||
.definer = null,
|
||||
.descriptor = null,
|
||||
.data = null,
|
||||
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
||||
};
|
||||
v8.v8__ObjectTemplate__SetNamedHandler(instance, &configuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt {
|
||||
|
||||
@@ -18,8 +18,10 @@
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const SSO = @import("../../string.zig").String;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const v8 = js.v8;
|
||||
|
||||
@@ -28,26 +30,82 @@ const String = @This();
|
||||
local: *const js.Local,
|
||||
handle: *const v8.String,
|
||||
|
||||
pub const ToZigOpts = struct {
|
||||
allocator: ?Allocator = null,
|
||||
};
|
||||
|
||||
pub fn toZig(self: String, opts: ToZigOpts) ![]u8 {
|
||||
return self._toZig(false, opts);
|
||||
pub fn toSlice(self: String) ![]u8 {
|
||||
return self._toSlice(false, self.local.call_arena);
|
||||
}
|
||||
|
||||
pub fn toZigZ(self: String, opts: ToZigOpts) ![:0]u8 {
|
||||
return self._toZig(true, opts);
|
||||
pub fn toSliceZ(self: String) ![:0]u8 {
|
||||
return self._toSlice(true, self.local.call_arena);
|
||||
}
|
||||
pub fn toSliceWithAlloc(self: String, allocator: Allocator) ![]u8 {
|
||||
return self._toSlice(false, allocator);
|
||||
}
|
||||
fn _toSlice(self: String, comptime null_terminate: bool, allocator: Allocator) !(if (null_terminate) [:0]u8 else []u8) {
|
||||
const local = self.local;
|
||||
const handle = self.handle;
|
||||
const isolate = local.isolate.handle;
|
||||
|
||||
fn _toZig(self: String, comptime null_terminate: bool, opts: ToZigOpts) !(if (null_terminate) [:0]u8 else []u8) {
|
||||
const isolate = self.local.isolate.handle;
|
||||
const allocator = opts.allocator orelse self.local.ctx.call_arena;
|
||||
const len: u32 = @intCast(v8.v8__String__Utf8Length(self.handle, isolate));
|
||||
const buf = if (null_terminate) try allocator.allocSentinel(u8, len, 0) else try allocator.alloc(u8, len);
|
||||
const len = v8.v8__String__Utf8Length(handle, isolate);
|
||||
const buf = try (if (comptime null_terminate) allocator.allocSentinel(u8, @intCast(len), 0) else allocator.alloc(u8, @intCast(len)));
|
||||
const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(n == len);
|
||||
}
|
||||
|
||||
const options = v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8;
|
||||
const n = v8.v8__String__WriteUtf8(self.handle, isolate, buf.ptr, buf.len, options);
|
||||
std.debug.assert(n == len);
|
||||
return buf;
|
||||
}
|
||||
|
||||
pub fn toSSO(self: String, comptime global: bool) !(if (global) SSO.Global else SSO) {
|
||||
if (comptime global) {
|
||||
return .{ .str = try self.toSSOWithAlloc(self.local.ctx.arena) };
|
||||
}
|
||||
return self.toSSOWithAlloc(self.local.call_arena);
|
||||
}
|
||||
pub fn toSSOWithAlloc(self: String, allocator: Allocator) !SSO {
|
||||
const handle = self.handle;
|
||||
const isolate = self.local.isolate.handle;
|
||||
|
||||
const len: usize = @intCast(v8.v8__String__Utf8Length(handle, isolate));
|
||||
|
||||
if (len <= 12) {
|
||||
var content: [12]u8 = undefined;
|
||||
const n = v8.v8__String__WriteUtf8(handle, isolate, &content[0], content.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(n == len);
|
||||
}
|
||||
// Weird that we do this _after_, but we have to..I've seen weird issues
|
||||
// in ReleaseMode where v8 won't write to content if it starts off zero
|
||||
// initiated
|
||||
@memset(content[len..], 0);
|
||||
return .{ .len = @intCast(len), .payload = .{ .content = content } };
|
||||
}
|
||||
|
||||
const buf = try allocator.alloc(u8, len);
|
||||
const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(n == len);
|
||||
}
|
||||
|
||||
var prefix: [4]u8 = @splat(0);
|
||||
@memcpy(&prefix, buf[0..4]);
|
||||
|
||||
return .{
|
||||
.len = @intCast(len),
|
||||
.payload = .{ .heap = .{
|
||||
.prefix = prefix,
|
||||
.ptr = buf.ptr,
|
||||
} },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn format(self: String, writer: *std.Io.Writer) !void {
|
||||
const local = self.local;
|
||||
const handle = self.handle;
|
||||
const isolate = local.isolate.handle;
|
||||
|
||||
var small: [1024]u8 = undefined;
|
||||
const len = v8.v8__String__Utf8Length(handle, isolate);
|
||||
var buf = if (len < 1024) &small else local.call_arena.alloc(u8, @intCast(len)) catch return error.WriteFailed;
|
||||
|
||||
const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||
return writer.writeAll(buf[0..n]);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const TryCatch = @This();
|
||||
@@ -32,8 +34,19 @@ pub fn init(self: *TryCatch, l: *const js.Local) void {
|
||||
v8.v8__TryCatch__CONSTRUCT(&self.handle, l.isolate.handle);
|
||||
}
|
||||
|
||||
pub fn hasCaught(self: TryCatch) bool {
|
||||
return v8.v8__TryCatch__HasCaught(&self.handle);
|
||||
}
|
||||
|
||||
pub fn rethrow(self: *TryCatch) void {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(self.hasCaught());
|
||||
}
|
||||
_ = v8.v8__TryCatch__ReThrow(&self.handle);
|
||||
}
|
||||
|
||||
pub fn caught(self: TryCatch, allocator: Allocator) ?Caught {
|
||||
if (!v8.v8__TryCatch__HasCaught(&self.handle)) {
|
||||
if (self.hasCaught() == false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -46,12 +59,38 @@ pub fn caught(self: TryCatch, allocator: Allocator) ?Caught {
|
||||
|
||||
const exception: ?[]const u8 = blk: {
|
||||
const handle = v8.v8__TryCatch__Exception(&self.handle) orelse break :blk null;
|
||||
break :blk l.valueHandleToString(@ptrCast(handle), .{ .allocator = allocator }) catch |err| @errorName(err);
|
||||
var js_val = js.Value{ .local = l, .handle = handle };
|
||||
|
||||
// If it's an Error object, try to get the message property
|
||||
if (js_val.isObject()) {
|
||||
const js_obj = js_val.toObject();
|
||||
if (js_obj.has("message")) {
|
||||
js_val = js_obj.get("message") catch break :blk null;
|
||||
}
|
||||
}
|
||||
|
||||
if (js_val.isString()) |js_str| {
|
||||
break :blk js_str.toSliceWithAlloc(allocator) catch |err| @errorName(err);
|
||||
}
|
||||
break :blk null;
|
||||
};
|
||||
|
||||
const stack: ?[]const u8 = blk: {
|
||||
const handle = v8.v8__TryCatch__StackTrace(&self.handle, l.handle) orelse break :blk null;
|
||||
break :blk l.valueHandleToString(@ptrCast(handle), .{ .allocator = allocator }) catch |err| @errorName(err);
|
||||
var js_val = js.Value{ .local = l, .handle = handle };
|
||||
|
||||
// If it's an Error object, try to get the stack property
|
||||
if (js_val.isObject()) {
|
||||
const js_obj = js_val.toObject();
|
||||
if (js_obj.has("stack")) {
|
||||
js_val = js_obj.get("stack") catch break :blk null;
|
||||
}
|
||||
}
|
||||
|
||||
if (js_val.isString()) |js_str| {
|
||||
break :blk js_str.toSliceWithAlloc(allocator) catch |err| @errorName(err);
|
||||
}
|
||||
break :blk null;
|
||||
};
|
||||
|
||||
return .{
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const SSO = @import("../../string.zig").String;
|
||||
|
||||
const v8 = js.v8;
|
||||
|
||||
@@ -34,8 +35,12 @@ pub fn isObject(self: Value) bool {
|
||||
return v8.v8__Value__IsObject(self.handle);
|
||||
}
|
||||
|
||||
pub fn isString(self: Value) bool {
|
||||
return v8.v8__Value__IsString(self.handle);
|
||||
pub fn isString(self: Value) ?js.String {
|
||||
const handle = self.handle;
|
||||
if (!v8.v8__Value__IsString(handle)) {
|
||||
return null;
|
||||
}
|
||||
return .{ .local = self.local, .handle = @ptrCast(handle) };
|
||||
}
|
||||
|
||||
pub fn isArray(self: Value) bool {
|
||||
@@ -204,35 +209,40 @@ pub fn toPromise(self: Value) js.Promise {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toString(self: Value, opts: js.String.ToZigOpts) ![]u8 {
|
||||
return self._toString(false, opts);
|
||||
pub fn toString(self: Value) !js.String {
|
||||
const l = self.local;
|
||||
const value_handle: *const v8.Value = blk: {
|
||||
if (self.isSymbol()) {
|
||||
break :blk @ptrCast(v8.v8__Symbol__Description(@ptrCast(self.handle), l.isolate.handle).?);
|
||||
}
|
||||
break :blk self.handle;
|
||||
};
|
||||
|
||||
const str_handle = v8.v8__Value__ToString(value_handle, l.handle) orelse return error.JsException;
|
||||
return .{ .local = self.local, .handle = str_handle };
|
||||
}
|
||||
pub fn toStringZ(self: Value, opts: js.String.ToZigOpts) ![:0]u8 {
|
||||
return self._toString(true, opts);
|
||||
|
||||
pub fn toSSO(self: Value, comptime global: bool) !(if (global) SSO.Global else SSO) {
|
||||
return (try self.toString()).toSSO(global);
|
||||
}
|
||||
pub fn toSSOWithAlloc(self: Value, allocator: Allocator) !SSO {
|
||||
return (try self.toString()).toSSOWithAlloc(allocator);
|
||||
}
|
||||
|
||||
pub fn toStringSlice(self: Value) ![]u8 {
|
||||
return (try self.toString()).toSlice();
|
||||
}
|
||||
pub fn toStringSliceZ(self: Value) ![:0]u8 {
|
||||
return (try self.toString()).toSliceZ();
|
||||
}
|
||||
pub fn toStringSliceWithAlloc(self: Value, allocator: Allocator) ![]u8 {
|
||||
return (try self.toString()).toSliceWithAlloc(allocator);
|
||||
}
|
||||
|
||||
pub fn toJson(self: Value, allocator: Allocator) ![]u8 {
|
||||
const json_str_handle = v8.v8__JSON__Stringify(self.local.handle, self.handle, null) orelse return error.JsException;
|
||||
return self.local.jsStringToZig(json_str_handle, .{ .allocator = allocator });
|
||||
}
|
||||
|
||||
fn _toString(self: Value, comptime null_terminate: bool, opts: js.String.ToZigOpts) !(if (null_terminate) [:0]u8 else []u8) {
|
||||
const l = self.local;
|
||||
|
||||
if (self.isSymbol()) {
|
||||
const sym_handle = v8.v8__Symbol__Description(@ptrCast(self.handle), l.isolate.handle).?;
|
||||
return _toString(.{ .handle = @ptrCast(sym_handle), .local = l }, null_terminate, opts);
|
||||
}
|
||||
|
||||
const str_handle = v8.v8__Value__ToString(self.handle, l.handle) orelse {
|
||||
return error.JsException;
|
||||
};
|
||||
|
||||
const str = js.String{ .local = l, .handle = str_handle };
|
||||
if (comptime null_terminate) {
|
||||
return js.String.toZigZ(str, opts);
|
||||
}
|
||||
return js.String.toZig(str, opts);
|
||||
const local = self.local;
|
||||
const str_handle = v8.v8__JSON__Stringify(local.handle, self.handle, null) orelse return error.JsException;
|
||||
return js.String.toSliceWithAlloc(.{ .local = local, .handle = str_handle }, allocator);
|
||||
}
|
||||
|
||||
pub fn persist(self: Value) !Global {
|
||||
@@ -296,8 +306,8 @@ pub fn format(self: Value, writer: *std.Io.Writer) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
return self.local.debugValue(self, writer);
|
||||
}
|
||||
const str = self.toString(.{}) catch return error.WriteFailed;
|
||||
return writer.writeAll(str);
|
||||
const js_str = self.toString() catch return error.WriteFailed;
|
||||
return js_str.format(writer);
|
||||
}
|
||||
|
||||
pub const Temp = G(0);
|
||||
|
||||
@@ -62,9 +62,21 @@ pub fn Builder(comptime T: type) type {
|
||||
return Callable.init(T, func, opts);
|
||||
}
|
||||
|
||||
pub fn property(value: anytype) Property {
|
||||
pub fn property(value: anytype, opts: Property.Opts) Property {
|
||||
switch (@typeInfo(@TypeOf(value))) {
|
||||
.comptime_int, .int => return .{ .int = value },
|
||||
.bool => return Property.init(.{ .bool = value }, opts),
|
||||
.null => return Property.init(.null, opts),
|
||||
.comptime_int, .int => return Property.init(.{ .int = value }, opts),
|
||||
.comptime_float, .float => return Property.init(.{ .float = value }, opts),
|
||||
.pointer => |ptr| switch (ptr.size) {
|
||||
.one => {
|
||||
const one_info = @typeInfo(ptr.child);
|
||||
if (one_info == .array and one_info.array.child == u8) {
|
||||
return Property.init(.{ .string = value }, opts);
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
@compileError("Property for " ++ @typeName(@TypeOf(value)) ++ " hasn't been defined yet");
|
||||
@@ -103,20 +115,18 @@ pub fn Builder(comptime T: type) type {
|
||||
.from_v8 = struct {
|
||||
fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
|
||||
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
|
||||
const self: *T = @ptrCast(@alignCast(ptr));
|
||||
// This is simply a requirement of any type that Finalizes:
|
||||
// It must have a _page: *Page field. We need it because
|
||||
// we need to check the item has already been cleared
|
||||
// (There are all types of weird timing issues that seem
|
||||
// to be possible between finalization and context shutdown,
|
||||
// we need to be defensive).
|
||||
// There _ARE_ alternatives to this. But this is simple.
|
||||
const ctx = self._page.js;
|
||||
if (!ctx.identity_map.contains(@intFromPtr(ptr))) {
|
||||
return;
|
||||
const fc: *Context.FinalizerCallback = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const ctx = fc.ctx;
|
||||
const value_ptr = fc.ptr;
|
||||
if (ctx.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
|
||||
func(@ptrCast(@alignCast(value_ptr)), false);
|
||||
ctx.release(value_ptr);
|
||||
} else {
|
||||
// A bit weird, but v8 _requires_ that we release it
|
||||
// If we don't. We'll 100% crash.
|
||||
v8.v8__Global__Reset(&fc.global);
|
||||
}
|
||||
func(self, false);
|
||||
ctx.release(ptr);
|
||||
}
|
||||
}.wrap,
|
||||
};
|
||||
@@ -149,6 +159,7 @@ pub const Constructor = struct {
|
||||
|
||||
pub const Function = struct {
|
||||
static: bool,
|
||||
arity: usize,
|
||||
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
||||
|
||||
const Opts = struct {
|
||||
@@ -161,6 +172,7 @@ pub const Function = struct {
|
||||
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Function {
|
||||
return .{
|
||||
.static = opts.static,
|
||||
.arity = getArity(@TypeOf(func)),
|
||||
.func = struct {
|
||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
||||
@@ -185,6 +197,22 @@ pub const Function = struct {
|
||||
}.wrap,
|
||||
};
|
||||
}
|
||||
|
||||
fn getArity(comptime T: type) usize {
|
||||
var count: usize = 0;
|
||||
var params = @typeInfo(T).@"fn".params;
|
||||
for (params[1..]) |p| { // start at 1, skip self
|
||||
const PT = p.type.?;
|
||||
if (PT == *Page or PT == *const Page) {
|
||||
break;
|
||||
}
|
||||
if (@typeInfo(PT) == .optional) {
|
||||
break;
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Accessor = struct {
|
||||
@@ -194,9 +222,9 @@ pub const Accessor = struct {
|
||||
|
||||
const Opts = struct {
|
||||
static: bool = false,
|
||||
cache: ?[]const u8 = null,
|
||||
as_typed_array: bool = false,
|
||||
null_as_undefined: bool = false,
|
||||
dom_exception: bool = false,
|
||||
};
|
||||
|
||||
fn init(comptime T: type, comptime getter: anytype, comptime setter: anytype, comptime opts: Opts) Accessor {
|
||||
@@ -214,13 +242,13 @@ pub const Accessor = struct {
|
||||
|
||||
if (comptime opts.static) {
|
||||
caller.function(T, getter, handle.?, .{
|
||||
.cache = opts.cache,
|
||||
.dom_exception = opts.dom_exception,
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
} else {
|
||||
caller.method(T, getter, handle.?, .{
|
||||
.cache = opts.cache,
|
||||
.dom_exception = opts.dom_exception,
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
@@ -238,6 +266,7 @@ pub const Accessor = struct {
|
||||
defer caller.deinit();
|
||||
|
||||
caller.method(T, setter, handle.?, .{
|
||||
.dom_exception = opts.dom_exception,
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
@@ -395,8 +424,35 @@ pub const Callable = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const Property = union(enum) {
|
||||
int: i64,
|
||||
pub const Property = struct {
|
||||
value: Value,
|
||||
template: bool,
|
||||
|
||||
const Value = union(enum) {
|
||||
null,
|
||||
int: i64,
|
||||
float: f64,
|
||||
bool: bool,
|
||||
string: []const u8,
|
||||
};
|
||||
|
||||
const Opts = struct {
|
||||
template: bool,
|
||||
};
|
||||
|
||||
fn init(value: Value, opts: Opts) Property {
|
||||
return .{
|
||||
.value = value,
|
||||
.template = opts.template,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getter(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const value = v8.v8__FunctionCallbackInfo__Data(handle.?);
|
||||
var rv: v8.ReturnValue = undefined;
|
||||
v8.v8__FunctionCallbackInfo__GetReturnValue(handle.?, &rv);
|
||||
v8.v8__ReturnValue__Set(rv, value);
|
||||
}
|
||||
};
|
||||
|
||||
const Finalizer = struct {
|
||||
@@ -410,7 +466,7 @@ const Finalizer = struct {
|
||||
from_v8: *const fn (?*const v8.WeakCallbackInfo) callconv(.c) void,
|
||||
};
|
||||
|
||||
pub fn unknownPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||
pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
caller.init(v8_isolate);
|
||||
@@ -422,7 +478,7 @@ pub fn unknownPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.Prope
|
||||
hs.init(local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const property: []const u8 = local.valueHandleToString(@ptrCast(c_name.?), .{}) catch {
|
||||
const property: []const u8 = js.String.toSlice(.{ .local = local, .handle = @ptrCast(c_name.?) }) catch {
|
||||
return 0;
|
||||
};
|
||||
|
||||
@@ -437,11 +493,21 @@ pub fn unknownPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.Prope
|
||||
}
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
if (std.mem.startsWith(u8, property, "__")) {
|
||||
// some frameworks will extend built-in types using a __ prefix
|
||||
// these should always be safe to ignore.
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ignored = std.StaticStringMap(void).initComptime(.{
|
||||
.{ "Deno", {} },
|
||||
.{ "process", {} },
|
||||
.{ "ShadyDOM", {} },
|
||||
.{ "ShadyCSS", {} },
|
||||
|
||||
// a lot of sites seem to like having their own window.config.
|
||||
.{ "config", {} },
|
||||
|
||||
.{ "litNonce", {} },
|
||||
.{ "litHtmlVersions", {} },
|
||||
.{ "litElementVersions", {} },
|
||||
@@ -457,13 +523,12 @@ pub fn unknownPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.Prope
|
||||
.{ "__google_recaptcha_client", {} },
|
||||
|
||||
.{ "CLOSURE_FLAGS", {} },
|
||||
.{ "__REACT_DEVTOOLS_GLOBAL_HOOK__", {} },
|
||||
.{ "ApplePaySession", {} },
|
||||
});
|
||||
if (!ignored.has(property)) {
|
||||
log.debug(.unknown_prop, "unknown global property", .{
|
||||
.info = "but the property can exist in pure JS",
|
||||
.stack = local.stackTrace() catch "???",
|
||||
.property = property,
|
||||
});
|
||||
const key = std.fmt.bufPrint(&local.ctx.page.buf, "Window:{s}", .{property}) catch return 0;
|
||||
logUnknownProperty(local, key) catch return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,6 +536,83 @@ pub fn unknownPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.Prope
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Only used for debugging
|
||||
pub fn unknownObjectPropertyCallback(comptime JsApi: type) *const fn (?*const v8.Name, ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||
if (comptime !IS_DEBUG) {
|
||||
@compileError("unknownObjectPropertyCallback should only be used in debug builds");
|
||||
}
|
||||
|
||||
return struct {
|
||||
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||
|
||||
var caller: Caller = undefined;
|
||||
caller.init(v8_isolate);
|
||||
defer caller.deinit();
|
||||
|
||||
const local = &caller.local;
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const property: []const u8 = js.String.toSlice(.{ .local = local, .handle = @ptrCast(c_name.?) }) catch {
|
||||
return 0;
|
||||
};
|
||||
|
||||
if (std.mem.startsWith(u8, property, "__")) {
|
||||
// some frameworks will extend built-in types using a __ prefix
|
||||
// these should always be safe to ignore.
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, property, "jQuery")) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (JsApi == @import("../webapi/cdata/Text.zig").JsApi or JsApi == @import("../webapi/cdata/Comment.zig").JsApi) {
|
||||
if (std.mem.eql(u8, property, "tagName")) {
|
||||
// knockout does this, a lot.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (JsApi == @import("../webapi/element/Html.zig").JsApi or JsApi == @import("../webapi/Element.zig").JsApi or JsApi == @import("../webapi/element/html/Custom.zig").JsApi) {
|
||||
// react ?
|
||||
if (std.mem.eql(u8, property, "props")) return 0;
|
||||
if (std.mem.eql(u8, property, "hydrated")) return 0;
|
||||
if (std.mem.eql(u8, property, "isHydrated")) return 0;
|
||||
}
|
||||
|
||||
if (JsApi == @import("../webapi/Console.zig").JsApi) {
|
||||
if (std.mem.eql(u8, property, "firebug")) return 0;
|
||||
}
|
||||
|
||||
const ignored = std.StaticStringMap(void).initComptime(.{});
|
||||
if (!ignored.has(property)) {
|
||||
const key = std.fmt.bufPrint(&local.ctx.page.buf, "{s}:{s}", .{ if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi), property }) catch return 0;
|
||||
logUnknownProperty(local, key) catch return 0;
|
||||
}
|
||||
// not intercepted
|
||||
return 0;
|
||||
}
|
||||
}.wrap;
|
||||
}
|
||||
|
||||
fn logUnknownProperty(local: *const js.Local, key: []const u8) !void {
|
||||
const ctx = local.ctx;
|
||||
const gop = try ctx.unknown_properties.getOrPut(ctx.arena, key);
|
||||
if (gop.found_existing) {
|
||||
gop.value_ptr.count += 1;
|
||||
} else {
|
||||
gop.key_ptr.* = try ctx.arena.dupe(u8, key);
|
||||
gop.value_ptr.* = .{
|
||||
.count = 1,
|
||||
.first_stack = try ctx.arena.dupe(u8, (try local.stackTrace()) orelse "???"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Given a Type, returns the length of the prototype chain, including self
|
||||
fn prototypeChainLength(comptime T: type) usize {
|
||||
var l: usize = 1;
|
||||
@@ -696,6 +838,7 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/element/html/Option.zig"),
|
||||
@import("../webapi/element/html/Output.zig"),
|
||||
@import("../webapi/element/html/Paragraph.zig"),
|
||||
@import("../webapi/element/html/Picture.zig"),
|
||||
@import("../webapi/element/html/Param.zig"),
|
||||
@import("../webapi/element/html/Pre.zig"),
|
||||
@import("../webapi/element/html/Progress.zig"),
|
||||
@@ -737,6 +880,7 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/event/MouseEvent.zig"),
|
||||
@import("../webapi/event/PointerEvent.zig"),
|
||||
@import("../webapi/event/KeyboardEvent.zig"),
|
||||
@import("../webapi/event/PromiseRejectionEvent.zig"),
|
||||
@import("../webapi/MessageChannel.zig"),
|
||||
@import("../webapi/MessagePort.zig"),
|
||||
@import("../webapi/media/MediaError.zig"),
|
||||
@@ -761,6 +905,7 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/URL.zig"),
|
||||
@import("../webapi/Window.zig"),
|
||||
@import("../webapi/Performance.zig"),
|
||||
@import("../webapi/PluginArray.zig"),
|
||||
@import("../webapi/MutationObserver.zig"),
|
||||
@import("../webapi/IntersectionObserver.zig"),
|
||||
@import("../webapi/CustomElementRegistry.zig"),
|
||||
@@ -769,6 +914,7 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/Blob.zig"),
|
||||
@import("../webapi/File.zig"),
|
||||
@import("../webapi/Screen.zig"),
|
||||
@import("../webapi/VisualViewport.zig"),
|
||||
@import("../webapi/PerformanceObserver.zig"),
|
||||
@import("../webapi/navigation/Navigation.zig"),
|
||||
@import("../webapi/navigation/NavigationEventTarget.zig"),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -19,12 +19,10 @@
|
||||
const std = @import("std");
|
||||
pub const v8 = @import("v8").c;
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const string = @import("../../string.zig");
|
||||
|
||||
pub const Env = @import("Env.zig");
|
||||
pub const bridge = @import("bridge.zig");
|
||||
pub const ExecutionWorld = @import("ExecutionWorld.zig");
|
||||
pub const Caller = @import("Caller.zig");
|
||||
pub const Context = @import("Context.zig");
|
||||
pub const Local = @import("Local.zig");
|
||||
@@ -34,7 +32,6 @@ pub const Platform = @import("Platform.zig");
|
||||
pub const Isolate = @import("Isolate.zig");
|
||||
pub const HandleScope = @import("HandleScope.zig");
|
||||
|
||||
pub const Name = @import("Name.zig");
|
||||
pub const Value = @import("Value.zig");
|
||||
pub const Array = @import("Array.zig");
|
||||
pub const String = @import("String.zig");
|
||||
@@ -47,6 +44,7 @@ pub const BigInt = @import("BigInt.zig");
|
||||
pub const Number = @import("Number.zig");
|
||||
pub const Integer = @import("Integer.zig");
|
||||
pub const PromiseResolver = @import("PromiseResolver.zig");
|
||||
pub const PromiseRejection = @import("PromiseRejection.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -16,8 +16,6 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
// Gets the Parent of child.
|
||||
// HtmlElement.of(script) -> *HTMLElement
|
||||
pub fn Struct(comptime T: type) type {
|
||||
@@ -28,37 +26,3 @@ pub fn Struct(comptime T: type) type {
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
// Creates an enum of N enums. Doesn't perserve their underlying integer
|
||||
pub fn mergeEnums(comptime enums: []const type) type {
|
||||
const field_count = blk: {
|
||||
var count: usize = 0;
|
||||
inline for (enums) |e| {
|
||||
count += @typeInfo(e).@"enum".fields.len;
|
||||
}
|
||||
break :blk count;
|
||||
};
|
||||
|
||||
var i: usize = 0;
|
||||
var fields: [field_count]std.builtin.Type.EnumField = undefined;
|
||||
for (enums) |e| {
|
||||
for (@typeInfo(e).@"enum".fields) |f| {
|
||||
fields[i] = .{
|
||||
.name = f.name,
|
||||
.value = i,
|
||||
};
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return @Type(.{ .@"enum" = .{
|
||||
.decls = &.{},
|
||||
.tag_type = blk: {
|
||||
if (field_count <= std.math.maxInt(u8)) break :blk u8;
|
||||
if (field_count <= std.math.maxInt(u16)) break :blk u16;
|
||||
unreachable;
|
||||
},
|
||||
.fields = &fields,
|
||||
.is_exhaustive = true,
|
||||
} });
|
||||
}
|
||||
|
||||
28
src/browser/tests/console/console.html
Normal file
28
src/browser/tests/console/console.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id="time">
|
||||
// should not crash
|
||||
console.time();
|
||||
console.timeLog();
|
||||
console.timeEnd();
|
||||
|
||||
console.time("test");
|
||||
console.timeLog("test");
|
||||
console.timeEnd("test");
|
||||
|
||||
testing.expectEqual(true, true);
|
||||
</script>
|
||||
|
||||
<script id="count">
|
||||
// should not crash
|
||||
console.count();
|
||||
console.count();
|
||||
console.countReset();
|
||||
|
||||
console.count("test");
|
||||
console.count("test");
|
||||
console.countReset("test");
|
||||
|
||||
testing.expectEqual(true, true);
|
||||
</script>
|
||||
@@ -20,8 +20,10 @@
|
||||
{
|
||||
testing.expectEqual('\\30 abc', CSS.escape('0abc'));
|
||||
testing.expectEqual('\\31 23', CSS.escape('123'));
|
||||
testing.expectEqual('\\-test', CSS.escape('-test'));
|
||||
testing.expectEqual('\\--test', CSS.escape('--test'));
|
||||
testing.expectEqual('\\-', CSS.escape('-'));
|
||||
testing.expectEqual('-test', CSS.escape('-test'));
|
||||
testing.expectEqual('--test', CSS.escape('--test'));
|
||||
testing.expectEqual('-\\33 ', CSS.escape('-3'));
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
<body></body>
|
||||
<script src="../testing.js"></script>
|
||||
<script id=createElement>
|
||||
const div = document.createElement('div');
|
||||
testing.expectEqual("DIV", div.tagName);
|
||||
div.id = "hello";
|
||||
testing.expectEqual(1, document.createElement.length);
|
||||
|
||||
const div1 = document.createElement('div');
|
||||
testing.expectEqual(true, div1 instanceof HTMLDivElement);
|
||||
testing.expectEqual("DIV", div1.tagName);
|
||||
div1.id = "hello";
|
||||
testing.expectEqual(null, $('#hello'));
|
||||
|
||||
document.getElementsByTagName('body')[0].appendChild(div);
|
||||
testing.expectEqual(div, $('#hello'));
|
||||
const div2 = document.createElement('DIV');
|
||||
testing.expectEqual(true, div2 instanceof HTMLDivElement);
|
||||
|
||||
document.getElementsByTagName('body')[0].appendChild(div1);
|
||||
testing.expectEqual(div1, $('#hello'));
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
<body></body>
|
||||
<script src="../testing.js"></script>
|
||||
<script id=createElementNS>
|
||||
const htmlDiv = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
|
||||
testing.expectEqual('DIV', htmlDiv.tagName);
|
||||
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv.namespaceURI);
|
||||
const htmlDiv1 = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
|
||||
testing.expectEqual('DIV', htmlDiv1.tagName);
|
||||
testing.expectEqual(true, htmlDiv1 instanceof HTMLDivElement);
|
||||
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv1.namespaceURI);
|
||||
|
||||
const htmlDiv2 = document.createElementNS('http://www.w3.org/1999/xhtml', 'DIV');
|
||||
testing.expectEqual('DIV', htmlDiv2.tagName);
|
||||
testing.expectEqual(true, htmlDiv2 instanceof HTMLDivElement);
|
||||
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv2.namespaceURI);
|
||||
|
||||
const svgRect = document.createElementNS('http://www.w3.org/2000/svg', 'RecT');
|
||||
testing.expectEqual('RecT', svgRect.tagName);
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
testing.expectEqual(undefined, document.getCurrentScript);
|
||||
testing.expectEqual("http://127.0.0.1:9582/src/browser/tests/document/document.html", document.URL);
|
||||
testing.expectEqual(window, document.defaultView);
|
||||
testing.expectEqual(false, document.hidden);
|
||||
testing.expectEqual("visible", document.visibilityState);
|
||||
testing.expectEqual(false, document.prerendering);
|
||||
testing.expectEqual(undefined, Document.prerendering);
|
||||
</script>
|
||||
|
||||
<script id=headAndbody>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<main>Main content</main>
|
||||
|
||||
<script id=byId name="test1">
|
||||
testing.expectEqual(1, document.querySelector.length);
|
||||
testing.expectError("SyntaxError: Syntax Error", () => document.querySelector(''));
|
||||
testing.withError((err) => {
|
||||
testing.expectEqual(12, err.code);
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id=legacy></a>
|
||||
<a id=legacy></a>
|
||||
<script id=legacy>
|
||||
{
|
||||
let a = document.getElementById('legacy').attributes;
|
||||
@@ -266,3 +266,19 @@
|
||||
testing.expectEqual('abc123', a[0].value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="nsa"></div>
|
||||
<script id=non-string-attr>
|
||||
{
|
||||
let nsa = document.getElementById('nsa');
|
||||
|
||||
nsa.setAttribute('int', 1);
|
||||
testing.expectEqual('1', nsa.getAttribute('int'));
|
||||
|
||||
nsa.setAttribute('obj', {});
|
||||
testing.expectEqual('[object Object]', nsa.getAttribute('obj'));
|
||||
|
||||
nsa.setAttribute('arr', []);
|
||||
testing.expectEqual('', nsa.getAttribute('arr'));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -488,3 +488,27 @@
|
||||
testing.expectEqual('function', typeof div.onratechange);
|
||||
}
|
||||
</script>
|
||||
|
||||
<img src="https://cdn.lightpanda.io/website/assets/images/docs/hn.png" />
|
||||
|
||||
<script id="document-element-load">
|
||||
{
|
||||
let asyncBlockDispatched = false;
|
||||
const docElement = document.documentElement;
|
||||
|
||||
testing.async(async () => {
|
||||
const result = await new Promise(resolve => {
|
||||
// We should get this fired at capturing phase when a resource loaded.
|
||||
docElement.addEventListener("load", e => {
|
||||
testing.expectEqual(e.eventPhase, Event.CAPTURING_PHASE);
|
||||
return resolve(true);
|
||||
}, true);
|
||||
});
|
||||
|
||||
asyncBlockDispatched = true;
|
||||
testing.expectEqual(true, result);
|
||||
});
|
||||
|
||||
testing.eventually(() => testing.expectEqual(true, asyncBlockDispatched));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -97,3 +97,62 @@
|
||||
testing.expectEqual('lazy', img.getAttribute('loading'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="load-trigger-event">
|
||||
{
|
||||
const img = document.createElement("img");
|
||||
let count = 0;
|
||||
img.addEventListener("load", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
|
||||
testing.expectEqual(true, count < 3);
|
||||
count++;
|
||||
|
||||
testing.expectEqual(false, bubbles);
|
||||
testing.expectEqual(false, cancelBubble);
|
||||
testing.expectEqual(false, cancelable);
|
||||
testing.expectEqual(false, composed);
|
||||
testing.expectEqual(true, isTrusted);
|
||||
testing.expectEqual(img, target);
|
||||
});
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
img.src = "https://cdn.lightpanda.io/website/assets/images/docs/hn.png";
|
||||
testing.expectEqual("https://cdn.lightpanda.io/website/assets/images/docs/hn.png", img.src);
|
||||
}
|
||||
|
||||
// Make sure count is incremented asynchronously.
|
||||
testing.expectEqual(0, count);
|
||||
}
|
||||
</script>
|
||||
|
||||
<img
|
||||
id="inline-img"
|
||||
src="https://cdn.lightpanda.io/website/assets/images/docs/hn.png"
|
||||
onload="(() => testing.expectEqual(true, true))()"
|
||||
/>
|
||||
|
||||
<script id="inline-on-load">
|
||||
{
|
||||
const img = document.getElementById("inline-img");
|
||||
testing.expectEqual(true, img.onload instanceof Function);
|
||||
// Also call inline to double check.
|
||||
img.onload();
|
||||
|
||||
// Make sure ones attached with `addEventListener` also executed.
|
||||
testing.async(async () => {
|
||||
const result = await new Promise(resolve => {
|
||||
img.addEventListener("load", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
|
||||
testing.expectEqual(false, bubbles);
|
||||
testing.expectEqual(false, cancelBubble);
|
||||
testing.expectEqual(false, cancelable);
|
||||
testing.expectEqual(false, composed);
|
||||
testing.expectEqual(true, isTrusted);
|
||||
testing.expectEqual(img, target);
|
||||
|
||||
return resolve(true);
|
||||
});
|
||||
});
|
||||
|
||||
testing.expectEqual(true, result);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -183,6 +183,44 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="selectionchange_event">
|
||||
{
|
||||
const input = document.createElement('input');
|
||||
input.value = 'Hello World';
|
||||
document.body.appendChild(input);
|
||||
|
||||
let eventCount = 0;
|
||||
let lastEvent = null;
|
||||
|
||||
input.addEventListener('selectionchange', (e) => {
|
||||
eventCount++;
|
||||
lastEvent = e;
|
||||
});
|
||||
|
||||
testing.expectEqual(0, eventCount);
|
||||
|
||||
input.setSelectionRange(0, 5);
|
||||
input.select();
|
||||
input.selectionStart = 3;
|
||||
input.selectionEnd = 8;
|
||||
|
||||
let bubbledToBody = false;
|
||||
document.body.addEventListener('selectionchange', () => {
|
||||
bubbledToBody = true;
|
||||
});
|
||||
input.setSelectionRange(1, 4);
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(5, eventCount);
|
||||
testing.expectEqual('selectionchange', lastEvent.type);
|
||||
testing.expectEqual(input, lastEvent.target);
|
||||
testing.expectEqual(true, lastEvent.bubbles);
|
||||
testing.expectEqual(false, lastEvent.cancelable);
|
||||
testing.expectEqual(true, bubbledToBody);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- <script id="defaultChecked">
|
||||
testing.expectEqual(true, $('#check1').defaultChecked)
|
||||
testing.expectEqual(false, $('#check2').defaultChecked)
|
||||
|
||||
55
src/browser/tests/element/html/picture.html
Normal file
55
src/browser/tests/element/html/picture.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<!-- <script id="createElement">
|
||||
{
|
||||
const picture = document.createElement('picture');
|
||||
testing.expectEqual('PICTURE', picture.tagName);
|
||||
testing.expectEqual('[object HTMLPictureElement]', Object.prototype.toString.call(picture));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="constructor_type">
|
||||
{
|
||||
const picture = document.createElement('picture');
|
||||
testing.expectEqual(true, picture instanceof HTMLElement);
|
||||
testing.expectEqual(true, picture instanceof Element);
|
||||
testing.expectEqual(true, picture instanceof Node);
|
||||
}
|
||||
</script> -->
|
||||
|
||||
<picture id="inline-picture">
|
||||
<source media="(min-width: 800px)" srcset="large.jpg">
|
||||
<source media="(min-width: 400px)" srcset="medium.jpg">
|
||||
<img src="small.jpg" alt="Test image">
|
||||
</picture>
|
||||
|
||||
<script id="inline_picture">
|
||||
{
|
||||
const picture = document.getElementById('inline-picture');
|
||||
testing.expectEqual('PICTURE', picture.tagName);
|
||||
testing.expectEqual(3, picture.children.length);
|
||||
|
||||
const sources = picture.querySelectorAll('source');
|
||||
testing.expectEqual(2, sources.length);
|
||||
|
||||
// const img = picture.querySelector('img');
|
||||
// testing.expectEqual('IMG', img.tagName);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- <script id="appendChild">
|
||||
{
|
||||
const picture = document.createElement('picture');
|
||||
const source = document.createElement('source');
|
||||
const img = document.createElement('img');
|
||||
|
||||
picture.appendChild(source);
|
||||
picture.appendChild(img);
|
||||
|
||||
testing.expectEqual(2, picture.children.length);
|
||||
testing.expectEqual('SOURCE', picture.children[0].tagName);
|
||||
testing.expectEqual('IMG', picture.children[1].tagName);
|
||||
}
|
||||
</script>
|
||||
-->
|
||||
@@ -229,3 +229,41 @@
|
||||
testing.expectEqual('some content', clone.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="selectionchange_event">
|
||||
{
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = 'Hello World';
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
let eventCount = 0;
|
||||
let lastEvent = null;
|
||||
|
||||
textarea.addEventListener('selectionchange', (e) => {
|
||||
eventCount++;
|
||||
lastEvent = e;
|
||||
});
|
||||
|
||||
testing.expectEqual(0, eventCount);
|
||||
|
||||
textarea.setSelectionRange(0, 5);
|
||||
textarea.select();
|
||||
textarea.selectionStart = 3;
|
||||
textarea.selectionEnd = 8;
|
||||
|
||||
let bubbledToBody = false;
|
||||
document.body.addEventListener('selectionchange', () => {
|
||||
bubbledToBody = true;
|
||||
});
|
||||
textarea.setSelectionRange(1, 4);
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(5, eventCount);
|
||||
testing.expectEqual('selectionchange', lastEvent.type);
|
||||
testing.expectEqual(textarea, lastEvent.target);
|
||||
testing.expectEqual(true, lastEvent.bubbles);
|
||||
testing.expectEqual(false, lastEvent.cancelable);
|
||||
testing.expectEqual(true, bubbledToBody);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
334
src/browser/tests/element/replace_with.html
Normal file
334
src/browser/tests/element/replace_with.html
Normal file
@@ -0,0 +1,334 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<!-- Test 1: Basic single element replacement -->
|
||||
<div id="test1">
|
||||
<div id="parent1">
|
||||
<div id="old1">Old Content</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test1-basic-replacement">
|
||||
const old1 = $('#old1');
|
||||
const parent1 = $('#parent1');
|
||||
|
||||
testing.expectEqual(1, parent1.childElementCount);
|
||||
testing.expectEqual(old1, document.getElementById('old1'));
|
||||
|
||||
const new1 = document.createElement('div');
|
||||
new1.id = 'new1';
|
||||
new1.textContent = 'New Content';
|
||||
|
||||
old1.replaceWith(new1);
|
||||
|
||||
testing.expectEqual(1, parent1.childElementCount);
|
||||
testing.expectEqual(null, document.getElementById('old1'));
|
||||
testing.expectEqual(new1, document.getElementById('new1'));
|
||||
testing.expectEqual(parent1, new1.parentElement);
|
||||
</script>
|
||||
|
||||
<!-- Test 2: Replace with multiple elements -->
|
||||
<div id="test2">
|
||||
<div id="parent2">
|
||||
<div id="old2">Old</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test2-multiple-elements">
|
||||
const old2 = $('#old2');
|
||||
const parent2 = $('#parent2');
|
||||
|
||||
testing.expectEqual(1, parent2.childElementCount);
|
||||
|
||||
const new2a = document.createElement('div');
|
||||
new2a.id = 'new2a';
|
||||
const new2b = document.createElement('div');
|
||||
new2b.id = 'new2b';
|
||||
const new2c = document.createElement('div');
|
||||
new2c.id = 'new2c';
|
||||
|
||||
old2.replaceWith(new2a, new2b, new2c);
|
||||
|
||||
testing.expectEqual(3, parent2.childElementCount);
|
||||
testing.expectEqual(null, document.getElementById('old2'));
|
||||
testing.expectEqual(new2a, document.getElementById('new2a'));
|
||||
testing.expectEqual(new2b, document.getElementById('new2b'));
|
||||
testing.expectEqual(new2c, document.getElementById('new2c'));
|
||||
|
||||
// Check order
|
||||
testing.expectEqual(new2a, parent2.children[0]);
|
||||
testing.expectEqual(new2b, parent2.children[1]);
|
||||
testing.expectEqual(new2c, parent2.children[2]);
|
||||
</script>
|
||||
|
||||
<!-- Test 3: Replace with text nodes -->
|
||||
<div id="test3">
|
||||
<div id="parent3"><div id="old3">Old</div></div>
|
||||
</div>
|
||||
|
||||
<script id="test3-text-nodes">
|
||||
const old3 = $('#old3');
|
||||
const parent3 = $('#parent3');
|
||||
|
||||
old3.replaceWith('Text1', ' ', 'Text2');
|
||||
|
||||
testing.expectEqual(null, document.getElementById('old3'));
|
||||
testing.expectEqual('Text1 Text2', parent3.textContent);
|
||||
</script>
|
||||
|
||||
<!-- Test 4: Replace with mix of elements and text -->
|
||||
<div id="test4">
|
||||
<div id="parent4"><div id="old4">Old</div></div>
|
||||
</div>
|
||||
|
||||
<script id="test4-mixed">
|
||||
const old4 = $('#old4');
|
||||
const parent4 = $('#parent4');
|
||||
|
||||
const new4 = document.createElement('span');
|
||||
new4.id = 'new4';
|
||||
new4.textContent = 'Element';
|
||||
|
||||
old4.replaceWith('Before ', new4, ' After');
|
||||
|
||||
testing.expectEqual(null, document.getElementById('old4'));
|
||||
testing.expectEqual(new4, document.getElementById('new4'));
|
||||
testing.expectEqual('Before Element After', parent4.textContent);
|
||||
</script>
|
||||
|
||||
<!-- Test 5: Replace element not connected to document -->
|
||||
<script id="test5-not-connected">
|
||||
const disconnected = document.createElement('div');
|
||||
disconnected.id = 'disconnected5';
|
||||
|
||||
const replacement = document.createElement('div');
|
||||
replacement.id = 'replacement5';
|
||||
|
||||
// Should do nothing since element has no parent
|
||||
disconnected.replaceWith(replacement);
|
||||
|
||||
// Neither should be in the document
|
||||
testing.expectEqual(null, document.getElementById('disconnected5'));
|
||||
testing.expectEqual(null, document.getElementById('replacement5'));
|
||||
</script>
|
||||
|
||||
<!-- Test 6: Replace with nodes that already have a parent -->
|
||||
<div id="test6">
|
||||
<div id="parent6a">
|
||||
<div id="old6">Old</div>
|
||||
</div>
|
||||
<div id="parent6b">
|
||||
<div id="moving6a">Moving A</div>
|
||||
<div id="moving6b">Moving B</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test6-moving-nodes">
|
||||
const old6 = $('#old6');
|
||||
const parent6a = $('#parent6a');
|
||||
const parent6b = $('#parent6b');
|
||||
const moving6a = $('#moving6a');
|
||||
const moving6b = $('#moving6b');
|
||||
|
||||
testing.expectEqual(1, parent6a.childElementCount);
|
||||
testing.expectEqual(2, parent6b.childElementCount);
|
||||
|
||||
// Replace old6 with nodes that already have parent6b as parent
|
||||
old6.replaceWith(moving6a, moving6b);
|
||||
|
||||
// old6 should be gone
|
||||
testing.expectEqual(null, document.getElementById('old6'));
|
||||
|
||||
// parent6a should now have the moved elements
|
||||
testing.expectEqual(2, parent6a.childElementCount);
|
||||
testing.expectEqual(moving6a, parent6a.children[0]);
|
||||
testing.expectEqual(moving6b, parent6a.children[1]);
|
||||
|
||||
// parent6b should now be empty
|
||||
testing.expectEqual(0, parent6b.childElementCount);
|
||||
|
||||
// getElementById should still work
|
||||
testing.expectEqual(moving6a, document.getElementById('moving6a'));
|
||||
testing.expectEqual(moving6b, document.getElementById('moving6b'));
|
||||
testing.expectEqual(parent6a, moving6a.parentElement);
|
||||
testing.expectEqual(parent6a, moving6b.parentElement);
|
||||
</script>
|
||||
|
||||
<!-- Test 7: Replace with nested elements -->
|
||||
<div id="test7">
|
||||
<div id="parent7">
|
||||
<div id="old7">Old</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test7-nested">
|
||||
const old7 = $('#old7');
|
||||
const parent7 = $('#parent7');
|
||||
|
||||
const new7 = document.createElement('div');
|
||||
new7.id = 'new7';
|
||||
|
||||
const child7a = document.createElement('div');
|
||||
child7a.id = 'child7a';
|
||||
const child7b = document.createElement('div');
|
||||
child7b.id = 'child7b';
|
||||
|
||||
new7.appendChild(child7a);
|
||||
new7.appendChild(child7b);
|
||||
|
||||
old7.replaceWith(new7);
|
||||
|
||||
testing.expectEqual(null, document.getElementById('old7'));
|
||||
testing.expectEqual(new7, document.getElementById('new7'));
|
||||
testing.expectEqual(child7a, document.getElementById('child7a'));
|
||||
testing.expectEqual(child7b, document.getElementById('child7b'));
|
||||
testing.expectEqual(2, new7.childElementCount);
|
||||
</script>
|
||||
|
||||
<!-- Test 8: Replace maintains sibling order -->
|
||||
<div id="test8">
|
||||
<div id="parent8">
|
||||
<div id="before8">Before</div>
|
||||
<div id="old8">Old</div>
|
||||
<div id="after8">After</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test8-sibling-order">
|
||||
const old8 = $('#old8');
|
||||
const parent8 = $('#parent8');
|
||||
const before8 = $('#before8');
|
||||
const after8 = $('#after8');
|
||||
|
||||
testing.expectEqual(3, parent8.childElementCount);
|
||||
|
||||
const new8 = document.createElement('div');
|
||||
new8.id = 'new8';
|
||||
|
||||
old8.replaceWith(new8);
|
||||
|
||||
testing.expectEqual(3, parent8.childElementCount);
|
||||
testing.expectEqual(before8, parent8.children[0]);
|
||||
testing.expectEqual(new8, parent8.children[1]);
|
||||
testing.expectEqual(after8, parent8.children[2]);
|
||||
</script>
|
||||
|
||||
<!-- Test 9: Replace first child -->
|
||||
<div id="test9">
|
||||
<div id="parent9">
|
||||
<div id="first9">First</div>
|
||||
<div id="second9">Second</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test9-first-child">
|
||||
const first9 = $('#first9');
|
||||
const parent9 = $('#parent9');
|
||||
|
||||
const new9 = document.createElement('div');
|
||||
new9.id = 'new9';
|
||||
|
||||
first9.replaceWith(new9);
|
||||
|
||||
testing.expectEqual(null, document.getElementById('first9'));
|
||||
testing.expectEqual(new9, parent9.firstElementChild);
|
||||
testing.expectEqual(new9, parent9.children[0]);
|
||||
</script>
|
||||
|
||||
<!-- Test 10: Replace last child -->
|
||||
<div id="test10">
|
||||
<div id="parent10">
|
||||
<div id="first10">First</div>
|
||||
<div id="last10">Last</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test10-last-child">
|
||||
const last10 = $('#last10');
|
||||
const parent10 = $('#parent10');
|
||||
|
||||
const new10 = document.createElement('div');
|
||||
new10.id = 'new10';
|
||||
|
||||
last10.replaceWith(new10);
|
||||
|
||||
testing.expectEqual(null, document.getElementById('last10'));
|
||||
testing.expectEqual(new10, parent10.lastElementChild);
|
||||
testing.expectEqual(new10, parent10.children[1]);
|
||||
</script>
|
||||
|
||||
<!-- Test 11: Replace with empty (no arguments) - effectively removes the element -->
|
||||
<div id="test11">
|
||||
<div id="parent11">
|
||||
<div id="old11">Old</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test11-empty">
|
||||
const old11 = $('#old11');
|
||||
const parent11 = $('#parent11');
|
||||
|
||||
testing.expectEqual(1, parent11.childElementCount);
|
||||
|
||||
// Calling replaceWith() with no args should just remove the element
|
||||
old11.replaceWith();
|
||||
|
||||
// Element should be removed, leaving parent empty
|
||||
testing.expectEqual(0, parent11.childElementCount);
|
||||
testing.expectEqual(null, document.getElementById('old11'));
|
||||
testing.expectEqual(null, old11.parentElement);
|
||||
</script>
|
||||
|
||||
<!-- Test 12: Replace and check childElementCount updates -->
|
||||
<div id="test12">
|
||||
<div id="parent12">
|
||||
<div id="a12">A</div>
|
||||
<div id="b12">B</div>
|
||||
<div id="c12">C</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test12-child-count">
|
||||
const b12 = $('#b12');
|
||||
const parent12 = $('#parent12');
|
||||
|
||||
testing.expectEqual(3, parent12.childElementCount);
|
||||
|
||||
// Replace with 2 elements
|
||||
const new12a = document.createElement('div');
|
||||
new12a.id = 'new12a';
|
||||
const new12b = document.createElement('div');
|
||||
new12b.id = 'new12b';
|
||||
|
||||
b12.replaceWith(new12a, new12b);
|
||||
|
||||
testing.expectEqual(4, parent12.childElementCount);
|
||||
testing.expectEqual(null, document.getElementById('b12'));
|
||||
</script>
|
||||
|
||||
<!-- Test 13: Replace deeply nested element -->
|
||||
<div id="test13">
|
||||
<div id="l1">
|
||||
<div id="l2">
|
||||
<div id="l3">
|
||||
<div id="l4">
|
||||
<div id="old13">Deep</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test13-deeply-nested">
|
||||
const old13 = $('#old13');
|
||||
const l4 = $('#l4');
|
||||
|
||||
const new13 = document.createElement('div');
|
||||
new13.id = 'new13';
|
||||
|
||||
old13.replaceWith(new13);
|
||||
|
||||
testing.expectEqual(null, document.getElementById('old13'));
|
||||
testing.expectEqual(new13, document.getElementById('new13'));
|
||||
testing.expectEqual(l4, new13.parentElement);
|
||||
</script>
|
||||
22
src/browser/tests/event/promise_rejection.html
Normal file
22
src/browser/tests/event/promise_rejection.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id=project_rejection>
|
||||
{
|
||||
let e1 = new PromiseRejectionEvent("rejectionhandled");
|
||||
testing.expectEqual(true, e1 instanceof PromiseRejectionEvent);
|
||||
testing.expectEqual(true, e1 instanceof Event);
|
||||
|
||||
testing.expectEqual("rejectionhandled", e1.type);
|
||||
testing.expectEqual(null, e1.reason);
|
||||
testing.expectEqual(null, e1.promise);
|
||||
|
||||
let e2 = new PromiseRejectionEvent("rejectionhandled", {reason: ['tea']});
|
||||
testing.expectEqual(true, e2 instanceof PromiseRejectionEvent);
|
||||
testing.expectEqual(true, e2 instanceof Event);
|
||||
|
||||
testing.expectEqual("rejectionhandled", e2.type);
|
||||
testing.expectEqual(['tea'], e2.reason);
|
||||
testing.expectEqual(null, e2.promise);
|
||||
}
|
||||
</script>
|
||||
@@ -106,3 +106,17 @@
|
||||
testing.expectEqual(req5, target);
|
||||
})
|
||||
</script>
|
||||
|
||||
<script id=xhr6 type=module>
|
||||
const req5 = new XMLHttpRequest()
|
||||
const promise5 = new Promise((resolve) => {
|
||||
req5.onload = resolve;
|
||||
req5.open('PROPFIND', 'http://127.0.0.1:9589/xhr')
|
||||
req5.send('foo')
|
||||
});
|
||||
|
||||
testing.async(promise5, () => {
|
||||
testing.expectEqual(200, req5.status);
|
||||
testing.expectEqual('OK', req5.statusText);
|
||||
testing.expectEqual(true, req5.responseText.length > 65);
|
||||
});
|
||||
|
||||
@@ -130,3 +130,10 @@
|
||||
testing.expectEqual("you", request2.headers.get("target"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=propfind>
|
||||
{
|
||||
const req = new Request('https://example.com/api', { method: 'propfind' });
|
||||
testing.expectEqual('PROPFIND', req.method);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -125,6 +125,26 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<script id=xhr6>
|
||||
const req6 = new XMLHttpRequest()
|
||||
testing.async(async (restore) => {
|
||||
await new Promise((resolve) => {
|
||||
req6.onload = resolve;
|
||||
req6.open('GET', 'http://127.0.0.1:9582/xhr/binary')
|
||||
req6.responseType ='arraybuffer'
|
||||
req6.send()
|
||||
});
|
||||
|
||||
restore();
|
||||
testing.expectEqual(200, req6.status);
|
||||
testing.expectEqual('OK', req6.statusText);
|
||||
testing.expectEqual(7, req6.response.byteLength);
|
||||
testing.expectEqual([0, 0, 1, 2, 0, 0, 9], new Int32Array(req6.response));
|
||||
testing.expectEqual('', typeof req6.response);
|
||||
testing.expectEqual('arraybuffer', req6.responseType);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id=xhr_redirect>
|
||||
testing.async(async (restore) => {
|
||||
const req = new XMLHttpRequest();
|
||||
|
||||
@@ -541,3 +541,55 @@
|
||||
testing.expectEqual(3, sel.focusOffset);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=selectionChangeEvent>
|
||||
{
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
|
||||
let eventCount = 0;
|
||||
let lastEvent = null;
|
||||
|
||||
document.addEventListener('selectionchange', (e) => {
|
||||
eventCount++;
|
||||
lastEvent = e;
|
||||
});
|
||||
|
||||
const p1 = document.getElementById("p1");
|
||||
const textNode = p1.firstChild;
|
||||
const nested = document.getElementById("nested");
|
||||
|
||||
sel.collapse(textNode, 5);
|
||||
sel.extend(textNode, 10);
|
||||
sel.collapseToStart();
|
||||
sel.collapseToEnd();
|
||||
|
||||
sel.removeAllRanges();
|
||||
const range = document.createRange();
|
||||
range.setStart(textNode, 4);
|
||||
range.setEnd(textNode, 15);
|
||||
sel.addRange(range);
|
||||
|
||||
sel.removeRange(range);
|
||||
|
||||
const newRange = document.createRange();
|
||||
newRange.selectNodeContents(p1);
|
||||
sel.addRange(newRange);
|
||||
sel.removeAllRanges();
|
||||
|
||||
sel.selectAllChildren(nested);
|
||||
sel.setBaseAndExtent(textNode, 4, textNode, 15);
|
||||
|
||||
sel.collapse(textNode, 5);
|
||||
sel.extend(textNode, 10);
|
||||
sel.deleteFromDocument();
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(14, eventCount);
|
||||
testing.expectEqual('selectionchange', lastEvent.type);
|
||||
testing.expectEqual(document, lastEvent.target);
|
||||
testing.expectEqual(false, lastEvent.bubbles);
|
||||
testing.expectEqual(false, lastEvent.cancelable);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
|
||||
<script id=setTimeout>
|
||||
testing.expectEqual(1, window.setTimeout.length);
|
||||
let wst2 = 1;
|
||||
window.setTimeout((a, b) => {
|
||||
wst2 = a + b;
|
||||
|
||||
14
src/browser/tests/window/visual_viewport.html
Normal file
14
src/browser/tests/window/visual_viewport.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id=visual_viewport>
|
||||
const vp = window.visualViewport;
|
||||
testing.expectEqual(vp, window.visualViewport);
|
||||
testing.expectEqual(0, vp.offsetLeft);
|
||||
testing.expectEqual(0, vp.offsetTop);
|
||||
testing.expectEqual(0, vp.pageLeft);
|
||||
testing.expectEqual(0, vp.pageTop);
|
||||
testing.expectEqual(1920, vp.width);
|
||||
testing.expectEqual(1080, vp.height);
|
||||
testing.expectEqual(1.0, vp.scale);
|
||||
</script>
|
||||
@@ -5,6 +5,7 @@
|
||||
testing.expectEqual(window, globalThis);
|
||||
testing.expectEqual(window, self);
|
||||
testing.expectEqual(window, window.self);
|
||||
testing.expectEqual(null, window.opener);
|
||||
|
||||
testing.expectEqual(1080, innerHeight);
|
||||
testing.expectEqual(1920, innerWidth);
|
||||
@@ -51,6 +52,7 @@
|
||||
<script id=btoa>
|
||||
testing.expectEqual('SGVsbG8gV29ybGQh', btoa('Hello World!'));
|
||||
testing.expectEqual('', btoa(''));
|
||||
testing.expectEqual('IA==', btoa(' '));
|
||||
testing.expectEqual('YQ==', btoa('a'));
|
||||
testing.expectEqual('YWI=', btoa('ab'));
|
||||
testing.expectEqual('YWJj', btoa('abc'));
|
||||
@@ -61,6 +63,13 @@
|
||||
<script id=atob>
|
||||
testing.expectEqual('Hello World!', atob('SGVsbG8gV29ybGQh'));
|
||||
testing.expectEqual('', atob(''));
|
||||
|
||||
// atob must trim input
|
||||
testing.expectEqual('', atob(' '));
|
||||
testing.expectEqual(' ', atob('IA=='));
|
||||
testing.expectEqual(' ', atob(' IA=='));
|
||||
testing.expectEqual(' ', atob('IA== '));
|
||||
|
||||
testing.expectEqual('a', atob('YQ=='));
|
||||
testing.expectEqual('ab', atob('YWI='));
|
||||
testing.expectEqual('abc', atob('YWJj'));
|
||||
@@ -105,3 +114,30 @@
|
||||
testing.expectEqual(24, screen.pixelDepth);
|
||||
testing.expectEqual(screen, window.screen);
|
||||
</script>
|
||||
|
||||
<script id=unhandled_rejection>
|
||||
{
|
||||
let unhandledCalled = 0;
|
||||
window.onunhandledrejection = function(e) {
|
||||
testing.expectEqual(true, e instanceof PromiseRejectionEvent);
|
||||
testing.expectEqual({x: 'Fail'}, e.reason);
|
||||
testing.expectEqual('unhandledrejection', e.type);
|
||||
testing.expectEqual(window, e.target);
|
||||
testing.expectEqual(window, e.srcElement);
|
||||
testing.expectEqual(window, e.currentTarget);
|
||||
unhandledCalled += 1;
|
||||
}
|
||||
|
||||
window.addEventListener('unhandledrejection', function(e) {
|
||||
testing.expectEqual(true, e instanceof PromiseRejectionEvent);
|
||||
testing.expectEqual({x: 'Fail'}, e.reason);
|
||||
testing.expectEqual('unhandledrejection', e.type);
|
||||
testing.expectEqual(window, e.target);
|
||||
testing.expectEqual(window, e.srcElement);
|
||||
testing.expectEqual(window, e.currentTarget);
|
||||
unhandledCalled += 1;
|
||||
});
|
||||
Promise.reject({x: 'Fail'});
|
||||
testing.eventually(() => testing.expectEqual(2, unhandledCalled));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -76,7 +76,9 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, local: *const js.Local, page:
|
||||
}
|
||||
|
||||
// Dispatch abort event
|
||||
const event = try Event.initTrusted("abort", .{}, page);
|
||||
const event = try Event.initTrusted(comptime .wrap("abort"), .{}, page);
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
|
||||
try page._event_manager.dispatchWithFunction(
|
||||
self.asEventTarget(),
|
||||
event,
|
||||
@@ -99,7 +101,7 @@ pub fn createTimeout(delay: u32, page: *Page) !*AbortSignal {
|
||||
.signal = try init(page),
|
||||
};
|
||||
|
||||
try page.scheduler.add(callback, TimeoutCallback.run, delay, .{
|
||||
try page.js.scheduler.add(callback, TimeoutCallback.run, delay, .{
|
||||
.name = "AbortSignal.timeout",
|
||||
});
|
||||
|
||||
@@ -116,7 +118,7 @@ pub fn throwIfAborted(self: *const AbortSignal, page: *Page) !ThrowIfAborted {
|
||||
if (self._aborted) {
|
||||
const exception = switch (self._reason) {
|
||||
.string => |str| local.throw(str),
|
||||
.js_val => |js_val| local.throw(try local.toLocal(js_val).toString(.{ .allocator = page.call_arena })),
|
||||
.js_val => |js_val| local.throw(try local.toLocal(js_val).toStringSlice()),
|
||||
.undefined => local.throw("AbortError"),
|
||||
};
|
||||
return .{ .exception = exception };
|
||||
|
||||
@@ -42,15 +42,23 @@ pub fn parseDimension(value: []const u8) ?f64 {
|
||||
/// https://drafts.csswg.org/cssom/#the-css.escape()-method
|
||||
pub fn escape(_: *const CSS, value: []const u8, page: *Page) ![]const u8 {
|
||||
if (value.len == 0) {
|
||||
return error.InvalidCharacterError;
|
||||
return "";
|
||||
}
|
||||
|
||||
const first = value[0];
|
||||
if (first == '-' and value.len == 1) {
|
||||
return "\\-";
|
||||
}
|
||||
|
||||
// Count how many characters we need for the output
|
||||
var out_len: usize = escapeLen(true, first);
|
||||
for (value[1..]) |c| {
|
||||
out_len += escapeLen(false, c);
|
||||
for (value[1..], 0..) |c, i| {
|
||||
// Second char (i==0) is a digit and first is '-', needs hex escape
|
||||
if (i == 0 and first == '-' and c >= '0' and c <= '9') {
|
||||
out_len += 2 + hexDigitsNeeded(c);
|
||||
} else {
|
||||
out_len += escapeLen(false, c);
|
||||
}
|
||||
}
|
||||
|
||||
if (out_len == value.len) {
|
||||
@@ -67,8 +75,13 @@ pub fn escape(_: *const CSS, value: []const u8, page: *Page) ![]const u8 {
|
||||
pos = 1;
|
||||
}
|
||||
|
||||
for (value[1..]) |c| {
|
||||
if (!needsEscape(false, c)) {
|
||||
for (value[1..], 0..) |c, i| {
|
||||
// Second char (i==0) is a digit and first is '-', needs hex escape
|
||||
if (i == 0 and first == '-' and c >= '0' and c <= '9') {
|
||||
result[pos] = '\\';
|
||||
const hex_str = std.fmt.bufPrint(result[pos + 1 ..], "{x} ", .{c}) catch unreachable;
|
||||
pos += 1 + hex_str.len;
|
||||
} else if (!needsEscape(false, c)) {
|
||||
result[pos] = c;
|
||||
pos += 1;
|
||||
} else {
|
||||
@@ -105,9 +118,6 @@ fn needsEscape(comptime is_first: bool, c: u8) bool {
|
||||
if (c >= '0' and c <= '9') {
|
||||
return true;
|
||||
}
|
||||
if (c == '-') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Characters that need escaping
|
||||
|
||||
@@ -65,6 +65,10 @@ pub fn @"error"(_: *const Console, values: []js.Value, page: *Page) void {
|
||||
logger.warn(.js, "console.error", .{ValueWriter{ .page = page, .values = values, .include_stack = true }});
|
||||
}
|
||||
|
||||
pub fn table(_: *const Console, data: js.Value, columns: ?js.Value) void {
|
||||
logger.info(.js, "console.table", .{ .data = data, .columns = columns });
|
||||
}
|
||||
|
||||
pub fn count(self: *Console, label_: ?[]const u8, page: *Page) !void {
|
||||
const label = label_ orelse "default";
|
||||
const gop = try self._counts.getOrPut(page.arena, label);
|
||||
@@ -146,7 +150,7 @@ const ValueWriter = struct {
|
||||
var buf: [32]u8 = undefined;
|
||||
for (self.values, 0..) |value, i| {
|
||||
const name = try std.fmt.bufPrint(&buf, "param.{d}", .{i});
|
||||
try writer.write(name, try value.toString(.{}));
|
||||
try writer.write(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +171,6 @@ pub const JsApi = struct {
|
||||
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const empty_with_no_proto = true;
|
||||
};
|
||||
|
||||
pub const trace = bridge.function(Console.trace, .{});
|
||||
@@ -179,9 +182,15 @@ pub const JsApi = struct {
|
||||
pub const assert = bridge.function(Console.assert, .{});
|
||||
pub const @"error" = bridge.function(Console.@"error", .{});
|
||||
pub const exception = bridge.function(Console.@"error", .{});
|
||||
pub const table = bridge.function(Console.table, .{});
|
||||
pub const count = bridge.function(Console.count, .{});
|
||||
pub const countReset = bridge.function(Console.countReset, .{});
|
||||
pub const time = bridge.function(Console.time, .{});
|
||||
pub const timeLog = bridge.function(Console.timeLog, .{});
|
||||
pub const timeEnd = bridge.function(Console.timeEnd, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "WebApi: Console" {
|
||||
try testing.htmlRunner("console", .{});
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
|
||||
const SubtleCrypto = @import("SubtleCrypto.zig");
|
||||
|
||||
const Crypto = @This();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -20,7 +20,6 @@ const std = @import("std");
|
||||
const String = @import("../../string.zig").String;
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const Element = @import("Element.zig");
|
||||
|
||||
const CustomElementDefinition = @This();
|
||||
|
||||
@@ -73,9 +73,8 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
|
||||
var js_arr = observed_attrs.toArray();
|
||||
for (0..js_arr.len()) |i| {
|
||||
const attr_val = js_arr.get(@intCast(i)) catch continue;
|
||||
const attr_name = attr_val.toString(.{ .allocator = page.arena }) catch continue;
|
||||
const owned_attr = page.dupeString(attr_name) catch continue;
|
||||
definition.observed_attributes.put(page.arena, owned_attr, {}) catch continue;
|
||||
const attr_name = attr_val.toStringSliceWithAlloc(page.arena) catch continue;
|
||||
definition.observed_attributes.put(page.arena, attr_name, {}) catch continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ _what_to_show: u32,
|
||||
_filter: NodeFilter,
|
||||
_reference_node: *Node,
|
||||
_pointer_before_reference_node: bool,
|
||||
_active: bool = false,
|
||||
|
||||
pub fn init(root: *Node, what_to_show: u32, filter: ?FilterOpts, page: *Page) !*DOMNodeIterator {
|
||||
const node_filter = try NodeFilter.init(filter);
|
||||
@@ -64,6 +65,13 @@ pub fn getFilter(self: *const DOMNodeIterator) ?FilterOpts {
|
||||
}
|
||||
|
||||
pub fn nextNode(self: *DOMNodeIterator, page: *Page) !?*Node {
|
||||
if (self._active) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
|
||||
self._active = true;
|
||||
defer self._active = false;
|
||||
|
||||
var node = self._reference_node;
|
||||
var before_node = self._pointer_before_reference_node;
|
||||
|
||||
@@ -95,6 +103,13 @@ pub fn nextNode(self: *DOMNodeIterator, page: *Page) !?*Node {
|
||||
}
|
||||
|
||||
pub fn previousNode(self: *DOMNodeIterator, page: *Page) !?*Node {
|
||||
if (self._active) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
|
||||
self._active = true;
|
||||
defer self._active = false;
|
||||
|
||||
var node = self._reference_node;
|
||||
var before_node = self._pointer_before_reference_node;
|
||||
|
||||
@@ -119,6 +134,10 @@ pub fn previousNode(self: *DOMNodeIterator, page: *Page) !?*Node {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detach(_: *const DOMNodeIterator) void {
|
||||
// no-op legacy
|
||||
}
|
||||
|
||||
fn filterNode(self: *const DOMNodeIterator, node: *Node, page: *Page) !i32 {
|
||||
// First check whatToShow
|
||||
if (!NodeFilter.shouldShow(node, self._what_to_show)) {
|
||||
@@ -181,6 +200,7 @@ pub const JsApi = struct {
|
||||
pub const whatToShow = bridge.accessor(DOMNodeIterator.getWhatToShow, null, .{});
|
||||
pub const filter = bridge.accessor(DOMNodeIterator.getFilter, null, .{});
|
||||
|
||||
pub const nextNode = bridge.function(DOMNodeIterator.nextNode, .{});
|
||||
pub const previousNode = bridge.function(DOMNodeIterator.previousNode, .{});
|
||||
pub const nextNode = bridge.function(DOMNodeIterator.nextNode, .{ .dom_exception = true });
|
||||
pub const previousNode = bridge.function(DOMNodeIterator.previousNode, .{ .dom_exception = true });
|
||||
pub const detach = bridge.function(DOMNodeIterator.detach, .{});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -26,7 +26,6 @@ const Parser = @import("../parser/Parser.zig");
|
||||
const HTMLDocument = @import("HTMLDocument.zig");
|
||||
const XMLDocument = @import("XMLDocument.zig");
|
||||
const Document = @import("Document.zig");
|
||||
const ProcessingInstruction = @import("../webapi/cdata/ProcessingInstruction.zig");
|
||||
|
||||
const DOMParser = @This();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -30,8 +30,6 @@ const Location = @import("Location.zig");
|
||||
const Parser = @import("../parser/Parser.zig");
|
||||
const collections = @import("collections.zig");
|
||||
const Selector = @import("selector/Selector.zig");
|
||||
const NodeFilter = @import("NodeFilter.zig");
|
||||
const DocumentType = @import("DocumentType.zig");
|
||||
const DOMTreeWalker = @import("DOMTreeWalker.zig");
|
||||
const DOMNodeIterator = @import("DOMNodeIterator.zig");
|
||||
const DOMImplementation = @import("DOMImplementation.zig");
|
||||
@@ -58,6 +56,20 @@ _script_created_parser: ?Parser.Streaming = null,
|
||||
_adopted_style_sheets: ?js.Object.Global = null,
|
||||
_selection: Selection = .init,
|
||||
|
||||
_on_selectionchange: ?js.Function.Global = null,
|
||||
|
||||
pub fn getOnSelectionChange(self: *Document) ?js.Function.Global {
|
||||
return self._on_selectionchange;
|
||||
}
|
||||
|
||||
pub fn setOnSelectionChange(self: *Document, listener: ?js.Function) !void {
|
||||
if (listener) |listen| {
|
||||
self._on_selectionchange = try listen.persistWithThis(self);
|
||||
} else {
|
||||
self._on_selectionchange = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub const Type = union(enum) {
|
||||
generic,
|
||||
html: *HTMLDocument,
|
||||
@@ -105,18 +117,6 @@ pub fn getContentType(self: *const Document) []const u8 {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getCharacterSet(_: *const Document) []const u8 {
|
||||
return "UTF-8";
|
||||
}
|
||||
|
||||
pub fn getCompatMode(_: *const Document) []const u8 {
|
||||
return "CSS1Compat";
|
||||
}
|
||||
|
||||
pub fn getReferrer(_: *const Document) []const u8 {
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn getDomain(_: *const Document, page: *const Page) []const u8 {
|
||||
return URL.getHostname(page.url);
|
||||
}
|
||||
@@ -127,14 +127,16 @@ const CreateElementOptions = struct {
|
||||
|
||||
pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element {
|
||||
try validateElementName(name);
|
||||
const namespace: Element.Namespace = blk: {
|
||||
const ns: Element.Namespace, const normalized_name = blk: {
|
||||
if (self._type == .html) {
|
||||
break :blk .html;
|
||||
break :blk .{ .html, std.ascii.lowerString(&page.buf, name) };
|
||||
}
|
||||
// Generic and XML documents create XML elements
|
||||
break :blk .xml;
|
||||
break :blk .{ .xml, name };
|
||||
};
|
||||
const node = try page.createElementNS(namespace, name, null);
|
||||
// HTML documents are case-insensitive - lowercase the tag name
|
||||
|
||||
const node = try page.createElementNS(ns, normalized_name, null);
|
||||
const element = node.as(Element);
|
||||
|
||||
// Track owner document if it's not the main document
|
||||
@@ -153,7 +155,9 @@ pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElement
|
||||
|
||||
pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element {
|
||||
try validateElementName(name);
|
||||
const node = try page.createElementNS(Element.Namespace.parse(namespace), name, null);
|
||||
const ns = Element.Namespace.parse(namespace);
|
||||
const normalized_name = if (ns == .html) std.ascii.lowerString(&page.buf, name) else name;
|
||||
const node = try page.createElementNS(ns, normalized_name, null);
|
||||
|
||||
// Track owner document if it's not the main document
|
||||
if (self != page.document) {
|
||||
@@ -249,7 +253,7 @@ pub fn getElementsByClassName(self: *Document, class_name: []const u8, page: *Pa
|
||||
|
||||
// Parse space-separated class names
|
||||
var class_names: std.ArrayList([]const u8) = .empty;
|
||||
var it = std.mem.tokenizeAny(u8, class_name, &std.ascii.whitespace);
|
||||
var it = std.mem.tokenizeAny(u8, class_name, "\t\n\x0C\r ");
|
||||
while (it.next()) |name| {
|
||||
try class_names.append(arena, try page.dupeString(name));
|
||||
}
|
||||
@@ -794,6 +798,17 @@ pub fn setAdoptedStyleSheets(self: *Document, sheets: js.Object) !void {
|
||||
self._adopted_style_sheets = try sheets.persist();
|
||||
}
|
||||
|
||||
pub fn getHidden(_: *const Document) bool {
|
||||
// it's hidden when, for example, the decive is locked, or user is on a
|
||||
// a different tab.
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn getVisibilityState(_: *const Document) []const u8 {
|
||||
// See getHidden above, possible options are "visible" or "hidden"
|
||||
return "visible";
|
||||
}
|
||||
|
||||
// Validates that nodes can be inserted into a Document, respecting Document constraints:
|
||||
// - At most one Element child
|
||||
// - At most one DocumentType child
|
||||
@@ -929,6 +944,7 @@ pub const JsApi = struct {
|
||||
});
|
||||
}
|
||||
|
||||
pub const onselectionchange = bridge.accessor(Document.getOnSelectionChange, Document.setOnSelectionChange, .{});
|
||||
pub const URL = bridge.accessor(Document.getURL, null, .{});
|
||||
pub const documentURI = bridge.accessor(Document.getURL, null, .{});
|
||||
pub const documentElement = bridge.accessor(Document.getDocumentElement, null, .{});
|
||||
@@ -938,11 +954,6 @@ pub const JsApi = struct {
|
||||
pub const activeElement = bridge.accessor(Document.getActiveElement, null, .{});
|
||||
pub const styleSheets = bridge.accessor(Document.getStyleSheets, null, .{});
|
||||
pub const contentType = bridge.accessor(Document.getContentType, null, .{});
|
||||
pub const characterSet = bridge.accessor(Document.getCharacterSet, null, .{});
|
||||
pub const charset = bridge.accessor(Document.getCharacterSet, null, .{});
|
||||
pub const inputEncoding = bridge.accessor(Document.getCharacterSet, null, .{});
|
||||
pub const compatMode = bridge.accessor(Document.getCompatMode, null, .{});
|
||||
pub const referrer = bridge.accessor(Document.getReferrer, null, .{});
|
||||
pub const domain = bridge.accessor(Document.getDomain, null, .{});
|
||||
pub const createElement = bridge.function(Document.createElement, .{ .dom_exception = true });
|
||||
pub const createElementNS = bridge.function(Document.createElementNS, .{ .dom_exception = true });
|
||||
@@ -989,13 +1000,21 @@ pub const JsApi = struct {
|
||||
pub const lastElementChild = bridge.accessor(Document.getLastElementChild, null, .{});
|
||||
pub const childElementCount = bridge.accessor(Document.getChildElementCount, null, .{});
|
||||
pub const adoptedStyleSheets = bridge.accessor(Document.getAdoptedStyleSheets, Document.setAdoptedStyleSheets, .{});
|
||||
|
||||
pub const hidden = bridge.accessor(Document.getHidden, null, .{});
|
||||
pub const visibilityState = bridge.accessor(Document.getVisibilityState, null, .{});
|
||||
pub const defaultView = bridge.accessor(struct {
|
||||
fn defaultView(_: *const Document, page: *Page) *@import("Window.zig") {
|
||||
return page.window;
|
||||
}
|
||||
}.defaultView, null, .{ .cache = "defaultView" });
|
||||
}.defaultView, null, .{});
|
||||
pub const hasFocus = bridge.function(Document.hasFocus, .{});
|
||||
|
||||
pub const prerendering = bridge.property(false, .{ .template = false });
|
||||
pub const characterSet = bridge.property("UTF-8", .{ .template = false });
|
||||
pub const charset = bridge.property("UTF-8", .{ .template = false });
|
||||
pub const inputEncoding = bridge.property("UTF-8", .{ .template = false });
|
||||
pub const compatMode = bridge.property("CSS1Compat", .{ .template = false });
|
||||
pub const referrer = bridge.property("", .{ .template = false });
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
@@ -49,129 +49,6 @@ pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTok
|
||||
pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);
|
||||
pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot);
|
||||
|
||||
/// Better to discriminate it since not directly a pointer int.
|
||||
///
|
||||
/// See `calcAttrListenerKey` to obtain one.
|
||||
const AttrListenerKey = u64;
|
||||
/// Use `getAttrListenerKey` to create a key.
|
||||
pub const AttrListenerLookup = std.AutoHashMapUnmanaged(AttrListenerKey, js.Function.Global);
|
||||
|
||||
/// Enum of known event listeners; increasing the size of it (u7)
|
||||
/// can cause `AttrListenerKey` to behave incorrectly.
|
||||
pub const KnownListener = enum(u7) {
|
||||
abort,
|
||||
animationcancel,
|
||||
animationend,
|
||||
animationiteration,
|
||||
animationstart,
|
||||
auxclick,
|
||||
beforeinput,
|
||||
beforematch,
|
||||
beforetoggle,
|
||||
blur,
|
||||
cancel,
|
||||
canplay,
|
||||
canplaythrough,
|
||||
change,
|
||||
click,
|
||||
close,
|
||||
command,
|
||||
contentvisibilityautostatechange,
|
||||
contextlost,
|
||||
contextmenu,
|
||||
contextrestored,
|
||||
copy,
|
||||
cuechange,
|
||||
cut,
|
||||
dblclick,
|
||||
drag,
|
||||
dragend,
|
||||
dragenter,
|
||||
dragexit,
|
||||
dragleave,
|
||||
dragover,
|
||||
dragstart,
|
||||
drop,
|
||||
durationchange,
|
||||
emptied,
|
||||
ended,
|
||||
@"error",
|
||||
focus,
|
||||
formdata,
|
||||
fullscreenchange,
|
||||
fullscreenerror,
|
||||
gotpointercapture,
|
||||
input,
|
||||
invalid,
|
||||
keydown,
|
||||
keypress,
|
||||
keyup,
|
||||
load,
|
||||
loadeddata,
|
||||
loadedmetadata,
|
||||
loadstart,
|
||||
lostpointercapture,
|
||||
mousedown,
|
||||
mousemove,
|
||||
mouseout,
|
||||
mouseover,
|
||||
mouseup,
|
||||
paste,
|
||||
pause,
|
||||
play,
|
||||
playing,
|
||||
pointercancel,
|
||||
pointerdown,
|
||||
pointerenter,
|
||||
pointerleave,
|
||||
pointermove,
|
||||
pointerout,
|
||||
pointerover,
|
||||
pointerrawupdate,
|
||||
pointerup,
|
||||
progress,
|
||||
ratechange,
|
||||
reset,
|
||||
resize,
|
||||
scroll,
|
||||
scrollend,
|
||||
securitypolicyviolation,
|
||||
seeked,
|
||||
seeking,
|
||||
select,
|
||||
selectionchange,
|
||||
selectstart,
|
||||
slotchange,
|
||||
stalled,
|
||||
submit,
|
||||
@"suspend",
|
||||
timeupdate,
|
||||
toggle,
|
||||
transitioncancel,
|
||||
transitionend,
|
||||
transitionrun,
|
||||
transitionstart,
|
||||
volumechange,
|
||||
waiting,
|
||||
wheel,
|
||||
};
|
||||
|
||||
/// Calculates a lookup key (`AttrListenerKey`) to use with `AttrListenerLookup` for an element.
|
||||
/// NEVER use generated key to retrieve a pointer back. Portability is not guaranteed.
|
||||
pub fn calcAttrListenerKey(self: *Element, event_type: KnownListener) AttrListenerKey {
|
||||
// We can use `Element` for the key too; `EventTarget` is strict about
|
||||
// its size and alignment, though.
|
||||
const target = self.asEventTarget();
|
||||
// Check if we have 3 bits available from alignment of 8.
|
||||
lp.assert(@alignOf(@TypeOf(target)) == 8, "createLookupKey: incorrect alignment", .{
|
||||
.event_target_alignment = @alignOf(@TypeOf(target)),
|
||||
});
|
||||
|
||||
const ptr = @intFromPtr(target) >> 3;
|
||||
lp.assert(ptr < (1 << 57), "createLookupKey: pointer overflow", .{ .ptr = ptr });
|
||||
return ptr | (@as(u64, @intFromEnum(event_type)) << 57);
|
||||
}
|
||||
|
||||
pub const Namespace = enum(u8) {
|
||||
html,
|
||||
svg,
|
||||
@@ -358,6 +235,7 @@ pub fn getTagNameLower(self: *const Element) []const u8 {
|
||||
.option => "option",
|
||||
.output => "output",
|
||||
.p => "p",
|
||||
.picture => "picture",
|
||||
.param => "param",
|
||||
.pre => "pre",
|
||||
.progress => "progress",
|
||||
@@ -434,6 +312,7 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {
|
||||
.option => "OPTION",
|
||||
.output => "OUTPUT",
|
||||
.p => "P",
|
||||
.picture => "PICTURE",
|
||||
.param => "PARAM",
|
||||
.pre => "PRE",
|
||||
.progress => "PROGRESS",
|
||||
@@ -643,7 +522,7 @@ pub fn setAttributeNS(
|
||||
self: *Element,
|
||||
maybe_namespace: ?[]const u8,
|
||||
qualified_name: []const u8,
|
||||
value: []const u8,
|
||||
value: String,
|
||||
page: *Page,
|
||||
) !void {
|
||||
if (maybe_namespace) |namespace| {
|
||||
@@ -656,7 +535,7 @@ pub fn setAttributeNS(
|
||||
qualified_name[idx + 1 ..]
|
||||
else
|
||||
qualified_name;
|
||||
return self.setAttribute(.wrap(local_name), .wrap(value), page);
|
||||
return self.setAttribute(.wrap(local_name), value, page);
|
||||
}
|
||||
|
||||
pub fn setAttributeSafe(self: *Element, name: String, value: String, page: *Page) !void {
|
||||
@@ -837,6 +716,44 @@ pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, page: *Pa
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replaceWith(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
|
||||
page.domChanged();
|
||||
|
||||
const ref_node = self.asNode();
|
||||
const parent = ref_node._parent orelse return;
|
||||
|
||||
const parent_is_connected = parent.isConnected();
|
||||
|
||||
// Detect if the ref_node must be removed (byt default) or kept.
|
||||
// We kept it when ref_node is present into the nodes list.
|
||||
var rm_ref_node = true;
|
||||
|
||||
for (nodes) |node_or_text| {
|
||||
const child = try node_or_text.toNode(page);
|
||||
|
||||
// If a child is the ref node. We keep it at its own current position.
|
||||
if (child == ref_node) {
|
||||
rm_ref_node = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (child._parent) |current_parent| {
|
||||
page.removeNode(current_parent, child, .{ .will_be_reconnected = parent_is_connected });
|
||||
}
|
||||
|
||||
try page.insertNodeRelative(
|
||||
parent,
|
||||
child,
|
||||
.{ .before = ref_node },
|
||||
.{ .child_already_connected = child.isConnected() },
|
||||
);
|
||||
}
|
||||
|
||||
if (rm_ref_node) {
|
||||
page.removeNode(parent, ref_node, .{ .will_be_reconnected = false });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(self: *Element, page: *Page) void {
|
||||
page.domChanged();
|
||||
const node = self.asNode();
|
||||
@@ -852,7 +769,8 @@ pub fn focus(self: *Element, page: *Page) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
const blur_event = try Event.initTrusted("blur", null, page);
|
||||
const blur_event = try Event.initTrusted(comptime .wrap("blur"), null, page);
|
||||
defer if (!blur_event._v8_handoff) blur_event.deinit(false);
|
||||
try page._event_manager.dispatch(old.asEventTarget(), blur_event);
|
||||
}
|
||||
|
||||
@@ -860,7 +778,8 @@ pub fn focus(self: *Element, page: *Page) !void {
|
||||
page.document._active_element = self;
|
||||
}
|
||||
|
||||
const focus_event = try Event.initTrusted("focus", null, page);
|
||||
const focus_event = try Event.initTrusted(comptime .wrap("focus"), null, page);
|
||||
defer if (!focus_event._v8_handoff) focus_event.deinit(false);
|
||||
try page._event_manager.dispatch(self.asEventTarget(), focus_event);
|
||||
}
|
||||
|
||||
@@ -870,7 +789,8 @@ pub fn blur(self: *Element, page: *Page) !void {
|
||||
page.document._active_element = null;
|
||||
|
||||
const Event = @import("Event.zig");
|
||||
const blur_event = try Event.initTrusted("blur", null, page);
|
||||
const blur_event = try Event.initTrusted(comptime .wrap("blur"), null, page);
|
||||
defer if (!blur_event._v8_handoff) blur_event.deinit(false);
|
||||
try page._event_manager.dispatch(self.asEventTarget(), blur_event);
|
||||
}
|
||||
|
||||
@@ -1220,7 +1140,7 @@ pub fn getElementsByClassName(self: *Element, class_name: []const u8, page: *Pag
|
||||
|
||||
// Parse space-separated class names
|
||||
var class_names: std.ArrayList([]const u8) = .empty;
|
||||
var it = std.mem.tokenizeAny(u8, class_name, &std.ascii.whitespace);
|
||||
var it = std.mem.tokenizeAny(u8, class_name, "\t\n\x0C\r ");
|
||||
while (it.next()) |name| {
|
||||
try class_names.append(arena, try page.dupeString(name));
|
||||
}
|
||||
@@ -1255,6 +1175,14 @@ pub fn scrollIntoViewIfNeeded(_: *const Element, center_if_needed: ?bool) void {
|
||||
_ = center_if_needed;
|
||||
}
|
||||
|
||||
const ScrollIntoViewOpts = union {
|
||||
align_to_top: bool,
|
||||
obj: js.Object,
|
||||
};
|
||||
pub fn scrollIntoView(_: *const Element, opts: ?ScrollIntoViewOpts) void {
|
||||
_ = opts;
|
||||
}
|
||||
|
||||
pub fn format(self: *Element, writer: *std.Io.Writer) !void {
|
||||
try writer.writeByte('<');
|
||||
try writer.writeAll(self.getTagNameDump());
|
||||
@@ -1298,26 +1226,27 @@ pub fn getTag(self: *const Element) Tag {
|
||||
.data => .data,
|
||||
.datalist => .datalist,
|
||||
.dialog => .dialog,
|
||||
.directory => .unknown,
|
||||
.directory => .directory,
|
||||
.iframe => .iframe,
|
||||
.img => .img,
|
||||
.br => .br,
|
||||
.button => .button,
|
||||
.canvas => .canvas,
|
||||
.fieldset => .fieldset,
|
||||
.font => .unknown,
|
||||
.font => .font,
|
||||
.heading => |h| h._tag,
|
||||
.label => .unknown,
|
||||
.legend => .unknown,
|
||||
.label => .label,
|
||||
.legend => .legend,
|
||||
.li => .li,
|
||||
.map => .unknown,
|
||||
.map => .map,
|
||||
.ul => .ul,
|
||||
.ol => .ol,
|
||||
.object => .unknown,
|
||||
.object => .object,
|
||||
.optgroup => .optgroup,
|
||||
.output => .unknown,
|
||||
.param => .unknown,
|
||||
.pre => .unknown,
|
||||
.output => .output,
|
||||
.picture => .picture,
|
||||
.param => .param,
|
||||
.pre => .pre,
|
||||
.generic => |g| g._tag,
|
||||
.media => |m| switch (m._type) {
|
||||
.audio => .audio,
|
||||
@@ -1331,7 +1260,7 @@ pub fn getTag(self: *const Element) Tag {
|
||||
.script => .script,
|
||||
.select => .select,
|
||||
.slot => .slot,
|
||||
.source => .unknown,
|
||||
.source => .source,
|
||||
.span => .span,
|
||||
.option => .option,
|
||||
.table => .table,
|
||||
@@ -1343,7 +1272,7 @@ pub fn getTag(self: *const Element) Tag {
|
||||
.template => .template,
|
||||
.textarea => .textarea,
|
||||
.time => .time,
|
||||
.track => .unknown,
|
||||
.track => .track,
|
||||
.input => .input,
|
||||
.link => .link,
|
||||
.meta => .meta,
|
||||
@@ -1390,6 +1319,7 @@ pub const Tag = enum {
|
||||
dfn,
|
||||
dialog,
|
||||
div,
|
||||
directory,
|
||||
dl,
|
||||
dt,
|
||||
embed,
|
||||
@@ -1398,6 +1328,7 @@ pub const Tag = enum {
|
||||
fieldset,
|
||||
figure,
|
||||
form,
|
||||
font,
|
||||
footer,
|
||||
g,
|
||||
h1,
|
||||
@@ -1417,10 +1348,13 @@ pub const Tag = enum {
|
||||
img,
|
||||
input,
|
||||
ins,
|
||||
label,
|
||||
legend,
|
||||
li,
|
||||
line,
|
||||
link,
|
||||
main,
|
||||
map,
|
||||
marquee,
|
||||
media,
|
||||
menu,
|
||||
@@ -1436,8 +1370,10 @@ pub const Tag = enum {
|
||||
p,
|
||||
path,
|
||||
param,
|
||||
picture,
|
||||
polygon,
|
||||
polyline,
|
||||
pre,
|
||||
progress,
|
||||
quote,
|
||||
rect,
|
||||
@@ -1528,6 +1464,16 @@ pub const JsApi = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub const setAttribute = bridge.function(_setAttribute, .{ .dom_exception = true });
|
||||
fn _setAttribute(self: *Element, name: String, value: js.Value, page: *Page) !void {
|
||||
return self.setAttribute(name, .wrap(try value.toStringSlice()), page);
|
||||
}
|
||||
|
||||
pub const setAttributeNS = bridge.function(_setAttributeNS, .{ .dom_exception = true });
|
||||
fn _setAttributeNS(self: *Element, maybe_ns: ?[]const u8, qn: []const u8, value: js.Value, page: *Page) !void {
|
||||
return self.setAttributeNS(maybe_ns, qn, .wrap(try value.toStringSlice()), page);
|
||||
}
|
||||
|
||||
pub const localName = bridge.accessor(Element.getLocalName, null, .{});
|
||||
pub const id = bridge.accessor(Element.getId, Element.setId, .{});
|
||||
pub const slot = bridge.accessor(Element.getSlot, Element.setSlot, .{});
|
||||
@@ -1542,8 +1488,6 @@ pub const JsApi = struct {
|
||||
pub const getAttribute = bridge.function(Element.getAttribute, .{});
|
||||
pub const getAttributeNS = bridge.function(Element.getAttributeNS, .{});
|
||||
pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{});
|
||||
pub const setAttribute = bridge.function(Element.setAttribute, .{ .dom_exception = true });
|
||||
pub const setAttributeNS = bridge.function(Element.setAttributeNS, .{ .dom_exception = true });
|
||||
pub const setAttributeNode = bridge.function(Element.setAttributeNode, .{});
|
||||
pub const removeAttribute = bridge.function(Element.removeAttribute, .{});
|
||||
pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true });
|
||||
@@ -1563,6 +1507,7 @@ pub const JsApi = struct {
|
||||
return self.attachShadow(init.mode, page);
|
||||
}
|
||||
pub const replaceChildren = bridge.function(Element.replaceChildren, .{});
|
||||
pub const replaceWith = bridge.function(Element.replaceWith, .{});
|
||||
pub const remove = bridge.function(Element.remove, .{});
|
||||
pub const append = bridge.function(Element.append, .{});
|
||||
pub const prepend = bridge.function(Element.prepend, .{});
|
||||
@@ -1589,6 +1534,7 @@ pub const JsApi = struct {
|
||||
pub const children = bridge.accessor(Element.getChildren, null, .{});
|
||||
pub const focus = bridge.function(Element.focus, .{});
|
||||
pub const blur = bridge.function(Element.blur, .{});
|
||||
pub const scrollIntoView = bridge.function(Element.scrollIntoView, .{});
|
||||
pub const scrollIntoViewIfNeeded = bridge.function(Element.scrollIntoViewIfNeeded, .{});
|
||||
};
|
||||
|
||||
|
||||
@@ -24,11 +24,14 @@ const EventTarget = @import("EventTarget.zig");
|
||||
const Node = @import("Node.zig");
|
||||
const String = @import("../../string.zig").String;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const Event = @This();
|
||||
|
||||
pub const _prototype_root = true;
|
||||
_type: Type,
|
||||
|
||||
_page: *Page,
|
||||
_arena: Allocator,
|
||||
_bubbles: bool = false,
|
||||
_cancelable: bool = false,
|
||||
_composed: bool = false,
|
||||
@@ -44,6 +47,12 @@ _time_stamp: u64,
|
||||
_needs_retargeting: bool = false,
|
||||
_isTrusted: bool = false,
|
||||
|
||||
// There's a period of time between creating an event and handing it off to v8
|
||||
// where things can fail. If it does fail, we need to deinit the event. This flag
|
||||
// when true, tells us the event is registered in the js.Contxt and thus, at
|
||||
// the very least, will be finalized on context shutdown.
|
||||
_v8_handoff: bool = false,
|
||||
|
||||
pub const EventPhase = enum(u8) {
|
||||
none = 0,
|
||||
capturing_phase = 1,
|
||||
@@ -62,6 +71,7 @@ pub const Type = union(enum) {
|
||||
page_transition_event: *@import("event/PageTransitionEvent.zig"),
|
||||
pop_state_event: *@import("event/PopStateEvent.zig"),
|
||||
ui_event: *@import("event/UIEvent.zig"),
|
||||
promise_rejection_event: *@import("event/PromiseRejectionEvent.zig"),
|
||||
};
|
||||
|
||||
pub const Options = struct {
|
||||
@@ -70,31 +80,38 @@ pub const Options = struct {
|
||||
composed: bool = false,
|
||||
};
|
||||
|
||||
pub fn initTrusted(typ: []const u8, opts_: ?Options, page: *Page) !*Event {
|
||||
return initWithTrusted(typ, opts_, true, page);
|
||||
}
|
||||
|
||||
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event {
|
||||
return initWithTrusted(typ, opts_, false, page);
|
||||
const arena = try page.getArena(.{ .debug = "Event" });
|
||||
errdefer page.releaseArena(arena);
|
||||
const str = try String.init(arena, typ, .{});
|
||||
return initWithTrusted(arena, str, opts_, false, page);
|
||||
}
|
||||
|
||||
fn initWithTrusted(typ: []const u8, opts_: ?Options, trusted: bool, page: *Page) !*Event {
|
||||
pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*Event {
|
||||
const arena = try page.getArena(.{ .debug = "Event.trusted" });
|
||||
errdefer page.releaseArena(arena);
|
||||
return initWithTrusted(arena, typ, opts_, true, page);
|
||||
}
|
||||
|
||||
fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, page: *Page) !*Event {
|
||||
const opts = opts_ orelse Options{};
|
||||
|
||||
// Round to 2ms for privacy (browsers do this)
|
||||
const raw_timestamp = @import("../../datetime.zig").milliTimestamp(.monotonic);
|
||||
const time_stamp = (raw_timestamp / 2) * 2;
|
||||
|
||||
const event = try page._factory.create(Event{
|
||||
const event = try arena.create(Event);
|
||||
event.* = .{
|
||||
._page = page,
|
||||
._arena = arena,
|
||||
._type = .generic,
|
||||
._bubbles = opts.bubbles,
|
||||
._time_stamp = time_stamp,
|
||||
._cancelable = opts.cancelable,
|
||||
._composed = opts.composed,
|
||||
._type_string = try String.init(page.arena, typ, .{}),
|
||||
});
|
||||
|
||||
event._isTrusted = trusted;
|
||||
._type_string = typ,
|
||||
._isTrusted = trusted,
|
||||
};
|
||||
return event;
|
||||
}
|
||||
|
||||
@@ -103,18 +120,22 @@ pub fn initEvent(
|
||||
event_string: []const u8,
|
||||
bubbles: ?bool,
|
||||
cancelable: ?bool,
|
||||
page: *Page,
|
||||
) !void {
|
||||
if (self._event_phase != .none) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._type_string = try String.init(page.arena, event_string, .{});
|
||||
self._type_string = try String.init(self._arena, event_string, .{});
|
||||
self._bubbles = bubbles orelse false;
|
||||
self._cancelable = cancelable orelse false;
|
||||
self._stop_propagation = false;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Event, shutdown: bool) void {
|
||||
_ = shutdown;
|
||||
self._page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn as(self: *Event, comptime T: type) *T {
|
||||
return self.is(T).?;
|
||||
}
|
||||
@@ -130,6 +151,7 @@ pub fn is(self: *Event, comptime T: type) ?*T {
|
||||
.navigation_current_entry_change_event => |e| return if (T == @import("event/NavigationCurrentEntryChangeEvent.zig")) e else null,
|
||||
.page_transition_event => |e| return if (T == @import("event/PageTransitionEvent.zig")) e else null,
|
||||
.pop_state_event => |e| return if (T == @import("event/PopStateEvent.zig")) e else null,
|
||||
.promise_rejection_event => |e| return if (T == @import("event/PromiseRejectionEvent.zig")) e else null,
|
||||
.ui_event => |e| {
|
||||
if (T == @import("event/UIEvent.zig")) {
|
||||
return e;
|
||||
@@ -385,6 +407,8 @@ pub const JsApi = struct {
|
||||
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const weak = true;
|
||||
pub const finalizer = bridge.finalizer(Event.deinit);
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(Event.init, .{});
|
||||
@@ -410,10 +434,10 @@ pub const JsApi = struct {
|
||||
pub const cancelBubble = bridge.accessor(Event.getCancelBubble, Event.setCancelBubble, .{});
|
||||
|
||||
// Event phase constants
|
||||
pub const NONE = bridge.property(@intFromEnum(EventPhase.none));
|
||||
pub const CAPTURING_PHASE = bridge.property(@intFromEnum(EventPhase.capturing_phase));
|
||||
pub const AT_TARGET = bridge.property(@intFromEnum(EventPhase.at_target));
|
||||
pub const BUBBLING_PHASE = bridge.property(@intFromEnum(EventPhase.bubbling_phase));
|
||||
pub const NONE = bridge.property(@intFromEnum(EventPhase.none), .{ .template = true });
|
||||
pub const CAPTURING_PHASE = bridge.property(@intFromEnum(EventPhase.capturing_phase), .{ .template = true });
|
||||
pub const AT_TARGET = bridge.property(@intFromEnum(EventPhase.at_target), .{ .template = true });
|
||||
pub const BUBBLING_PHASE = bridge.property(@intFromEnum(EventPhase.bubbling_phase), .{ .template = true });
|
||||
};
|
||||
|
||||
// tested in event_target
|
||||
|
||||
@@ -42,6 +42,7 @@ pub const Type = union(enum) {
|
||||
navigation: *@import("navigation/NavigationEventTarget.zig"),
|
||||
screen: *@import("Screen.zig"),
|
||||
screen_orientation: *@import("Screen.zig").Orientation,
|
||||
visual_viewport: *@import("VisualViewport.zig"),
|
||||
};
|
||||
|
||||
pub fn init(page: *Page) !*EventTarget {
|
||||
@@ -132,12 +133,13 @@ pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void {
|
||||
.navigation => writer.writeAll("<Navigation>"),
|
||||
.screen => writer.writeAll("<Screen>"),
|
||||
.screen_orientation => writer.writeAll("<ScreenOrientation>"),
|
||||
.visual_viewport => writer.writeAll("<VisualViewport>"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toString(self: *EventTarget) []const u8 {
|
||||
return switch (self._type) {
|
||||
.node => |n| return n.className(),
|
||||
.node => return "[object Node]",
|
||||
.generic => return "[object EventTarget]",
|
||||
.window => return "[object Window]",
|
||||
.xhr => return "[object XMLHttpRequestEventTarget]",
|
||||
@@ -148,6 +150,7 @@ pub fn toString(self: *EventTarget) []const u8 {
|
||||
.navigation => return "[object Navigation]",
|
||||
.screen => return "[object Screen]",
|
||||
.screen_orientation => return "[object ScreenOrientation]",
|
||||
.visual_viewport => return "[object VisualViewport]",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -79,11 +79,12 @@ fn goInner(delta: i32, page: *Page) !void {
|
||||
|
||||
if (entry._url) |url| {
|
||||
if (try page.isSameOrigin(url)) {
|
||||
const event = try PopStateEvent.initTrusted("popstate", .{ .state = entry._state.value }, page);
|
||||
const event = (try PopStateEvent.initTrusted(comptime .wrap("popstate"), .{ .state = entry._state.value }, page)).asEvent();
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
|
||||
try page._event_manager.dispatchWithFunction(
|
||||
page.window.asEventTarget(),
|
||||
event.asEvent(),
|
||||
event,
|
||||
page.js.toLocal(page.window._on_popstate),
|
||||
.{ .context = "Pop State" },
|
||||
);
|
||||
|
||||
@@ -24,10 +24,6 @@ pub fn init() IdleDeadline {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn getDidTimeout(_: *const IdleDeadline) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn timeRemaining(_: *const IdleDeadline) f64 {
|
||||
// Return a fixed 50ms.
|
||||
// This allows idle callbacks to perform work without complex
|
||||
@@ -47,5 +43,5 @@ pub const JsApi = struct {
|
||||
};
|
||||
|
||||
pub const timeRemaining = bridge.function(IdleDeadline.timeRemaining, .{});
|
||||
pub const didTimeout = bridge.accessor(IdleDeadline.getDidTimeout, null, .{});
|
||||
pub const didTimeout = bridge.property(false, .{ .template = false });
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -19,6 +19,10 @@ const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
const Element = @import("Element.zig");
|
||||
const DOMRect = @import("DOMRect.zig");
|
||||
@@ -32,7 +36,9 @@ pub fn registerTypes() []const type {
|
||||
|
||||
const IntersectionObserver = @This();
|
||||
|
||||
_callback: js.Function.Global,
|
||||
_page: *Page,
|
||||
_arena: Allocator,
|
||||
_callback: js.Function.Temp,
|
||||
_observing: std.ArrayList(*Element) = .{},
|
||||
_root: ?*Element = null,
|
||||
_root_margin: []const u8 = "0px",
|
||||
@@ -59,25 +65,42 @@ pub const ObserverInit = struct {
|
||||
};
|
||||
};
|
||||
|
||||
pub fn init(callback: js.Function.Global, options: ?ObserverInit, page: *Page) !*IntersectionObserver {
|
||||
pub fn init(callback: js.Function.Temp, options: ?ObserverInit, page: *Page) !*IntersectionObserver {
|
||||
const arena = try page.getArena(.{ .debug = "IntersectionObserver" });
|
||||
errdefer page.releaseArena(arena);
|
||||
|
||||
const opts = options orelse ObserverInit{};
|
||||
const root_margin = if (opts.rootMargin) |rm| try page.arena.dupe(u8, rm) else "0px";
|
||||
const root_margin = if (opts.rootMargin) |rm| try arena.dupe(u8, rm) else "0px";
|
||||
|
||||
const threshold = switch (opts.threshold) {
|
||||
.scalar => |s| blk: {
|
||||
const arr = try page.arena.alloc(f64, 1);
|
||||
const arr = try arena.alloc(f64, 1);
|
||||
arr[0] = s;
|
||||
break :blk arr;
|
||||
},
|
||||
.array => |arr| try page.arena.dupe(f64, arr),
|
||||
.array => |arr| try arena.dupe(f64, arr),
|
||||
};
|
||||
|
||||
return page._factory.create(IntersectionObserver{
|
||||
const self = try arena.create(IntersectionObserver);
|
||||
self.* = .{
|
||||
._page = page,
|
||||
._arena = arena,
|
||||
._callback = callback,
|
||||
._root = opts.root,
|
||||
._root_margin = root_margin,
|
||||
._threshold = threshold,
|
||||
});
|
||||
};
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *IntersectionObserver, shutdown: bool) void {
|
||||
const page = self._page;
|
||||
page.js.release(self._callback);
|
||||
if ((comptime IS_DEBUG) and !shutdown) {
|
||||
std.debug.assert(self._observing.items.len == 0);
|
||||
}
|
||||
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void {
|
||||
@@ -90,10 +113,11 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void
|
||||
|
||||
// Register with page if this is our first observation
|
||||
if (self._observing.items.len == 0) {
|
||||
page.js.strongRef(self);
|
||||
try page.registerIntersectionObserver(self);
|
||||
}
|
||||
|
||||
try self._observing.append(page.arena, target);
|
||||
try self._observing.append(self._arena, target);
|
||||
|
||||
// Don't initialize previous state yet - let checkIntersection do it
|
||||
// This ensures we get an entry on first observation
|
||||
@@ -105,7 +129,7 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unobserve(self: *IntersectionObserver, target: *Element) void {
|
||||
pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) void {
|
||||
for (self._observing.items, 0..) |elem, i| {
|
||||
if (elem == target) {
|
||||
_ = self._observing.swapRemove(i);
|
||||
@@ -115,21 +139,31 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element) void {
|
||||
var j: usize = 0;
|
||||
while (j < self._pending_entries.items.len) {
|
||||
if (self._pending_entries.items[j]._target == target) {
|
||||
_ = self._pending_entries.swapRemove(j);
|
||||
const entry = self._pending_entries.swapRemove(j);
|
||||
entry.deinit(false);
|
||||
} else {
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
return;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (self._observing.items.len == 0) {
|
||||
page.js.safeWeakRef(self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
|
||||
page.unregisterIntersectionObserver(self);
|
||||
self._observing.clearRetainingCapacity();
|
||||
self._previous_states.clearRetainingCapacity();
|
||||
|
||||
for (self._pending_entries.items) |entry| {
|
||||
entry.deinit(false);
|
||||
}
|
||||
self._pending_entries.clearRetainingCapacity();
|
||||
page.js.safeWeakRef(self);
|
||||
}
|
||||
|
||||
pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {
|
||||
@@ -206,8 +240,11 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page)
|
||||
(was_intersecting_opt != null and was_intersecting_opt.? != is_now_intersecting);
|
||||
|
||||
if (should_report) {
|
||||
const entry = try page.arena.create(IntersectionObserverEntry);
|
||||
const arena = try page.getArena(.{ .debug = "IntersectionObserverEntry" });
|
||||
const entry = try arena.create(IntersectionObserverEntry);
|
||||
entry.* = .{
|
||||
._page = page,
|
||||
._arena = arena,
|
||||
._target = target,
|
||||
._time = 0.0, // TODO: Get actual timestamp
|
||||
._bounding_client_rect = data.bounding_client_rect,
|
||||
@@ -217,12 +254,12 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page)
|
||||
._is_intersecting = is_now_intersecting,
|
||||
};
|
||||
|
||||
try self._pending_entries.append(page.arena, entry);
|
||||
try self._pending_entries.append(self._arena, entry);
|
||||
}
|
||||
|
||||
// Always update the previous state, even if we didn't report
|
||||
// This ensures we can detect state changes on subsequent checks
|
||||
try self._previous_states.put(page.arena, target, is_now_intersecting);
|
||||
try self._previous_states.put(self._arena, target, is_now_intersecting);
|
||||
}
|
||||
|
||||
pub fn checkIntersections(self: *IntersectionObserver, page: *Page) !void {
|
||||
@@ -258,14 +295,20 @@ pub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void {
|
||||
}
|
||||
|
||||
pub const IntersectionObserverEntry = struct {
|
||||
_target: *Element,
|
||||
_page: *Page,
|
||||
_arena: Allocator,
|
||||
_time: f64,
|
||||
_target: *Element,
|
||||
_bounding_client_rect: *DOMRect,
|
||||
_intersection_rect: *DOMRect,
|
||||
_root_bounds: *DOMRect,
|
||||
_intersection_ratio: f64,
|
||||
_is_intersecting: bool,
|
||||
|
||||
pub fn deinit(self: *const IntersectionObserverEntry, _: bool) void {
|
||||
self._page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn getTarget(self: *const IntersectionObserverEntry) *Element {
|
||||
return self._target;
|
||||
}
|
||||
@@ -301,6 +344,8 @@ pub const IntersectionObserverEntry = struct {
|
||||
pub const name = "IntersectionObserverEntry";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const weak = true;
|
||||
pub const finalizer = bridge.finalizer(IntersectionObserverEntry.deinit);
|
||||
};
|
||||
|
||||
pub const target = bridge.accessor(IntersectionObserverEntry.getTarget, null, .{});
|
||||
@@ -320,6 +365,8 @@ pub const JsApi = struct {
|
||||
pub const name = "IntersectionObserver";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const weak = true;
|
||||
pub const finalizer = bridge.finalizer(IntersectionObserver.deinit);
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(init, .{});
|
||||
|
||||
@@ -46,7 +46,7 @@ pub const Entry = struct {
|
||||
|
||||
pub const KeyValueList = @This();
|
||||
|
||||
_entries: std.ArrayListUnmanaged(Entry) = .empty,
|
||||
_entries: std.ArrayList(Entry) = .empty,
|
||||
|
||||
pub const empty: KeyValueList = .{
|
||||
._entries = .empty,
|
||||
@@ -68,12 +68,11 @@ pub fn fromJsObject(arena: Allocator, js_obj: js.Object, comptime normalizer: ?N
|
||||
|
||||
while (try it.next()) |name| {
|
||||
const js_value = try js_obj.get(name);
|
||||
const value = try js_value.toString(.{});
|
||||
const normalized = if (comptime normalizer) |n| n(name, page) else name;
|
||||
|
||||
list._entries.appendAssumeCapacity(.{
|
||||
.name = try String.init(arena, normalized, .{}),
|
||||
.value = try String.init(arena, value, .{}),
|
||||
.value = try js_value.toSSOWithAlloc(arena),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ pub fn entangle(port1: *MessagePort, port2: *MessagePort) void {
|
||||
port2._entangled_port = port1;
|
||||
}
|
||||
|
||||
pub fn postMessage(self: *MessagePort, message: js.Value.Global, page: *Page) !void {
|
||||
pub fn postMessage(self: *MessagePort, message: js.Value.Temp, page: *Page) !void {
|
||||
if (self._closed) {
|
||||
return;
|
||||
}
|
||||
@@ -65,7 +65,7 @@ pub fn postMessage(self: *MessagePort, message: js.Value.Global, page: *Page) !v
|
||||
.message = message,
|
||||
});
|
||||
|
||||
try page.scheduler.add(callback, PostMessageCallback.run, 0, .{
|
||||
try page.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
|
||||
.name = "MessagePort.postMessage",
|
||||
.low_priority = false,
|
||||
});
|
||||
@@ -106,7 +106,7 @@ pub fn setOnMessageError(self: *MessagePort, cb: ?js.Function.Global) !void {
|
||||
|
||||
const PostMessageCallback = struct {
|
||||
port: *MessagePort,
|
||||
message: js.Value.Global,
|
||||
message: js.Value.Temp,
|
||||
page: *Page,
|
||||
|
||||
fn deinit(self: *PostMessageCallback) void {
|
||||
@@ -122,14 +122,15 @@ const PostMessageCallback = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
const event = MessageEvent.initTrusted("message", .{
|
||||
const event = (MessageEvent.initTrusted(comptime .wrap("message"), .{
|
||||
.data = self.message,
|
||||
.origin = "",
|
||||
.source = null,
|
||||
}, page) catch |err| {
|
||||
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
|
||||
return null;
|
||||
};
|
||||
}).asEvent();
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
page.js.localScope(&ls);
|
||||
@@ -137,7 +138,7 @@ const PostMessageCallback = struct {
|
||||
|
||||
page._event_manager.dispatchWithFunction(
|
||||
self.port.asEventTarget(),
|
||||
event.asEvent(),
|
||||
event,
|
||||
ls.toLocal(self.port._on_message),
|
||||
.{ .context = "MessagePort message" },
|
||||
) catch |err| {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -25,6 +25,10 @@ const Node = @import("Node.zig");
|
||||
const Element = @import("Element.zig");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub fn registerTypes() []const type {
|
||||
return &.{
|
||||
MutationObserver,
|
||||
@@ -34,9 +38,12 @@ pub fn registerTypes() []const type {
|
||||
|
||||
const MutationObserver = @This();
|
||||
|
||||
_callback: js.Function.Global,
|
||||
_page: *Page,
|
||||
_arena: Allocator,
|
||||
_callback: js.Function.Temp,
|
||||
_observing: std.ArrayList(Observing) = .{},
|
||||
_pending_records: std.ArrayList(*MutationRecord) = .{},
|
||||
|
||||
/// Intrusively linked to next element (see Page.zig).
|
||||
node: std.DoublyLinkedList.Node = .{},
|
||||
|
||||
@@ -55,19 +62,38 @@ pub const ObserveOptions = struct {
|
||||
attributeFilter: ?[]const []const u8 = null,
|
||||
};
|
||||
|
||||
pub fn init(callback: js.Function.Global, page: *Page) !*MutationObserver {
|
||||
return page._factory.create(MutationObserver{
|
||||
pub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver {
|
||||
const arena = try page.getArena(.{ .debug = "MutationObserver" });
|
||||
errdefer page.releaseArena(arena);
|
||||
|
||||
const self = try arena.create(MutationObserver);
|
||||
self.* = .{
|
||||
._page = page,
|
||||
._arena = arena,
|
||||
._callback = callback,
|
||||
});
|
||||
};
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *MutationObserver, shutdown: bool) void {
|
||||
const page = self._page;
|
||||
page.js.release(self._callback);
|
||||
if ((comptime IS_DEBUG) and !shutdown) {
|
||||
std.debug.assert(self._observing.items.len == 0);
|
||||
}
|
||||
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {
|
||||
const arena = self._arena;
|
||||
|
||||
// Deep copy attributeFilter if present
|
||||
var copied_options = options;
|
||||
if (options.attributeFilter) |filter| {
|
||||
const filter_copy = try page.arena.alloc([]const u8, filter.len);
|
||||
const filter_copy = try arena.alloc([]const u8, filter.len);
|
||||
for (filter, 0..) |name, i| {
|
||||
filter_copy[i] = try page.arena.dupe(u8, name);
|
||||
filter_copy[i] = try arena.dupe(u8, name);
|
||||
}
|
||||
copied_options.attributeFilter = filter_copy;
|
||||
}
|
||||
@@ -86,10 +112,11 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
|
||||
|
||||
// Register with page if this is our first observation
|
||||
if (self._observing.items.len == 0) {
|
||||
page.js.strongRef(self);
|
||||
try page.registerMutationObserver(self);
|
||||
}
|
||||
|
||||
try self._observing.append(page.arena, .{
|
||||
try self._observing.append(arena, .{
|
||||
.target = target,
|
||||
.options = copied_options,
|
||||
});
|
||||
@@ -98,7 +125,11 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
|
||||
pub fn disconnect(self: *MutationObserver, page: *Page) void {
|
||||
page.unregisterMutationObserver(self);
|
||||
self._observing.clearRetainingCapacity();
|
||||
for (self._pending_records.items) |record| {
|
||||
record.deinit(false);
|
||||
}
|
||||
self._pending_records.clearRetainingCapacity();
|
||||
page.js.safeWeakRef(self);
|
||||
}
|
||||
|
||||
pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord {
|
||||
@@ -139,21 +170,25 @@ pub fn notifyAttributeChange(
|
||||
}
|
||||
}
|
||||
|
||||
const record = try page._factory.create(MutationRecord{
|
||||
const arena = try self._page.getArena(.{ .debug = "MutationRecord" });
|
||||
const record = try arena.create(MutationRecord);
|
||||
record.* = .{
|
||||
._page = page,
|
||||
._arena = arena,
|
||||
._type = .attributes,
|
||||
._target = target_node,
|
||||
._attribute_name = try page.arena.dupe(u8, attribute_name.str()),
|
||||
._attribute_name = try arena.dupe(u8, attribute_name.str()),
|
||||
._old_value = if (obs.options.attributeOldValue and old_value != null)
|
||||
try page.arena.dupe(u8, old_value.?.str())
|
||||
try arena.dupe(u8, old_value.?.str())
|
||||
else
|
||||
null,
|
||||
._added_nodes = &.{},
|
||||
._removed_nodes = &.{},
|
||||
._previous_sibling = null,
|
||||
._next_sibling = null,
|
||||
});
|
||||
};
|
||||
|
||||
try self._pending_records.append(page.arena, record);
|
||||
try self._pending_records.append(self._arena, record);
|
||||
|
||||
try page.scheduleMutationDelivery();
|
||||
break;
|
||||
@@ -180,21 +215,25 @@ pub fn notifyCharacterDataChange(
|
||||
continue;
|
||||
}
|
||||
|
||||
const record = try page._factory.create(MutationRecord{
|
||||
const arena = try self._page.getArena(.{ .debug = "MutationRecord" });
|
||||
const record = try arena.create(MutationRecord);
|
||||
record.* = .{
|
||||
._page = page,
|
||||
._arena = arena,
|
||||
._type = .characterData,
|
||||
._target = target,
|
||||
._attribute_name = null,
|
||||
._old_value = if (obs.options.characterDataOldValue and old_value != null)
|
||||
try page.arena.dupe(u8, old_value.?)
|
||||
try arena.dupe(u8, old_value.?)
|
||||
else
|
||||
null,
|
||||
._added_nodes = &.{},
|
||||
._removed_nodes = &.{},
|
||||
._previous_sibling = null,
|
||||
._next_sibling = null,
|
||||
});
|
||||
};
|
||||
|
||||
try self._pending_records.append(page.arena, record);
|
||||
try self._pending_records.append(self._arena, record);
|
||||
|
||||
try page.scheduleMutationDelivery();
|
||||
break;
|
||||
@@ -224,18 +263,22 @@ pub fn notifyChildListChange(
|
||||
continue;
|
||||
}
|
||||
|
||||
const record = try page._factory.create(MutationRecord{
|
||||
const arena = try self._page.getArena(.{ .debug = "MutationRecord" });
|
||||
const record = try arena.create(MutationRecord);
|
||||
record.* = .{
|
||||
._page = page,
|
||||
._arena = arena,
|
||||
._type = .childList,
|
||||
._target = target,
|
||||
._attribute_name = null,
|
||||
._old_value = null,
|
||||
._added_nodes = try page.arena.dupe(*Node, added_nodes),
|
||||
._removed_nodes = try page.arena.dupe(*Node, removed_nodes),
|
||||
._added_nodes = try arena.dupe(*Node, added_nodes),
|
||||
._removed_nodes = try arena.dupe(*Node, removed_nodes),
|
||||
._previous_sibling = previous_sibling,
|
||||
._next_sibling = next_sibling,
|
||||
});
|
||||
};
|
||||
|
||||
try self._pending_records.append(page.arena, record);
|
||||
try self._pending_records.append(self._arena, record);
|
||||
|
||||
try page.scheduleMutationDelivery();
|
||||
break;
|
||||
@@ -263,7 +306,9 @@ pub fn deliverRecords(self: *MutationObserver, page: *Page) !void {
|
||||
|
||||
pub const MutationRecord = struct {
|
||||
_type: Type,
|
||||
_page: *Page,
|
||||
_target: *Node,
|
||||
_arena: Allocator,
|
||||
_attribute_name: ?[]const u8,
|
||||
_old_value: ?[]const u8,
|
||||
_added_nodes: []const *Node,
|
||||
@@ -277,6 +322,10 @@ pub const MutationRecord = struct {
|
||||
characterData,
|
||||
};
|
||||
|
||||
pub fn deinit(self: *const MutationRecord, _: bool) void {
|
||||
self._page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn getType(self: *const MutationRecord) []const u8 {
|
||||
return switch (self._type) {
|
||||
.attributes => "attributes",
|
||||
@@ -327,6 +376,8 @@ pub const MutationRecord = struct {
|
||||
pub const name = "MutationRecord";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const weak = true;
|
||||
pub const finalizer = bridge.finalizer(MutationRecord.deinit);
|
||||
};
|
||||
|
||||
pub const @"type" = bridge.accessor(MutationRecord.getType, null, .{});
|
||||
@@ -348,6 +399,8 @@ pub const JsApi = struct {
|
||||
pub const name = "MutationObserver";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const weak = true;
|
||||
pub const finalizer = bridge.finalizer(MutationObserver.deinit);
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(MutationObserver.init, .{});
|
||||
|
||||
@@ -20,26 +20,20 @@ const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const PluginArray = @import("PluginArray.zig");
|
||||
|
||||
const Navigator = @This();
|
||||
_pad: bool = false,
|
||||
_plugins: PluginArray = .{},
|
||||
|
||||
pub const init: Navigator = .{};
|
||||
|
||||
pub fn getUserAgent(_: *const Navigator, page: *Page) []const u8 {
|
||||
return page._session.browser.app.config.user_agent;
|
||||
return page._session.browser.app.config.http_headers.user_agent;
|
||||
}
|
||||
|
||||
pub fn getAppName(_: *const Navigator) []const u8 {
|
||||
return "Netscape";
|
||||
}
|
||||
|
||||
pub fn getAppCodeName(_: *const Navigator) []const u8 {
|
||||
return "Netscape";
|
||||
}
|
||||
|
||||
pub fn getAppVersion(_: *const Navigator) []const u8 {
|
||||
return "1.0";
|
||||
pub fn getLanguages(_: *const Navigator) [1][]const u8 {
|
||||
return .{"en-US"};
|
||||
}
|
||||
|
||||
pub fn getPlatform(_: *const Navigator) []const u8 {
|
||||
@@ -52,48 +46,13 @@ pub fn getPlatform(_: *const Navigator) []const u8 {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getLanguage(_: *const Navigator) []const u8 {
|
||||
return "en-US";
|
||||
}
|
||||
|
||||
pub fn getLanguages(_: *const Navigator) [1][]const u8 {
|
||||
return .{"en-US"};
|
||||
}
|
||||
|
||||
pub fn getOnLine(_: *const Navigator) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn getCookieEnabled(_: *const Navigator) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn getHardwareConcurrency(_: *const Navigator) u32 {
|
||||
return 4;
|
||||
}
|
||||
|
||||
pub fn getMaxTouchPoints(_: *const Navigator) u32 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Returns the vendor name
|
||||
pub fn getVendor(_: *const Navigator) []const u8 {
|
||||
return "";
|
||||
}
|
||||
|
||||
/// Returns the product name (typically "Gecko" for compatibility)
|
||||
pub fn getProduct(_: *const Navigator) []const u8 {
|
||||
return "Gecko";
|
||||
}
|
||||
|
||||
/// Returns whether Java is enabled (always false)
|
||||
pub fn javaEnabled(_: *const Navigator) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns whether the browser is controlled by automation (always false)
|
||||
pub fn getWebdriver(_: *const Navigator) bool {
|
||||
return false;
|
||||
pub fn getPlugins(self: *Navigator) *PluginArray {
|
||||
return &self._plugins;
|
||||
}
|
||||
|
||||
pub fn registerProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void {
|
||||
@@ -176,19 +135,22 @@ pub const JsApi = struct {
|
||||
|
||||
// Read-only properties
|
||||
pub const userAgent = bridge.accessor(Navigator.getUserAgent, null, .{});
|
||||
pub const appName = bridge.accessor(Navigator.getAppName, null, .{});
|
||||
pub const appCodeName = bridge.accessor(Navigator.getAppCodeName, null, .{});
|
||||
pub const appVersion = bridge.accessor(Navigator.getAppVersion, null, .{});
|
||||
pub const appName = bridge.property("Netscape", .{ .template = false });
|
||||
pub const appCodeName = bridge.property("Netscape", .{ .template = false });
|
||||
pub const appVersion = bridge.property("1.0", .{ .template = false });
|
||||
pub const platform = bridge.accessor(Navigator.getPlatform, null, .{});
|
||||
pub const language = bridge.accessor(Navigator.getLanguage, null, .{});
|
||||
pub const language = bridge.property("en-US", .{ .template = false });
|
||||
pub const languages = bridge.accessor(Navigator.getLanguages, null, .{});
|
||||
pub const onLine = bridge.accessor(Navigator.getOnLine, null, .{});
|
||||
pub const cookieEnabled = bridge.accessor(Navigator.getCookieEnabled, null, .{});
|
||||
pub const hardwareConcurrency = bridge.accessor(Navigator.getHardwareConcurrency, null, .{});
|
||||
pub const maxTouchPoints = bridge.accessor(Navigator.getMaxTouchPoints, null, .{});
|
||||
pub const vendor = bridge.accessor(Navigator.getVendor, null, .{});
|
||||
pub const product = bridge.accessor(Navigator.getProduct, null, .{});
|
||||
pub const webdriver = bridge.accessor(Navigator.getWebdriver, null, .{});
|
||||
pub const onLine = bridge.property(true, .{ .template = false });
|
||||
pub const cookieEnabled = bridge.property(true, .{ .template = false });
|
||||
pub const hardwareConcurrency = bridge.property(4, .{ .template = false });
|
||||
pub const maxTouchPoints = bridge.property(0, .{ .template = false });
|
||||
pub const vendor = bridge.property("", .{ .template = false });
|
||||
pub const product = bridge.property("Gecko", .{ .template = false });
|
||||
pub const webdriver = bridge.property(false, .{ .template = false });
|
||||
pub const plugins = bridge.accessor(Navigator.getPlugins, null, .{});
|
||||
pub const doNotTrack = bridge.property(null, .{ .template = false });
|
||||
pub const globalPrivacyControl = bridge.property(true, .{ .template = false });
|
||||
pub const registerProtocolHandler = bridge.function(Navigator.registerProtocolHandler, .{ .dom_exception = true });
|
||||
pub const unregisterProtocolHandler = bridge.function(Navigator.unregisterProtocolHandler, .{ .dom_exception = true });
|
||||
|
||||
|
||||
@@ -457,6 +457,13 @@ pub fn ownerDocument(self: *const Node, page: *const Page) ?*Document {
|
||||
return page.document;
|
||||
}
|
||||
|
||||
pub fn isSameDocumentAs(self: *const Node, other: *const Node, page: *const Page) bool {
|
||||
// Get the root document for each node
|
||||
const self_doc = if (self._type == .document) self._type.document else self.ownerDocument(page);
|
||||
const other_doc = if (other._type == .document) other._type.document else other.ownerDocument(page);
|
||||
return self_doc == other_doc;
|
||||
}
|
||||
|
||||
pub fn hasChildNodes(self: *const Node) bool {
|
||||
return self.firstChild() != null;
|
||||
}
|
||||
@@ -667,7 +674,7 @@ pub fn setData(self: *Node, data: []const u8, page: *Page) !void {
|
||||
}
|
||||
|
||||
pub fn normalize(self: *Node, page: *Page) !void {
|
||||
var buffer: std.ArrayListUnmanaged(u8) = .empty;
|
||||
var buffer: std.ArrayList(u8) = .empty;
|
||||
return self._normalize(page.call_arena, &buffer, page);
|
||||
}
|
||||
|
||||
@@ -797,7 +804,7 @@ fn isNodeBefore(node1: *const Node, node2: *const Node) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
fn _normalize(self: *Node, allocator: Allocator, buffer: *std.ArrayListUnmanaged(u8), page: *Page) !void {
|
||||
fn _normalize(self: *Node, allocator: Allocator, buffer: *std.ArrayList(u8), page: *Page) !void {
|
||||
var it = self.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
try child._normalize(allocator, buffer, page);
|
||||
@@ -874,25 +881,25 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const ELEMENT_NODE = bridge.property(1);
|
||||
pub const ATTRIBUTE_NODE = bridge.property(2);
|
||||
pub const TEXT_NODE = bridge.property(3);
|
||||
pub const CDATA_SECTION_NODE = bridge.property(4);
|
||||
pub const ENTITY_REFERENCE_NODE = bridge.property(5);
|
||||
pub const ENTITY_NODE = bridge.property(6);
|
||||
pub const PROCESSING_INSTRUCTION_NODE = bridge.property(7);
|
||||
pub const COMMENT_NODE = bridge.property(8);
|
||||
pub const DOCUMENT_NODE = bridge.property(9);
|
||||
pub const DOCUMENT_TYPE_NODE = bridge.property(10);
|
||||
pub const DOCUMENT_FRAGMENT_NODE = bridge.property(11);
|
||||
pub const NOTATION_NODE = bridge.property(12);
|
||||
pub const ELEMENT_NODE = bridge.property(1, .{ .template = true });
|
||||
pub const ATTRIBUTE_NODE = bridge.property(2, .{ .template = true });
|
||||
pub const TEXT_NODE = bridge.property(3, .{ .template = true });
|
||||
pub const CDATA_SECTION_NODE = bridge.property(4, .{ .template = true });
|
||||
pub const ENTITY_REFERENCE_NODE = bridge.property(5, .{ .template = true });
|
||||
pub const ENTITY_NODE = bridge.property(6, .{ .template = true });
|
||||
pub const PROCESSING_INSTRUCTION_NODE = bridge.property(7, .{ .template = true });
|
||||
pub const COMMENT_NODE = bridge.property(8, .{ .template = true });
|
||||
pub const DOCUMENT_NODE = bridge.property(9, .{ .template = true });
|
||||
pub const DOCUMENT_TYPE_NODE = bridge.property(10, .{ .template = true });
|
||||
pub const DOCUMENT_FRAGMENT_NODE = bridge.property(11, .{ .template = true });
|
||||
pub const NOTATION_NODE = bridge.property(12, .{ .template = true });
|
||||
|
||||
pub const DOCUMENT_POSITION_DISCONNECTED = bridge.property(0x01);
|
||||
pub const DOCUMENT_POSITION_PRECEDING = bridge.property(0x02);
|
||||
pub const DOCUMENT_POSITION_FOLLOWING = bridge.property(0x04);
|
||||
pub const DOCUMENT_POSITION_CONTAINS = bridge.property(0x08);
|
||||
pub const DOCUMENT_POSITION_CONTAINED_BY = bridge.property(0x10);
|
||||
pub const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = bridge.property(0x20);
|
||||
pub const DOCUMENT_POSITION_DISCONNECTED = bridge.property(0x01, .{ .template = true });
|
||||
pub const DOCUMENT_POSITION_PRECEDING = bridge.property(0x02, .{ .template = true });
|
||||
pub const DOCUMENT_POSITION_FOLLOWING = bridge.property(0x04, .{ .template = true });
|
||||
pub const DOCUMENT_POSITION_CONTAINS = bridge.property(0x08, .{ .template = true });
|
||||
pub const DOCUMENT_POSITION_CONTAINED_BY = bridge.property(0x10, .{ .template = true });
|
||||
pub const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = bridge.property(0x20, .{ .template = true });
|
||||
|
||||
pub const nodeName = bridge.accessor(struct {
|
||||
fn wrap(self: *const Node, page: *Page) []const u8 {
|
||||
|
||||
@@ -67,7 +67,7 @@ pub const SHOW_NOTATION: u32 = 0x800;
|
||||
|
||||
pub fn acceptNode(self: *const NodeFilter, node: *Node, local: *const js.Local) !i32 {
|
||||
const func = self._func orelse return FILTER_ACCEPT;
|
||||
return local.toLocal(func).call(i32, .{node});
|
||||
return local.toLocal(func).callRethrow(i32, .{node});
|
||||
}
|
||||
|
||||
pub fn shouldShow(node: *const Node, what_to_show: u32) bool {
|
||||
@@ -90,21 +90,21 @@ pub const JsApi = struct {
|
||||
pub const empty_with_no_proto = true;
|
||||
};
|
||||
|
||||
pub const FILTER_ACCEPT = bridge.property(NodeFilter.FILTER_ACCEPT);
|
||||
pub const FILTER_REJECT = bridge.property(NodeFilter.FILTER_REJECT);
|
||||
pub const FILTER_SKIP = bridge.property(NodeFilter.FILTER_SKIP);
|
||||
pub const FILTER_ACCEPT = bridge.property(NodeFilter.FILTER_ACCEPT, .{ .template = true });
|
||||
pub const FILTER_REJECT = bridge.property(NodeFilter.FILTER_REJECT, .{ .template = true });
|
||||
pub const FILTER_SKIP = bridge.property(NodeFilter.FILTER_SKIP, .{ .template = true });
|
||||
|
||||
pub const SHOW_ALL = bridge.property(NodeFilter.SHOW_ALL);
|
||||
pub const SHOW_ELEMENT = bridge.property(NodeFilter.SHOW_ELEMENT);
|
||||
pub const SHOW_ATTRIBUTE = bridge.property(NodeFilter.SHOW_ATTRIBUTE);
|
||||
pub const SHOW_TEXT = bridge.property(NodeFilter.SHOW_TEXT);
|
||||
pub const SHOW_CDATA_SECTION = bridge.property(NodeFilter.SHOW_CDATA_SECTION);
|
||||
pub const SHOW_ENTITY_REFERENCE = bridge.property(NodeFilter.SHOW_ENTITY_REFERENCE);
|
||||
pub const SHOW_ENTITY = bridge.property(NodeFilter.SHOW_ENTITY);
|
||||
pub const SHOW_PROCESSING_INSTRUCTION = bridge.property(NodeFilter.SHOW_PROCESSING_INSTRUCTION);
|
||||
pub const SHOW_COMMENT = bridge.property(NodeFilter.SHOW_COMMENT);
|
||||
pub const SHOW_DOCUMENT = bridge.property(NodeFilter.SHOW_DOCUMENT);
|
||||
pub const SHOW_DOCUMENT_TYPE = bridge.property(NodeFilter.SHOW_DOCUMENT_TYPE);
|
||||
pub const SHOW_DOCUMENT_FRAGMENT = bridge.property(NodeFilter.SHOW_DOCUMENT_FRAGMENT);
|
||||
pub const SHOW_NOTATION = bridge.property(NodeFilter.SHOW_NOTATION);
|
||||
pub const SHOW_ALL = bridge.property(NodeFilter.SHOW_ALL, .{ .template = true });
|
||||
pub const SHOW_ELEMENT = bridge.property(NodeFilter.SHOW_ELEMENT, .{ .template = true });
|
||||
pub const SHOW_ATTRIBUTE = bridge.property(NodeFilter.SHOW_ATTRIBUTE, .{ .template = true });
|
||||
pub const SHOW_TEXT = bridge.property(NodeFilter.SHOW_TEXT, .{ .template = true });
|
||||
pub const SHOW_CDATA_SECTION = bridge.property(NodeFilter.SHOW_CDATA_SECTION, .{ .template = true });
|
||||
pub const SHOW_ENTITY_REFERENCE = bridge.property(NodeFilter.SHOW_ENTITY_REFERENCE, .{ .template = true });
|
||||
pub const SHOW_ENTITY = bridge.property(NodeFilter.SHOW_ENTITY, .{ .template = true });
|
||||
pub const SHOW_PROCESSING_INSTRUCTION = bridge.property(NodeFilter.SHOW_PROCESSING_INSTRUCTION, .{ .template = true });
|
||||
pub const SHOW_COMMENT = bridge.property(NodeFilter.SHOW_COMMENT, .{ .template = true });
|
||||
pub const SHOW_DOCUMENT = bridge.property(NodeFilter.SHOW_DOCUMENT, .{ .template = true });
|
||||
pub const SHOW_DOCUMENT_TYPE = bridge.property(NodeFilter.SHOW_DOCUMENT_TYPE, .{ .template = true });
|
||||
pub const SHOW_DOCUMENT_FRAGMENT = bridge.property(NodeFilter.SHOW_DOCUMENT_FRAGMENT, .{ .template = true });
|
||||
pub const SHOW_NOTATION = bridge.property(NodeFilter.SHOW_NOTATION, .{ .template = true });
|
||||
};
|
||||
|
||||
76
src/browser/webapi/PluginArray.zig
Normal file
76
src/browser/webapi/PluginArray.zig
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
pub fn registerTypes() []const type {
|
||||
return &.{ PluginArray, Plugin };
|
||||
}
|
||||
|
||||
const PluginArray = @This();
|
||||
|
||||
_pad: bool = false,
|
||||
|
||||
pub fn refresh(_: *const PluginArray) void {}
|
||||
pub fn getAtIndex(_: *const PluginArray, index: usize) ?*Plugin {
|
||||
_ = index;
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getByName(_: *const PluginArray, name: []const u8) ?*Plugin {
|
||||
_ = name;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cannot be constructed, and we currently never return any, so no reason to
|
||||
// implement anything on it (for now)
|
||||
const Plugin = struct {
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Plugin);
|
||||
pub const Meta = struct {
|
||||
pub const name = "Plugin";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const empty_with_no_proto = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(PluginArray);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "PluginArray";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const empty_with_no_proto = true;
|
||||
};
|
||||
|
||||
pub const length = bridge.property(0, .{ .template = false });
|
||||
pub const refresh = bridge.function(PluginArray.refresh, .{});
|
||||
pub const @"[int]" = bridge.indexed(PluginArray.getAtIndex, .{ .null_as_undefined = true });
|
||||
pub const @"[str]" = bridge.namedIndexed(PluginArray.getByName, null, null, .{ .null_as_undefined = true });
|
||||
pub const item = bridge.function(_item, .{});
|
||||
fn _item(self: *const PluginArray, index: i32) ?*Plugin {
|
||||
if (index < 0) {
|
||||
return null;
|
||||
}
|
||||
return self.getAtIndex(@intCast(index));
|
||||
}
|
||||
pub const namedItem = bridge.function(PluginArray.getByName, .{});
|
||||
};
|
||||
@@ -565,10 +565,10 @@ pub const JsApi = struct {
|
||||
};
|
||||
|
||||
// Constants for compareBoundaryPoints
|
||||
pub const START_TO_START = bridge.property(0);
|
||||
pub const START_TO_END = bridge.property(1);
|
||||
pub const END_TO_END = bridge.property(2);
|
||||
pub const END_TO_START = bridge.property(3);
|
||||
pub const START_TO_START = bridge.property(0, .{ .template = true });
|
||||
pub const START_TO_END = bridge.property(1, .{ .template = true });
|
||||
pub const END_TO_END = bridge.property(2, .{ .template = true });
|
||||
pub const END_TO_START = bridge.property(3, .{ .template = true });
|
||||
|
||||
pub const constructor = bridge.constructor(Range.init, .{});
|
||||
pub const setStart = bridge.function(Range.setStart, .{ .dom_exception = true });
|
||||
|
||||
@@ -43,30 +43,6 @@ pub fn asEventTarget(self: *Screen) *EventTarget {
|
||||
return self._proto;
|
||||
}
|
||||
|
||||
pub fn getWidth(_: *const Screen) u32 {
|
||||
return 1920;
|
||||
}
|
||||
|
||||
pub fn getHeight(_: *const Screen) u32 {
|
||||
return 1080;
|
||||
}
|
||||
|
||||
pub fn getAvailWidth(_: *const Screen) u32 {
|
||||
return 1920;
|
||||
}
|
||||
|
||||
pub fn getAvailHeight(_: *const Screen) u32 {
|
||||
return 1040; // 40px reserved for taskbar/dock
|
||||
}
|
||||
|
||||
pub fn getColorDepth(_: *const Screen) u32 {
|
||||
return 24;
|
||||
}
|
||||
|
||||
pub fn getPixelDepth(_: *const Screen) u32 {
|
||||
return 24;
|
||||
}
|
||||
|
||||
pub fn getOrientation(self: *Screen, page: *Page) !*Orientation {
|
||||
if (self._orientation) |orientation| {
|
||||
return orientation;
|
||||
@@ -85,12 +61,12 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const width = bridge.accessor(Screen.getWidth, null, .{});
|
||||
pub const height = bridge.accessor(Screen.getHeight, null, .{});
|
||||
pub const availWidth = bridge.accessor(Screen.getAvailWidth, null, .{});
|
||||
pub const availHeight = bridge.accessor(Screen.getAvailHeight, null, .{});
|
||||
pub const colorDepth = bridge.accessor(Screen.getColorDepth, null, .{});
|
||||
pub const pixelDepth = bridge.accessor(Screen.getPixelDepth, null, .{});
|
||||
pub const width = bridge.property(1920, .{ .template = false });
|
||||
pub const height = bridge.property(1080, .{ .template = false });
|
||||
pub const availWidth = bridge.property(1920, .{ .template = false });
|
||||
pub const availHeight = bridge.property(1040, .{ .template = false });
|
||||
pub const colorDepth = bridge.property(24, .{ .template = false });
|
||||
pub const pixelDepth = bridge.property(24, .{ .template = false });
|
||||
pub const orientation = bridge.accessor(Screen.getOrientation, null, .{});
|
||||
};
|
||||
|
||||
@@ -107,14 +83,6 @@ pub const Orientation = struct {
|
||||
return self._proto;
|
||||
}
|
||||
|
||||
pub fn getAngle(_: *const Orientation) u32 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn getType(_: *const Orientation) []const u8 {
|
||||
return "landscape-primary";
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Orientation);
|
||||
|
||||
@@ -124,7 +92,7 @@ pub const Orientation = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const angle = bridge.accessor(Orientation.getAngle, null, .{});
|
||||
pub const @"type" = bridge.accessor(Orientation.getType, null, .{});
|
||||
pub const angle = bridge.property(0, .{ .template = false });
|
||||
pub const @"type" = bridge.property("landscape-primary", .{ .template = false });
|
||||
};
|
||||
};
|
||||
|
||||
@@ -24,6 +24,8 @@ const Page = @import("../Page.zig");
|
||||
const Range = @import("Range.zig");
|
||||
const AbstractRange = @import("AbstractRange.zig");
|
||||
const Node = @import("Node.zig");
|
||||
const Event = @import("Event.zig");
|
||||
const Document = @import("Document.zig");
|
||||
|
||||
/// https://w3c.github.io/selection-api/
|
||||
const Selection = @This();
|
||||
@@ -35,6 +37,12 @@ _direction: SelectionDirection = .none,
|
||||
|
||||
pub const init: Selection = .{};
|
||||
|
||||
fn dispatchSelectionChangeEvent(page: *Page) !void {
|
||||
const event = try Event.init("selectionchange", .{}, page);
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
try page._event_manager.dispatch(page.document.asEventTarget(), event);
|
||||
}
|
||||
|
||||
fn isInTree(self: *const Selection) bool {
|
||||
if (self._range == null) return false;
|
||||
const anchor_node = self.getAnchorNode() orelse return false;
|
||||
@@ -110,23 +118,33 @@ pub fn getType(self: *const Selection) []const u8 {
|
||||
return "Range";
|
||||
}
|
||||
|
||||
pub fn addRange(self: *Selection, range: *Range) !void {
|
||||
pub fn addRange(self: *Selection, range: *Range, page: *Page) !void {
|
||||
if (self._range != null) return;
|
||||
|
||||
// Only add the range if its root node is in the document associated with this selection
|
||||
const start_node = range.asAbstractRange().getStartContainer();
|
||||
if (!page.document.asNode().contains(start_node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._range = range;
|
||||
try dispatchSelectionChangeEvent(page);
|
||||
}
|
||||
|
||||
pub fn removeRange(self: *Selection, range: *Range) !void {
|
||||
pub fn removeRange(self: *Selection, range: *Range, page: *Page) !void {
|
||||
if (self._range == range) {
|
||||
self._range = null;
|
||||
try dispatchSelectionChangeEvent(page);
|
||||
return;
|
||||
} else {
|
||||
return error.NotFound;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn removeAllRanges(self: *Selection) void {
|
||||
pub fn removeAllRanges(self: *Selection, page: *Page) !void {
|
||||
self._range = null;
|
||||
self._direction = .none;
|
||||
try dispatchSelectionChangeEvent(page);
|
||||
}
|
||||
|
||||
pub fn collapseToEnd(self: *Selection, page: *Page) !void {
|
||||
@@ -142,10 +160,11 @@ pub fn collapseToEnd(self: *Selection, page: *Page) !void {
|
||||
|
||||
self._range = new_range;
|
||||
self._direction = .none;
|
||||
try dispatchSelectionChangeEvent(page);
|
||||
}
|
||||
|
||||
pub fn collapseToStart(self: *Selection, page: *Page) !void {
|
||||
const range = self._range orelse return;
|
||||
const range = self._range orelse return error.InvalidStateError;
|
||||
|
||||
const abstract = range.asAbstractRange();
|
||||
const first_node = abstract.getStartContainer();
|
||||
@@ -157,6 +176,7 @@ pub fn collapseToStart(self: *Selection, page: *Page) !void {
|
||||
|
||||
self._range = new_range;
|
||||
self._direction = .none;
|
||||
try dispatchSelectionChangeEvent(page);
|
||||
}
|
||||
|
||||
pub fn containsNode(self: *const Selection, node: *Node, partial: bool) !bool {
|
||||
@@ -187,14 +207,21 @@ pub fn containsNode(self: *const Selection, node: *Node, partial: bool) !bool {
|
||||
|
||||
pub fn deleteFromDocument(self: *Selection, page: *Page) !void {
|
||||
const range = self._range orelse return;
|
||||
|
||||
try range.deleteContents(page);
|
||||
try dispatchSelectionChangeEvent(page);
|
||||
}
|
||||
|
||||
pub fn extend(self: *Selection, node: *Node, _offset: ?u32, page: *Page) !void {
|
||||
const range = self._range orelse return error.InvalidState;
|
||||
const offset = _offset orelse 0;
|
||||
|
||||
// If the node is not contained in the document, do not change the selection
|
||||
if (!page.document.asNode().contains(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node._type == .document_type) return error.InvalidNodeType;
|
||||
|
||||
if (offset > node.getLength()) {
|
||||
return error.IndexSizeError;
|
||||
}
|
||||
@@ -230,6 +257,7 @@ pub fn extend(self: *Selection, node: *Node, _offset: ?u32, page: *Page) !void {
|
||||
}
|
||||
|
||||
self._range = new_range;
|
||||
try dispatchSelectionChangeEvent(page);
|
||||
}
|
||||
|
||||
pub fn getRangeAt(self: *Selection, index: u32) !*Range {
|
||||
@@ -299,16 +327,22 @@ pub fn modify(
|
||||
}
|
||||
|
||||
pub fn selectAllChildren(self: *Selection, parent: *Node, page: *Page) !void {
|
||||
if (parent._type == .document_type) return error.InvalidNodeTypeError;
|
||||
if (parent._type == .document_type) return error.InvalidNodeType;
|
||||
|
||||
// If the node is not contained in the document, do not change the selection
|
||||
if (!page.document.asNode().contains(parent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const range = try Range.init(page);
|
||||
try range.setStart(parent, 0);
|
||||
|
||||
const child_count = parent.getLength();
|
||||
const child_count = parent.getChildrenCount();
|
||||
try range.setEnd(parent, @intCast(child_count));
|
||||
|
||||
self._range = range;
|
||||
self._direction = .forward;
|
||||
try dispatchSelectionChangeEvent(page);
|
||||
}
|
||||
|
||||
pub fn setBaseAndExtent(
|
||||
@@ -355,11 +389,12 @@ pub fn setBaseAndExtent(
|
||||
}
|
||||
|
||||
self._range = range;
|
||||
try dispatchSelectionChangeEvent(page);
|
||||
}
|
||||
|
||||
pub fn collapse(self: *Selection, _node: ?*Node, _offset: ?u32, page: *Page) !void {
|
||||
const node = _node orelse {
|
||||
self.removeAllRanges();
|
||||
try self.removeAllRanges(page);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -370,12 +405,18 @@ pub fn collapse(self: *Selection, _node: ?*Node, _offset: ?u32, page: *Page) !vo
|
||||
return error.IndexSizeError;
|
||||
}
|
||||
|
||||
// If the node is not contained in the document, do not change the selection
|
||||
if (!page.document.asNode().contains(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const range = try Range.init(page);
|
||||
try range.setStart(node, offset);
|
||||
try range.setEnd(node, offset);
|
||||
|
||||
self._range = range;
|
||||
self._direction = .none;
|
||||
try dispatchSelectionChangeEvent(page);
|
||||
}
|
||||
|
||||
pub fn toString(self: *const Selection, page: *Page) ![]const u8 {
|
||||
@@ -404,7 +445,7 @@ pub const JsApi = struct {
|
||||
pub const addRange = bridge.function(Selection.addRange, .{});
|
||||
pub const collapse = bridge.function(Selection.collapse, .{ .dom_exception = true });
|
||||
pub const collapseToEnd = bridge.function(Selection.collapseToEnd, .{});
|
||||
pub const collapseToStart = bridge.function(Selection.collapseToStart, .{});
|
||||
pub const collapseToStart = bridge.function(Selection.collapseToStart, .{ .dom_exception = true });
|
||||
pub const containsNode = bridge.function(Selection.containsNode, .{});
|
||||
pub const deleteFromDocument = bridge.function(Selection.deleteFromDocument, .{});
|
||||
pub const empty = bridge.function(Selection.removeAllRanges, .{});
|
||||
@@ -414,9 +455,9 @@ pub const JsApi = struct {
|
||||
pub const modify = bridge.function(Selection.modify, .{});
|
||||
pub const removeAllRanges = bridge.function(Selection.removeAllRanges, .{});
|
||||
pub const removeRange = bridge.function(Selection.removeRange, .{ .dom_exception = true });
|
||||
pub const selectAllChildren = bridge.function(Selection.selectAllChildren, .{});
|
||||
pub const selectAllChildren = bridge.function(Selection.selectAllChildren, .{ .dom_exception = true });
|
||||
pub const setBaseAndExtent = bridge.function(Selection.setBaseAndExtent, .{ .dom_exception = true });
|
||||
pub const setPosition = bridge.function(Selection.collapse, .{});
|
||||
pub const setPosition = bridge.function(Selection.collapse, .{ .dom_exception = true });
|
||||
pub const toString = bridge.function(Selection.toString, .{});
|
||||
};
|
||||
|
||||
|
||||
64
src/browser/webapi/VisualViewport.zig
Normal file
64
src/browser/webapi/VisualViewport.zig
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const EventTarget = @import("EventTarget.zig");
|
||||
const Window = @import("Window.zig");
|
||||
|
||||
const VisualViewport = @This();
|
||||
|
||||
_proto: *EventTarget,
|
||||
|
||||
pub fn init(page: *Page) !*VisualViewport {
|
||||
return page._factory.eventTarget(VisualViewport{
|
||||
._proto = undefined,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn asEventTarget(self: *VisualViewport) *EventTarget {
|
||||
return self._proto;
|
||||
}
|
||||
|
||||
pub fn getPageLeft(_: *const VisualViewport, page: *Page) u32 {
|
||||
return page.window.getScrollX();
|
||||
}
|
||||
|
||||
pub fn getPageTop(_: *const VisualViewport, page: *Page) u32 {
|
||||
return page.window.getScrollY();
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(VisualViewport);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "VisualViewport";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
// Static viewport properties for headless browser
|
||||
// No pinch-zoom or mobile viewport, so values are straightforward
|
||||
pub const offsetLeft = bridge.property(0, .{ .template = false });
|
||||
pub const offsetTop = bridge.property(0, .{ .template = false });
|
||||
pub const pageLeft = bridge.accessor(VisualViewport.getPageLeft, null, .{});
|
||||
pub const pageTop = bridge.accessor(VisualViewport.getPageTop, null, .{});
|
||||
pub const width = bridge.property(1920, .{ .template = false });
|
||||
pub const height = bridge.property(1080, .{ .template = false });
|
||||
pub const scale = bridge.property(1.0, .{ .template = false });
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -29,6 +29,7 @@ const Crypto = @import("Crypto.zig");
|
||||
const CSS = @import("CSS.zig");
|
||||
const Navigator = @import("Navigator.zig");
|
||||
const Screen = @import("Screen.zig");
|
||||
const VisualViewport = @import("VisualViewport.zig");
|
||||
const Performance = @import("Performance.zig");
|
||||
const Document = @import("Document.zig");
|
||||
const Location = @import("Location.zig");
|
||||
@@ -44,6 +45,10 @@ const CSSStyleProperties = @import("css/CSSStyleProperties.zig");
|
||||
const CustomElementRegistry = @import("CustomElementRegistry.zig");
|
||||
const Selection = @import("Selection.zig");
|
||||
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Window = @This();
|
||||
|
||||
_proto: *EventTarget,
|
||||
@@ -53,6 +58,7 @@ _crypto: Crypto = .init,
|
||||
_console: Console = .init,
|
||||
_navigator: Navigator = .init,
|
||||
_screen: *Screen,
|
||||
_visual_viewport: *VisualViewport,
|
||||
_performance: Performance,
|
||||
_storage_bucket: *storage.Bucket,
|
||||
_on_load: ?js.Function.Global = null,
|
||||
@@ -106,6 +112,10 @@ pub fn getScreen(self: *Window) *Screen {
|
||||
return self._screen;
|
||||
}
|
||||
|
||||
pub fn getVisualViewport(self: *const Window) *VisualViewport {
|
||||
return self._visual_viewport;
|
||||
}
|
||||
|
||||
pub fn getCrypto(self: *Window) *Crypto {
|
||||
return &self._crypto;
|
||||
}
|
||||
@@ -275,14 +285,15 @@ pub fn cancelIdleCallback(self: *Window, id: u32) void {
|
||||
}
|
||||
|
||||
pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
|
||||
const error_event = try ErrorEvent.initTrusted("error", .{
|
||||
.@"error" = try err.persist(),
|
||||
.message = err.toString(.{}) catch "Unknown error",
|
||||
const error_event = try ErrorEvent.initTrusted(comptime .wrap("error"), .{
|
||||
.@"error" = try err.temp(),
|
||||
.message = err.toStringSlice() catch "Unknown error",
|
||||
.bubbles = false,
|
||||
.cancelable = true,
|
||||
}, page);
|
||||
|
||||
const event = error_event.asEvent();
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
|
||||
// Invoke window.onerror callback if set (per WHATWG spec, this is called
|
||||
// with 5 arguments: message, source, lineno, colno, error)
|
||||
@@ -339,32 +350,27 @@ pub fn getComputedStyle(_: *const Window, element: *Element, pseudo_element: ?[]
|
||||
return CSSStyleProperties.init(element, true, page);
|
||||
}
|
||||
|
||||
pub fn getIsSecureContext(_: *const Window) bool {
|
||||
// Return false since we don't have secure-context-only APIs implemented
|
||||
// (webcam, geolocation, clipboard, etc.)
|
||||
// This is safer and could help avoid processing errors by hinting at
|
||||
// sites not to try to access those features
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn postMessage(self: *Window, message: js.Value.Global, target_origin: ?[]const u8, page: *Page) !void {
|
||||
pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]const u8, page: *Page) !void {
|
||||
// For now, we ignore targetOrigin checking and just dispatch the message
|
||||
// In a full implementation, we would validate the origin
|
||||
_ = target_origin;
|
||||
|
||||
// postMessage queues a task (not a microtask), so use the scheduler
|
||||
const origin = try self._location.getOrigin(page);
|
||||
const callback = try page._factory.create(PostMessageCallback{
|
||||
.window = self,
|
||||
.message = message,
|
||||
.origin = try page.arena.dupe(u8, origin),
|
||||
.page = page,
|
||||
});
|
||||
errdefer page._factory.destroy(callback);
|
||||
const arena = try page.getArena(.{ .debug = "Window.schedule" });
|
||||
errdefer page.releaseArena(arena);
|
||||
|
||||
try page.scheduler.add(callback, PostMessageCallback.run, 0, .{
|
||||
const origin = try self._location.getOrigin(page);
|
||||
const callback = try arena.create(PostMessageCallback);
|
||||
callback.* = .{
|
||||
.page = page,
|
||||
.arena = arena,
|
||||
.message = message,
|
||||
.origin = try arena.dupe(u8, origin),
|
||||
};
|
||||
try page.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
|
||||
.name = "postMessage",
|
||||
.low_priority = false,
|
||||
.finalizer = PostMessageCallback.cancelled,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -375,9 +381,10 @@ pub fn btoa(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
|
||||
}
|
||||
|
||||
pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
|
||||
const decoded_len = std.base64.standard.Decoder.calcSizeForSlice(input) catch return error.InvalidCharacterError;
|
||||
const trimmed = std.mem.trim(u8, input, &std.ascii.whitespace);
|
||||
const decoded_len = std.base64.standard.Decoder.calcSizeForSlice(trimmed) catch return error.InvalidCharacterError;
|
||||
const decoded = try page.call_arena.alloc(u8, decoded_len);
|
||||
std.base64.standard.Decoder.decode(decoded, input) catch return error.InvalidCharacterError;
|
||||
std.base64.standard.Decoder.decode(decoded, trimmed) catch return error.InvalidCharacterError;
|
||||
return decoded;
|
||||
}
|
||||
|
||||
@@ -400,14 +407,6 @@ pub fn getFramesLength(self: *const Window) u32 {
|
||||
return ln;
|
||||
}
|
||||
|
||||
pub fn getInnerWidth(_: *const Window) u32 {
|
||||
return 1920;
|
||||
}
|
||||
|
||||
pub fn getInnerHeight(_: *const Window) u32 {
|
||||
return 1080;
|
||||
}
|
||||
|
||||
pub fn getScrollX(self: *const Window) u32 {
|
||||
return self._scroll_pos.x;
|
||||
}
|
||||
@@ -442,7 +441,7 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
|
||||
|
||||
// We dispatch scroll event asynchronously after 10ms. So we can throttle
|
||||
// them.
|
||||
try page.scheduler.add(
|
||||
try page.js.scheduler.add(
|
||||
page,
|
||||
struct {
|
||||
fn dispatch(_page: *anyopaque) anyerror!?u32 {
|
||||
@@ -454,7 +453,8 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
|
||||
return null;
|
||||
}
|
||||
|
||||
const event = try Event.initTrusted("scroll", .{ .bubbles = true }, p);
|
||||
const event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, p);
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
try p._event_manager.dispatch(p.document.asEventTarget(), event);
|
||||
|
||||
pos.state = .end;
|
||||
@@ -466,7 +466,7 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
|
||||
.{ .low_priority = true },
|
||||
);
|
||||
// We dispatch scrollend event asynchronously after 20ms.
|
||||
try page.scheduler.add(
|
||||
try page.js.scheduler.add(
|
||||
page,
|
||||
struct {
|
||||
fn dispatch(_page: *anyopaque) anyerror!?u32 {
|
||||
@@ -481,7 +481,8 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
|
||||
.end => {},
|
||||
.done => return null,
|
||||
}
|
||||
const event = try Event.initTrusted("scrollend", .{ .bubbles = true }, p);
|
||||
const event = try Event.initTrusted(comptime .wrap("scrollend"), .{ .bubbles = true }, p);
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
try p._event_manager.dispatch(p.document.asEventTarget(), event);
|
||||
|
||||
pos.state = .done;
|
||||
@@ -494,6 +495,28 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn unhandledPromiseRejection(self: *Window, rejection: js.PromiseRejection, page: *Page) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.js, "unhandled rejection", .{
|
||||
.value = rejection.reason(),
|
||||
.stack = rejection.local.stackTrace() catch |err| @errorName(err) orelse "???",
|
||||
});
|
||||
}
|
||||
|
||||
var event = (try @import("event/PromiseRejectionEvent.zig").init("unhandledrejection", .{
|
||||
.reason = if (rejection.reason()) |r| try r.temp() else null,
|
||||
.promise = try rejection.promise().temp(),
|
||||
}, page)).asEvent();
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
|
||||
try page._event_manager.dispatchWithFunction(
|
||||
self.asEventTarget(),
|
||||
event,
|
||||
rejection.local.toLocal(self._on_unhandled_rejection),
|
||||
.{ .inject_target = true, .context = "window.unhandledrejection" },
|
||||
);
|
||||
}
|
||||
|
||||
const ScheduleOpts = struct {
|
||||
repeat: bool,
|
||||
params: []js.Value.Temp,
|
||||
@@ -508,13 +531,16 @@ fn scheduleCallback(self: *Window, cb: js.Function.Temp, delay_ms: u32, opts: Sc
|
||||
return error.TooManyTimeout;
|
||||
}
|
||||
|
||||
const arena = try page.getArena(.{ .debug = "Window.schedule" });
|
||||
errdefer page.releaseArena(arena);
|
||||
|
||||
const timer_id = self._timer_id +% 1;
|
||||
self._timer_id = timer_id;
|
||||
|
||||
const params = opts.params;
|
||||
var persisted_params: []js.Value.Temp = &.{};
|
||||
if (params.len > 0) {
|
||||
persisted_params = try page.arena.dupe(js.Value.Temp, params);
|
||||
persisted_params = try arena.dupe(js.Value.Temp, params);
|
||||
}
|
||||
|
||||
const gop = try self._timers.getOrPut(page.arena, timer_id);
|
||||
@@ -524,21 +550,23 @@ fn scheduleCallback(self: *Window, cb: js.Function.Temp, delay_ms: u32, opts: Sc
|
||||
}
|
||||
errdefer _ = self._timers.remove(timer_id);
|
||||
|
||||
const callback = try page._factory.create(ScheduleCallback{
|
||||
const callback = try arena.create(ScheduleCallback);
|
||||
callback.* = .{
|
||||
.cb = cb,
|
||||
.page = page,
|
||||
.arena = arena,
|
||||
.mode = opts.mode,
|
||||
.name = opts.name,
|
||||
.timer_id = timer_id,
|
||||
.params = persisted_params,
|
||||
.repeat_ms = if (opts.repeat) if (delay_ms == 0) 1 else delay_ms else null,
|
||||
});
|
||||
};
|
||||
gop.value_ptr.* = callback;
|
||||
errdefer page._factory.destroy(callback);
|
||||
|
||||
try page.scheduler.add(callback, ScheduleCallback.run, delay_ms, .{
|
||||
try page.js.scheduler.add(callback, ScheduleCallback.run, delay_ms, .{
|
||||
.name = opts.name,
|
||||
.low_priority = opts.low_priority,
|
||||
.finalizer = ScheduleCallback.cancelled,
|
||||
});
|
||||
|
||||
return timer_id;
|
||||
@@ -556,13 +584,11 @@ const ScheduleCallback = struct {
|
||||
|
||||
cb: js.Function.Temp,
|
||||
|
||||
page: *Page,
|
||||
|
||||
params: []const js.Value.Temp,
|
||||
|
||||
removed: bool = false,
|
||||
|
||||
mode: Mode,
|
||||
page: *Page,
|
||||
arena: Allocator,
|
||||
removed: bool = false,
|
||||
params: []const js.Value.Temp,
|
||||
|
||||
const Mode = enum {
|
||||
idle,
|
||||
@@ -570,19 +596,26 @@ const ScheduleCallback = struct {
|
||||
animation_frame,
|
||||
};
|
||||
|
||||
fn cancelled(ctx: *anyopaque) void {
|
||||
var self: *ScheduleCallback = @ptrCast(@alignCast(ctx));
|
||||
self.deinit();
|
||||
}
|
||||
|
||||
fn deinit(self: *ScheduleCallback) void {
|
||||
self.page.js.release(self.cb);
|
||||
for (self.params) |param| {
|
||||
self.page.js.release(param);
|
||||
}
|
||||
self.page._factory.destroy(self);
|
||||
self.page.releaseArena(self.arena);
|
||||
}
|
||||
|
||||
fn run(ctx: *anyopaque) !?u32 {
|
||||
const self: *ScheduleCallback = @ptrCast(@alignCast(ctx));
|
||||
const page = self.page;
|
||||
const window = page.window;
|
||||
|
||||
if (self.removed) {
|
||||
_ = page.window._timers.remove(self.timer_id);
|
||||
_ = window._timers.remove(self.timer_id);
|
||||
self.deinit();
|
||||
return null;
|
||||
}
|
||||
@@ -599,7 +632,7 @@ const ScheduleCallback = struct {
|
||||
};
|
||||
},
|
||||
.animation_frame => {
|
||||
ls.toLocal(self.cb).call(void, .{page.window._performance.now()}) catch |err| {
|
||||
ls.toLocal(self.cb).call(void, .{window._performance.now()}) catch |err| {
|
||||
log.warn(.js, "window.RAF", .{ .name = self.name, .err = err });
|
||||
};
|
||||
},
|
||||
@@ -614,35 +647,42 @@ const ScheduleCallback = struct {
|
||||
return ms;
|
||||
}
|
||||
defer self.deinit();
|
||||
_ = page.window._timers.remove(self.timer_id);
|
||||
_ = window._timers.remove(self.timer_id);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const PostMessageCallback = struct {
|
||||
window: *Window,
|
||||
message: js.Value.Global,
|
||||
origin: []const u8,
|
||||
page: *Page,
|
||||
arena: Allocator,
|
||||
origin: []const u8,
|
||||
message: js.Value.Temp,
|
||||
|
||||
fn deinit(self: *PostMessageCallback) void {
|
||||
self.page._factory.destroy(self);
|
||||
self.page.releaseArena(self.arena);
|
||||
}
|
||||
|
||||
fn cancelled(ctx: *anyopaque) void {
|
||||
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
|
||||
self.page.releaseArena(self.arena);
|
||||
}
|
||||
|
||||
fn run(ctx: *anyopaque) !?u32 {
|
||||
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
|
||||
defer self.deinit();
|
||||
|
||||
const message_event = try MessageEvent.initTrusted("message", .{
|
||||
const page = self.page;
|
||||
const window = page.window;
|
||||
|
||||
const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{
|
||||
.data = self.message,
|
||||
.origin = self.origin,
|
||||
.source = self.window,
|
||||
.source = window,
|
||||
.bubbles = false,
|
||||
.cancelable = false,
|
||||
}, self.page);
|
||||
|
||||
const event = message_event.asEvent();
|
||||
try self.page._event_manager.dispatch(self.window.asEventTarget(), event);
|
||||
}, page)).asEvent();
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
try page._event_manager.dispatch(window.asEventTarget(), event);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -673,23 +713,24 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const top = bridge.accessor(Window.getWindow, null, .{ .cache = "top" });
|
||||
pub const self = bridge.accessor(Window.getWindow, null, .{ .cache = "self" });
|
||||
pub const window = bridge.accessor(Window.getWindow, null, .{ .cache = "window" });
|
||||
pub const parent = bridge.accessor(Window.getWindow, null, .{ .cache = "parent" });
|
||||
pub const console = bridge.accessor(Window.getConsole, null, .{ .cache = "console" });
|
||||
pub const navigator = bridge.accessor(Window.getNavigator, null, .{ .cache = "navigator" });
|
||||
pub const screen = bridge.accessor(Window.getScreen, null, .{ .cache = "screen" });
|
||||
pub const performance = bridge.accessor(Window.getPerformance, null, .{ .cache = "performance" });
|
||||
pub const localStorage = bridge.accessor(Window.getLocalStorage, null, .{ .cache = "localStorage" });
|
||||
pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{ .cache = "sessionStorage" });
|
||||
pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = "document" });
|
||||
pub const top = bridge.accessor(Window.getWindow, null, .{});
|
||||
pub const self = bridge.accessor(Window.getWindow, null, .{});
|
||||
pub const window = bridge.accessor(Window.getWindow, null, .{});
|
||||
pub const parent = bridge.accessor(Window.getWindow, null, .{});
|
||||
pub const console = bridge.accessor(Window.getConsole, null, .{});
|
||||
pub const navigator = bridge.accessor(Window.getNavigator, null, .{});
|
||||
pub const screen = bridge.accessor(Window.getScreen, null, .{});
|
||||
pub const visualViewport = bridge.accessor(Window.getVisualViewport, null, .{});
|
||||
pub const performance = bridge.accessor(Window.getPerformance, null, .{});
|
||||
pub const localStorage = bridge.accessor(Window.getLocalStorage, null, .{});
|
||||
pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{});
|
||||
pub const document = bridge.accessor(Window.getDocument, null, .{});
|
||||
pub const location = bridge.accessor(Window.getLocation, Window.setLocation, .{});
|
||||
pub const history = bridge.accessor(Window.getHistory, null, .{});
|
||||
pub const navigation = bridge.accessor(Window.getNavigation, null, .{});
|
||||
pub const crypto = bridge.accessor(Window.getCrypto, null, .{ .cache = "crypto" });
|
||||
pub const CSS = bridge.accessor(Window.getCSS, null, .{ .cache = "CSS" });
|
||||
pub const customElements = bridge.accessor(Window.getCustomElements, null, .{ .cache = "customElements" });
|
||||
pub const crypto = bridge.accessor(Window.getCrypto, null, .{});
|
||||
pub const CSS = bridge.accessor(Window.getCSS, null, .{});
|
||||
pub const customElements = bridge.accessor(Window.getCustomElements, null, .{});
|
||||
pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{});
|
||||
pub const onpageshow = bridge.accessor(Window.getOnPageShow, Window.setOnPageShow, .{});
|
||||
pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{});
|
||||
@@ -714,18 +755,28 @@ pub const JsApi = struct {
|
||||
pub const reportError = bridge.function(Window.reportError, .{});
|
||||
pub const getComputedStyle = bridge.function(Window.getComputedStyle, .{});
|
||||
pub const getSelection = bridge.function(Window.getSelection, .{});
|
||||
pub const isSecureContext = bridge.accessor(Window.getIsSecureContext, null, .{});
|
||||
pub const frames = bridge.accessor(Window.getWindow, null, .{ .cache = "frames" });
|
||||
|
||||
pub const frames = bridge.accessor(Window.getWindow, null, .{});
|
||||
pub const index = bridge.indexed(Window.getFrame, .{ .null_as_undefined = true });
|
||||
pub const length = bridge.accessor(Window.getFramesLength, null, .{ .cache = "length" });
|
||||
pub const innerWidth = bridge.accessor(Window.getInnerWidth, null, .{ .cache = "innerWidth" });
|
||||
pub const innerHeight = bridge.accessor(Window.getInnerHeight, null, .{ .cache = "innerHeight" });
|
||||
pub const scrollX = bridge.accessor(Window.getScrollX, null, .{ .cache = "scrollX" });
|
||||
pub const scrollY = bridge.accessor(Window.getScrollY, null, .{ .cache = "scrollY" });
|
||||
pub const pageXOffset = bridge.accessor(Window.getScrollX, null, .{ .cache = "pageXOffset" });
|
||||
pub const pageYOffset = bridge.accessor(Window.getScrollY, null, .{ .cache = "pageYOffset" });
|
||||
pub const length = bridge.accessor(Window.getFramesLength, null, .{});
|
||||
pub const scrollX = bridge.accessor(Window.getScrollX, null, .{});
|
||||
pub const scrollY = bridge.accessor(Window.getScrollY, null, .{});
|
||||
pub const pageXOffset = bridge.accessor(Window.getScrollX, null, .{});
|
||||
pub const pageYOffset = bridge.accessor(Window.getScrollY, null, .{});
|
||||
pub const scrollTo = bridge.function(Window.scrollTo, .{});
|
||||
pub const scroll = bridge.function(Window.scrollTo, .{});
|
||||
|
||||
// Return false since we don't have secure-context-only APIs implemented
|
||||
// (webcam, geolocation, clipboard, etc.)
|
||||
// This is safer and could help avoid processing errors by hinting at
|
||||
// sites not to try to access those features
|
||||
pub const isSecureContext = bridge.property(false, .{ .template = false });
|
||||
|
||||
pub const innerWidth = bridge.property(1920, .{ .template = false });
|
||||
pub const innerHeight = bridge.property(1080, .{ .template = false });
|
||||
// This should return a window-like object in specific conditions. Would be
|
||||
// pretty complicated to properly support I think.
|
||||
pub const opener = bridge.property(null, .{ .template = false });
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
@@ -36,14 +36,6 @@ pub fn cancel(_: *Animation) void {}
|
||||
pub fn finish(_: *Animation) void {}
|
||||
pub fn reverse(_: *Animation) void {}
|
||||
|
||||
pub fn getPlayState(_: *const Animation) []const u8 {
|
||||
return "finished";
|
||||
}
|
||||
|
||||
pub fn getPending(_: *const Animation) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn getFinished(self: *Animation, page: *Page) !js.Promise {
|
||||
if (self._finished_resolver == null) {
|
||||
const resolver = page.js.local.?.createPromiseResolver();
|
||||
@@ -94,8 +86,8 @@ pub const JsApi = struct {
|
||||
pub const cancel = bridge.function(Animation.cancel, .{});
|
||||
pub const finish = bridge.function(Animation.finish, .{});
|
||||
pub const reverse = bridge.function(Animation.reverse, .{});
|
||||
pub const playState = bridge.accessor(Animation.getPlayState, null, .{});
|
||||
pub const pending = bridge.accessor(Animation.getPending, null, .{});
|
||||
pub const playState = bridge.property("finished", .{ .template = false });
|
||||
pub const pending = bridge.property(false, .{ .template = false });
|
||||
pub const finished = bridge.accessor(Animation.getFinished, null, .{});
|
||||
pub const ready = bridge.accessor(Animation.getReady, null, .{});
|
||||
pub const effect = bridge.accessor(Animation.getEffect, Animation.setEffect, .{});
|
||||
|
||||
@@ -218,7 +218,7 @@ fn getDefaultDisplay(element: *const Element) []const u8 {
|
||||
.html => |html| {
|
||||
return switch (html._type) {
|
||||
.anchor, .br, .span, .label, .time, .font, .mod, .quote => "inline",
|
||||
.body, .div, .p, .heading, .form, .button, .canvas, .dialog, .embed, .head, .html, .hr, .iframe, .img, .input, .li, .link, .meta, .ol, .option, .script, .select, .slot, .style, .template, .textarea, .title, .ul, .media, .area, .base, .datalist, .directory, .fieldset, .legend, .map, .meter, .object, .optgroup, .output, .param, .pre, .progress, .source, .table, .table_caption, .table_cell, .table_col, .table_row, .table_section, .track => "block",
|
||||
.body, .div, .p, .heading, .form, .button, .canvas, .dialog, .embed, .head, .html, .hr, .iframe, .img, .input, .li, .link, .meta, .ol, .option, .script, .select, .slot, .style, .template, .textarea, .title, .ul, .media, .area, .base, .datalist, .directory, .fieldset, .legend, .map, .meter, .object, .optgroup, .output, .param, .picture, .pre, .progress, .source, .table, .table_caption, .table_cell, .table_col, .table_row, .table_section, .track => "block",
|
||||
.generic, .custom, .unknown, .data => blk: {
|
||||
const tag = element.getTagNameLower();
|
||||
if (isInlineTag(tag)) break :blk "inline";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -241,7 +241,6 @@ pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Custom);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "TODO-CUSTOM-NAME";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
@@ -32,6 +32,9 @@ pub const TextArea = @import("TextArea.zig");
|
||||
const Form = @This();
|
||||
_proto: *HtmlElement,
|
||||
|
||||
pub fn asHtmlElement(self: *Form) *HtmlElement {
|
||||
return self._proto;
|
||||
}
|
||||
fn asConstElement(self: *const Form) *const Element {
|
||||
return self._proto._proto;
|
||||
}
|
||||
@@ -88,7 +91,7 @@ pub fn getLength(self: *Form, page: *Page) !u32 {
|
||||
}
|
||||
|
||||
pub fn submit(self: *Form, page: *Page) !void {
|
||||
return page.submitForm(null, self);
|
||||
return page.submitForm(null, self, .{ .fire_event = false });
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -5,6 +5,10 @@ const URL = @import("../../../URL.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
const Event = @import("../../Event.zig");
|
||||
const log = @import("../../../../log.zig");
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const Image = @This();
|
||||
_proto: *HtmlElement,
|
||||
@@ -96,6 +100,18 @@ pub fn setLoading(self: *Image, value: []const u8, page: *Page) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("loading"), .wrap(value), page);
|
||||
}
|
||||
|
||||
pub fn getNaturalWidth(_: *const Image) u32 {
|
||||
// this is a valid response under a number of normal conditions, but could
|
||||
// be used to detect the nature of Browser.
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn getNaturalHeight(_: *const Image) u32 {
|
||||
// this is a valid response under a number of normal conditions, but could
|
||||
// be used to detect the nature of Browser.
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Image);
|
||||
|
||||
@@ -113,6 +129,21 @@ pub const JsApi = struct {
|
||||
pub const height = bridge.accessor(Image.getHeight, Image.setHeight, .{});
|
||||
pub const crossOrigin = bridge.accessor(Image.getCrossOrigin, Image.setCrossOrigin, .{});
|
||||
pub const loading = bridge.accessor(Image.getLoading, Image.setLoading, .{});
|
||||
pub const naturalWidth = bridge.accessor(Image.getNaturalWidth, null, .{});
|
||||
pub const naturalHeight = bridge.accessor(Image.getNaturalHeight, null, .{});
|
||||
};
|
||||
|
||||
pub const Build = struct {
|
||||
pub fn created(node: *Node, page: *Page) !void {
|
||||
const self = node.as(Image);
|
||||
const image = self.asElement();
|
||||
// Exit if src not set.
|
||||
// TODO: We might want to check if src point to valid image.
|
||||
_ = image.getAttributeSafe(comptime .wrap("src")) orelse return;
|
||||
|
||||
// Push to `_to_load` to dispatch load event just before window load event.
|
||||
return page._to_load.append(page.arena, image);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../../../testing.zig");
|
||||
|
||||
@@ -27,6 +27,7 @@ const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
const Form = @import("Form.zig");
|
||||
const Selection = @import("../../Selection.zig");
|
||||
const Event = @import("../../Event.zig");
|
||||
|
||||
const Input = @This();
|
||||
|
||||
@@ -83,6 +84,26 @@ _selection_start: u32 = 0,
|
||||
_selection_end: u32 = 0,
|
||||
_selection_direction: Selection.SelectionDirection = .none,
|
||||
|
||||
_on_selectionchange: ?js.Function.Global = null,
|
||||
|
||||
pub fn getOnSelectionChange(self: *Input) ?js.Function.Global {
|
||||
return self._on_selectionchange;
|
||||
}
|
||||
|
||||
pub fn setOnSelectionChange(self: *Input, listener: ?js.Function) !void {
|
||||
if (listener) |listen| {
|
||||
self._on_selectionchange = try listen.persistWithThis(self);
|
||||
} else {
|
||||
self._on_selectionchange = null;
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatchSelectionChangeEvent(self: *Input, page: *Page) !void {
|
||||
const event = try Event.init("selectionchange", .{ .bubbles = true }, page);
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
try page._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
||||
}
|
||||
|
||||
pub fn asElement(self: *Input) *Element {
|
||||
return self._proto._proto;
|
||||
}
|
||||
@@ -108,8 +129,13 @@ pub fn getValue(self: *const Input) []const u8 {
|
||||
}
|
||||
|
||||
pub fn setValue(self: *Input, value: []const u8, page: *Page) !void {
|
||||
// This should _not_ call setAttribute. It updates the default state only
|
||||
const owned = try page.dupeString(value);
|
||||
// File inputs cannot have their value set programmatically for security reasons
|
||||
if (self._input_type == .file) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
// This should _not_ call setAttribute. It updates the current state only
|
||||
const sanitized = try self.sanitizeValue(value, page);
|
||||
const owned = try page.dupeString(sanitized);
|
||||
self._value = owned;
|
||||
}
|
||||
|
||||
@@ -261,9 +287,9 @@ pub fn setRequired(self: *Input, required: bool, page: *Page) !void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select(self: *Input) !void {
|
||||
pub fn select(self: *Input, page: *Page) !void {
|
||||
const len = if (self._value) |v| @as(u32, @intCast(v.len)) else 0;
|
||||
try self.setSelectionRange(0, len, null);
|
||||
try self.setSelectionRange(0, len, null, page);
|
||||
}
|
||||
|
||||
fn selectionAvailable(self: *const Input) bool {
|
||||
@@ -295,6 +321,7 @@ pub fn innerInsert(self: *Input, str: []const u8, page: *Page) !void {
|
||||
self._selection_start = @intCast(new_value.len);
|
||||
self._selection_end = @intCast(new_value.len);
|
||||
self._selection_direction = .none;
|
||||
try self.dispatchSelectionChangeEvent(page);
|
||||
},
|
||||
.partial => |range| {
|
||||
// if the input is partially selected, replace the selected content.
|
||||
@@ -313,6 +340,7 @@ pub fn innerInsert(self: *Input, str: []const u8, page: *Page) !void {
|
||||
self._selection_start = @intCast(new_pos);
|
||||
self._selection_end = @intCast(new_pos);
|
||||
self._selection_direction = .none;
|
||||
try self.dispatchSelectionChangeEvent(page);
|
||||
},
|
||||
.none => {
|
||||
// if the input is not selected, just insert at cursor.
|
||||
@@ -332,9 +360,10 @@ pub fn getSelectionStart(self: *const Input) !?u32 {
|
||||
return self._selection_start;
|
||||
}
|
||||
|
||||
pub fn setSelectionStart(self: *Input, value: u32) !void {
|
||||
pub fn setSelectionStart(self: *Input, value: u32, page: *Page) !void {
|
||||
if (!self.selectionAvailable()) return error.InvalidStateError;
|
||||
self._selection_start = value;
|
||||
try self.dispatchSelectionChangeEvent(page);
|
||||
}
|
||||
|
||||
pub fn getSelectionEnd(self: *const Input) !?u32 {
|
||||
@@ -342,12 +371,19 @@ pub fn getSelectionEnd(self: *const Input) !?u32 {
|
||||
return self._selection_end;
|
||||
}
|
||||
|
||||
pub fn setSelectionEnd(self: *Input, value: u32) !void {
|
||||
pub fn setSelectionEnd(self: *Input, value: u32, page: *Page) !void {
|
||||
if (!self.selectionAvailable()) return error.InvalidStateError;
|
||||
self._selection_end = value;
|
||||
try self.dispatchSelectionChangeEvent(page);
|
||||
}
|
||||
|
||||
pub fn setSelectionRange(self: *Input, selection_start: u32, selection_end: u32, selection_dir: ?[]const u8) !void {
|
||||
pub fn setSelectionRange(
|
||||
self: *Input,
|
||||
selection_start: u32,
|
||||
selection_end: u32,
|
||||
selection_dir: ?[]const u8,
|
||||
page: *Page,
|
||||
) !void {
|
||||
if (!self.selectionAvailable()) return error.InvalidStateError;
|
||||
|
||||
const direction = blk: {
|
||||
@@ -375,6 +411,8 @@ pub fn setSelectionRange(self: *Input, selection_start: u32, selection_end: u32,
|
||||
self._selection_direction = direction;
|
||||
self._selection_start = start;
|
||||
self._selection_end = end;
|
||||
|
||||
try self.dispatchSelectionChangeEvent(page);
|
||||
}
|
||||
|
||||
pub fn getForm(self: *Input, page: *Page) ?*Form {
|
||||
@@ -401,6 +439,53 @@ pub fn getForm(self: *Input, page: *Page) ?*Form {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Sanitize the value according to the current input type
|
||||
fn sanitizeValue(self: *Input, value: []const u8, page: *Page) ![]const u8 {
|
||||
switch (self._input_type) {
|
||||
.text, .search, .tel, .password, .url, .email => {
|
||||
var i: usize = 0;
|
||||
const result = try page.call_arena.alloc(u8, value.len);
|
||||
for (value) |c| {
|
||||
if (c != '\r' and c != '\n') {
|
||||
result[i] = c;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
const sanitized = result[0..i];
|
||||
return switch (self._input_type) {
|
||||
.url, .email => std.mem.trim(u8, sanitized, &std.ascii.whitespace),
|
||||
else => sanitized,
|
||||
};
|
||||
},
|
||||
.date, .time, .@"datetime-local", .month, .week => {
|
||||
// TODO, we should sanitize this, but lack the necessary functions
|
||||
// datetime.zig could handle date and time, but not the other three
|
||||
// for now, allow al values.
|
||||
return value;
|
||||
},
|
||||
.number => {
|
||||
_ = std.fmt.parseFloat(f64, value) catch return "";
|
||||
return value;
|
||||
},
|
||||
.range => {
|
||||
// Range: default to "50" if invalid
|
||||
_ = std.fmt.parseFloat(f64, value) catch return "50";
|
||||
return value;
|
||||
},
|
||||
.color => {
|
||||
if (value.len == 7 and value[0] == '#') {
|
||||
for (value[1..]) |c| {
|
||||
if (!std.ascii.isHex(c)) return "#000000";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return "#000000";
|
||||
},
|
||||
.file => return "", // File: always empty
|
||||
.checkbox, .radio, .submit, .image, .reset, .button, .hidden => return value, // no sanitization
|
||||
}
|
||||
}
|
||||
|
||||
fn uncheckRadioGroup(self: *Input, page: *Page) !void {
|
||||
const element = self.asElement();
|
||||
|
||||
@@ -453,8 +538,9 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const onselectionchange = bridge.accessor(Input.getOnSelectionChange, Input.setOnSelectionChange, .{});
|
||||
pub const @"type" = bridge.accessor(Input.getType, Input.setType, .{});
|
||||
pub const value = bridge.accessor(Input.getValue, Input.setValue, .{});
|
||||
pub const value = bridge.accessor(Input.getValue, Input.setValue, .{ .dom_exception = true });
|
||||
pub const defaultValue = bridge.accessor(Input.getDefaultValue, Input.setDefaultValue, .{});
|
||||
pub const checked = bridge.accessor(Input.getChecked, Input.setChecked, .{});
|
||||
pub const defaultChecked = bridge.accessor(Input.getDefaultChecked, Input.setDefaultChecked, .{});
|
||||
@@ -505,7 +591,17 @@ pub const Build = struct {
|
||||
const attribute = std.meta.stringToEnum(enum { type, value, checked }, name.str()) orelse return;
|
||||
const self = element.as(Input);
|
||||
switch (attribute) {
|
||||
.type => self._input_type = Type.fromString(value.str()),
|
||||
.type => {
|
||||
self._input_type = Type.fromString(value.str());
|
||||
// Sanitize the current value according to the new type
|
||||
if (self._value) |current_value| {
|
||||
self._value = try self.sanitizeValue(current_value, page);
|
||||
// Apply default value for checkbox/radio if value is now empty
|
||||
if (self._value.?.len == 0 and (self._input_type == .checkbox or self._input_type == .radio)) {
|
||||
self._value = "on";
|
||||
}
|
||||
}
|
||||
},
|
||||
.value => self._default_value = try page.arena.dupe(u8, value.str()),
|
||||
.checked => {
|
||||
self._default_checked = true;
|
||||
|
||||
@@ -30,6 +30,9 @@ _proto: *HtmlElement,
|
||||
pub fn asElement(self: *Link) *Element {
|
||||
return self._proto._proto;
|
||||
}
|
||||
pub fn asConstElement(self: *const Link) *const Element {
|
||||
return self._proto._proto;
|
||||
}
|
||||
pub fn asNode(self: *Link) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
@@ -57,6 +60,14 @@ pub fn setRel(self: *Link, value: []const u8, page: *Page) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("rel"), .wrap(value), page);
|
||||
}
|
||||
|
||||
pub fn getAs(self: *const Link) []const u8 {
|
||||
return self.asConstElement().getAttributeSafe(comptime .wrap("as")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setAs(self: *Link, value: []const u8, page: *Page) !void {
|
||||
return self.asElement().setAttributeSafe(comptime .wrap("as"), .wrap(value), page);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Link);
|
||||
|
||||
@@ -66,6 +77,7 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const as = bridge.accessor(Link.getAs, Link.setAs, .{});
|
||||
pub const rel = bridge.accessor(Link.getRel, Link.setRel, .{});
|
||||
pub const href = bridge.accessor(Link.getHref, Link.setHref, .{});
|
||||
pub const relList = bridge.accessor(_getRelList, null, .{ .null_as_undefined = true });
|
||||
|
||||
@@ -284,16 +284,16 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const NETWORK_EMPTY = bridge.property(@intFromEnum(NetworkState.NETWORK_EMPTY));
|
||||
pub const NETWORK_IDLE = bridge.property(@intFromEnum(NetworkState.NETWORK_IDLE));
|
||||
pub const NETWORK_LOADING = bridge.property(@intFromEnum(NetworkState.NETWORK_LOADING));
|
||||
pub const NETWORK_NO_SOURCE = bridge.property(@intFromEnum(NetworkState.NETWORK_NO_SOURCE));
|
||||
pub const NETWORK_EMPTY = bridge.property(@intFromEnum(NetworkState.NETWORK_EMPTY), .{ .template = true });
|
||||
pub const NETWORK_IDLE = bridge.property(@intFromEnum(NetworkState.NETWORK_IDLE), .{ .template = true });
|
||||
pub const NETWORK_LOADING = bridge.property(@intFromEnum(NetworkState.NETWORK_LOADING), .{ .template = true });
|
||||
pub const NETWORK_NO_SOURCE = bridge.property(@intFromEnum(NetworkState.NETWORK_NO_SOURCE), .{ .template = true });
|
||||
|
||||
pub const HAVE_NOTHING = bridge.property(@intFromEnum(ReadyState.HAVE_NOTHING));
|
||||
pub const HAVE_METADATA = bridge.property(@intFromEnum(ReadyState.HAVE_METADATA));
|
||||
pub const HAVE_CURRENT_DATA = bridge.property(@intFromEnum(ReadyState.HAVE_CURRENT_DATA));
|
||||
pub const HAVE_FUTURE_DATA = bridge.property(@intFromEnum(ReadyState.HAVE_FUTURE_DATA));
|
||||
pub const HAVE_ENOUGH_DATA = bridge.property(@intFromEnum(ReadyState.HAVE_ENOUGH_DATA));
|
||||
pub const HAVE_NOTHING = bridge.property(@intFromEnum(ReadyState.HAVE_NOTHING), .{ .template = true });
|
||||
pub const HAVE_METADATA = bridge.property(@intFromEnum(ReadyState.HAVE_METADATA), .{ .template = true });
|
||||
pub const HAVE_CURRENT_DATA = bridge.property(@intFromEnum(ReadyState.HAVE_CURRENT_DATA), .{ .template = true });
|
||||
pub const HAVE_FUTURE_DATA = bridge.property(@intFromEnum(ReadyState.HAVE_FUTURE_DATA), .{ .template = true });
|
||||
pub const HAVE_ENOUGH_DATA = bridge.property(@intFromEnum(ReadyState.HAVE_ENOUGH_DATA), .{ .template = true });
|
||||
|
||||
pub const src = bridge.accessor(Media.getSrc, Media.setSrc, .{});
|
||||
pub const autoplay = bridge.accessor(Media.getAutoplay, Media.setAutoplay, .{});
|
||||
|
||||
30
src/browser/webapi/element/html/Picture.zig
Normal file
30
src/browser/webapi/element/html/Picture.zig
Normal file
@@ -0,0 +1,30 @@
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
|
||||
const Picture = @This();
|
||||
|
||||
_proto: *HtmlElement,
|
||||
|
||||
pub fn asElement(self: *Picture) *Element {
|
||||
return self._proto._proto;
|
||||
}
|
||||
pub fn asNode(self: *Picture) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Picture);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "HTMLPictureElement";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
};
|
||||
|
||||
const testing = @import("../../../../testing.zig");
|
||||
test "WebApi: Picture" {
|
||||
try testing.htmlRunner("element/html/picture.html", .{});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user