mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-30 17:18:57 +00:00
Compare commits
359 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 | ||
|
|
f02fc95958 | ||
|
|
175edca8c7 | ||
|
|
f1f0a66f41 | ||
|
|
496c6905af | ||
|
|
c84106570f | ||
|
|
1a05da9e55 | ||
|
|
232e7a1759 | ||
|
|
c440d41d57 | ||
|
|
dfe5c24404 | ||
|
|
eba5773d56 | ||
|
|
5d56fea2d3 | ||
|
|
946f02b7a2 | ||
|
|
8e8ffd21d5 | ||
|
|
d02d974cd0 | ||
|
|
0a68be695d | ||
|
|
335e781d0c | ||
|
|
9f5c2e4ca7 | ||
|
|
76a53bedbe | ||
|
|
b0bc84ed21 | ||
|
|
ae298fc2e6 | ||
|
|
3b809b2910 | ||
|
|
68fbc0bde3 | ||
|
|
9d8e5263a6 | ||
|
|
7eb026cc0d | ||
|
|
e51e6aa2b0 | ||
|
|
bc700d2044 | ||
|
|
30ed58ff07 | ||
|
|
066069baad | ||
|
|
068ec68917 | ||
|
|
560f028bda | ||
|
|
fd1e77df8f | ||
|
|
864ac08f16 | ||
|
|
6ad1a11593 | ||
|
|
89174ba0b6 | ||
|
|
fc5496e570 | ||
|
|
fd21d952ac | ||
|
|
073fea2bde | ||
|
|
e548712f5e | ||
|
|
c3ba83ff93 | ||
|
|
451dd0fd64 | ||
|
|
aa805c2428 | ||
|
|
58a7590aff | ||
|
|
563ab30564 | ||
|
|
5050b34361 | ||
|
|
3bb86f196b | ||
|
|
51dca3be11 | ||
|
|
adeda6cd75 | ||
|
|
09665c3a4a | ||
|
|
8f5f6212d2 | ||
|
|
a11ae912b4 | ||
|
|
3b12240615 | ||
|
|
862520e4b1 | ||
|
|
a3d2dd8366 | ||
|
|
16ef487871 | ||
|
|
54c45a0cfd | ||
|
|
1f14eb62d4 | ||
|
|
0db86a8b3d | ||
|
|
c63c85071a | ||
|
|
b63d93e325 | ||
|
|
12c6e50e16 | ||
|
|
53ccc2e04c | ||
|
|
2d3234b54d | ||
|
|
9a57c2a0d4 | ||
|
|
fc64abee8f | ||
|
|
d5f26f6d15 | ||
|
|
97f9c2991b | ||
|
|
81378d4353 | ||
|
|
9f0c902030 | ||
|
|
3c0c75be10 | ||
|
|
90d23abe18 | ||
|
|
82eccf36d4 | ||
|
|
342cb52887 | ||
|
|
cafa4f5173 | ||
|
|
67cff5af8b | ||
|
|
6d23d91aa5 | ||
|
|
3a0699fc1d | ||
|
|
027e569087 | ||
|
|
830f759f0b | ||
|
|
969891c71c | ||
|
|
4eb5c3e907 | ||
|
|
23303a759b | ||
|
|
d1e7f46994 | ||
|
|
65ea70ae90 | ||
|
|
7522b71c86 | ||
|
|
70625c86c3 | ||
|
|
74354d2027 | ||
|
|
f6397e2731 | ||
|
|
065ca39d60 | ||
|
|
b4759ae261 | ||
|
|
c095950ef9 | ||
|
|
24b7035b1b | ||
|
|
7b1f157cf8 | ||
|
|
8b8bee4e9c | ||
|
|
c27ab35600 | ||
|
|
446b4dc461 | ||
|
|
ff8ed24622 | ||
|
|
ae2d6a122b | ||
|
|
3cac375f21 | ||
|
|
7d806dd161 | ||
|
|
db037c704e | ||
|
|
954184f742 | ||
|
|
7650e0b61a | ||
|
|
4a5c93988f | ||
|
|
8ceaf0ac66 | ||
|
|
ca60aa1cc6 | ||
|
|
596d5906a0 | ||
|
|
c02db94522 | ||
|
|
3970803575 | ||
|
|
43805ad698 | ||
|
|
2498e12f19 | ||
|
|
6f3cb4b48e | ||
|
|
fbd047599e | ||
|
|
da00117622 | ||
|
|
e44c73bdf6 | ||
|
|
e3cb7bd9f0 | ||
|
|
08f5889ee5 | ||
|
|
d5bfe74e1a | ||
|
|
d7015fa3b6 | ||
|
|
9092651b5b | ||
|
|
2c53b48e0a | ||
|
|
319a1c3367 | ||
|
|
80dd590e8f | ||
|
|
992a8e8774 | ||
|
|
f56d3bd193 | ||
|
|
4ecc59d0c0 | ||
|
|
5ebf82874b | ||
|
|
12670a3153 | ||
|
|
fa3a23134e | ||
|
|
8291044abc | ||
|
|
505e0799da | ||
|
|
be1d463775 | ||
|
|
a6fc5aa345 | ||
|
|
0e6e4db08b | ||
|
|
0edc1fcec7 | ||
|
|
b46d3b22e2 | ||
|
|
412c881cd4 | ||
|
|
48f07a110f | ||
|
|
5c1b7935e2 | ||
|
|
62aa564df1 | ||
|
|
798ee4a4d5 | ||
|
|
7d87fb80ec |
14
.github/actions/install/action.yml
vendored
14
.github/actions/install/action.yml
vendored
@@ -13,7 +13,7 @@ inputs:
|
|||||||
zig-v8:
|
zig-v8:
|
||||||
description: 'zig v8 version to install'
|
description: 'zig v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
default: 'v0.2.4'
|
default: 'v0.2.8'
|
||||||
v8:
|
v8:
|
||||||
description: 'v8 version to install'
|
description: 'v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
@@ -22,6 +22,10 @@ inputs:
|
|||||||
description: 'cache dir to use'
|
description: 'cache dir to use'
|
||||||
required: false
|
required: false
|
||||||
default: '~/.cache'
|
default: '~/.cache'
|
||||||
|
debug:
|
||||||
|
description: 'enable v8 pre-built debug version, only available for linux x86_64'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
@@ -32,7 +36,7 @@ runs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang
|
sudo apt-get install -y wget xz-utils ca-certificates clang make git
|
||||||
|
|
||||||
# Zig version used from the `minimum_zig_version` field in build.zig.zon
|
# Zig version used from the `minimum_zig_version` field in build.zig.zon
|
||||||
- uses: mlugg/setup-zig@v2
|
- uses: mlugg/setup-zig@v2
|
||||||
@@ -47,17 +51,17 @@ runs:
|
|||||||
cache-name: cache-v8
|
cache-name: cache-v8
|
||||||
with:
|
with:
|
||||||
path: ${{ inputs.cache-dir }}/v8
|
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' }}
|
- if: ${{ steps.cache-v8.outputs.cache-hit != 'true' }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ${{ inputs.cache-dir }}/v8
|
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
|
- name: install v8
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
mkdir -p v8
|
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:
|
with:
|
||||||
os: ${{env.OS}}
|
os: ${{env.OS}}
|
||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
mode: 'release'
|
|
||||||
|
|
||||||
- name: v8 snapshot
|
- name: v8 snapshot
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
@@ -83,7 +82,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
os: ${{env.OS}}
|
os: ${{env.OS}}
|
||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
mode: 'release'
|
|
||||||
|
|
||||||
- name: v8 snapshot
|
- name: v8 snapshot
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
@@ -128,7 +126,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
os: ${{env.OS}}
|
os: ${{env.OS}}
|
||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
mode: 'release'
|
|
||||||
|
|
||||||
- name: v8 snapshot
|
- name: v8 snapshot
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
@@ -171,7 +168,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
os: ${{env.OS}}
|
os: ${{env.OS}}
|
||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
mode: 'release'
|
|
||||||
|
|
||||||
- name: v8 snapshot
|
- name: v8 snapshot
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|||||||
6
.github/workflows/e2e-test.yml
vendored
6
.github/workflows/e2e-test.yml
vendored
@@ -56,8 +56,6 @@ jobs:
|
|||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
with:
|
|
||||||
mode: 'release'
|
|
||||||
|
|
||||||
- name: zig build 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 }})
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||||
@@ -124,8 +122,8 @@ jobs:
|
|||||||
needs: zig-build-release
|
needs: zig-build-release
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MAX_MEMORY: 28000
|
MAX_MEMORY: 26000
|
||||||
MAX_AVG_DURATION: 23
|
MAX_AVG_DURATION: 17
|
||||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||||
|
|
||||||
# use a self host runner.
|
# use a self host runner.
|
||||||
|
|||||||
23
.github/workflows/zig-test.yml
vendored
23
.github/workflows/zig-test.yml
vendored
@@ -12,8 +12,7 @@ on:
|
|||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- "build.zig"
|
- "build.zig"
|
||||||
- "src/**/*.zig"
|
- "src/**"
|
||||||
- "src/*.zig"
|
|
||||||
- "vendor/zig-js-runtime"
|
- "vendor/zig-js-runtime"
|
||||||
- ".github/**"
|
- ".github/**"
|
||||||
- "vendor/**"
|
- "vendor/**"
|
||||||
@@ -38,6 +37,26 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
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:
|
zig-test:
|
||||||
name: zig test
|
name: zig test
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ FROM debian:stable-slim
|
|||||||
ARG MINISIG=0.12
|
ARG MINISIG=0.12
|
||||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||||
ARG V8=14.0.365.4
|
ARG V8=14.0.365.4
|
||||||
ARG ZIG_V8=v0.2.4
|
ARG ZIG_V8=v0.2.8
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
RUN apt-get update -yq && \
|
RUN apt-get update -yq && \
|
||||||
apt-get install -yq xz-utils ca-certificates \
|
apt-get install -yq xz-utils ca-certificates \
|
||||||
|
pkg-config libglib2.0-dev \
|
||||||
clang make curl git
|
clang make curl git
|
||||||
|
|
||||||
# Get Rust
|
# Get Rust
|
||||||
|
|||||||
44
README.md
44
README.md
@@ -78,23 +78,49 @@ docker run -d --name lightpanda -p 9222:9222 lightpanda/browser:nightly
|
|||||||
### Dump a URL
|
### Dump a URL
|
||||||
|
|
||||||
```console
|
```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
|
```console
|
||||||
info(browser): GET https://lightpanda.io/ http.Status.ok
|
INFO telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
||||||
info(browser): fetch script https://api.website.lightpanda.io/js/script.js: http.Status.ok
|
disabled = false
|
||||||
info(browser): eval remote https://api.website.lightpanda.io/js/script.js: TypeError: Cannot read properties of undefined (reading 'pushState')
|
|
||||||
|
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>
|
<!DOCTYPE html>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Start a CDP server
|
### Start a CDP server
|
||||||
|
|
||||||
```console
|
```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
|
```console
|
||||||
info(websocket): starting blocking worker to listen on 127.0.0.1:9222
|
INFO telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
||||||
info(server): accepting new conn...
|
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
|
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();
|
const page = await context.newPage();
|
||||||
|
|
||||||
// Dump all the links from the page.
|
// 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(() => {
|
const links = await page.evaluate(() => {
|
||||||
return Array.from(document.querySelectorAll('a')).map(row => {
|
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] Custom HTTP headers
|
||||||
- [x] Proxy support
|
- [x] Proxy support
|
||||||
- [x] Network interception
|
- [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.
|
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
||||||
|
|
||||||
@@ -178,6 +205,7 @@ For **Debian/Ubuntu based Linux**:
|
|||||||
|
|
||||||
```
|
```
|
||||||
sudo apt install xz-utils ca-certificates \
|
sudo apt install xz-utils ca-certificates \
|
||||||
|
pkg-config libglib2.0-dev \
|
||||||
clang make curl git
|
clang make curl git
|
||||||
```
|
```
|
||||||
You also need to [install Rust](https://rust-lang.org/tools/install/).
|
You also need to [install Rust](https://rust-lang.org/tools/install/).
|
||||||
|
|||||||
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, "git_commit", git_commit orelse "dev");
|
||||||
opts.addOption(?[]const u8, "snapshot_path", snapshot_path);
|
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 enable_csan = b.option(std.zig.SanitizeC, "csan", "Enable C Sanitizers");
|
||||||
|
|
||||||
const lightpanda_module = blk: {
|
const lightpanda_module = blk: {
|
||||||
@@ -50,7 +51,7 @@ pub fn build(b: *Build) !void {
|
|||||||
});
|
});
|
||||||
mod.addImport("lightpanda", mod); // allow circular "lightpanda" import
|
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;
|
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());
|
mod.addImport("build_config", opts.createModule());
|
||||||
|
|
||||||
const target = mod.resolved_target.?;
|
const target = mod.resolved_target.?;
|
||||||
const dep_opts = .{
|
const dep_opts = .{
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = mod.optimize.?,
|
.optimize = mod.optimize.?,
|
||||||
.prebuilt_v8_path = prebuilt_v8_path,
|
|
||||||
.cache_root = b.pathFromRoot(".lp-cache"),
|
.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"));
|
mod.addIncludePath(b.path("vendor/lightpanda"));
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
.minimum_zig_version = "0.15.2",
|
.minimum_zig_version = "0.15.2",
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
.v8 = .{
|
.v8 = .{
|
||||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/v0.2.4.tar.gz",
|
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.2.8.tar.gz",
|
||||||
.hash = "v8-0.0.0-xddH66YvBAD0YI9xr6F0Xgnw9wN30FdZ10FLyuoV3e66",
|
.hash = "v8-0.0.0-xddH63lfBADJ7UE2zAJ8nJIBnxoSiimXSaX6Q_M_7DS3",
|
||||||
},
|
},
|
||||||
// .v8 = .{ .path = "../zig-v8-fork" },
|
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||||
.@"boringssl-zig" = .{
|
.@"boringssl-zig" = .{
|
||||||
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
|
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
|
||||||
.hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK",
|
.hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK",
|
||||||
|
|||||||
24
flake.lock
generated
24
flake.lock
generated
@@ -8,11 +8,11 @@
|
|||||||
"rust-analyzer-src": "rust-analyzer-src"
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763016383,
|
"lastModified": 1770708269,
|
||||||
"narHash": "sha256-eYmo7FNvm3q08iROzwIi8i9dWuUbJJl3uLR3OLnSmdI=",
|
"narHash": "sha256-OnZW86app7hHJJoB5lC9GNXY5QBBIESJB+sIdwEyld0=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "fenix",
|
"repo": "fenix",
|
||||||
"rev": "0fad5c0e5c531358e7174cd666af4608f08bc3ba",
|
"rev": "6b5325a017a9a9fe7e6252ccac3680cc7181cd63",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -96,11 +96,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763043403,
|
"lastModified": 1768649915,
|
||||||
"narHash": "sha256-DgCTbHdIpzbXSlQlOZEWj8oPt2lrRMlSk03oIstvkVQ=",
|
"narHash": "sha256-jc21hKogFnxU7KXSVTRmxC7u5D4RHwm9BAvDf5/Z1Uo=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "75e04ecd084f93d4105ce68c07dac7656291fe2e",
|
"rev": "3e3f3c7f9977dc123c23ee21e8085ed63daf8c37",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -122,11 +122,11 @@
|
|||||||
"rust-analyzer-src": {
|
"rust-analyzer-src": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1762860488,
|
"lastModified": 1770668050,
|
||||||
"narHash": "sha256-rMfWMCOo/pPefM2We0iMBLi2kLBAnYoB9thi4qS7uk4=",
|
"narHash": "sha256-Q05yaIZtQrBKHpyWaPmyJmDRj0lojnVf8nUFE0vydcY=",
|
||||||
"owner": "rust-lang",
|
"owner": "rust-lang",
|
||||||
"repo": "rust-analyzer",
|
"repo": "rust-analyzer",
|
||||||
"rev": "2efc80078029894eec0699f62ec8d5c1a56af763",
|
"rev": "9efc1f709f3c8134c3acac5d3592a8e4c184a0c6",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -175,11 +175,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1762907712,
|
"lastModified": 1770598090,
|
||||||
"narHash": "sha256-VNW/+VYIg6N4b9Iq+F0YZmm22n74IdFS7hsPLblWuOY=",
|
"narHash": "sha256-k+82IDgTd9o5sxHIqGlvfwseKln3Ejx1edGtDltuPXo=",
|
||||||
"owner": "mitchellh",
|
"owner": "mitchellh",
|
||||||
"repo": "zig-overlay",
|
"repo": "zig-overlay",
|
||||||
"rev": "d16453ee78765e49527c56d23386cead799b6b53",
|
"rev": "142495696982c88edddc8e17e4da90d8164acadf",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
60
src/App.zig
60
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>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -21,66 +21,38 @@ const std = @import("std");
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const log = @import("log.zig");
|
const log = @import("log.zig");
|
||||||
const Http = @import("http/Http.zig");
|
const Config = @import("Config.zig");
|
||||||
const Snapshot = @import("browser/js/Snapshot.zig");
|
const Snapshot = @import("browser/js/Snapshot.zig");
|
||||||
const Platform = @import("browser/js/Platform.zig");
|
const Platform = @import("browser/js/Platform.zig");
|
||||||
|
|
||||||
const Notification = @import("Notification.zig");
|
|
||||||
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
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");
|
||||||
|
|
||||||
// Container for global state / objects that various parts of the system
|
|
||||||
// might need.
|
|
||||||
const App = @This();
|
const App = @This();
|
||||||
|
|
||||||
http: Http,
|
http: Http,
|
||||||
config: Config,
|
config: *const Config,
|
||||||
platform: Platform,
|
platform: Platform,
|
||||||
snapshot: Snapshot,
|
snapshot: Snapshot,
|
||||||
telemetry: Telemetry,
|
telemetry: Telemetry,
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
|
arena_pool: ArenaPool,
|
||||||
|
robots: RobotStore,
|
||||||
app_dir_path: ?[]const u8,
|
app_dir_path: ?[]const u8,
|
||||||
notification: *Notification,
|
|
||||||
shutdown: bool = false,
|
shutdown: bool = false,
|
||||||
|
|
||||||
pub const RunMode = enum {
|
pub fn init(allocator: Allocator, config: *const Config) !*App {
|
||||||
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 {
|
|
||||||
const app = try allocator.create(App);
|
const app = try allocator.create(App);
|
||||||
errdefer allocator.destroy(app);
|
errdefer allocator.destroy(app);
|
||||||
|
|
||||||
app.config = config;
|
app.config = config;
|
||||||
app.allocator = allocator;
|
app.allocator = allocator;
|
||||||
|
|
||||||
app.notification = try Notification.init(allocator, null);
|
app.robots = RobotStore.init(allocator);
|
||||||
errdefer app.notification.deinit();
|
|
||||||
|
|
||||||
app.http = try Http.init(allocator, .{
|
app.http = try Http.init(allocator, &app.robots, config);
|
||||||
.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,
|
|
||||||
});
|
|
||||||
errdefer app.http.deinit();
|
errdefer app.http.deinit();
|
||||||
|
|
||||||
app.platform = try Platform.init();
|
app.platform = try Platform.init();
|
||||||
@@ -91,10 +63,11 @@ pub fn init(allocator: Allocator, config: Config) !*App {
|
|||||||
|
|
||||||
app.app_dir_path = getAndMakeAppDir(allocator);
|
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();
|
errdefer app.telemetry.deinit();
|
||||||
|
|
||||||
try app.telemetry.register(app.notification);
|
app.arena_pool = ArenaPool.init(allocator);
|
||||||
|
errdefer app.arena_pool.deinit();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
@@ -110,10 +83,11 @@ pub fn deinit(self: *App) void {
|
|||||||
self.app_dir_path = null;
|
self.app_dir_path = null;
|
||||||
}
|
}
|
||||||
self.telemetry.deinit();
|
self.telemetry.deinit();
|
||||||
self.notification.deinit();
|
self.robots.deinit();
|
||||||
self.http.deinit();
|
self.http.deinit();
|
||||||
self.snapshot.deinit();
|
self.snapshot.deinit();
|
||||||
self.platform.deinit();
|
self.platform.deinit();
|
||||||
|
self.arena_pool.deinit();
|
||||||
|
|
||||||
allocator.destroy(self);
|
allocator.destroy(self);
|
||||||
}
|
}
|
||||||
|
|||||||
87
src/ArenaPool.zig
Normal file
87
src/ArenaPool.zig
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// 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 Allocator = std.mem.Allocator;
|
||||||
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
|
const ArenaPool = @This();
|
||||||
|
|
||||||
|
allocator: Allocator,
|
||||||
|
retain_bytes: usize,
|
||||||
|
free_list_len: u16 = 0,
|
||||||
|
free_list: ?*Entry = null,
|
||||||
|
free_list_max: u16,
|
||||||
|
entry_pool: std.heap.MemoryPool(Entry),
|
||||||
|
|
||||||
|
const Entry = struct {
|
||||||
|
next: ?*Entry,
|
||||||
|
arena: ArenaAllocator,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator) ArenaPool {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.free_list_max = 512, // TODO make configurable
|
||||||
|
.retain_bytes = 1024 * 16, // TODO make configurable
|
||||||
|
.entry_pool = std.heap.MemoryPool(Entry).init(allocator),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *ArenaPool) void {
|
||||||
|
var entry = self.free_list;
|
||||||
|
while (entry) |e| {
|
||||||
|
entry = e.next;
|
||||||
|
e.arena.deinit();
|
||||||
|
}
|
||||||
|
self.entry_pool.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = try self.entry_pool.create();
|
||||||
|
entry.* = .{
|
||||||
|
.next = null,
|
||||||
|
.arena = ArenaAllocator.init(self.allocator),
|
||||||
|
};
|
||||||
|
|
||||||
|
return entry.arena.allocator();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn release(self: *ArenaPool, allocator: Allocator) void {
|
||||||
|
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
||||||
|
const entry: *Entry = @fieldParentPtr("arena", arena);
|
||||||
|
|
||||||
|
const free_list_len = self.free_list_len;
|
||||||
|
if (free_list_len == self.free_list_max) {
|
||||||
|
arena.deinit();
|
||||||
|
self.entry_pool.destroy(entry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = 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
|
// 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
|
// 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.
|
// 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
|
// That is, it would work until multiple CDP clients connect, and because
|
||||||
// because everything's just one big global, that gets picked up by the
|
// everything's just one big global, events from one CDP session would be sent
|
||||||
// registered CDP listener, and the telemetry network activity gets sent to the
|
// to all CDP clients.
|
||||||
// CDP client.
|
|
||||||
//
|
//
|
||||||
// To avoid this, one way or another, we need scoping. We could still have
|
// 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
|
// 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.
|
// between components to share a common scope.
|
||||||
//
|
//
|
||||||
// Instead, the approach that we take is to have a notification instance per
|
// 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
|
// CDP connection (BrowserContext). Each CDP connection has its own notification
|
||||||
// notification instances at a given time: one in a Browser and one in the App.
|
// that is shared across all Sessions (tabs) within that connection. This ensures
|
||||||
// What about something like Telemetry, which lives outside of a Browser but
|
// proper isolation between different CDP clients while allowing a single client
|
||||||
// still cares about Browser-events (like .page_navigate)? When the Browser
|
// to receive events from all its tabs.
|
||||||
// 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.
|
|
||||||
const Notification = @This();
|
const Notification = @This();
|
||||||
// Every event type (which are hard-coded), has a list of Listeners.
|
// Every event type (which are hard-coded), has a list of Listeners.
|
||||||
// When the event happens, we dispatch to those listener.
|
// When the event happens, we dispatch to those listener.
|
||||||
@@ -66,7 +61,7 @@ event_listeners: EventListeners,
|
|||||||
// list of listeners for a specified receiver
|
// list of listeners for a specified receiver
|
||||||
// @intFromPtr(receiver) -> [listener1, listener2, ...]
|
// @intFromPtr(receiver) -> [listener1, listener2, ...]
|
||||||
// Used when `unregisterAll` is called.
|
// Used when `unregisterAll` is called.
|
||||||
listeners: std.AutoHashMapUnmanaged(usize, std.ArrayListUnmanaged(*Listener)),
|
listeners: std.AutoHashMapUnmanaged(usize, std.ArrayList(*Listener)),
|
||||||
|
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
mem_pool: std.heap.MemoryPool(Listener),
|
mem_pool: std.heap.MemoryPool(Listener),
|
||||||
@@ -85,7 +80,6 @@ const EventListeners = struct {
|
|||||||
http_request_auth_required: List = .{},
|
http_request_auth_required: List = .{},
|
||||||
http_response_data: List = .{},
|
http_response_data: List = .{},
|
||||||
http_response_header_done: List = .{},
|
http_response_header_done: List = .{},
|
||||||
notification_created: List = .{},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Events = union(enum) {
|
const Events = union(enum) {
|
||||||
@@ -102,7 +96,6 @@ const Events = union(enum) {
|
|||||||
http_request_done: *const RequestDone,
|
http_request_done: *const RequestDone,
|
||||||
http_response_data: *const ResponseData,
|
http_response_data: *const ResponseData,
|
||||||
http_response_header_done: *const ResponseHeaderDone,
|
http_response_header_done: *const ResponseHeaderDone,
|
||||||
notification_created: *Notification,
|
|
||||||
};
|
};
|
||||||
const EventType = std.meta.FieldEnum(Events);
|
const EventType = std.meta.FieldEnum(Events);
|
||||||
|
|
||||||
@@ -162,12 +155,7 @@ pub const RequestFail = struct {
|
|||||||
err: anyerror,
|
err: anyerror,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, parent: ?*Notification) !*Notification {
|
pub fn init(allocator: Allocator) !*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.
|
|
||||||
const notification = try allocator.create(Notification);
|
const notification = try allocator.create(Notification);
|
||||||
errdefer allocator.destroy(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),
|
.mem_pool = std.heap.MemoryPool(Listener).init(allocator),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (parent) |pn| {
|
|
||||||
pn.dispatch(.notification_created, notification);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 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 {
|
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));
|
const list = &@field(self.event_listeners, @tagName(event));
|
||||||
|
|
||||||
var node = list.first;
|
var node = list.first;
|
||||||
@@ -313,7 +300,7 @@ const Listener = struct {
|
|||||||
|
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
test "Notification" {
|
test "Notification" {
|
||||||
var notifier = try Notification.init(testing.allocator, null);
|
var notifier = try Notification.init(testing.allocator);
|
||||||
defer notifier.deinit();
|
defer notifier.deinit();
|
||||||
|
|
||||||
// noop
|
// noop
|
||||||
|
|||||||
@@ -205,7 +205,6 @@ fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: u32) !void {
|
|||||||
}
|
}
|
||||||
ms_remaining -= @intCast(elapsed);
|
ms_remaining -= @intCast(elapsed);
|
||||||
},
|
},
|
||||||
.navigate => unreachable, // must have been handled by the session
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -561,7 +560,7 @@ pub const Client = struct {
|
|||||||
|
|
||||||
pub fn sendJSONRaw(
|
pub fn sendJSONRaw(
|
||||||
self: *Client,
|
self: *Client,
|
||||||
buf: std.ArrayListUnmanaged(u8),
|
buf: std.ArrayList(u8),
|
||||||
) !void {
|
) !void {
|
||||||
// Dangerous API!. We assume the caller has reserved the first 10
|
// Dangerous API!. We assume the caller has reserved the first 10
|
||||||
// bytes in `buf`.
|
// bytes in `buf`.
|
||||||
@@ -883,7 +882,7 @@ fn growBuffer(allocator: Allocator, buf: []u8, required_capacity: usize) ![]u8 {
|
|||||||
|
|
||||||
const Fragments = struct {
|
const Fragments = struct {
|
||||||
type: Message.Type,
|
type: Message.Type,
|
||||||
message: std.ArrayListUnmanaged(u8),
|
message: std.ArrayList(u8),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Message = struct {
|
const Message = struct {
|
||||||
@@ -907,7 +906,7 @@ const OpCode = enum(u8) {
|
|||||||
pong = 128 | 10,
|
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
|
// can't use buf[0..10] here, because the header length
|
||||||
// is variable. If it's just 2 bytes, for example, we need the
|
// is variable. If it's just 2 bytes, for example, we need the
|
||||||
// framed message to be:
|
// framed message to be:
|
||||||
@@ -1342,7 +1341,7 @@ fn assertWebSocketMessage(expected: []const u8, input: []const u8) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MockCDP = struct {
|
const MockCDP = struct {
|
||||||
messages: std.ArrayListUnmanaged([]const u8) = .{},
|
messages: std.ArrayList([]const u8) = .{},
|
||||||
|
|
||||||
allocator: Allocator = testing.allocator,
|
allocator: Allocator = testing.allocator,
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const std = @import("std");
|
|||||||
|
|
||||||
const TestHTTPServer = @This();
|
const TestHTTPServer = @This();
|
||||||
|
|
||||||
shutdown: bool,
|
shutdown: std.atomic.Value(bool),
|
||||||
listener: ?std.net.Server,
|
listener: ?std.net.Server,
|
||||||
handler: Handler,
|
handler: Handler,
|
||||||
|
|
||||||
@@ -28,16 +28,23 @@ const Handler = *const fn (req: *std.http.Server.Request) anyerror!void;
|
|||||||
|
|
||||||
pub fn init(handler: Handler) TestHTTPServer {
|
pub fn init(handler: Handler) TestHTTPServer {
|
||||||
return .{
|
return .{
|
||||||
.shutdown = true,
|
.shutdown = .init(true),
|
||||||
.listener = null,
|
.listener = null,
|
||||||
.handler = handler,
|
.handler = handler,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *TestHTTPServer) void {
|
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| {
|
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 });
|
self.listener = try address.listen(.{ .reuse_address = true });
|
||||||
var listener = &self.listener.?;
|
var listener = &self.listener.?;
|
||||||
|
self.shutdown.store(false, .release);
|
||||||
|
|
||||||
wg.finish();
|
wg.finish();
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const conn = listener.accept() catch |err| {
|
const conn = listener.accept() catch |err| {
|
||||||
if (self.shutdown) {
|
if (self.shutdown.load(.acquire) or err == error.SocketNotListening) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return err;
|
return err;
|
||||||
|
|||||||
@@ -24,12 +24,14 @@ const ArenaAllocator = std.heap.ArenaAllocator;
|
|||||||
const js = @import("js/js.zig");
|
const js = @import("js/js.zig");
|
||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
const App = @import("../App.zig");
|
const App = @import("../App.zig");
|
||||||
const HttpClient = @import("../http/Client.zig");
|
|
||||||
const Notification = @import("../Notification.zig");
|
const ArenaPool = App.ArenaPool;
|
||||||
|
const HttpClient = App.Http.Client;
|
||||||
|
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
const Session = @import("Session.zig");
|
const Session = @import("Session.zig");
|
||||||
|
const Notification = @import("../Notification.zig");
|
||||||
|
|
||||||
// Browser is an instance of the browser.
|
// Browser is an instance of the browser.
|
||||||
// You can create multiple browser instances.
|
// You can create multiple browser instances.
|
||||||
@@ -40,30 +42,29 @@ env: js.Env,
|
|||||||
app: *App,
|
app: *App,
|
||||||
session: ?Session,
|
session: ?Session,
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
|
arena_pool: *ArenaPool,
|
||||||
http_client: *HttpClient,
|
http_client: *HttpClient,
|
||||||
call_arena: ArenaAllocator,
|
call_arena: ArenaAllocator,
|
||||||
page_arena: ArenaAllocator,
|
page_arena: ArenaAllocator,
|
||||||
session_arena: ArenaAllocator,
|
session_arena: ArenaAllocator,
|
||||||
transfer_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;
|
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();
|
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 .{
|
return .{
|
||||||
.app = app,
|
.app = app,
|
||||||
.env = env,
|
.env = env,
|
||||||
.session = null,
|
.session = null,
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.notification = notification,
|
.arena_pool = &app.arena_pool,
|
||||||
.http_client = app.http.client,
|
.http_client = app.http.client,
|
||||||
.call_arena = ArenaAllocator.init(allocator),
|
.call_arena = ArenaAllocator.init(allocator),
|
||||||
.page_arena = ArenaAllocator.init(allocator),
|
.page_arena = ArenaAllocator.init(allocator),
|
||||||
@@ -79,15 +80,13 @@ pub fn deinit(self: *Browser) void {
|
|||||||
self.page_arena.deinit();
|
self.page_arena.deinit();
|
||||||
self.session_arena.deinit();
|
self.session_arena.deinit();
|
||||||
self.transfer_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.closeSession();
|
||||||
self.session = @as(Session, undefined);
|
self.session = @as(Session, undefined);
|
||||||
const session = &self.session.?;
|
const session = &self.session.?;
|
||||||
try Session.init(session, self);
|
try Session.init(session, self, notification);
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +95,7 @@ pub fn closeSession(self: *Browser) void {
|
|||||||
session.deinit();
|
session.deinit();
|
||||||
self.session = null;
|
self.session = null;
|
||||||
_ = self.session_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
_ = self.session_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||||
self.env.lowMemoryNotification();
|
self.env.memoryPressureNotification(.critical);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +103,10 @@ pub fn runMicrotasks(self: *const Browser) void {
|
|||||||
self.env.runMicrotasks();
|
self.env.runMicrotasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn runMacrotasks(self: *Browser) !?u64 {
|
||||||
|
return try self.env.runMacrotasks();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn runMessageLoop(self: *const Browser) void {
|
pub fn runMessageLoop(self: *const Browser) void {
|
||||||
while (self.env.pumpMessageLoop()) {
|
while (self.env.pumpMessageLoop()) {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
|
|||||||
@@ -33,13 +33,36 @@ const Allocator = std.mem.Allocator;
|
|||||||
|
|
||||||
const IS_DEBUG = builtin.mode == .Debug;
|
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();
|
pub const EventManager = @This();
|
||||||
|
|
||||||
page: *Page,
|
page: *Page,
|
||||||
arena: Allocator,
|
arena: Allocator,
|
||||||
listener_pool: std.heap.MemoryPool(Listener),
|
listener_pool: std.heap.MemoryPool(Listener),
|
||||||
list_pool: std.heap.MemoryPool(std.DoublyLinkedList),
|
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,
|
dispatch_depth: usize,
|
||||||
deferred_removals: std.ArrayList(struct { list: *std.DoublyLinkedList, listener: *Listener }),
|
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 {
|
pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, opts: RegisterOptions) !void {
|
||||||
if (comptime IS_DEBUG) {
|
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
|
// 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) {
|
if (gop.found_existing) {
|
||||||
// check for duplicate callbacks already registered
|
// check for duplicate callbacks already registered
|
||||||
var node = gop.value_ptr.*.first;
|
var node = gop.value_ptr.*.first;
|
||||||
while (node) |n| {
|
while (node) |n| {
|
||||||
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||||
if (listener.typ.eqlSlice(typ)) {
|
const is_duplicate = switch (callback) {
|
||||||
const is_duplicate = switch (callback) {
|
.object => |obj| listener.function.eqlObject(obj),
|
||||||
.object => |obj| listener.function.eqlObject(obj),
|
.function => |func| listener.function.eqlFunction(func),
|
||||||
.function => |func| listener.function.eqlFunction(func),
|
};
|
||||||
};
|
if (is_duplicate and listener.capture == opts.capture) {
|
||||||
if (is_duplicate and listener.capture == opts.capture) {
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
node = n.next;
|
node = n.next;
|
||||||
}
|
}
|
||||||
@@ -114,20 +141,34 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
|||||||
.passive = opts.passive,
|
.passive = opts.passive,
|
||||||
.function = func,
|
.function = func,
|
||||||
.signal = opts.signal,
|
.signal = opts.signal,
|
||||||
.typ = try String.init(self.arena, typ, .{}),
|
.typ = type_string,
|
||||||
};
|
};
|
||||||
// append the listener to the list of listeners for this target
|
// append the listener to the list of listeners for this target
|
||||||
gop.value_ptr.*.append(&listener.node);
|
gop.value_ptr.*.append(&listener.node);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void {
|
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;
|
const list = self.lookup.get(.{
|
||||||
if (findListener(list, typ, callback, use_capture)) |listener| {
|
.type_string = .wrap(typ),
|
||||||
|
.event_target = @intFromPtr(target),
|
||||||
|
}) orelse return;
|
||||||
|
if (findListener(list, callback, use_capture)) |listener| {
|
||||||
self.removeListener(list, 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) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
|
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
|
||||||
}
|
}
|
||||||
@@ -137,7 +178,10 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void
|
|||||||
var was_handled = false;
|
var was_handled = false;
|
||||||
|
|
||||||
defer if (was_handled) {
|
defer if (was_handled) {
|
||||||
self.page.js.runMicrotasks();
|
var ls: js.Local.Scope = undefined;
|
||||||
|
self.page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
ls.local.runMicrotasks();
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (target._type) {
|
switch (target._type) {
|
||||||
@@ -151,9 +195,13 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void
|
|||||||
.navigation,
|
.navigation,
|
||||||
.screen,
|
.screen,
|
||||||
.screen_orientation,
|
.screen_orientation,
|
||||||
|
.visual_viewport,
|
||||||
.generic,
|
.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);
|
try self.dispatchAll(list, target, event, &was_handled);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -180,7 +228,10 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
|
|||||||
|
|
||||||
var was_dispatched = false;
|
var was_dispatched = false;
|
||||||
defer if (was_dispatched) {
|
defer if (was_dispatched) {
|
||||||
self.page.js.runMicrotasks();
|
var ls: js.Local.Scope = undefined;
|
||||||
|
self.page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
ls.local.runMicrotasks();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (function_) |func| {
|
if (function_) |func| {
|
||||||
@@ -193,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);
|
try self.dispatchAll(list, target, event, &was_dispatched);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +315,10 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
|||||||
while (i > 1) {
|
while (i > 1) {
|
||||||
i -= 1;
|
i -= 1;
|
||||||
const current_target = path[i];
|
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);
|
try self.dispatchPhase(list, current_target, event, was_handled, true);
|
||||||
if (event._stop_propagation) {
|
if (event._stop_propagation) {
|
||||||
return;
|
return;
|
||||||
@@ -272,7 +329,10 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
|||||||
// Phase 2: At target
|
// Phase 2: At target
|
||||||
event._event_phase = .at_target;
|
event._event_phase = .at_target;
|
||||||
const target_et = target.asEventTarget();
|
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);
|
try self.dispatchPhase(list, target_et, event, was_handled, null);
|
||||||
if (event._stop_propagation) {
|
if (event._stop_propagation) {
|
||||||
return;
|
return;
|
||||||
@@ -284,7 +344,10 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
|||||||
if (event._bubbles) {
|
if (event._bubbles) {
|
||||||
event._event_phase = .bubbling_phase;
|
event._event_phase = .bubbling_phase;
|
||||||
for (path[1..]) |current_target| {
|
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);
|
try self.dispatchPhase(list, current_target, event, was_handled, false);
|
||||||
if (event._stop_propagation) {
|
if (event._stop_propagation) {
|
||||||
break;
|
break;
|
||||||
@@ -296,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 {
|
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 page = self.page;
|
||||||
const typ = event._type_string;
|
|
||||||
|
|
||||||
// Track dispatch depth for deferred removal
|
// Track dispatch depth for deferred removal
|
||||||
self.dispatch_depth += 1;
|
self.dispatch_depth += 1;
|
||||||
@@ -331,9 +393,6 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
node = n.next;
|
node = n.next;
|
||||||
|
|
||||||
// Skip non-matching listeners
|
// Skip non-matching listeners
|
||||||
if (!listener.typ.eql(typ)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (comptime capture_only) |capture| {
|
if (comptime capture_only) |capture| {
|
||||||
if (listener.capture != capture) {
|
if (listener.capture != capture) {
|
||||||
continue;
|
continue;
|
||||||
@@ -367,14 +426,18 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
event._target = getAdjustedTarget(original_target, current_target);
|
event._target = getAdjustedTarget(original_target, current_target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
switch (listener.function) {
|
switch (listener.function) {
|
||||||
.value => |value| try value.local().callWithThis(void, current_target, .{event}),
|
.value => |value| try ls.toLocal(value).callWithThis(void, current_target, .{event}),
|
||||||
.string => |string| {
|
.string => |string| {
|
||||||
const str = try page.call_arena.dupeZ(u8, string.str());
|
const str = try page.call_arena.dupeZ(u8, string.str());
|
||||||
try self.page.js.eval(str, null);
|
try ls.local.eval(str, null);
|
||||||
},
|
},
|
||||||
.object => |*obj_global| {
|
.object => |obj_global| {
|
||||||
const obj = obj_global.local();
|
const obj = ls.toLocal(obj_global);
|
||||||
if (try obj.getFunction("handleEvent")) |handleEvent| {
|
if (try obj.getFunction("handleEvent")) |handleEvent| {
|
||||||
try handleEvent.callWithThis(void, obj, .{event});
|
try handleEvent.callWithThis(void, obj, .{event});
|
||||||
}
|
}
|
||||||
@@ -409,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;
|
var node = list.first;
|
||||||
while (node) |n| {
|
while (node) |n| {
|
||||||
node = n.next;
|
node = n.next;
|
||||||
@@ -424,9 +487,6 @@ fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Ca
|
|||||||
if (listener.capture != capture) {
|
if (listener.capture != capture) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!listener.typ.eqlSlice(typ)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return listener;
|
return listener;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -29,6 +29,7 @@ const Page = @import("Page.zig");
|
|||||||
const Node = @import("webapi/Node.zig");
|
const Node = @import("webapi/Node.zig");
|
||||||
const Event = @import("webapi/Event.zig");
|
const Event = @import("webapi/Event.zig");
|
||||||
const UIEvent = @import("webapi/event/UIEvent.zig");
|
const UIEvent = @import("webapi/event/UIEvent.zig");
|
||||||
|
const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
||||||
const Element = @import("webapi/Element.zig");
|
const Element = @import("webapi/Element.zig");
|
||||||
const Document = @import("webapi/Document.zig");
|
const Document = @import("webapi/Document.zig");
|
||||||
const EventTarget = @import("webapi/EventTarget.zig");
|
const EventTarget = @import("webapi/EventTarget.zig");
|
||||||
@@ -36,6 +37,8 @@ const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.
|
|||||||
const Blob = @import("webapi/Blob.zig");
|
const Blob = @import("webapi/Blob.zig");
|
||||||
const AbstractRange = @import("webapi/AbstractRange.zig");
|
const AbstractRange = @import("webapi/AbstractRange.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const IS_DEBUG = builtin.mode == .Debug;
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
@@ -169,50 +172,69 @@ pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
|||||||
return chain.get(1);
|
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
|
// this is a root object
|
||||||
pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
|
pub fn event(self: *Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
||||||
const allocator = self._slab.allocator();
|
|
||||||
|
|
||||||
const chain = try PrototypeChain(
|
const chain = try PrototypeChain(
|
||||||
&.{ Event, @TypeOf(child) },
|
&.{ Event, @TypeOf(child) },
|
||||||
).allocate(allocator);
|
).allocate(arena);
|
||||||
|
|
||||||
// Special case: Event has a _type_string field, so we need manual setup
|
// Special case: Event has a _type_string field, so we need manual setup
|
||||||
const event_ptr = chain.get(0);
|
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);
|
chain.setLeaf(1, child);
|
||||||
|
|
||||||
return chain.get(1);
|
return chain.get(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uiEvent(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
|
pub fn uiEvent(self: *Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
||||||
const allocator = self._slab.allocator();
|
|
||||||
|
|
||||||
const chain = try PrototypeChain(
|
const chain = try PrototypeChain(
|
||||||
&.{ Event, UIEvent, @TypeOf(child) },
|
&.{ Event, UIEvent, @TypeOf(child) },
|
||||||
).allocate(allocator);
|
).allocate(arena);
|
||||||
|
|
||||||
// Special case: Event has a _type_string field, so we need manual setup
|
// Special case: Event has a _type_string field, so we need manual setup
|
||||||
const event_ptr = chain.get(0);
|
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.setMiddle(1, UIEvent.Type);
|
||||||
chain.setLeaf(2, child);
|
chain.setLeaf(2, child);
|
||||||
|
|
||||||
return chain.get(2);
|
return chain.get(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(arena);
|
||||||
|
|
||||||
|
// Special case: Event has a _type_string field, so we need manual setup
|
||||||
|
const event_ptr = chain.get(0);
|
||||||
|
event_ptr.* = try self.eventInit(arena, typ, chain.get(1));
|
||||||
|
chain.setMiddle(1, UIEvent.Type);
|
||||||
|
|
||||||
|
// Set MouseEvent with all its fields
|
||||||
|
const mouse_ptr = chain.get(2);
|
||||||
|
mouse_ptr.* = mouse;
|
||||||
|
mouse_ptr._proto = chain.get(1);
|
||||||
|
mouse_ptr._type = unionInit(MouseEvent.Type, chain.get(3));
|
||||||
|
|
||||||
|
chain.setLeaf(3, child);
|
||||||
|
|
||||||
|
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) {
|
pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
const allocator = self._slab.allocator();
|
const allocator = self._slab.allocator();
|
||||||
|
|
||||||
@@ -320,9 +342,7 @@ pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeO
|
|||||||
return chain.get(4);
|
return chain.get(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn xhrEventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
pub fn xhrEventTarget(_: *const Factory, allocator: Allocator, child: anytype) !*@TypeOf(child) {
|
||||||
const allocator = self._slab.allocator();
|
|
||||||
|
|
||||||
return try AutoPrototypeChain(
|
return try AutoPrototypeChain(
|
||||||
&.{ EventTarget, XMLHttpRequestEventTarget, @TypeOf(child) },
|
&.{ EventTarget, XMLHttpRequestEventTarget, @TypeOf(child) },
|
||||||
).create(allocator, child);
|
).create(allocator, child);
|
||||||
@@ -337,32 +357,6 @@ pub fn textTrackCue(self: *Factory, child: anytype) !*@TypeOf(child) {
|
|||||||
).create(allocator, child);
|
).create(allocator, child);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hasChainRoot(comptime T: type) bool {
|
|
||||||
// Check if this is a root
|
|
||||||
if (@hasDecl(T, "_prototype_root")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no _proto field, we're at the top but not a recognized root
|
|
||||||
if (!@hasField(T, "_proto")) return false;
|
|
||||||
|
|
||||||
// Get the _proto field's type and recurse
|
|
||||||
const fields = @typeInfo(T).@"struct".fields;
|
|
||||||
inline for (fields) |field| {
|
|
||||||
if (std.mem.eql(u8, field.name, "_proto")) {
|
|
||||||
const ProtoType = reflect.Struct(field.type);
|
|
||||||
return hasChainRoot(ProtoType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isChainType(comptime T: type) bool {
|
|
||||||
if (@hasField(T, "_proto")) return false;
|
|
||||||
return comptime hasChainRoot(T);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn destroy(self: *Factory, value: anytype) void {
|
pub fn destroy(self: *Factory, value: anytype) void {
|
||||||
const S = reflect.Struct(@TypeOf(value));
|
const S = reflect.Struct(@TypeOf(value));
|
||||||
|
|
||||||
@@ -379,7 +373,7 @@ pub fn destroy(self: *Factory, value: anytype) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comptime isChainType(S)) {
|
if (comptime @hasField(S, "_proto")) {
|
||||||
self.destroyChain(value, true, 0, std.mem.Alignment.@"1");
|
self.destroyChain(value, true, 0, std.mem.Alignment.@"1");
|
||||||
} else {
|
} else {
|
||||||
self.destroyStandalone(value);
|
self.destroyStandalone(value);
|
||||||
@@ -387,20 +381,7 @@ pub fn destroy(self: *Factory, value: anytype) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn destroyStandalone(self: *Factory, value: anytype) void {
|
pub fn destroyStandalone(self: *Factory, value: anytype) void {
|
||||||
const S = reflect.Struct(@TypeOf(value));
|
|
||||||
assert(!@hasDecl(S, "_prototype_root"));
|
|
||||||
|
|
||||||
const allocator = self._slab.allocator();
|
const allocator = self._slab.allocator();
|
||||||
|
|
||||||
if (@hasDecl(S, "deinit")) {
|
|
||||||
// And it has a deinit, we'll call it
|
|
||||||
switch (@typeInfo(@TypeOf(S.deinit)).@"fn".params.len) {
|
|
||||||
1 => value.deinit(),
|
|
||||||
2 => value.deinit(self._page),
|
|
||||||
else => @compileLog(@typeName(S) ++ " has an invalid deinit function"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allocator.destroy(value);
|
allocator.destroy(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,10 +397,8 @@ fn destroyChain(
|
|||||||
|
|
||||||
// aligns the old size to the alignment of this element
|
// aligns the old size to the alignment of this element
|
||||||
const current_size = std.mem.alignForward(usize, old_size, @alignOf(S));
|
const current_size = std.mem.alignForward(usize, old_size, @alignOf(S));
|
||||||
const alignment = std.mem.Alignment.fromByteUnits(@alignOf(S));
|
|
||||||
|
|
||||||
const new_align = std.mem.Alignment.max(old_align, alignment);
|
|
||||||
const new_size = current_size + @sizeOf(S);
|
const new_size = current_size + @sizeOf(S);
|
||||||
|
const new_align = std.mem.Alignment.max(old_align, std.mem.Alignment.of(S));
|
||||||
|
|
||||||
// This is initially called from a deinit. We don't want to call that
|
// This is initially called from a deinit. We don't want to call that
|
||||||
// same deinit. So when this is the first time destroyChain is called
|
// same deinit. So when this is the first time destroyChain is called
|
||||||
@@ -438,20 +417,15 @@ fn destroyChain(
|
|||||||
|
|
||||||
if (@hasField(S, "_proto")) {
|
if (@hasField(S, "_proto")) {
|
||||||
self.destroyChain(value._proto, false, new_size, new_align);
|
self.destroyChain(value._proto, false, new_size, new_align);
|
||||||
} else if (@hasDecl(S, "JsApi")) {
|
|
||||||
// Doesn't have a _proto, but has a JsApi.
|
|
||||||
if (self._page.js.removeTaggedMapping(@intFromPtr(value))) |tagged| {
|
|
||||||
allocator.destroy(tagged);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// no proto so this is the head of the chain.
|
// no proto so this is the head of the chain.
|
||||||
// we use this as the ptr to the start of the chain.
|
// we use this as the ptr to the start of the chain.
|
||||||
// and we have summed up the length.
|
// and we have summed up the length.
|
||||||
assert(@hasDecl(S, "_prototype_root"));
|
assert(@hasDecl(S, "_prototype_root"));
|
||||||
|
|
||||||
const memory_ptr: [*]const u8 = @ptrCast(value);
|
const memory_ptr: [*]u8 = @ptrCast(@constCast(value));
|
||||||
const len = std.mem.alignForward(usize, new_size, new_align.toByteUnits());
|
const len = std.mem.alignForward(usize, new_size, new_align.toByteUnits());
|
||||||
allocator.free(memory_ptr[0..len]);
|
allocator.rawFree(memory_ptr[0..len], new_align, @returnAddress());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
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 Element = @import("webapi/Element.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArrayListUnmanaged = std.ArrayListUnmanaged;
|
const ArrayList = std.ArrayList;
|
||||||
|
|
||||||
const IS_DEBUG = builtin.mode == .Debug;
|
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 {
|
pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_element: *Element.Html.Script, comptime ctx: []const u8) !void {
|
||||||
if (script_element._executed) {
|
if (script_element._executed) {
|
||||||
// If a script tag gets dynamically created and added to the dom:
|
// If a script tag gets dynamically created and added to the dom:
|
||||||
@@ -152,14 +158,14 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
script_element._executed = true;
|
script_element._executed = true;
|
||||||
|
|
||||||
const element = script_element.asElement();
|
const element = script_element.asElement();
|
||||||
if (element.getAttributeSafe("nomodule") != null) {
|
if (element.getAttributeSafe(comptime .wrap("nomodule")) != null) {
|
||||||
// these scripts should only be loaded if we don't support modules
|
// these scripts should only be loaded if we don't support modules
|
||||||
// but since we do support modules, we can just skip them.
|
// but since we do support modules, we can just skip them.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const kind: Script.Kind = blk: {
|
const kind: Script.Kind = blk: {
|
||||||
const script_type = element.getAttributeSafe("type") orelse break :blk .javascript;
|
const script_type = element.getAttributeSafe(comptime .wrap("type")) orelse break :blk .javascript;
|
||||||
if (script_type.len == 0) {
|
if (script_type.len == 0) {
|
||||||
break :blk .javascript;
|
break :blk .javascript;
|
||||||
}
|
}
|
||||||
@@ -186,7 +192,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
var source: Script.Source = undefined;
|
var source: Script.Source = undefined;
|
||||||
var remote_url: ?[:0]const u8 = null;
|
var remote_url: ?[:0]const u8 = null;
|
||||||
const base_url = page.base();
|
const base_url = page.base();
|
||||||
if (element.getAttributeSafe("src")) |src| {
|
if (element.getAttributeSafe(comptime .wrap("src"))) |src| {
|
||||||
if (try parseDataURI(page.arena, src)) |data_uri| {
|
if (try parseDataURI(page.arena, src)) |data_uri| {
|
||||||
source = .{ .@"inline" = data_uri };
|
source = .{ .@"inline" = data_uri };
|
||||||
} else {
|
} else {
|
||||||
@@ -217,12 +223,12 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
break :blk if (kind == .module) .@"defer" else .normal;
|
break :blk if (kind == .module) .@"defer" else .normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element.getAttributeSafe("async") != null) {
|
if (element.getAttributeSafe(comptime .wrap("async")) != null) {
|
||||||
break :blk .async;
|
break :blk .async;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for defer or module (before checking dynamic script default)
|
// Check for defer or module (before checking dynamic script default)
|
||||||
if (kind == .module or element.getAttributeSafe("defer") != null) {
|
if (kind == .module or element.getAttributeSafe(comptime .wrap("defer")) != null) {
|
||||||
break :blk .@"defer";
|
break :blk .@"defer";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,17 +258,15 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
script.deinit(true);
|
script.deinit(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
var headers = try self.client.newHeaders();
|
|
||||||
try page.requestCookie(.{}).headersForRequest(page.arena, url, &headers);
|
|
||||||
|
|
||||||
try self.client.request(.{
|
try self.client.request(.{
|
||||||
.url = url,
|
.url = url,
|
||||||
.ctx = script,
|
.ctx = script,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.headers = headers,
|
.headers = try self.getHeaders(url),
|
||||||
.blocking = is_blocking,
|
.blocking = is_blocking,
|
||||||
.cookie_jar = &page._session.cookie_jar,
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
|
.notification = page._session.notification,
|
||||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||||
.header_callback = Script.headerCallback,
|
.header_callback = Script.headerCallback,
|
||||||
.data_callback = Script.dataCallback,
|
.data_callback = Script.dataCallback,
|
||||||
@@ -271,11 +275,15 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
log.debug(.http, "script queue", .{
|
log.debug(.http, "script queue", .{
|
||||||
.ctx = ctx,
|
.ctx = ctx,
|
||||||
.url = remote_url.?,
|
.url = remote_url.?,
|
||||||
.element = element,
|
.element = element,
|
||||||
.stack = page.js.stackTrace() catch "???",
|
.stack = ls.local.stackTrace() catch "???",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,15 +361,16 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
.manager = self,
|
.manager = self,
|
||||||
};
|
};
|
||||||
|
|
||||||
var headers = try self.client.newHeaders();
|
|
||||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
self.page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
log.debug(.http, "script queue", .{
|
log.debug(.http, "script queue", .{
|
||||||
.url = url,
|
.url = url,
|
||||||
.ctx = "module",
|
.ctx = "module",
|
||||||
.referrer = referrer,
|
.referrer = referrer,
|
||||||
.stack = self.page.js.stackTrace() catch "???",
|
.stack = ls.local.stackTrace() catch "???",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,9 +378,10 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
.url = url,
|
.url = url,
|
||||||
.ctx = script,
|
.ctx = script,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.headers = headers,
|
.headers = try self.getHeaders(url),
|
||||||
.cookie_jar = &self.page._session.cookie_jar,
|
.cookie_jar = &self.page._session.cookie_jar,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
|
.notification = self.page._session.notification,
|
||||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||||
.header_callback = Script.headerCallback,
|
.header_callback = Script.headerCallback,
|
||||||
.data_callback = Script.dataCallback,
|
.data_callback = Script.dataCallback,
|
||||||
@@ -444,15 +454,16 @@ 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) {
|
if (comptime IS_DEBUG) {
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
self.page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
log.debug(.http, "script queue", .{
|
log.debug(.http, "script queue", .{
|
||||||
.url = url,
|
.url = url,
|
||||||
.ctx = "dynamic module",
|
.ctx = "dynamic module",
|
||||||
.referrer = referrer,
|
.referrer = referrer,
|
||||||
.stack = self.page.js.stackTrace() catch "???",
|
.stack = ls.local.stackTrace() catch "???",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,10 +479,11 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
|||||||
try self.client.request(.{
|
try self.client.request(.{
|
||||||
.url = url,
|
.url = url,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.headers = headers,
|
.headers = try self.getHeaders(url),
|
||||||
.ctx = script,
|
.ctx = script,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
.cookie_jar = &self.page._session.cookie_jar,
|
.cookie_jar = &self.page._session.cookie_jar,
|
||||||
|
.notification = self.page._session.notification,
|
||||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||||
.header_callback = Script.headerCallback,
|
.header_callback = Script.headerCallback,
|
||||||
.data_callback = Script.dataCallback,
|
.data_callback = Script.dataCallback,
|
||||||
@@ -622,7 +634,7 @@ pub const Script = struct {
|
|||||||
|
|
||||||
const Source = union(enum) {
|
const Source = union(enum) {
|
||||||
@"inline": []const u8,
|
@"inline": []const u8,
|
||||||
remote: std.ArrayListUnmanaged(u8),
|
remote: std.ArrayList(u8),
|
||||||
|
|
||||||
fn content(self: Source) []const u8 {
|
fn content(self: Source) []const u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
@@ -651,7 +663,7 @@ pub const Script = struct {
|
|||||||
log.debug(.http, "script fetch start", .{ .req = transfer });
|
log.debug(.http, "script fetch start", .{ .req = transfer });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn headerCallback(transfer: *Http.Transfer) !void {
|
fn headerCallback(transfer: *Http.Transfer) !bool {
|
||||||
const self: *Script = @ptrCast(@alignCast(transfer.ctx));
|
const self: *Script = @ptrCast(@alignCast(transfer.ctx));
|
||||||
const header = &transfer.response_header.?;
|
const header = &transfer.response_header.?;
|
||||||
self.status = header.status;
|
self.status = header.status;
|
||||||
@@ -661,7 +673,7 @@ pub const Script = struct {
|
|||||||
.status = header.status,
|
.status = header.status,
|
||||||
.content_type = header.contentType(),
|
.content_type = header.contentType(),
|
||||||
});
|
});
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
@@ -672,16 +684,13 @@ 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 });
|
lp.assert(self.source.remote.capacity == 0, "ScriptManager.HeaderCallback", .{ .capacity = self.source.remote.capacity });
|
||||||
var buffer = self.manager.buffer_pool.get();
|
var buffer = self.manager.buffer_pool.get();
|
||||||
if (transfer.getContentLength()) |cl| {
|
if (transfer.getContentLength()) |cl| {
|
||||||
try buffer.ensureTotalCapacity(self.manager.allocator, cl);
|
try buffer.ensureTotalCapacity(self.manager.allocator, cl);
|
||||||
}
|
}
|
||||||
self.source = .{ .remote = buffer };
|
self.source = .{ .remote = buffer };
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||||
@@ -721,7 +730,7 @@ pub const Script = struct {
|
|||||||
log.warn(.http, "script fetch error", .{
|
log.warn(.http, "script fetch error", .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.req = self.url,
|
.req = self.url,
|
||||||
.mode = self.mode,
|
.mode = std.meta.activeTag(self.mode),
|
||||||
.kind = self.kind,
|
.kind = self.kind,
|
||||||
.status = self.status,
|
.status = self.status,
|
||||||
});
|
});
|
||||||
@@ -741,9 +750,13 @@ pub const Script = struct {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.mode == .import) {
|
switch (self.mode) {
|
||||||
const entry = self.manager.imported_modules.getPtr(self.url).?;
|
.import_async => |ia| ia.callback(ia.data, error.FailedToLoad),
|
||||||
entry.state = .err;
|
.import => {
|
||||||
|
const entry = manager.imported_modules.getPtr(self.url).?;
|
||||||
|
entry.state = .err;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
}
|
}
|
||||||
self.deinit(true);
|
self.deinit(true);
|
||||||
manager.evaluate();
|
manager.evaluate();
|
||||||
@@ -785,6 +798,12 @@ pub const Script = struct {
|
|||||||
.cacheable = cacheable,
|
.cacheable = cacheable,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
|
const local = &ls.local;
|
||||||
|
|
||||||
// Handle importmap special case here: the content is a JSON containing
|
// Handle importmap special case here: the content is a JSON containing
|
||||||
// imports.
|
// imports.
|
||||||
if (self.kind == .importmap) {
|
if (self.kind == .importmap) {
|
||||||
@@ -795,25 +814,24 @@ pub const Script = struct {
|
|||||||
.kind = self.kind,
|
.kind = self.kind,
|
||||||
.cacheable = cacheable,
|
.cacheable = cacheable,
|
||||||
});
|
});
|
||||||
self.executeCallback("error", script_element._on_error, page);
|
self.executeCallback("error", local.toLocal(script_element._on_error), page);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.executeCallback("load", script_element._on_load, page);
|
self.executeCallback("load", local.toLocal(script_element._on_load), page);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const js_context = page.js;
|
|
||||||
var try_catch: js.TryCatch = undefined;
|
var try_catch: js.TryCatch = undefined;
|
||||||
try_catch.init(js_context);
|
try_catch.init(local);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
const success = blk: {
|
const success = blk: {
|
||||||
const content = self.source.content();
|
const content = self.source.content();
|
||||||
switch (self.kind) {
|
switch (self.kind) {
|
||||||
.javascript => _ = js_context.eval(content, url) catch break :blk false,
|
.javascript => _ = local.eval(content, url) catch break :blk false,
|
||||||
.module => {
|
.module => {
|
||||||
// We don't care about waiting for the evaluation here.
|
// We don't care about waiting for the evaluation here.
|
||||||
js_context.module(false, content, url, cacheable) catch break :blk false;
|
page.js.module(false, local, content, url, cacheable) catch break :blk false;
|
||||||
},
|
},
|
||||||
.importmap => unreachable, // handled before the try/catch.
|
.importmap => unreachable, // handled before the try/catch.
|
||||||
}
|
}
|
||||||
@@ -826,14 +844,14 @@ pub const Script = struct {
|
|||||||
|
|
||||||
defer {
|
defer {
|
||||||
// We should run microtasks even if script execution fails.
|
// We should run microtasks even if script execution fails.
|
||||||
page.js.runMicrotasks();
|
local.runMicrotasks();
|
||||||
_ = page.scheduler.run() catch |err| {
|
_ = page.js.scheduler.run() catch |err| {
|
||||||
log.err(.page, "scheduler", .{ .err = err });
|
log.err(.page, "scheduler", .{ .err = err });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
self.executeCallback("load", script_element._on_load, page);
|
self.executeCallback("load", local.toLocal(script_element._on_load), page);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -844,15 +862,14 @@ pub const Script = struct {
|
|||||||
.cacheable = cacheable,
|
.cacheable = cacheable,
|
||||||
});
|
});
|
||||||
|
|
||||||
self.executeCallback("error", script_element._on_error, page);
|
self.executeCallback("error", local.toLocal(script_element._on_error), page);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function.Global, page: *Page) void {
|
fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function, page: *Page) void {
|
||||||
const cb_global = cb_ orelse return;
|
const cb = cb_ orelse return;
|
||||||
const cb = cb_global.local();
|
|
||||||
|
|
||||||
const Event = @import("webapi/Event.zig");
|
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", .{
|
log.warn(.js, "script internal callback", .{
|
||||||
.url = self.url,
|
.url = self.url,
|
||||||
.type = typ,
|
.type = typ,
|
||||||
@@ -860,6 +877,7 @@ pub const Script = struct {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
defer if (!event._v8_handoff) event.deinit(false);
|
||||||
|
|
||||||
var caught: js.TryCatch.Caught = undefined;
|
var caught: js.TryCatch.Caught = undefined;
|
||||||
cb.tryCall(void, .{event}, &caught) catch {
|
cb.tryCall(void, .{event}, &caught) catch {
|
||||||
@@ -883,7 +901,7 @@ const BufferPool = struct {
|
|||||||
|
|
||||||
const Container = struct {
|
const Container = struct {
|
||||||
node: List.Node,
|
node: List.Node,
|
||||||
buf: std.ArrayListUnmanaged(u8),
|
buf: std.ArrayList(u8),
|
||||||
};
|
};
|
||||||
|
|
||||||
fn init(allocator: Allocator, max_concurrent_transfers: u8) BufferPool {
|
fn init(allocator: Allocator, max_concurrent_transfers: u8) BufferPool {
|
||||||
@@ -908,7 +926,7 @@ const BufferPool = struct {
|
|||||||
self.mem_pool.deinit();
|
self.mem_pool.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(self: *BufferPool) std.ArrayListUnmanaged(u8) {
|
fn get(self: *BufferPool) std.ArrayList(u8) {
|
||||||
const node = self.available.popFirst() orelse {
|
const node = self.available.popFirst() orelse {
|
||||||
// return a new buffer
|
// return a new buffer
|
||||||
return .{};
|
return .{};
|
||||||
@@ -920,7 +938,7 @@ const BufferPool = struct {
|
|||||||
return container.buf;
|
return container.buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn release(self: *BufferPool, buffer: ArrayListUnmanaged(u8)) void {
|
fn release(self: *BufferPool, buffer: ArrayList(u8)) void {
|
||||||
// create mutable copy
|
// create mutable copy
|
||||||
var b = buffer;
|
var b = buffer;
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const History = @import("webapi/History.zig");
|
|||||||
|
|
||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
const Browser = @import("Browser.zig");
|
const Browser = @import("Browser.zig");
|
||||||
|
const Notification = @import("../Notification.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
@@ -39,6 +40,7 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
|
|||||||
const Session = @This();
|
const Session = @This();
|
||||||
|
|
||||||
browser: *Browser,
|
browser: *Browser,
|
||||||
|
notification: *Notification,
|
||||||
|
|
||||||
// Used to create our Inspector and in the BrowserContext.
|
// Used to create our Inspector and in the BrowserContext.
|
||||||
arena: Allocator,
|
arena: Allocator,
|
||||||
@@ -53,30 +55,27 @@ arena: Allocator,
|
|||||||
// page and start another.
|
// page and start another.
|
||||||
transfer_arena: Allocator,
|
transfer_arena: Allocator,
|
||||||
|
|
||||||
executor: js.ExecutionWorld,
|
|
||||||
cookie_jar: storage.Cookie.Jar,
|
cookie_jar: storage.Cookie.Jar,
|
||||||
storage_shed: storage.Shed,
|
storage_shed: storage.Shed,
|
||||||
|
|
||||||
history: History,
|
history: History,
|
||||||
navigation: Navigation,
|
navigation: Navigation,
|
||||||
|
|
||||||
page: ?*Page = null,
|
page: ?Page,
|
||||||
|
|
||||||
pub fn init(self: *Session, browser: *Browser) !void {
|
|
||||||
var executor = try browser.env.newExecutionWorld();
|
|
||||||
errdefer executor.deinit();
|
|
||||||
|
|
||||||
|
pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void {
|
||||||
const allocator = browser.app.allocator;
|
const allocator = browser.app.allocator;
|
||||||
const session_allocator = browser.session_arena.allocator();
|
const session_allocator = browser.session_arena.allocator();
|
||||||
|
|
||||||
self.* = .{
|
self.* = .{
|
||||||
.browser = browser,
|
.page = null,
|
||||||
.executor = executor,
|
.history = .{},
|
||||||
|
.navigation = .{},
|
||||||
.storage_shed = .{},
|
.storage_shed = .{},
|
||||||
|
.browser = browser,
|
||||||
|
.notification = notification,
|
||||||
.arena = session_allocator,
|
.arena = session_allocator,
|
||||||
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
||||||
.navigation = .{},
|
|
||||||
.history = .{},
|
|
||||||
.transfer_arena = browser.transfer_arena.allocator(),
|
.transfer_arena = browser.transfer_arena.allocator(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -87,7 +86,6 @@ pub fn deinit(self: *Session) void {
|
|||||||
}
|
}
|
||||||
self.cookie_jar.deinit();
|
self.cookie_jar.deinit();
|
||||||
self.storage_shed.deinit(self.browser.app.allocator);
|
self.storage_shed.deinit(self.browser.app.allocator);
|
||||||
self.executor.deinit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: the caller is not the owner of the returned value,
|
// 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 {
|
pub fn createPage(self: *Session) !*Page {
|
||||||
lp.assert(self.page == null, "Session.createPage - page not null", .{});
|
lp.assert(self.page == null, "Session.createPage - page not null", .{});
|
||||||
|
|
||||||
const page_arena = &self.browser.page_arena;
|
_ = self.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||||
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
|
||||||
|
|
||||||
self.page = try Page.init(page_arena.allocator(), self.browser.call_arena.allocator(), self);
|
self.page = @as(Page, undefined);
|
||||||
const page = self.page.?;
|
const page = &self.page.?;
|
||||||
|
try Page.init(page, self);
|
||||||
|
|
||||||
// Creates a new NavigationEventTarget for this page.
|
// Creates a new NavigationEventTarget for this page.
|
||||||
try self.navigation.onNewPage(page);
|
try self.navigation.onNewPage(page);
|
||||||
@@ -109,14 +107,14 @@ pub fn createPage(self: *Session) !*Page {
|
|||||||
}
|
}
|
||||||
// start JS env
|
// start JS env
|
||||||
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
// 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;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn removePage(self: *Session) void {
|
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
|
// 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", .{});
|
lp.assert(self.page != null, "Session.removePage - page is null", .{});
|
||||||
|
|
||||||
self.page.?.deinit();
|
self.page.?.deinit();
|
||||||
@@ -130,22 +128,29 @@ pub fn removePage(self: *Session) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn currentPage(self: *Session) ?*Page {
|
pub fn currentPage(self: *Session) ?*Page {
|
||||||
return self.page orelse return null;
|
return &(self.page orelse return null);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const WaitResult = enum {
|
pub const WaitResult = enum {
|
||||||
done,
|
done,
|
||||||
no_page,
|
no_page,
|
||||||
cdp_socket,
|
cdp_socket,
|
||||||
navigate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
||||||
while (true) {
|
while (true) {
|
||||||
const page = self.page orelse return .no_page;
|
if (self.page) |*page| {
|
||||||
switch (page.wait(wait_ms)) {
|
switch (page.wait(wait_ms)) {
|
||||||
.navigate => self.processScheduledNavigation() catch return .done,
|
.done => {
|
||||||
else => |result| return result,
|
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
|
// if we've successfull navigated, we'll give the new page another
|
||||||
// page.wait(wait_ms)
|
// page.wait(wait_ms)
|
||||||
@@ -153,24 +158,32 @@ pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn processScheduledNavigation(self: *Session) !void {
|
fn processScheduledNavigation(self: *Session) !void {
|
||||||
const qn = self.page.?._queued_navigation.?;
|
|
||||||
defer _ = self.browser.transfer_arena.reset(.{ .retain_with_limit = 8 * 1024 });
|
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
|
// 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
|
// bad if old requests went to the new page, so let's make double sure
|
||||||
self.browser.http_client.abort();
|
self.browser.http_client.abort();
|
||||||
self.removePage();
|
self.removePage();
|
||||||
|
|
||||||
|
break :blk .{ url, opts };
|
||||||
|
};
|
||||||
|
|
||||||
const page = self.createPage() catch |err| {
|
const page = self.createPage() catch |err| {
|
||||||
log.err(.browser, "queued navigation page error", .{
|
log.err(.browser, "queued navigation page error", .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.url = qn.url,
|
.url = url,
|
||||||
});
|
});
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
|
||||||
page.navigate(qn.url, qn.opts) catch |err| {
|
page.navigate(url, opts) catch |err| {
|
||||||
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
|
log.err(.browser, "queued navigation error", .{ .err = err, .url = url });
|
||||||
return err;
|
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>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const lp = @import("lightpanda");
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const ResolveOpts = struct {
|
const ResolveOpts = struct {
|
||||||
@@ -77,8 +76,9 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
|
|||||||
}
|
}
|
||||||
|
|
||||||
// trailing space so that we always have space to append the null terminator
|
// trailing space so that we always have space to append the null terminator
|
||||||
var out = try std.mem.join(allocator, "", &.{ normalized_base, "/", path, " " });
|
// and so that we can compare the next two characters without needing to length check
|
||||||
const end = out.len - 1;
|
var out = try std.mem.join(allocator, "", &.{ normalized_base, "/", path, " " });
|
||||||
|
const end = out.len - 2;
|
||||||
|
|
||||||
const path_marker = path_start + 1;
|
const path_marker = path_start + 1;
|
||||||
|
|
||||||
@@ -88,33 +88,39 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
|
|||||||
var in_i: usize = 0;
|
var in_i: usize = 0;
|
||||||
var out_i: usize = 0;
|
var out_i: usize = 0;
|
||||||
while (in_i < end) {
|
while (in_i < end) {
|
||||||
if (std.mem.startsWith(u8, out[in_i..], "./")) {
|
if (out[in_i] == '.' and (out_i == 0 or out[out_i - 1] == '/')) {
|
||||||
in_i += 2;
|
if (out[in_i + 1] == '/') { // always safe, because we added a whitespace
|
||||||
continue;
|
// /./
|
||||||
}
|
in_i += 2;
|
||||||
|
continue;
|
||||||
if (std.mem.startsWith(u8, out[in_i..], "../")) {
|
}
|
||||||
lp.assert(out[out_i - 1] == '/', "URL.resolve", .{ .out = out });
|
if (out[in_i + 1] == '.' and out[in_i + 2] == '/') { // always safe, because we added two whitespaces
|
||||||
|
// /../
|
||||||
if (out_i > path_marker) {
|
if (out_i > path_marker) {
|
||||||
// go back before the /
|
// go back before the /
|
||||||
out_i -= 2;
|
out_i -= 2;
|
||||||
while (out_i > 1 and out[out_i - 1] != '/') {
|
while (out_i > 1 and out[out_i - 1] != '/') {
|
||||||
out_i -= 1;
|
out_i -= 1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if out_i == path_marker, than we've reached the start of
|
// if out_i == path_marker, than we've reached the start of
|
||||||
// the path. We can't ../ any more. E.g.:
|
// the path. We can't ../ any more. E.g.:
|
||||||
// http://www.example.com/../hello.
|
// http://www.example.com/../hello.
|
||||||
// You might think that's an error, but, at least with
|
// You might think that's an error, but, at least with
|
||||||
// new URL('../hello', 'http://www.example.com/')
|
// new URL('../hello', 'http://www.example.com/')
|
||||||
// it just ignores the extra ../
|
// it just ignores the extra ../
|
||||||
|
}
|
||||||
|
in_i += 3;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (in_i == end - 1) {
|
||||||
|
// ignore trailing dot
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
in_i += 3;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
out[out_i] = out[in_i];
|
const c = out[in_i];
|
||||||
|
out[out_i] = c;
|
||||||
in_i += 1;
|
in_i += 1;
|
||||||
out_i += 1;
|
out_i += 1;
|
||||||
}
|
}
|
||||||
@@ -496,6 +502,16 @@ pub fn concatQueryString(arena: Allocator, url: []const u8, query_string: []cons
|
|||||||
return buf.items[0 .. buf.items.len - 1 :0];
|
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");
|
const testing = @import("../testing.zig");
|
||||||
test "URL: isCompleteHTTPUrl" {
|
test "URL: isCompleteHTTPUrl" {
|
||||||
try testing.expectEqual(true, isCompleteHTTPUrl("http://example.com/about"));
|
try testing.expectEqual(true, isCompleteHTTPUrl("http://example.com/about"));
|
||||||
@@ -542,6 +558,21 @@ test "URL: resolve" {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cases = [_]Case{
|
const cases = [_]Case{
|
||||||
|
.{
|
||||||
|
.base = "https://example/dir",
|
||||||
|
.path = "abc../test",
|
||||||
|
.expected = "https://example/abc../test",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example/dir",
|
||||||
|
.path = "abc.",
|
||||||
|
.expected = "https://example/abc.",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example/dir",
|
||||||
|
.path = "abc/.",
|
||||||
|
.expected = "https://example/abc/",
|
||||||
|
},
|
||||||
.{
|
.{
|
||||||
.base = "https://example/xyz/abc/123",
|
.base = "https://example/xyz/abc/123",
|
||||||
.path = "something.js",
|
.path = "something.js",
|
||||||
@@ -757,3 +788,31 @@ test "URL: concatQueryString" {
|
|||||||
try testing.expectEqual("https://www.lightpanda.io/index?1=2&a=b", url);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,11 +51,22 @@ pub const Opts = struct {
|
|||||||
|
|
||||||
pub fn root(doc: *Node.Document, opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void {
|
pub fn root(doc: *Node.Document, opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
if (doc.is(Node.Document.HTMLDocument)) |html_doc| {
|
if (doc.is(Node.Document.HTMLDocument)) |html_doc| {
|
||||||
try writer.writeAll("<!DOCTYPE html>");
|
blk: {
|
||||||
|
// Ideally we just render the doctype which is part of the document
|
||||||
|
if (doc.asNode().firstChild()) |first| {
|
||||||
|
if (first._type == .document_type) {
|
||||||
|
break :blk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// But if the doc has no child, or the first child isn't a doctype
|
||||||
|
// well force it.
|
||||||
|
try writer.writeAll("<!DOCTYPE html>");
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.with_base) {
|
if (opts.with_base) {
|
||||||
const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode();
|
const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode();
|
||||||
const base = try doc.createElement("base", null, page);
|
const base = try doc.createElement("base", null, page);
|
||||||
try base.setAttributeSafe("base", page.base(), page);
|
try base.setAttributeSafe(comptime .wrap("base"), .wrap(page.base()), page);
|
||||||
_ = try parent.insertBefore(base.asNode(), parent.firstChild(), page);
|
_ = try parent.insertBefore(base.asNode(), parent.firstChild(), page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +110,7 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
|
|||||||
// to render that "active" content, so when we're trying to render
|
// to render that "active" content, so when we're trying to render
|
||||||
// it, we don't want to skip it.
|
// it, we don't want to skip it.
|
||||||
if ((comptime force_slot == false) and opts.shadow == .rendered) {
|
if ((comptime force_slot == false) and opts.shadow == .rendered) {
|
||||||
if (el.getAttributeSafe("slot")) |_| {
|
if (el.getAttributeSafe(comptime .wrap("slot"))) |_| {
|
||||||
// Skip - will be rendered by the Slot if it's the active container
|
// Skip - will be rendered by the Slot if it's the active container
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -242,12 +253,12 @@ fn shouldStripElement(el: *const Node.Element, opts: Opts) bool {
|
|||||||
if (std.mem.eql(u8, tag_name, "noscript")) return true;
|
if (std.mem.eql(u8, tag_name, "noscript")) return true;
|
||||||
|
|
||||||
if (std.mem.eql(u8, tag_name, "link")) {
|
if (std.mem.eql(u8, tag_name, "link")) {
|
||||||
if (el.getAttributeSafe("as")) |as| {
|
if (el.getAttributeSafe(comptime .wrap("as"))) |as| {
|
||||||
if (std.mem.eql(u8, as, "script")) return true;
|
if (std.mem.eql(u8, as, "script")) return true;
|
||||||
}
|
}
|
||||||
if (el.getAttributeSafe("rel")) |rel| {
|
if (el.getAttributeSafe(comptime .wrap("rel"))) |rel| {
|
||||||
if (std.mem.eql(u8, rel, "modulepreload") or std.mem.eql(u8, rel, "preload")) {
|
if (std.mem.eql(u8, rel, "modulepreload") or std.mem.eql(u8, rel, "preload")) {
|
||||||
if (el.getAttributeSafe("as")) |as| {
|
if (el.getAttributeSafe(comptime .wrap("as"))) |as| {
|
||||||
if (std.mem.eql(u8, as, "script")) return true;
|
if (std.mem.eql(u8, as, "script")) return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,7 +270,7 @@ fn shouldStripElement(el: *const Node.Element, opts: Opts) bool {
|
|||||||
if (std.mem.eql(u8, tag_name, "style")) return true;
|
if (std.mem.eql(u8, tag_name, "style")) return true;
|
||||||
|
|
||||||
if (std.mem.eql(u8, tag_name, "link")) {
|
if (std.mem.eql(u8, tag_name, "link")) {
|
||||||
if (el.getAttributeSafe("rel")) |rel| {
|
if (el.getAttributeSafe(comptime .wrap("rel"))) |rel| {
|
||||||
if (std.mem.eql(u8, rel, "stylesheet")) return true;
|
if (std.mem.eql(u8, rel, "stylesheet")) return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const v8 = js.v8;
|
|||||||
|
|
||||||
const Array = @This();
|
const Array = @This();
|
||||||
|
|
||||||
ctx: *js.Context,
|
local: *const js.Local,
|
||||||
handle: *const v8.Array,
|
handle: *const v8.Array,
|
||||||
|
|
||||||
pub fn len(self: Array) usize {
|
pub fn len(self: Array) usize {
|
||||||
@@ -30,39 +30,37 @@ pub fn len(self: Array) usize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(self: Array, index: u32) !js.Value {
|
pub fn get(self: Array, index: u32) !js.Value {
|
||||||
const ctx = self.ctx;
|
const ctx = self.local.ctx;
|
||||||
|
|
||||||
const idx = js.Integer.init(ctx.isolate.handle, index);
|
const idx = js.Integer.init(ctx.isolate.handle, index);
|
||||||
const handle = v8.v8__Object__Get(@ptrCast(self.handle), ctx.handle, idx.handle) orelse {
|
const handle = v8.v8__Object__Get(@ptrCast(self.handle), self.local.handle, idx.handle) orelse {
|
||||||
return error.JsException;
|
return error.JsException;
|
||||||
};
|
};
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = self.local,
|
||||||
.handle = handle,
|
.handle = handle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(self: Array, index: u32, value: anytype, comptime opts: js.bridge.Caller.CallOpts) !bool {
|
pub fn set(self: Array, index: u32, value: anytype, comptime opts: js.Caller.CallOpts) !bool {
|
||||||
const ctx = self.ctx;
|
const js_value = try self.local.zigValueToJs(value, opts);
|
||||||
|
|
||||||
const js_value = try ctx.zigValueToJs(value, opts);
|
|
||||||
|
|
||||||
var out: v8.MaybeBool = undefined;
|
var out: v8.MaybeBool = undefined;
|
||||||
v8.v8__Object__SetAtIndex(@ptrCast(self.handle), ctx.handle, index, js_value.handle, &out);
|
v8.v8__Object__SetAtIndex(@ptrCast(self.handle), self.local.handle, index, js_value.handle, &out);
|
||||||
return out.has_value;
|
return out.has_value;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toObject(self: Array) js.Object {
|
pub fn toObject(self: Array) js.Object {
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = self.local,
|
||||||
.handle = @ptrCast(self.handle),
|
.handle = @ptrCast(self.handle),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toValue(self: Array) js.Value {
|
pub fn toValue(self: Array) js.Value {
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = self.local,
|
||||||
.handle = @ptrCast(self.handle),
|
.handle = @ptrCast(self.handle),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
587
src/browser/js/Caller.zig
Normal file
587
src/browser/js/Caller.zig
Normal file
@@ -0,0 +1,587 @@
|
|||||||
|
// 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");
|
||||||
|
const string = @import("../../string.zig");
|
||||||
|
|
||||||
|
const Page = @import("../Page.zig");
|
||||||
|
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const Context = @import("Context.zig");
|
||||||
|
const TaggedOpaque = @import("TaggedOpaque.zig");
|
||||||
|
|
||||||
|
const v8 = js.v8;
|
||||||
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
|
const CALL_ARENA_RETAIN = 1024 * 16;
|
||||||
|
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 {
|
||||||
|
const v8_context_handle = v8.v8__Isolate__GetCurrentContext(v8_isolate);
|
||||||
|
|
||||||
|
const embedder_data = v8.v8__Context__GetEmbedderData(v8_context_handle, 1);
|
||||||
|
var lossless: bool = undefined;
|
||||||
|
const ctx: *Context = @ptrFromInt(v8.v8__BigInt__Uint64Value(embedder_data, &lossless));
|
||||||
|
|
||||||
|
ctx.call_depth += 1;
|
||||||
|
self.* = Caller{
|
||||||
|
.local = .{
|
||||||
|
.ctx = ctx,
|
||||||
|
.handle = v8_context_handle.?,
|
||||||
|
.call_arena = ctx.call_arena,
|
||||||
|
.isolate = .{ .handle = v8_isolate },
|
||||||
|
},
|
||||||
|
.prev_local = ctx.local,
|
||||||
|
.prev_context = ctx.page.js,
|
||||||
|
};
|
||||||
|
ctx.page.js = ctx;
|
||||||
|
ctx.local = &self.local;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Caller) void {
|
||||||
|
const ctx = self.local.ctx;
|
||||||
|
const call_depth = ctx.call_depth - 1;
|
||||||
|
|
||||||
|
// Because of callbacks, calls can be nested. Because of this, we
|
||||||
|
// can't clear the call_arena after _every_ call. Imagine we have
|
||||||
|
// arr.forEach((i) => { console.log(i); }
|
||||||
|
//
|
||||||
|
// First we call forEach. Inside of our forEach call,
|
||||||
|
// we call console.log. If we reset the call_arena after this call,
|
||||||
|
// it'll reset it for the `forEach` call after, which might still
|
||||||
|
// need the data.
|
||||||
|
//
|
||||||
|
// Therefore, we keep a call_depth, and only reset the call_arena
|
||||||
|
// when a top-level (call_depth == 0) function ends.
|
||||||
|
if (call_depth == 0) {
|
||||||
|
const arena: *ArenaAllocator = @ptrCast(@alignCast(ctx.call_arena.ptr));
|
||||||
|
_ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.call_depth = call_depth;
|
||||||
|
ctx.local = self.prev_local;
|
||||||
|
ctx.page.js = self.prev_context;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const CallOpts = struct {
|
||||||
|
dom_exception: bool = false,
|
||||||
|
null_as_undefined: bool = false,
|
||||||
|
as_typed_array: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn constructor(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(self.local.isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
|
||||||
|
const info = FunctionCallbackInfo{ .handle = handle };
|
||||||
|
|
||||||
|
if (!info.isConstructCall()) {
|
||||||
|
self.handleError(T, @TypeOf(func), error.InvalidArgument, info, opts);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self._constructor(func, info) catch |err| {
|
||||||
|
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void {
|
||||||
|
const F = @TypeOf(func);
|
||||||
|
const args = try self.getArgs(F, 0, info);
|
||||||
|
const res = @call(.auto, func, args);
|
||||||
|
|
||||||
|
const ReturnType = @typeInfo(F).@"fn".return_type orelse {
|
||||||
|
@compileError(@typeName(F) ++ " has a constructor without a return type");
|
||||||
|
};
|
||||||
|
|
||||||
|
const new_this_handle = info.getThis();
|
||||||
|
var this = js.Object{ .local = &self.local, .handle = new_this_handle };
|
||||||
|
if (@typeInfo(ReturnType) == .error_union) {
|
||||||
|
const non_error_res = res catch |err| return err;
|
||||||
|
this = try self.local.mapZigInstanceToJs(new_this_handle, non_error_res);
|
||||||
|
} else {
|
||||||
|
this = try self.local.mapZigInstanceToJs(new_this_handle, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got back a different object (existing wrapper), copy the prototype
|
||||||
|
// from new object. (this happens when we're upgrading an CustomElement)
|
||||||
|
if (this.handle != new_this_handle) {
|
||||||
|
const prototype_handle = v8.v8__Object__GetPrototype(new_this_handle).?;
|
||||||
|
var out: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Object__SetPrototype(this.handle, self.local.handle, prototype_handle, &out);
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(out.has_value and out.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.getReturnValue().set(this.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn method(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(self.local.isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
|
||||||
|
const info = FunctionCallbackInfo{ .handle = handle };
|
||||||
|
self._method(T, func, info, opts) catch |err| {
|
||||||
|
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _method(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void {
|
||||||
|
const F = @TypeOf(func);
|
||||||
|
var args = try self.getArgs(F, 1, info);
|
||||||
|
|
||||||
|
const js_this = info.getThis();
|
||||||
|
@field(args, "0") = try TaggedOpaque.fromJS(*T, js_this);
|
||||||
|
|
||||||
|
const res = @call(.auto, func, args);
|
||||||
|
|
||||||
|
const mapped = try self.local.zigValueToJs(res, opts);
|
||||||
|
const return_value = info.getReturnValue();
|
||||||
|
return_value.set(mapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn function(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(self.local.isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
|
||||||
|
const info = FunctionCallbackInfo{ .handle = handle };
|
||||||
|
self._function(func, info, opts) catch |err| {
|
||||||
|
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _function(self: *Caller, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void {
|
||||||
|
const F = @TypeOf(func);
|
||||||
|
const args = try self.getArgs(F, 0, info);
|
||||||
|
const res = @call(.auto, func, args);
|
||||||
|
info.getReturnValue().set(try self.local.zigValueToJs(res, opts));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(self.local.isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
|
||||||
|
const info = PropertyCallbackInfo{ .handle = handle };
|
||||||
|
return self._getIndex(T, func, idx, info, opts) catch |err| {
|
||||||
|
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||||
|
// not intercepted
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||||
|
const F = @TypeOf(func);
|
||||||
|
var args = try self.getArgs(F, 2, info);
|
||||||
|
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||||
|
@field(args, "1") = idx;
|
||||||
|
const ret = @call(.auto, func, args);
|
||||||
|
return self.handleIndexedReturn(T, F, true, ret, info, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(self.local.isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
|
||||||
|
const info = PropertyCallbackInfo{ .handle = handle };
|
||||||
|
return self._getNamedIndex(T, func, name, info, opts) catch |err| {
|
||||||
|
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||||
|
// not intercepted
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||||
|
const F = @TypeOf(func);
|
||||||
|
var args = try self.getArgs(F, 2, info);
|
||||||
|
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||||
|
@field(args, "1") = try self.nameToString(@TypeOf(args.@"1"), name);
|
||||||
|
const ret = @call(.auto, func, args);
|
||||||
|
return self.handleIndexedReturn(T, F, true, ret, info, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, js_value: *const v8.Value, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(self.local.isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
|
||||||
|
const info = PropertyCallbackInfo{ .handle = handle };
|
||||||
|
return self._setNamedIndex(T, func, name, .{ .local = &self.local, .handle = js_value }, info, opts) catch |err| {
|
||||||
|
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||||
|
// not intercepted
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, js_value: js.Value, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||||
|
const F = @TypeOf(func);
|
||||||
|
var args: ParameterTypes(F) = undefined;
|
||||||
|
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||||
|
@field(args, "1") = try self.nameToString(@TypeOf(args.@"1"), name);
|
||||||
|
@field(args, "2") = try self.local.jsValueToZig(@TypeOf(@field(args, "2")), js_value);
|
||||||
|
if (@typeInfo(F).@"fn".params.len == 4) {
|
||||||
|
@field(args, "3") = self.local.ctx.page;
|
||||||
|
}
|
||||||
|
const ret = @call(.auto, func, args);
|
||||||
|
return self.handleIndexedReturn(T, F, false, ret, info, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(self.local.isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
|
||||||
|
const info = PropertyCallbackInfo{ .handle = handle };
|
||||||
|
return self._deleteNamedIndex(T, func, name, info, opts) catch |err| {
|
||||||
|
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||||
|
const F = @TypeOf(func);
|
||||||
|
var args: ParameterTypes(F) = undefined;
|
||||||
|
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||||
|
@field(args, "1") = try self.nameToString(@TypeOf(args.@"1"), name);
|
||||||
|
if (@typeInfo(F).@"fn".params.len == 3) {
|
||||||
|
@field(args, "2") = self.local.ctx.page;
|
||||||
|
}
|
||||||
|
const ret = @call(.auto, func, args);
|
||||||
|
return self.handleIndexedReturn(T, F, false, ret, info, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, comptime getter: bool, ret: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||||
|
// need to unwrap this error immediately for when opts.null_as_undefined == true
|
||||||
|
// and we need to compare it to null;
|
||||||
|
const non_error_ret = switch (@typeInfo(@TypeOf(ret))) {
|
||||||
|
.error_union => |eu| blk: {
|
||||||
|
break :blk ret catch |err| {
|
||||||
|
// We can't compare err == error.NotHandled if error.NotHandled
|
||||||
|
// isn't part of the possible error set. So we first need to check
|
||||||
|
// if error.NotHandled is part of the error set.
|
||||||
|
if (isInErrorSet(error.NotHandled, eu.error_set)) {
|
||||||
|
if (err == error.NotHandled) {
|
||||||
|
// not intercepted
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.handleError(T, F, err, info, opts);
|
||||||
|
// not intercepted
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
else => ret,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (comptime getter) {
|
||||||
|
info.getReturnValue().set(try self.local.zigValueToJs(non_error_ret, opts));
|
||||||
|
}
|
||||||
|
// intercepted
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isInErrorSet(err: anyerror, comptime T: type) bool {
|
||||||
|
inline for (@typeInfo(T).error_set.?) |e| {
|
||||||
|
if (err == @field(anyerror, e.name)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nameToString(self: *const Caller, comptime T: type, name: *const v8.Name) !T {
|
||||||
|
const handle = @as(*const v8.String, @ptrCast(name));
|
||||||
|
if (T == string.String) {
|
||||||
|
return js.String.toSSO(.{ .local = &self.local, .handle = handle }, false);
|
||||||
|
}
|
||||||
|
if (T == string.Global) {
|
||||||
|
return js.String.toSSO(.{ .local = &self.local, .handle = handle }, true);
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
const isolate = self.local.isolate;
|
||||||
|
|
||||||
|
if (comptime @import("builtin").mode == .Debug and @TypeOf(info) == FunctionCallbackInfo) {
|
||||||
|
if (log.enabled(.js, .warn)) {
|
||||||
|
self.logFunctionCallError(@typeName(T), @typeName(F), err, info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"),
|
||||||
|
else => blk: {
|
||||||
|
if (comptime opts.dom_exception) {
|
||||||
|
const DOMException = @import("../webapi/DOMException.zig");
|
||||||
|
if (DOMException.fromError(err)) |ex| {
|
||||||
|
const value = self.local.zigValueToJs(ex, .{}) catch break :blk isolate.createError("internal error");
|
||||||
|
break :blk value.handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break :blk isolate.createError(@errorName(err));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const js_exception = isolate.throwException(js_err);
|
||||||
|
info.getReturnValue().setValueHandle(js_exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we call a method in javascript: cat.lives('nine');
|
||||||
|
//
|
||||||
|
// Then we'd expect a Zig function with 2 parameters: a self and the string.
|
||||||
|
// In this case, offset == 1. Offset is always 1 for setters or methods.
|
||||||
|
//
|
||||||
|
// Offset is always 0 for constructors.
|
||||||
|
//
|
||||||
|
// For constructors, setters and methods, we can further increase offset + 1
|
||||||
|
// if the first parameter is an instance of Page.
|
||||||
|
//
|
||||||
|
// Finally, if the JS function is called with _more_ parameters and
|
||||||
|
// the last parameter in Zig is an array, we'll try to slurp the additional
|
||||||
|
// parameters into the array.
|
||||||
|
fn getArgs(self: *const Caller, comptime F: type, comptime offset: usize, info: anytype) !ParameterTypes(F) {
|
||||||
|
const local = &self.local;
|
||||||
|
var args: ParameterTypes(F) = undefined;
|
||||||
|
|
||||||
|
const params = @typeInfo(F).@"fn".params[offset..];
|
||||||
|
// Except for the constructor, the first parameter is always `self`
|
||||||
|
// This isn't something we'll bind from JS, so skip it.
|
||||||
|
const params_to_map = blk: {
|
||||||
|
if (params.len == 0) {
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the last parameter is the Page, set it, and exclude it
|
||||||
|
// from our params slice, because we don't want to bind it to
|
||||||
|
// a JS argument
|
||||||
|
if (comptime isPage(params[params.len - 1].type.?)) {
|
||||||
|
@field(args, tupleFieldName(params.len - 1 + offset)) = local.ctx.page;
|
||||||
|
break :blk params[0 .. params.len - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have neither a Page nor a JsObject. All params must be
|
||||||
|
// bound to a JavaScript value.
|
||||||
|
break :blk params;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (params_to_map.len == 0) {
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
const js_parameter_count = info.length();
|
||||||
|
const last_js_parameter = params_to_map.len - 1;
|
||||||
|
var is_variadic = false;
|
||||||
|
|
||||||
|
{
|
||||||
|
// This is going to get complicated. If the last Zig parameter
|
||||||
|
// is a slice AND the corresponding javascript parameter is
|
||||||
|
// NOT an an array, then we'll treat it as a variadic.
|
||||||
|
|
||||||
|
const last_parameter_type = params_to_map[params_to_map.len - 1].type.?;
|
||||||
|
const last_parameter_type_info = @typeInfo(last_parameter_type);
|
||||||
|
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
|
||||||
|
const slice_type = last_parameter_type_info.pointer.child;
|
||||||
|
const corresponding_js_value = info.getArg(@intCast(last_js_parameter), local);
|
||||||
|
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
|
||||||
|
is_variadic = true;
|
||||||
|
if (js_parameter_count == 0) {
|
||||||
|
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
||||||
|
} else if (js_parameter_count >= params_to_map.len) {
|
||||||
|
const arr = try local.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
|
||||||
|
for (arr, last_js_parameter..) |*a, i| {
|
||||||
|
a.* = try local.jsValueToZig(slice_type, info.getArg(@intCast(i), local));
|
||||||
|
}
|
||||||
|
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
|
||||||
|
} else {
|
||||||
|
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline for (params_to_map, 0..) |param, i| {
|
||||||
|
const field_index = comptime i + offset;
|
||||||
|
if (comptime i == params_to_map.len - 1) {
|
||||||
|
if (is_variadic) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comptime isPage(param.type.?)) {
|
||||||
|
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ @typeName(F));
|
||||||
|
} else if (i >= js_parameter_count) {
|
||||||
|
if (@typeInfo(param.type.?) != .optional) {
|
||||||
|
return error.InvalidArgument;
|
||||||
|
}
|
||||||
|
@field(args, tupleFieldName(field_index)) = null;
|
||||||
|
} else {
|
||||||
|
const js_val = info.getArg(@intCast(i), local);
|
||||||
|
@field(args, tupleFieldName(field_index)) = local.jsValueToZig(param.type.?, js_val) catch {
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is extracted to speed up compilation. When left inlined in handleError,
|
||||||
|
// this can add as much as 10 seconds of compilation time.
|
||||||
|
fn logFunctionCallError(self: *Caller, type_name: []const u8, func: []const u8, err: anyerror, info: FunctionCallbackInfo) void {
|
||||||
|
const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args";
|
||||||
|
log.info(.js, "function call error", .{
|
||||||
|
.type = type_name,
|
||||||
|
.func = func,
|
||||||
|
.err = err,
|
||||||
|
.args = args_dump,
|
||||||
|
.stack = self.local.stackTrace() catch |err1| @errorName(err1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serializeFunctionArgs(self: *Caller, info: FunctionCallbackInfo) ![]const u8 {
|
||||||
|
const local = &self.local;
|
||||||
|
var buf = std.Io.Writer.Allocating.init(local.call_arena);
|
||||||
|
|
||||||
|
const separator = log.separator();
|
||||||
|
for (0..info.length()) |i| {
|
||||||
|
try buf.writer.print("{s}{d} - ", .{ separator, i + 1 });
|
||||||
|
const js_value = info.getArg(@intCast(i), local);
|
||||||
|
try local.debugValue(js_value, &buf.writer);
|
||||||
|
}
|
||||||
|
return buf.written();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes a function, and returns a tuple for its argument. Used when we
|
||||||
|
// @call a function
|
||||||
|
fn ParameterTypes(comptime F: type) type {
|
||||||
|
const params = @typeInfo(F).@"fn".params;
|
||||||
|
var fields: [params.len]std.builtin.Type.StructField = undefined;
|
||||||
|
|
||||||
|
inline for (params, 0..) |param, i| {
|
||||||
|
fields[i] = .{
|
||||||
|
.name = tupleFieldName(i),
|
||||||
|
.type = param.type.?,
|
||||||
|
.default_value_ptr = null,
|
||||||
|
.is_comptime = false,
|
||||||
|
.alignment = @alignOf(param.type.?),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return @Type(.{ .@"struct" = .{
|
||||||
|
.layout = .auto,
|
||||||
|
.decls = &.{},
|
||||||
|
.fields = &fields,
|
||||||
|
.is_tuple = true,
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tupleFieldName(comptime i: usize) [:0]const u8 {
|
||||||
|
return switch (i) {
|
||||||
|
0 => "0",
|
||||||
|
1 => "1",
|
||||||
|
2 => "2",
|
||||||
|
3 => "3",
|
||||||
|
4 => "4",
|
||||||
|
5 => "5",
|
||||||
|
6 => "6",
|
||||||
|
7 => "7",
|
||||||
|
8 => "8",
|
||||||
|
9 => "9",
|
||||||
|
else => std.fmt.comptimePrint("{d}", .{i}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isPage(comptime T: type) bool {
|
||||||
|
return T == *Page or T == *const Page;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These wrap the raw v8 C API to provide a cleaner interface.
|
||||||
|
pub const FunctionCallbackInfo = struct {
|
||||||
|
handle: *const v8.FunctionCallbackInfo,
|
||||||
|
|
||||||
|
pub fn length(self: FunctionCallbackInfo) u32 {
|
||||||
|
return @intCast(v8.v8__FunctionCallbackInfo__Length(self.handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getArg(self: FunctionCallbackInfo, index: u32, local: *const js.Local) js.Value {
|
||||||
|
return .{ .local = local, .handle = v8.v8__FunctionCallbackInfo__INDEX(self.handle, @intCast(index)).? };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getThis(self: FunctionCallbackInfo) *const v8.Object {
|
||||||
|
return v8.v8__FunctionCallbackInfo__This(self.handle).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getReturnValue(self: FunctionCallbackInfo) ReturnValue {
|
||||||
|
var rv: v8.ReturnValue = undefined;
|
||||||
|
v8.v8__FunctionCallbackInfo__GetReturnValue(self.handle, &rv);
|
||||||
|
return .{ .handle = rv };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isConstructCall(self: FunctionCallbackInfo) bool {
|
||||||
|
return v8.v8__FunctionCallbackInfo__IsConstructCall(self.handle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PropertyCallbackInfo = struct {
|
||||||
|
handle: *const v8.PropertyCallbackInfo,
|
||||||
|
|
||||||
|
pub fn getThis(self: PropertyCallbackInfo) *const v8.Object {
|
||||||
|
return v8.v8__PropertyCallbackInfo__This(self.handle).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getReturnValue(self: PropertyCallbackInfo) ReturnValue {
|
||||||
|
var rv: v8.ReturnValue = undefined;
|
||||||
|
v8.v8__PropertyCallbackInfo__GetReturnValue(self.handle, &rv);
|
||||||
|
return .{ .handle = rv };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReturnValue = struct {
|
||||||
|
handle: v8.ReturnValue,
|
||||||
|
|
||||||
|
pub fn set(self: ReturnValue, value: anytype) void {
|
||||||
|
const T = @TypeOf(value);
|
||||||
|
if (T == *const v8.Object) {
|
||||||
|
self.setValueHandle(@ptrCast(value));
|
||||||
|
} else if (T == *const v8.Value) {
|
||||||
|
self.setValueHandle(value);
|
||||||
|
} else if (T == js.Value) {
|
||||||
|
self.setValueHandle(value.handle);
|
||||||
|
} else {
|
||||||
|
@compileError("Unsupported type for ReturnValue.set: " ++ @typeName(T));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setValueHandle(self: ReturnValue, handle: *const v8.Value) void {
|
||||||
|
v8.v8__ReturnValue__Set(self.handle, handle);
|
||||||
|
}
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -18,20 +18,26 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const App = @import("../../App.zig");
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
const bridge = @import("bridge.zig");
|
const bridge = @import("bridge.zig");
|
||||||
const Context = @import("Context.zig");
|
const Context = @import("Context.zig");
|
||||||
|
const Isolate = @import("Isolate.zig");
|
||||||
const Platform = @import("Platform.zig");
|
const Platform = @import("Platform.zig");
|
||||||
const Snapshot = @import("Snapshot.zig");
|
const Snapshot = @import("Snapshot.zig");
|
||||||
const Inspector = @import("Inspector.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 JsApis = bridge.JsApis;
|
||||||
const Allocator = std.mem.Allocator;
|
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
|
// 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,
|
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
|
||||||
@@ -41,13 +47,15 @@ const ArenaAllocator = std.heap.ArenaAllocator;
|
|||||||
// The `types` parameter is a tuple of Zig structures we want to bind to V8.
|
// The `types` parameter is a tuple of Zig structures we want to bind to V8.
|
||||||
const Env = @This();
|
const Env = @This();
|
||||||
|
|
||||||
allocator: Allocator,
|
app: *App,
|
||||||
|
|
||||||
platform: *const Platform,
|
platform: *const Platform,
|
||||||
|
|
||||||
// the global isolate
|
// the global isolate
|
||||||
isolate: js.Isolate,
|
isolate: js.Isolate,
|
||||||
|
|
||||||
|
contexts: std.ArrayList(*js.Context),
|
||||||
|
|
||||||
// just kept around because we need to free it on deinit
|
// just kept around because we need to free it on deinit
|
||||||
isolate_params: *v8.CreateParams,
|
isolate_params: *v8.CreateParams,
|
||||||
|
|
||||||
@@ -59,7 +67,20 @@ eternal_function_templates: []v8.Eternal,
|
|||||||
// Dynamic slice to avoid circular dependency on JsApis.len at comptime
|
// Dynamic slice to avoid circular dependency on JsApis.len at comptime
|
||||||
templates: []*const v8.FunctionTemplate,
|
templates: []*const v8.FunctionTemplate,
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot) !Env {
|
// Global template created once per isolate and reused across all contexts
|
||||||
|
global_template: v8.Eternal,
|
||||||
|
|
||||||
|
// 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);
|
var params = try allocator.create(v8.CreateParams);
|
||||||
errdefer allocator.destroy(params);
|
errdefer allocator.destroy(params);
|
||||||
v8.v8__Isolate__CreateParams__CONSTRUCT(params);
|
v8.v8__Isolate__CreateParams__CONSTRUCT(params);
|
||||||
@@ -72,17 +93,18 @@ pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot
|
|||||||
|
|
||||||
var isolate = js.Isolate.init(params);
|
var isolate = js.Isolate.init(params);
|
||||||
errdefer isolate.deinit();
|
errdefer isolate.deinit();
|
||||||
|
const isolate_handle = isolate.handle;
|
||||||
|
|
||||||
v8.v8__Isolate__SetHostImportModuleDynamicallyCallback(isolate.handle, Context.dynamicModuleCallback);
|
v8.v8__Isolate__SetHostImportModuleDynamicallyCallback(isolate_handle, Context.dynamicModuleCallback);
|
||||||
v8.v8__Isolate__SetPromiseRejectCallback(isolate.handle, promiseRejectCallback);
|
v8.v8__Isolate__SetPromiseRejectCallback(isolate_handle, promiseRejectCallback);
|
||||||
v8.v8__Isolate__SetMicrotasksPolicy(isolate.handle, v8.kExplicit);
|
v8.v8__Isolate__SetMicrotasksPolicy(isolate_handle, v8.kExplicit);
|
||||||
v8.v8__Isolate__SetFatalErrorHandler(isolate.handle, fatalCallback);
|
v8.v8__Isolate__SetFatalErrorHandler(isolate_handle, fatalCallback);
|
||||||
v8.v8__Isolate__SetOOMErrorHandler(isolate.handle, oomCallback);
|
v8.v8__Isolate__SetOOMErrorHandler(isolate_handle, oomCallback);
|
||||||
|
|
||||||
isolate.enter();
|
isolate.enter();
|
||||||
errdefer isolate.exit();
|
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
|
// Allocate arrays dynamically to avoid comptime dependency on JsApis.len
|
||||||
const eternal_function_templates = try allocator.alloc(v8.Eternal, JsApis.len);
|
const eternal_function_templates = try allocator.alloc(v8.Eternal, JsApis.len);
|
||||||
@@ -91,6 +113,7 @@ pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot
|
|||||||
const templates = try allocator.alloc(*const v8.FunctionTemplate, JsApis.len);
|
const templates = try allocator.alloc(*const v8.FunctionTemplate, JsApis.len);
|
||||||
errdefer allocator.free(templates);
|
errdefer allocator.free(templates);
|
||||||
|
|
||||||
|
var global_eternal: v8.Eternal = undefined;
|
||||||
{
|
{
|
||||||
var temp_scope: js.HandleScope = undefined;
|
var temp_scope: js.HandleScope = undefined;
|
||||||
temp_scope.init(isolate);
|
temp_scope.init(isolate);
|
||||||
@@ -98,67 +121,212 @@ pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot
|
|||||||
|
|
||||||
inline for (JsApis, 0..) |JsApi, i| {
|
inline for (JsApis, 0..) |JsApi, i| {
|
||||||
JsApi.Meta.class_id = 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);
|
const function_handle: *const v8.FunctionTemplate = @ptrCast(data);
|
||||||
// Make function template eternal
|
// 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
|
// 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.?));
|
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);
|
||||||
|
v8.v8__FunctionTemplate__SetClassName(js_global, window_name);
|
||||||
|
|
||||||
|
// Find Window in JsApis by name (avoids circular import)
|
||||||
|
const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi);
|
||||||
|
v8.v8__FunctionTemplate__Inherit(js_global, templates[window_index]);
|
||||||
|
|
||||||
|
const global_template_local = v8.v8__FunctionTemplate__InstanceTemplate(js_global).?;
|
||||||
|
v8.v8__ObjectTemplate__SetNamedHandler(global_template_local, &.{
|
||||||
|
.getter = bridge.unknownWindowPropertyCallback,
|
||||||
|
.setter = null,
|
||||||
|
.query = null,
|
||||||
|
.deleter = null,
|
||||||
|
.enumerator = null,
|
||||||
|
.definer = null,
|
||||||
|
.descriptor = null,
|
||||||
|
.data = null,
|
||||||
|
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
||||||
|
});
|
||||||
|
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 .{
|
return .{
|
||||||
|
.app = app,
|
||||||
.context_id = 0,
|
.context_id = 0,
|
||||||
|
.contexts = .empty,
|
||||||
.isolate = isolate,
|
.isolate = isolate,
|
||||||
.platform = platform,
|
.platform = &app.platform,
|
||||||
.allocator = allocator,
|
|
||||||
.templates = templates,
|
.templates = templates,
|
||||||
.isolate_params = params,
|
.isolate_params = params,
|
||||||
|
.inspector = inspector,
|
||||||
.eternal_function_templates = eternal_function_templates,
|
.eternal_function_templates = eternal_function_templates,
|
||||||
|
.global_template = global_eternal,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Env) void {
|
pub fn deinit(self: *Env) void {
|
||||||
self.allocator.free(self.templates);
|
if (comptime IS_DEBUG) {
|
||||||
self.allocator.free(self.eternal_function_templates);
|
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.exit();
|
||||||
self.isolate.deinit();
|
self.isolate.deinit();
|
||||||
v8.v8__ArrayBuffer__Allocator__DELETE(self.isolate_params.array_buffer_allocator.?);
|
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 {
|
pub fn createContext(self: *Env, page: *Page, enter: bool) !*Context {
|
||||||
const inspector = try arena.create(Inspector);
|
const context_arena = try self.app.arena_pool.acquire();
|
||||||
try Inspector.init(inspector, self.isolate.handle, ctx);
|
errdefer self.app.arena_pool.release(context_arena);
|
||||||
return inspector;
|
|
||||||
|
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 {
|
pub fn runMicrotasks(self: *const Env) void {
|
||||||
self.isolate.performMicrotasksCheckpoint();
|
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 {
|
pub fn pumpMessageLoop(self: *const Env) bool {
|
||||||
|
var hs: v8.HandleScope = undefined;
|
||||||
|
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate.handle);
|
||||||
|
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||||
|
|
||||||
return v8.v8__Platform__PumpMessageLoop(self.platform.handle, self.isolate.handle, false);
|
return v8.v8__Platform__PumpMessageLoop(self.platform.handle, self.isolate.handle, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runIdleTasks(self: *const Env) void {
|
pub fn runIdleTasks(self: *const Env) void {
|
||||||
v8.v8__Platform__RunIdleTasks(self.platform.handle, self.isolate.handle, 1);
|
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
|
// V8 doesn't immediately free memory associated with
|
||||||
// a Context, it's managed by the garbage collector. We use the
|
// a Context, it's managed by the garbage collector. We use the
|
||||||
// `lowMemoryNotification` call on the isolate to encourage v8 to free
|
// `lowMemoryNotification` call on the isolate to encourage v8 to free
|
||||||
// any contexts which have been freed.
|
// any contexts which have been freed.
|
||||||
|
// This GC is very aggressive. Use memoryPressureNotification for less
|
||||||
|
// aggressive GC passes.
|
||||||
pub fn lowMemoryNotification(self: *Env) void {
|
pub fn lowMemoryNotification(self: *Env) void {
|
||||||
var handle_scope: js.HandleScope = undefined;
|
var handle_scope: js.HandleScope = undefined;
|
||||||
handle_scope.init(self.isolate);
|
handle_scope.init(self.isolate);
|
||||||
@@ -166,6 +334,21 @@ pub fn lowMemoryNotification(self: *Env) void {
|
|||||||
self.isolate.lowMemoryNotification();
|
self.isolate.lowMemoryNotification();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// V8 doesn't immediately free memory associated with
|
||||||
|
// a Context, it's managed by the garbage collector. We use the
|
||||||
|
// `memoryPressureNotification` call on the isolate to encourage v8 to free
|
||||||
|
// any contexts which have been freed.
|
||||||
|
// The level indicates the aggressivity of the GC required:
|
||||||
|
// moderate speeds up incremental GC
|
||||||
|
// critical runs one full GC
|
||||||
|
// For a more aggressive GC, use lowMemoryNotification.
|
||||||
|
pub fn memoryPressureNotification(self: *Env, level: Isolate.MemoryPressureLevel) void {
|
||||||
|
var handle_scope: js.HandleScope = undefined;
|
||||||
|
handle_scope.init(self.isolate);
|
||||||
|
defer handle_scope.deinit();
|
||||||
|
self.isolate.memoryPressureNotification(level);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn dumpMemoryStats(self: *Env) void {
|
pub fn dumpMemoryStats(self: *Env) void {
|
||||||
const stats = self.isolate.getHeapStatistics();
|
const stats = self.isolate.getHeapStatistics();
|
||||||
std.debug.print(
|
std.debug.print(
|
||||||
@@ -189,21 +372,24 @@ pub fn dumpMemoryStats(self: *Env) void {
|
|||||||
|
|
||||||
fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
|
fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
|
||||||
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
|
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
|
||||||
const isolate_handle = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
|
const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
|
||||||
const js_isolate = js.Isolate{ .handle = isolate_handle };
|
const js_isolate = js.Isolate{ .handle = v8_isolate };
|
||||||
const context = Context.fromIsolate(js_isolate);
|
const ctx = Context.fromIsolate(js_isolate);
|
||||||
|
|
||||||
const value =
|
const local = js.Local{
|
||||||
if (v8.v8__PromiseRejectMessage__GetValue(&message_handle)) |v8_value|
|
.ctx = ctx,
|
||||||
context.valueToString(.{ .ctx = context, .handle = v8_value }, .{}) catch |err| @errorName(err)
|
.isolate = js_isolate,
|
||||||
else
|
.handle = v8.v8__Isolate__GetCurrentContext(v8_isolate).?,
|
||||||
"no value";
|
.call_arena = ctx.call_arena,
|
||||||
|
};
|
||||||
|
|
||||||
log.debug(.js, "unhandled rejection", .{
|
const page = ctx.page;
|
||||||
.value = value,
|
page.window.unhandledPromiseRejection(.{
|
||||||
.stack = context.stackTrace() catch |err| @errorName(err) orelse "???",
|
.local = &local,
|
||||||
.note = "This should be updated to call window.unhandledrejection",
|
.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 {
|
fn fatalCallback(c_location: [*c]const u8, c_message: [*c]const u8) callconv(.c) void {
|
||||||
|
|||||||
@@ -1,163 +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,
|
|
||||||
persisted_context: ?js.Global(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();
|
|
||||||
|
|
||||||
const persisted_context: js.Global(Context) = blk: {
|
|
||||||
var temp_scope: js.HandleScope = undefined;
|
|
||||||
temp_scope.init(isolate);
|
|
||||||
defer temp_scope.deinit();
|
|
||||||
|
|
||||||
// Getting this into the snapshot is tricky (anything involving the
|
|
||||||
// global is tricky). Easier to do here
|
|
||||||
const global_template = @import("Snapshot.zig").createGlobalTemplate(isolate.handle, env.templates);
|
|
||||||
v8.v8__ObjectTemplate__SetNamedHandler(global_template, &.{
|
|
||||||
.getter = bridge.unknownPropertyCallback,
|
|
||||||
.setter = null,
|
|
||||||
.query = null,
|
|
||||||
.deleter = null,
|
|
||||||
.enumerator = null,
|
|
||||||
.definer = null,
|
|
||||||
.descriptor = null,
|
|
||||||
.data = null,
|
|
||||||
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
|
||||||
});
|
|
||||||
|
|
||||||
const context_handle = v8.v8__Context__New(isolate.handle, global_template, null).?;
|
|
||||||
break :blk js.Global(Context).init(isolate.handle, context_handle);
|
|
||||||
};
|
|
||||||
|
|
||||||
// For a Page we only create one HandleScope, it is stored in the main World (enter==true). A page can have multple contexts, 1 for each World.
|
|
||||||
// The main Context that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
|
|
||||||
// like isolated Worlds, will thereby place their objects on the main page's HandleScope. Note: In the furure the number of context will multiply multiple frames support
|
|
||||||
const v8_context = persisted_context.local();
|
|
||||||
var handle_scope: ?js.HandleScope = null;
|
|
||||||
if (enter) {
|
|
||||||
handle_scope = @as(js.HandleScope, undefined);
|
|
||||||
handle_scope.?.init(isolate);
|
|
||||||
v8.v8__Context__Enter(v8_context);
|
|
||||||
}
|
|
||||||
errdefer if (enter) {
|
|
||||||
v8.v8__Context__Exit(v8_context);
|
|
||||||
handle_scope.?.deinit();
|
|
||||||
};
|
|
||||||
|
|
||||||
const context_id = env.context_id;
|
|
||||||
env.context_id = context_id + 1;
|
|
||||||
|
|
||||||
self.context = Context{
|
|
||||||
.page = page,
|
|
||||||
.id = context_id,
|
|
||||||
.isolate = isolate,
|
|
||||||
.handle = v8_context,
|
|
||||||
.templates = env.templates,
|
|
||||||
.handle_scope = handle_scope,
|
|
||||||
.script_manager = &page._script_manager,
|
|
||||||
.call_arena = page.call_arena,
|
|
||||||
.arena = arena,
|
|
||||||
};
|
|
||||||
self.persisted_context = persisted_context;
|
|
||||||
|
|
||||||
var context = &self.context.?;
|
|
||||||
// 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(context.handle, 1, @ptrCast(data.handle));
|
|
||||||
|
|
||||||
try context.setupGlobal();
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn removeContext(self: *ExecutionWorld) void {
|
|
||||||
var context = &(self.context orelse return);
|
|
||||||
context.deinit();
|
|
||||||
self.context = null;
|
|
||||||
|
|
||||||
self.persisted_context.?.deinit();
|
|
||||||
self.persisted_context = null;
|
|
||||||
|
|
||||||
self.env.isolate.notifyContextDisposed();
|
|
||||||
_ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn terminateExecution(self: *const ExecutionWorld) void {
|
|
||||||
self.env.isolate.terminateExecution();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resumeExecution(self: *const ExecutionWorld) void {
|
|
||||||
self.env.isolate.cancelTerminateExecution();
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -20,11 +20,11 @@ const std = @import("std");
|
|||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
const Function = @This();
|
const Function = @This();
|
||||||
|
|
||||||
ctx: *js.Context,
|
local: *const js.Local,
|
||||||
this: ?*const v8.Object = null,
|
this: ?*const v8.Object = null,
|
||||||
handle: *const v8.Function,
|
handle: *const v8.Function,
|
||||||
|
|
||||||
@@ -34,60 +34,77 @@ pub const Result = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn withThis(self: *const Function, value: anytype) !Function {
|
pub fn withThis(self: *const Function, value: anytype) !Function {
|
||||||
|
const local = self.local;
|
||||||
const this_obj = if (@TypeOf(value) == js.Object)
|
const this_obj = if (@TypeOf(value) == js.Object)
|
||||||
value.handle
|
value.handle
|
||||||
else
|
else
|
||||||
(try self.ctx.zigValueToJs(value, .{})).handle;
|
(try local.zigValueToJs(value, .{})).handle;
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = local,
|
||||||
.this = this_obj,
|
.this = this_obj,
|
||||||
.handle = self.handle,
|
.handle = self.handle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Object {
|
pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Object {
|
||||||
const ctx = self.ctx;
|
const local = self.local;
|
||||||
|
|
||||||
var try_catch: js.TryCatch = undefined;
|
var try_catch: js.TryCatch = undefined;
|
||||||
try_catch.init(ctx);
|
try_catch.init(local);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
// This creates a new instance using this Function as a constructor.
|
// This creates a new instance using this Function as a constructor.
|
||||||
// const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{}));
|
// const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{}));
|
||||||
const handle = v8.v8__Function__NewInstance(self.handle, ctx.handle, 0, null) orelse {
|
const handle = v8.v8__Function__NewInstance(self.handle, local.handle, 0, null) orelse {
|
||||||
caught.* = try_catch.caughtOrError(ctx.call_arena, error.Unknown);
|
caught.* = try_catch.caughtOrError(local.call_arena, error.Unknown);
|
||||||
return error.JsConstructorFailed;
|
return error.JsConstructorFailed;
|
||||||
};
|
};
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.ctx = ctx,
|
.local = local,
|
||||||
.handle = handle,
|
.handle = handle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
|
pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
|
||||||
return self.callWithThis(T, self.getThis(), args);
|
var caught: js.TryCatch.Caught = undefined;
|
||||||
|
return self._tryCallWithThis(T, self.getThis(), args, &caught, .{}) catch |err| {
|
||||||
|
log.warn(.js, "call caught", .{ .err = err, .caught = caught });
|
||||||
|
return err;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, caught: *js.TryCatch.Caught) !T {
|
pub fn callRethrow(self: *const Function, comptime T: type, args: anytype) !T {
|
||||||
return self.tryCallWithThis(T, self.getThis(), args, caught);
|
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 });
|
||||||
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T {
|
|
||||||
var try_catch: js.TryCatch = undefined;
|
|
||||||
|
|
||||||
try_catch.init(self.ctx);
|
|
||||||
defer try_catch.deinit();
|
|
||||||
|
|
||||||
return self.callWithThis(T, this, args) catch |err| {
|
|
||||||
caught.* = try_catch.caughtOrError(self.ctx.call_arena, err);
|
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
|
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
|
||||||
const ctx = self.ctx;
|
var caught: js.TryCatch.Caught = undefined;
|
||||||
|
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, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
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, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
// When we're calling a function from within JavaScript itself, this isn't
|
// When we're calling a function from within JavaScript itself, this isn't
|
||||||
// necessary. We're within a Caller instantiation, which will already have
|
// necessary. We're within a Caller instantiation, which will already have
|
||||||
@@ -98,6 +115,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
|
|||||||
// need to increase the call_depth so that the call_arena remains valid for
|
// need to increase the call_depth so that the call_arena remains valid for
|
||||||
// the duration of the function call. If we don't do this, the call_arena
|
// the duration of the function call. If we don't do this, the call_arena
|
||||||
// will be reset after each statement of the function which executes Zig code.
|
// will be reset after each statement of the function which executes Zig code.
|
||||||
|
const ctx = local.ctx;
|
||||||
const call_depth = ctx.call_depth;
|
const call_depth = ctx.call_depth;
|
||||||
ctx.call_depth = call_depth + 1;
|
ctx.call_depth = call_depth + 1;
|
||||||
defer ctx.call_depth = call_depth;
|
defer ctx.call_depth = call_depth;
|
||||||
@@ -106,7 +124,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
|
|||||||
if (@TypeOf(this) == js.Object) {
|
if (@TypeOf(this) == js.Object) {
|
||||||
break :blk this;
|
break :blk this;
|
||||||
}
|
}
|
||||||
break :blk try ctx.zigValueToJs(this, .{});
|
break :blk try local.zigValueToJs(this, .{});
|
||||||
};
|
};
|
||||||
|
|
||||||
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
|
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
|
||||||
@@ -116,15 +134,15 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
|
|||||||
const fields = s.fields;
|
const fields = s.fields;
|
||||||
var js_args: [fields.len]*const v8.Value = undefined;
|
var js_args: [fields.len]*const v8.Value = undefined;
|
||||||
inline for (fields, 0..) |f, i| {
|
inline for (fields, 0..) |f, i| {
|
||||||
js_args[i] = (try ctx.zigValueToJs(@field(aargs, f.name), .{})).handle;
|
js_args[i] = (try local.zigValueToJs(@field(aargs, f.name), .{})).handle;
|
||||||
}
|
}
|
||||||
const cargs: [fields.len]*const v8.Value = js_args;
|
const cargs: [fields.len]*const v8.Value = js_args;
|
||||||
break :blk &cargs;
|
break :blk &cargs;
|
||||||
},
|
},
|
||||||
.pointer => blk: {
|
.pointer => blk: {
|
||||||
var values = try ctx.call_arena.alloc(*const v8.Value, args.len);
|
var values = try local.call_arena.alloc(*const v8.Value, args.len);
|
||||||
for (args, 0..) |a, i| {
|
for (args, 0..) |a, i| {
|
||||||
values[i] = (try ctx.zigValueToJs(a, .{})).handle;
|
values[i] = (try local.zigValueToJs(a, .{})).handle;
|
||||||
}
|
}
|
||||||
break :blk values;
|
break :blk values;
|
||||||
},
|
},
|
||||||
@@ -132,54 +150,75 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
|
|||||||
};
|
};
|
||||||
|
|
||||||
const c_args = @as(?[*]const ?*v8.Value, @ptrCast(js_args.ptr));
|
const c_args = @as(?[*]const ?*v8.Value, @ptrCast(js_args.ptr));
|
||||||
const handle = v8.v8__Function__Call(self.handle, ctx.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse {
|
|
||||||
// std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"});
|
var try_catch: js.TryCatch = undefined;
|
||||||
|
try_catch.init(local);
|
||||||
|
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;
|
return error.JSExecCallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (@typeInfo(T) == .void) {
|
if (@typeInfo(T) == .void) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return ctx.jsValueToZig(T, .{ .ctx = ctx, .handle = handle });
|
return local.jsValueToZig(T, .{ .local = local, .handle = handle });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getThis(self: *const Function) js.Object {
|
fn getThis(self: *const Function) js.Object {
|
||||||
const handle = if (self.this) |t| t else v8.v8__Context__Global(self.ctx.handle).?;
|
const handle = if (self.this) |t| t else v8.v8__Context__Global(self.local.handle).?;
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = self.local,
|
||||||
.handle = handle,
|
.handle = handle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn src(self: *const Function) ![]const u8 {
|
pub fn src(self: *const Function) ![]const u8 {
|
||||||
return self.context.valueToString(.{ .handle = @ptrCast(self.handle) }, .{});
|
return self.local.valueToString(.{ .local = self.local, .handle = @ptrCast(self.handle) }, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value {
|
pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value {
|
||||||
const ctx = self.ctx;
|
const local = self.local;
|
||||||
const key = ctx.isolate.initStringHandle(name);
|
const key = local.isolate.initStringHandle(name);
|
||||||
const handle = v8.v8__Object__Get(self.handle, ctx.handle, key) orelse {
|
const handle = v8.v8__Object__Get(self.handle, self.local.handle, key) orelse {
|
||||||
return error.JsException;
|
return error.JsException;
|
||||||
};
|
};
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.ctx = ctx,
|
.local = local,
|
||||||
.handle = handle,
|
.handle = handle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn persist(self: *const Function) !Global {
|
pub fn persist(self: *const Function) !Global {
|
||||||
var ctx = self.ctx;
|
return self._persist(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn temp(self: *const Function) !Temp {
|
||||||
|
return self._persist(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Global else Temp) {
|
||||||
|
var ctx = self.local.ctx;
|
||||||
|
|
||||||
var global: v8.Global = undefined;
|
var global: v8.Global = undefined;
|
||||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
|
if (comptime is_global) {
|
||||||
|
try ctx.global_functions.append(ctx.arena, global);
|
||||||
|
} else {
|
||||||
|
try ctx.global_functions_temp.put(ctx.arena, global.data_ptr, global);
|
||||||
|
}
|
||||||
|
return .{ .handle = global };
|
||||||
|
}
|
||||||
|
|
||||||
try ctx.global_functions.append(ctx.arena, global);
|
pub fn tempWithThis(self: *const Function, value: anytype) !Temp {
|
||||||
|
const with_this = try self.withThis(value);
|
||||||
return .{
|
return with_this.temp();
|
||||||
.handle = global,
|
|
||||||
.ctx = ctx,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn persistWithThis(self: *const Function, value: anytype) !Global {
|
pub fn persistWithThis(self: *const Function, value: anytype) !Global {
|
||||||
@@ -187,22 +226,31 @@ pub fn persistWithThis(self: *const Function, value: anytype) !Global {
|
|||||||
return with_this.persist();
|
return with_this.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Global = struct {
|
pub const Temp = G(0);
|
||||||
handle: v8.Global,
|
pub const Global = G(1);
|
||||||
ctx: *js.Context,
|
|
||||||
|
|
||||||
pub fn deinit(self: *Global) void {
|
fn G(comptime discriminator: u8) type {
|
||||||
v8.v8__Global__Reset(&self.handle);
|
return struct {
|
||||||
}
|
handle: v8.Global,
|
||||||
|
|
||||||
pub fn local(self: *const Global) Function {
|
// makes the types different (G(0) != G(1)), without taking up space
|
||||||
return .{
|
comptime _: u8 = discriminator,
|
||||||
.ctx = self.ctx,
|
|
||||||
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isEqual(self: *const Global, other: Function) bool {
|
const Self = @This();
|
||||||
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
|
||||||
}
|
pub fn deinit(self: *Self) void {
|
||||||
};
|
v8.v8__Global__Reset(&self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local(self: *const Self, l: *const js.Local) Function {
|
||||||
|
return .{
|
||||||
|
.local = l,
|
||||||
|
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isEqual(self: *const Self, other: Function) bool {
|
||||||
|
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,102 +20,79 @@ const std = @import("std");
|
|||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const Context = @import("Context.zig");
|
const TaggedOpaque = @import("TaggedOpaque.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const RndGen = std.Random.DefaultPrng;
|
|
||||||
|
|
||||||
const CONTEXT_GROUP_ID = 1;
|
const CONTEXT_GROUP_ID = 1;
|
||||||
const CLIENT_TRUST_LEVEL = 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();
|
const Inspector = @This();
|
||||||
|
|
||||||
handle: *v8.Inspector,
|
unique_id: i64,
|
||||||
isolate: *v8.Isolate,
|
isolate: *v8.Isolate,
|
||||||
client: Client,
|
handle: *v8.Inspector,
|
||||||
channel: Channel,
|
client: *v8.InspectorClientImpl,
|
||||||
session: Session,
|
default_context: ?v8.Global,
|
||||||
rnd: RndGen = RndGen.init(0),
|
session: ?Session,
|
||||||
default_context: ?*const v8.Context = null,
|
|
||||||
|
|
||||||
// We expect allocator to be an arena
|
pub fn init(allocator: Allocator, isolate: *v8.Isolate) !*Inspector {
|
||||||
// Note: This initializes the pre-allocated inspector in-place
|
const self = try allocator.create(Inspector);
|
||||||
pub fn init(self: *Inspector, isolate: *v8.Isolate, ctx: anytype) !void {
|
errdefer allocator.destroy(self);
|
||||||
const ContextT = @TypeOf(ctx);
|
|
||||||
|
|
||||||
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.* = .{
|
self.* = .{
|
||||||
.handle = undefined,
|
.unique_id = 1,
|
||||||
|
.session = null,
|
||||||
.isolate = isolate,
|
.isolate = isolate,
|
||||||
.client = undefined,
|
.client = undefined,
|
||||||
.channel = undefined,
|
.handle = undefined,
|
||||||
.rnd = RndGen.init(0),
|
|
||||||
.default_context = null,
|
.default_context = null,
|
||||||
.session = undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create client and set inspector data BEFORE creating the inspector
|
self.client = v8.v8_inspector__Client__IMPL__CREATE();
|
||||||
// because V8 will call generateUniqueId during inspector creation
|
errdefer v8.v8_inspector__Client__IMPL__DELETE(self.client);
|
||||||
const client = Client.init();
|
v8.v8_inspector__Client__IMPL__SET_DATA(self.client, self);
|
||||||
self.client = client;
|
|
||||||
client.setInspector(self);
|
|
||||||
|
|
||||||
// Now create the inspector - generateUniqueId will work because data is set
|
self.handle = v8.v8_inspector__Inspector__Create(isolate, self.client).?;
|
||||||
const handle = v8.v8_inspector__Inspector__Create(isolate, client.handle).?;
|
errdefer v8.v8_inspector__Inspector__DELETE(self.handle);
|
||||||
self.handle = handle;
|
|
||||||
|
|
||||||
// Create the channel
|
return self;
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *const Inspector) void {
|
pub fn deinit(self: *const Inspector, allocator: Allocator) void {
|
||||||
var temp_scope: v8.HandleScope = undefined;
|
var hs: v8.HandleScope = undefined;
|
||||||
v8.v8__HandleScope__CONSTRUCT(&temp_scope, self.isolate);
|
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate);
|
||||||
defer v8.v8__HandleScope__DESTRUCT(&temp_scope);
|
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||||
|
|
||||||
self.session.deinit();
|
if (self.session) |*s| {
|
||||||
self.client.deinit();
|
s.deinit();
|
||||||
self.channel.deinit();
|
}
|
||||||
|
v8.v8_inspector__Client__IMPL__DELETE(self.client);
|
||||||
v8.v8_inspector__Inspector__DELETE(self.handle);
|
v8.v8_inspector__Inspector__DELETE(self.handle);
|
||||||
|
allocator.destroy(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(self: *const Inspector, msg: []const u8) void {
|
pub fn startSession(self: *Inspector, ctx: anytype) *Session {
|
||||||
// Can't assume the main Context exists (with its HandleScope)
|
if (comptime IS_DEBUG) {
|
||||||
// available when doing this. Pages (and thus the HandleScope)
|
std.debug.assert(self.session == null);
|
||||||
// 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);
|
|
||||||
|
|
||||||
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
|
// From CDP docs
|
||||||
@@ -128,7 +105,7 @@ pub fn send(self: *const Inspector, msg: []const u8) void {
|
|||||||
// - is_default_context: Whether the execution context is default, should match the auxData
|
// - is_default_context: Whether the execution context is default, should match the auxData
|
||||||
pub fn contextCreated(
|
pub fn contextCreated(
|
||||||
self: *Inspector,
|
self: *Inspector,
|
||||||
context: *const Context,
|
local: *const js.Local,
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
origin: []const u8,
|
origin: []const u8,
|
||||||
aux_data: []const u8,
|
aux_data: []const u8,
|
||||||
@@ -143,56 +120,24 @@ pub fn contextCreated(
|
|||||||
aux_data.ptr,
|
aux_data.ptr,
|
||||||
aux_data.len,
|
aux_data.len,
|
||||||
CONTEXT_GROUP_ID,
|
CONTEXT_GROUP_ID,
|
||||||
context.handle,
|
local.handle,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (is_default_context) {
|
if (is_default_context) {
|
||||||
self.default_context = context.handle;
|
self.default_context = local.ctx.handle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieves the RemoteObject for a given value.
|
pub fn contextDestroyed(self: *Inspector, context: *const v8.Context) void {
|
||||||
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
|
v8.v8_inspector__Inspector__ContextDestroyed(self.handle, context);
|
||||||
// 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,
|
|
||||||
context: *Context,
|
|
||||||
group: []const u8,
|
|
||||||
value: anytype,
|
|
||||||
) !RemoteObject {
|
|
||||||
const js_value = try context.zigValueToJs(value, .{});
|
|
||||||
|
|
||||||
// We do not want to expose this as a parameter for now
|
|
||||||
const generate_preview = false;
|
|
||||||
return self.session.wrapObject(
|
|
||||||
context.isolate.handle,
|
|
||||||
context.handle,
|
|
||||||
js_value.handle,
|
|
||||||
group,
|
|
||||||
generate_preview,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets a value by object ID regardless of which context it is in.
|
pub fn resetContextGroup(self: *const Inspector) void {
|
||||||
// Our TaggedAnyOpaque stores the "resolved" ptr value (the most specific _type,
|
var hs: v8.HandleScope = undefined;
|
||||||
// e.g. we store the ptr to the Div not the EventTarget). But, this is asking for
|
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate);
|
||||||
// the pointer to the Node, so we need to use the same resolution mechanism which
|
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||||
// is used when we're calling a function to turn the Div into a Node, which is
|
|
||||||
// what Context.typeTaggedAnyOpaque does.
|
v8.v8_inspector__Inspector__ResetContextGroup(self.handle, CONTEXT_GROUP_ID);
|
||||||
pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8) !*anyopaque {
|
|
||||||
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 Context.typeTaggedAnyOpaque(*Node, @ptrCast(js_val)) catch {
|
|
||||||
return error.ObjectIdIsNotANode;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const RemoteObject = struct {
|
pub const RemoteObject = struct {
|
||||||
@@ -241,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,
|
handle: *v8.InspectorSession,
|
||||||
|
channel: *v8.InspectorChannelImpl,
|
||||||
|
|
||||||
fn deinit(self: Session) void {
|
// callbacks
|
||||||
v8.v8_inspector__Session__DELETE(self.handle);
|
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(
|
v8.v8_inspector__Session__dispatchProtocolMessage(
|
||||||
self.handle,
|
self.handle,
|
||||||
isolate,
|
isolate,
|
||||||
msg.ptr,
|
msg.ptr,
|
||||||
msg.len,
|
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(
|
fn wrapObject(
|
||||||
@@ -321,85 +355,7 @@ const UnwrappedObject = struct {
|
|||||||
object_group: ?[]const u8,
|
object_group: ?[]const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Channel = struct {
|
pub fn getTaggedOpaque(value: *const v8.Value) ?*TaggedOpaque {
|
||||||
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 getTaggedAnyOpaque(value: *const v8.Value) ?*js.TaggedAnyOpaque {
|
|
||||||
if (!v8.v8__Value__IsObject(value)) {
|
if (!v8.v8__Value__IsObject(value)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -424,24 +380,25 @@ pub export fn v8_inspector__Client__IMPL__generateUniqueId(
|
|||||||
data: *anyopaque,
|
data: *anyopaque,
|
||||||
) callconv(.c) i64 {
|
) callconv(.c) i64 {
|
||||||
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
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(
|
pub export fn v8_inspector__Client__IMPL__runMessageLoopOnPause(
|
||||||
_: *v8.InspectorClientImpl,
|
_: *v8.InspectorClientImpl,
|
||||||
data: *anyopaque,
|
data: *anyopaque,
|
||||||
ctx_group_id: c_int,
|
context_group_id: c_int,
|
||||||
) callconv(.c) void {
|
) callconv(.c) void {
|
||||||
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
_ = data;
|
||||||
inspector.channel.onRunMessageLoopOnPause(inspector.channel.ctx, @intCast(ctx_group_id));
|
_ = context_group_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub export fn v8_inspector__Client__IMPL__quitMessageLoopOnPause(
|
pub export fn v8_inspector__Client__IMPL__quitMessageLoopOnPause(
|
||||||
_: *v8.InspectorClientImpl,
|
_: *v8.InspectorClientImpl,
|
||||||
data: *anyopaque,
|
data: *anyopaque,
|
||||||
) callconv(.c) void {
|
) callconv(.c) void {
|
||||||
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
_ = data;
|
||||||
inspector.channel.onQuitMessageLoopOnPause(inspector.channel.ctx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub export fn v8_inspector__Client__IMPL__runIfWaitingForDebugger(
|
pub export fn v8_inspector__Client__IMPL__runIfWaitingForDebugger(
|
||||||
@@ -469,7 +426,8 @@ pub export fn v8_inspector__Client__IMPL__ensureDefaultContextInGroup(
|
|||||||
data: *anyopaque,
|
data: *anyopaque,
|
||||||
) callconv(.c) ?*const v8.Context {
|
) callconv(.c) ?*const v8.Context {
|
||||||
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
||||||
return inspector.default_context;
|
const global_handle = inspector.default_context orelse return null;
|
||||||
|
return v8.v8__Global__Get(&global_handle, inspector.isolate);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub export fn v8_inspector__Channel__IMPL__sendResponse(
|
pub export fn v8_inspector__Channel__IMPL__sendResponse(
|
||||||
@@ -479,8 +437,8 @@ pub export fn v8_inspector__Channel__IMPL__sendResponse(
|
|||||||
msg: [*c]u8,
|
msg: [*c]u8,
|
||||||
length: usize,
|
length: usize,
|
||||||
) callconv(.c) void {
|
) callconv(.c) void {
|
||||||
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
const session: *Session = @ptrCast(@alignCast(data));
|
||||||
inspector.channel.resp(@as(u32, @intCast(call_id)), msg[0..length]);
|
session.onResp(session.ctx, @intCast(call_id), msg[0..length]);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub export fn v8_inspector__Channel__IMPL__sendNotification(
|
pub export fn v8_inspector__Channel__IMPL__sendNotification(
|
||||||
@@ -489,8 +447,8 @@ pub export fn v8_inspector__Channel__IMPL__sendNotification(
|
|||||||
msg: [*c]u8,
|
msg: [*c]u8,
|
||||||
length: usize,
|
length: usize,
|
||||||
) callconv(.c) void {
|
) callconv(.c) void {
|
||||||
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
const session: *Session = @ptrCast(@alignCast(data));
|
||||||
inspector.channel.notif(msg[0..length]);
|
session.onNotif(session.ctx, msg[0..length]);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub export fn v8_inspector__Channel__IMPL__flushProtocolNotifications(
|
pub export fn v8_inspector__Channel__IMPL__flushProtocolNotifications(
|
||||||
|
|||||||
@@ -57,6 +57,16 @@ pub fn lowMemoryNotification(self: Isolate) void {
|
|||||||
v8.v8__Isolate__LowMemoryNotification(self.handle);
|
v8.v8__Isolate__LowMemoryNotification(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const MemoryPressureLevel = enum(u32) {
|
||||||
|
none = v8.kNone,
|
||||||
|
moderate = v8.kModerate,
|
||||||
|
critical = v8.kCritical,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn memoryPressureNotification(self: Isolate, level: MemoryPressureLevel) void {
|
||||||
|
v8.v8__Isolate__MemoryPressureNotification(self.handle, @intFromEnum(level));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn notifyContextDisposed(self: Isolate) void {
|
pub fn notifyContextDisposed(self: Isolate) void {
|
||||||
_ = v8.v8__Isolate__ContextDisposedNotification(self.handle);
|
_ = v8.v8__Isolate__ContextDisposedNotification(self.handle);
|
||||||
}
|
}
|
||||||
|
|||||||
1321
src/browser/js/Local.zig
Normal file
1321
src/browser/js/Local.zig
Normal file
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@ const v8 = js.v8;
|
|||||||
|
|
||||||
const Module = @This();
|
const Module = @This();
|
||||||
|
|
||||||
ctx: *js.Context,
|
local: *const js.Local,
|
||||||
handle: *const v8.Module,
|
handle: *const v8.Module,
|
||||||
|
|
||||||
pub const Status = enum(u32) {
|
pub const Status = enum(u32) {
|
||||||
@@ -39,21 +39,21 @@ pub fn getStatus(self: Module) Status {
|
|||||||
|
|
||||||
pub fn getException(self: Module) js.Value {
|
pub fn getException(self: Module) js.Value {
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = self.local,
|
||||||
.handle = v8.v8__Module__GetException(self.handle).?,
|
.handle = v8.v8__Module__GetException(self.handle).?,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getModuleRequests(self: Module) Requests {
|
pub fn getModuleRequests(self: Module) Requests {
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx.handle,
|
.context_handle = self.local.handle,
|
||||||
.handle = v8.v8__Module__GetModuleRequests(self.handle).?,
|
.handle = v8.v8__Module__GetModuleRequests(self.handle).?,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn instantiate(self: Module, cb: v8.ResolveModuleCallback) !bool {
|
pub fn instantiate(self: Module, cb: v8.ResolveModuleCallback) !bool {
|
||||||
var out: v8.MaybeBool = undefined;
|
var out: v8.MaybeBool = undefined;
|
||||||
v8.v8__Module__InstantiateModule(self.handle, self.ctx.handle, cb, &out);
|
v8.v8__Module__InstantiateModule(self.handle, self.local.handle, cb, &out);
|
||||||
if (out.has_value) {
|
if (out.has_value) {
|
||||||
return out.value;
|
return out.value;
|
||||||
}
|
}
|
||||||
@@ -61,15 +61,14 @@ pub fn instantiate(self: Module, cb: v8.ResolveModuleCallback) !bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn evaluate(self: Module) !js.Value {
|
pub fn evaluate(self: Module) !js.Value {
|
||||||
const ctx = self.ctx;
|
const res = v8.v8__Module__Evaluate(self.handle, self.local.handle) orelse return error.JsException;
|
||||||
const res = v8.v8__Module__Evaluate(self.handle, ctx.handle) orelse return error.JsException;
|
|
||||||
|
|
||||||
if (self.getStatus() == .kErrored) {
|
if (self.getStatus() == .kErrored) {
|
||||||
return error.JsException;
|
return error.JsException;
|
||||||
}
|
}
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.ctx = ctx,
|
.local = self.local,
|
||||||
.handle = res,
|
.handle = res,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -80,7 +79,7 @@ pub fn getIdentityHash(self: Module) u32 {
|
|||||||
|
|
||||||
pub fn getModuleNamespace(self: Module) js.Value {
|
pub fn getModuleNamespace(self: Module) js.Value {
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = self.local,
|
||||||
.handle = v8.v8__Module__GetModuleNamespace(self.handle).?,
|
.handle = v8.v8__Module__GetModuleNamespace(self.handle).?,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -90,28 +89,24 @@ pub fn getScriptId(self: Module) u32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn persist(self: Module) !Global {
|
pub fn persist(self: Module) !Global {
|
||||||
var ctx = self.ctx;
|
var ctx = self.local.ctx;
|
||||||
var global: v8.Global = undefined;
|
var global: v8.Global = undefined;
|
||||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
try ctx.global_modules.append(ctx.arena, global);
|
try ctx.global_modules.append(ctx.arena, global);
|
||||||
return .{
|
return .{ .handle = global };
|
||||||
.handle = global,
|
|
||||||
.ctx = ctx,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Global = struct {
|
pub const Global = struct {
|
||||||
handle: v8.Global,
|
handle: v8.Global,
|
||||||
ctx: *js.Context,
|
|
||||||
|
|
||||||
pub fn deinit(self: *Global) void {
|
pub fn deinit(self: *Global) void {
|
||||||
v8.v8__Global__Reset(&self.handle);
|
v8.v8__Global__Reset(&self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn local(self: *const Global) Module {
|
pub fn local(self: *const Global, l: *const js.Local) Module {
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = l,
|
||||||
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
|
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,22 +116,22 @@ pub const Global = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Requests = struct {
|
const Requests = struct {
|
||||||
ctx: *const v8.Context,
|
|
||||||
handle: *const v8.FixedArray,
|
handle: *const v8.FixedArray,
|
||||||
|
context_handle: *const v8.Context,
|
||||||
|
|
||||||
pub fn len(self: Requests) usize {
|
pub fn len(self: Requests) usize {
|
||||||
return @intCast(v8.v8__FixedArray__Length(self.handle));
|
return @intCast(v8.v8__FixedArray__Length(self.handle));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(self: Requests, idx: usize) Request {
|
pub fn get(self: Requests, idx: usize) Request {
|
||||||
return .{ .handle = v8.v8__FixedArray__Get(self.handle, self.ctx, @intCast(idx)).? };
|
return .{ .handle = v8.v8__FixedArray__Get(self.handle, self.context_handle, @intCast(idx)).? };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Request = struct {
|
const Request = struct {
|
||||||
handle: *const v8.ModuleRequest,
|
handle: *const v8.ModuleRequest,
|
||||||
|
|
||||||
pub fn specifier(self: Request) *const v8.String {
|
pub fn specifier(self: Request, local: *const js.Local) js.String {
|
||||||
return v8.v8__ModuleRequest__GetSpecifier(self.handle).?;
|
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>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -22,25 +22,17 @@ const v8 = js.v8;
|
|||||||
|
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
const Context = @import("Context.zig");
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const Object = @This();
|
const Object = @This();
|
||||||
|
|
||||||
ctx: *js.Context,
|
local: *const js.Local,
|
||||||
handle: *const v8.Object,
|
handle: *const v8.Object,
|
||||||
|
|
||||||
pub fn getId(self: Object) u32 {
|
|
||||||
return @bitCast(v8.v8__Object__GetIdentityHash(self.handle));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has(self: Object, key: anytype) bool {
|
pub fn has(self: Object, key: anytype) bool {
|
||||||
const ctx = self.ctx;
|
const ctx = self.local.ctx;
|
||||||
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
|
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
|
||||||
|
|
||||||
var out: v8.MaybeBool = undefined;
|
var out: v8.MaybeBool = undefined;
|
||||||
v8.v8__Object__Has(self.handle, self.ctx.handle, key_handle, &out);
|
v8.v8__Object__Has(self.handle, self.local.handle, key_handle, &out);
|
||||||
if (out.has_value) {
|
if (out.has_value) {
|
||||||
return out.value;
|
return out.value;
|
||||||
}
|
}
|
||||||
@@ -48,34 +40,34 @@ pub fn has(self: Object, key: anytype) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(self: Object, key: anytype) !js.Value {
|
pub fn get(self: Object, key: anytype) !js.Value {
|
||||||
const ctx = self.ctx;
|
const ctx = self.local.ctx;
|
||||||
|
|
||||||
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
|
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
|
||||||
const js_val_handle = v8.v8__Object__Get(self.handle, ctx.handle, key_handle) orelse return error.JsException;
|
const js_val_handle = v8.v8__Object__Get(self.handle, self.local.handle, key_handle) orelse return error.JsException;
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.ctx = ctx,
|
.local = self.local,
|
||||||
.handle = js_val_handle,
|
.handle = js_val_handle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(self: Object, key: anytype, value: anytype, comptime opts: js.bridge.Caller.CallOpts) !bool {
|
pub fn set(self: Object, key: anytype, value: anytype, comptime opts: js.Caller.CallOpts) !bool {
|
||||||
const ctx = self.ctx;
|
const ctx = self.local.ctx;
|
||||||
|
|
||||||
const js_value = try ctx.zigValueToJs(value, opts);
|
const js_value = try self.local.zigValueToJs(value, opts);
|
||||||
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
|
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
|
||||||
|
|
||||||
var out: v8.MaybeBool = undefined;
|
var out: v8.MaybeBool = undefined;
|
||||||
v8.v8__Object__Set(self.handle, ctx.handle, key_handle, js_value.handle, &out);
|
v8.v8__Object__Set(self.handle, self.local.handle, key_handle, js_value.handle, &out);
|
||||||
return out.has_value;
|
return out.has_value;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn defineOwnProperty(self: Object, name: []const u8, value: js.Value, attr: v8.PropertyAttribute) ?bool {
|
pub fn defineOwnProperty(self: Object, name: []const u8, value: js.Value, attr: v8.PropertyAttribute) ?bool {
|
||||||
const ctx = self.ctx;
|
const ctx = self.local.ctx;
|
||||||
const name_handle = ctx.isolate.initStringHandle(name);
|
const name_handle = ctx.isolate.initStringHandle(name);
|
||||||
|
|
||||||
var out: v8.MaybeBool = undefined;
|
var out: v8.MaybeBool = undefined;
|
||||||
v8.v8__Object__DefineOwnProperty(self.handle, ctx.handle, @ptrCast(name_handle), value.handle, attr, &out);
|
v8.v8__Object__DefineOwnProperty(self.handle, self.local.handle, @ptrCast(name_handle), value.handle, attr, &out);
|
||||||
|
|
||||||
if (out.has_value) {
|
if (out.has_value) {
|
||||||
return out.value;
|
return out.value;
|
||||||
@@ -84,53 +76,46 @@ pub fn defineOwnProperty(self: Object, name: []const u8, value: js.Value, attr:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toString(self: Object) ![]const u8 {
|
|
||||||
return self.ctx.valueToString(self.toValue(), .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toValue(self: Object) js.Value {
|
pub fn toValue(self: Object) js.Value {
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = self.local,
|
||||||
.handle = @ptrCast(self.handle),
|
.handle = @ptrCast(self.handle),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format(self: Object, writer: *std.Io.Writer) !void {
|
pub fn format(self: Object, writer: *std.Io.Writer) !void {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
return self.ctx.debugValue(self.toValue(), writer);
|
return self.local.ctx.debugValue(self.toValue(), writer);
|
||||||
}
|
}
|
||||||
const str = self.toString() catch return error.WriteFailed;
|
const str = self.toString() catch return error.WriteFailed;
|
||||||
return writer.writeAll(str);
|
return writer.writeAll(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn persist(self: Object) !Global {
|
pub fn persist(self: Object) !Global {
|
||||||
var ctx = self.ctx;
|
var ctx = self.local.ctx;
|
||||||
|
|
||||||
var global: v8.Global = undefined;
|
var global: v8.Global = undefined;
|
||||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
|
|
||||||
try ctx.global_objects.append(ctx.arena, global);
|
try ctx.global_objects.append(ctx.arena, global);
|
||||||
|
|
||||||
return .{
|
return .{ .handle = global };
|
||||||
.handle = global,
|
|
||||||
.ctx = ctx,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getFunction(self: Object, name: []const u8) !?js.Function {
|
pub fn getFunction(self: Object, name: []const u8) !?js.Function {
|
||||||
if (self.isNullOrUndefined()) {
|
if (self.isNullOrUndefined()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const ctx = self.ctx;
|
const local = self.local;
|
||||||
|
|
||||||
const js_name = ctx.isolate.initStringHandle(name);
|
const js_name = local.isolate.initStringHandle(name);
|
||||||
const js_val_handle = v8.v8__Object__Get(self.handle, ctx.handle, js_name) orelse return error.JsException;
|
const js_val_handle = v8.v8__Object__Get(self.handle, local.handle, js_name) orelse return error.JsException;
|
||||||
|
|
||||||
if (v8.v8__Value__IsFunction(js_val_handle) == false) {
|
if (v8.v8__Value__IsFunction(js_val_handle) == false) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return .{
|
return .{
|
||||||
.ctx = ctx,
|
.local = local,
|
||||||
.handle = @ptrCast(js_val_handle),
|
.handle = @ptrCast(js_val_handle),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -145,51 +130,48 @@ pub fn isNullOrUndefined(self: Object) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getOwnPropertyNames(self: Object) js.Array {
|
pub fn getOwnPropertyNames(self: Object) js.Array {
|
||||||
const handle = v8.v8__Object__GetOwnPropertyNames(self.handle, self.ctx.handle).?;
|
const handle = v8.v8__Object__GetOwnPropertyNames(self.handle, self.local.handle).?;
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = self.local,
|
||||||
.handle = handle,
|
.handle = handle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getPropertyNames(self: Object) js.Array {
|
pub fn getPropertyNames(self: Object) js.Array {
|
||||||
const handle = v8.v8__Object__GetPropertyNames(self.handle, self.ctx.handle).?;
|
const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle).?;
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = self.local,
|
||||||
.handle = handle,
|
.handle = handle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nameIterator(self: Object) NameIterator {
|
pub fn nameIterator(self: Object) NameIterator {
|
||||||
const ctx = self.ctx;
|
const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle).?;
|
||||||
|
|
||||||
const handle = v8.v8__Object__GetPropertyNames(self.handle, ctx.handle).?;
|
|
||||||
const count = v8.v8__Array__Length(handle);
|
const count = v8.v8__Array__Length(handle);
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.ctx = ctx,
|
.local = self.local,
|
||||||
.handle = handle,
|
.handle = handle,
|
||||||
.count = count,
|
.count = count,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toZig(self: Object, comptime T: type) !T {
|
pub fn toZig(self: Object, comptime T: type) !T {
|
||||||
const js_value = js.Value{ .ctx = self.ctx, .handle = @ptrCast(self.handle) };
|
const js_value = js.Value{ .local = self.local, .handle = @ptrCast(self.handle) };
|
||||||
return self.ctx.jsValueToZig(T, js_value);
|
return self.local.jsValueToZig(T, js_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Global = struct {
|
pub const Global = struct {
|
||||||
handle: v8.Global,
|
handle: v8.Global,
|
||||||
ctx: *js.Context,
|
|
||||||
|
|
||||||
pub fn deinit(self: *Global) void {
|
pub fn deinit(self: *Global) void {
|
||||||
v8.v8__Global__Reset(&self.handle);
|
v8.v8__Global__Reset(&self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn local(self: *const Global) Object {
|
pub fn local(self: *const Global, l: *const js.Local) Object {
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = l,
|
||||||
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
|
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +183,7 @@ pub const Global = struct {
|
|||||||
pub const NameIterator = struct {
|
pub const NameIterator = struct {
|
||||||
count: u32,
|
count: u32,
|
||||||
idx: u32 = 0,
|
idx: u32 = 0,
|
||||||
ctx: *Context,
|
local: *const js.Local,
|
||||||
handle: *const v8.Array,
|
handle: *const v8.Array,
|
||||||
|
|
||||||
pub fn next(self: *NameIterator) !?[]const u8 {
|
pub fn next(self: *NameIterator) !?[]const u8 {
|
||||||
@@ -211,8 +193,8 @@ pub const NameIterator = struct {
|
|||||||
}
|
}
|
||||||
self.idx += 1;
|
self.idx += 1;
|
||||||
|
|
||||||
const js_val_handle = v8.v8__Object__GetIndex(@ptrCast(self.handle), self.ctx.handle, idx) orelse return error.JsException;
|
const local = self.local;
|
||||||
const js_val = js.Value{ .ctx = self.ctx, .handle = js_val_handle };
|
const js_val_handle = v8.v8__Object__GetIndex(@ptrCast(self.handle), local.handle, idx) orelse return error.JsException;
|
||||||
return try self.ctx.valueToString(js_val, .{});
|
return try js.Value.toStringSlice(.{ .local = local, .handle = js_val_handle });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,63 +21,75 @@ const v8 = js.v8;
|
|||||||
|
|
||||||
const Promise = @This();
|
const Promise = @This();
|
||||||
|
|
||||||
ctx: *js.Context,
|
local: *const js.Local,
|
||||||
handle: *const v8.Promise,
|
handle: *const v8.Promise,
|
||||||
|
|
||||||
pub fn toObject(self: Promise) js.Object {
|
pub fn toObject(self: Promise) js.Object {
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = self.local,
|
||||||
.handle = @ptrCast(self.handle),
|
.handle = @ptrCast(self.handle),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toValue(self: Promise) js.Value {
|
pub fn toValue(self: Promise) js.Value {
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = self.local,
|
||||||
.handle = @ptrCast(self.handle),
|
.handle = @ptrCast(self.handle),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn thenAndCatch(self: Promise, on_fulfilled: js.Function, on_rejected: js.Function) !Promise {
|
pub fn thenAndCatch(self: Promise, on_fulfilled: js.Function, on_rejected: js.Function) !Promise {
|
||||||
if (v8.v8__Promise__Then2(self.handle, self.ctx.handle, on_fulfilled.handle, on_rejected.handle)) |handle| {
|
if (v8.v8__Promise__Then2(self.handle, self.local.handle, on_fulfilled.handle, on_rejected.handle)) |handle| {
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = self.local,
|
||||||
.handle = handle,
|
.handle = handle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return error.PromiseChainFailed;
|
return error.PromiseChainFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn persist(self: Promise) !Global {
|
pub fn persist(self: Promise) !Global {
|
||||||
var ctx = self.ctx;
|
return self._persist(true);
|
||||||
var global: v8.Global = undefined;
|
|
||||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
|
||||||
try ctx.global_promises.append(ctx.arena, global);
|
|
||||||
return .{
|
|
||||||
.handle = global,
|
|
||||||
.ctx = ctx,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Global = struct {
|
pub fn temp(self: Promise) !Temp {
|
||||||
handle: v8.Global,
|
return self._persist(false);
|
||||||
ctx: *js.Context,
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Global) void {
|
fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Global else Temp) {
|
||||||
v8.v8__Global__Reset(&self.handle);
|
var ctx = self.local.ctx;
|
||||||
}
|
|
||||||
|
|
||||||
pub fn local(self: *const Global) Promise {
|
var global: v8.Global = undefined;
|
||||||
return .{
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
.ctx = self.ctx,
|
if (comptime is_global) {
|
||||||
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
|
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 fn isEqual(self: *const Global, other: Promise) bool {
|
pub const Temp = G(0);
|
||||||
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
pub const Global = G(1);
|
||||||
}
|
|
||||||
|
|
||||||
pub fn promise(self: *const Global) Promise {
|
fn G(comptime discriminator: u8) type {
|
||||||
return self.local();
|
return struct {
|
||||||
}
|
handle: v8.Global,
|
||||||
};
|
|
||||||
|
// 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>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -19,6 +19,23 @@
|
|||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const v8 = js.v8;
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -22,19 +22,19 @@ const log = @import("../../log.zig");
|
|||||||
|
|
||||||
const PromiseResolver = @This();
|
const PromiseResolver = @This();
|
||||||
|
|
||||||
ctx: *js.Context,
|
local: *const js.Local,
|
||||||
handle: *const v8.PromiseResolver,
|
handle: *const v8.PromiseResolver,
|
||||||
|
|
||||||
pub fn init(ctx: *js.Context) PromiseResolver {
|
pub fn init(local: *const js.Local) PromiseResolver {
|
||||||
return .{
|
return .{
|
||||||
.ctx = ctx,
|
.local = local,
|
||||||
.handle = v8.v8__Promise__Resolver__New(ctx.handle).?,
|
.handle = v8.v8__Promise__Resolver__New(local.handle).?,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn promise(self: PromiseResolver) js.Promise {
|
pub fn promise(self: PromiseResolver) js.Promise {
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = self.local,
|
||||||
.handle = v8.v8__Promise__Resolver__GetPromise(self.handle).?,
|
.handle = v8.v8__Promise__Resolver__GetPromise(self.handle).?,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -46,15 +46,15 @@ pub fn resolve(self: PromiseResolver, comptime source: []const u8, value: anytyp
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn _resolve(self: PromiseResolver, value: anytype) !void {
|
fn _resolve(self: PromiseResolver, value: anytype) !void {
|
||||||
const ctx: *js.Context = @constCast(self.ctx);
|
const local = self.local;
|
||||||
const js_value = try ctx.zigValueToJs(value, .{});
|
const js_val = try local.zigValueToJs(value, .{});
|
||||||
|
|
||||||
var out: v8.MaybeBool = undefined;
|
var out: v8.MaybeBool = undefined;
|
||||||
v8.v8__Promise__Resolver__Resolve(self.handle, self.ctx.handle, js_value.handle, &out);
|
v8.v8__Promise__Resolver__Resolve(self.handle, self.local.handle, js_val.handle, &out);
|
||||||
if (!out.has_value or !out.value) {
|
if (!out.has_value or !out.value) {
|
||||||
return error.FailedToResolvePromise;
|
return error.FailedToResolvePromise;
|
||||||
}
|
}
|
||||||
ctx.runMicrotasks();
|
local.runMicrotasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
|
pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
|
||||||
@@ -64,44 +64,36 @@ pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn _reject(self: PromiseResolver, value: anytype) !void {
|
fn _reject(self: PromiseResolver, value: anytype) !void {
|
||||||
const ctx = self.ctx;
|
const local = self.local;
|
||||||
const js_value = try ctx.zigValueToJs(value, .{});
|
const js_val = try local.zigValueToJs(value, .{});
|
||||||
|
|
||||||
var out: v8.MaybeBool = undefined;
|
var out: v8.MaybeBool = undefined;
|
||||||
v8.v8__Promise__Resolver__Reject(self.handle, ctx.handle, js_value.handle, &out);
|
v8.v8__Promise__Resolver__Reject(self.handle, local.handle, js_val.handle, &out);
|
||||||
if (!out.has_value or !out.value) {
|
if (!out.has_value or !out.value) {
|
||||||
return error.FailedToRejectPromise;
|
return error.FailedToRejectPromise;
|
||||||
}
|
}
|
||||||
ctx.runMicrotasks();
|
local.runMicrotasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn persist(self: PromiseResolver) !Global {
|
pub fn persist(self: PromiseResolver) !Global {
|
||||||
var ctx = self.ctx;
|
var ctx = self.local.ctx;
|
||||||
var global: v8.Global = undefined;
|
var global: v8.Global = undefined;
|
||||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
try ctx.global_promise_resolvers.append(ctx.arena, global);
|
try ctx.global_promise_resolvers.append(ctx.arena, global);
|
||||||
return .{
|
return .{ .handle = global };
|
||||||
.handle = global,
|
|
||||||
.ctx = ctx,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Global = struct {
|
pub const Global = struct {
|
||||||
handle: v8.Global,
|
handle: v8.Global,
|
||||||
ctx: *js.Context,
|
|
||||||
|
|
||||||
pub fn deinit(self: *Global) void {
|
pub fn deinit(self: *Global) void {
|
||||||
v8.v8__Global__Reset(&self.handle);
|
v8.v8__Global__Reset(&self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn local(self: *const Global) PromiseResolver {
|
pub fn local(self: *const Global, l: *const js.Local) PromiseResolver {
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = l,
|
||||||
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
|
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isEqual(self: *const Global, other: PromiseResolver) bool {
|
|
||||||
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const log = @import("../log.zig");
|
const log = @import("../../log.zig");
|
||||||
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
|
||||||
|
|
||||||
const IS_DEBUG = builtin.mode == .Debug;
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
@@ -47,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 {
|
const AddOpts = struct {
|
||||||
name: []const u8 = "",
|
name: []const u8 = "",
|
||||||
low_priority: bool = false,
|
low_priority: bool = false,
|
||||||
|
finalizer: ?Finalizer = null,
|
||||||
};
|
};
|
||||||
pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void {
|
pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
@@ -63,6 +69,7 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts
|
|||||||
.callback = cb,
|
.callback = cb,
|
||||||
.sequence = seq,
|
.sequence = seq,
|
||||||
.name = opts.name,
|
.name = opts.name,
|
||||||
|
.finalizer = opts.finalizer,
|
||||||
.run_at = milliTimestamp(.monotonic) + run_in_ms,
|
.run_at = milliTimestamp(.monotonic) + run_in_ms,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -72,6 +79,11 @@ pub fn run(self: *Scheduler) !?u64 {
|
|||||||
return self.runQueue(&self.high_priority);
|
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 {
|
fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
||||||
if (queue.count() == 0) {
|
if (queue.count() == 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -105,12 +117,28 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
|||||||
return null;
|
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 {
|
const Task = struct {
|
||||||
run_at: u64,
|
run_at: u64,
|
||||||
sequence: u64,
|
sequence: u64,
|
||||||
ctx: *anyopaque,
|
ctx: *anyopaque,
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
callback: Callback,
|
callback: Callback,
|
||||||
|
finalizer: ?Finalizer,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Callback = *const fn (ctx: *anyopaque) anyerror!?u32;
|
const Callback = *const fn (ctx: *anyopaque) anyerror!?u32;
|
||||||
|
const Finalizer = *const fn (ctx: *anyopaque) void;
|
||||||
@@ -22,7 +22,6 @@ const bridge = @import("bridge.zig");
|
|||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
const Window = @import("../webapi/Window.zig");
|
|
||||||
|
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
const JsApis = bridge.JsApis;
|
const JsApis = bridge.JsApis;
|
||||||
@@ -114,20 +113,6 @@ fn isValid(self: Snapshot) bool {
|
|||||||
return v8.v8__StartupData__IsValid(self.startup_data);
|
return v8.v8__StartupData__IsValid(self.startup_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn createGlobalTemplate(isolate: *v8.Isolate, templates: anytype) *const v8.ObjectTemplate {
|
|
||||||
// Set up the global template to inherit from Window's template
|
|
||||||
// This way the global object gets all Window properties through inheritance
|
|
||||||
const js_global = v8.v8__FunctionTemplate__New__DEFAULT(isolate);
|
|
||||||
const window_name = v8.v8__String__NewFromUtf8(isolate, "Window", v8.kNormal, 6);
|
|
||||||
v8.v8__FunctionTemplate__SetClassName(js_global, window_name);
|
|
||||||
|
|
||||||
// Find Window in JsApis by name (avoids circular import)
|
|
||||||
const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi);
|
|
||||||
v8.v8__FunctionTemplate__Inherit(js_global, templates[window_index]);
|
|
||||||
|
|
||||||
return v8.v8__FunctionTemplate__InstanceTemplate(js_global).?;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create() !Snapshot {
|
pub fn create() !Snapshot {
|
||||||
var external_references = collectExternalReferences();
|
var external_references = collectExternalReferences();
|
||||||
|
|
||||||
@@ -169,8 +154,7 @@ pub fn create() !Snapshot {
|
|||||||
|
|
||||||
// Set up the global template to inherit from Window's template
|
// Set up the global template to inherit from Window's template
|
||||||
// This way the global object gets all Window properties through inheritance
|
// This way the global object gets all Window properties through inheritance
|
||||||
const global_template = createGlobalTemplate(isolate, templates[0..]);
|
const context = v8.v8__Context__New(isolate, null, null);
|
||||||
const context = v8.v8__Context__New(isolate, global_template, null);
|
|
||||||
v8.v8__Context__Enter(context);
|
v8.v8__Context__Enter(context);
|
||||||
defer v8.v8__Context__Exit(context);
|
defer v8.v8__Context__Exit(context);
|
||||||
|
|
||||||
@@ -277,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
|
// Count total callbacks needed for external_references array
|
||||||
fn countExternalReferences() comptime_int {
|
fn countExternalReferences() comptime_int {
|
||||||
@setEvalBranchQuota(100_000);
|
@setEvalBranchQuota(100_000);
|
||||||
|
|
||||||
// +1 for the illegal constructor callback
|
// +1 for the illegal constructor callback
|
||||||
var count: comptime_int = 1;
|
var count: comptime_int = 1;
|
||||||
|
var has_non_template_property: bool = false;
|
||||||
|
|
||||||
inline for (JsApis) |JsApi| {
|
inline for (JsApis) |JsApi| {
|
||||||
// Constructor (only if explicit)
|
// Constructor (only if explicit)
|
||||||
@@ -305,6 +303,10 @@ fn countExternalReferences() comptime_int {
|
|||||||
if (value.setter != null) count += 1; // setter
|
if (value.setter != null) count += 1; // setter
|
||||||
} else if (T == bridge.Function) {
|
} else if (T == bridge.Function) {
|
||||||
count += 1;
|
count += 1;
|
||||||
|
} else if (T == bridge.Property) {
|
||||||
|
if (value.template == false) {
|
||||||
|
has_non_template_property = true;
|
||||||
|
}
|
||||||
} else if (T == bridge.Iterator) {
|
} else if (T == bridge.Iterator) {
|
||||||
count += 1;
|
count += 1;
|
||||||
} else if (T == bridge.Indexed) {
|
} else if (T == bridge.Indexed) {
|
||||||
@@ -317,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
|
return count + 1; // +1 for null terminator
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,6 +342,8 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
references[idx] = @bitCast(@intFromPtr(&illegalConstructorCallback));
|
references[idx] = @bitCast(@intFromPtr(&illegalConstructorCallback));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
|
|
||||||
|
var has_non_template_property = false;
|
||||||
|
|
||||||
inline for (JsApis) |JsApi| {
|
inline for (JsApis) |JsApi| {
|
||||||
if (@hasDecl(JsApi, "constructor")) {
|
if (@hasDecl(JsApi, "constructor")) {
|
||||||
references[idx] = @bitCast(@intFromPtr(JsApi.constructor.func));
|
references[idx] = @bitCast(@intFromPtr(JsApi.constructor.func));
|
||||||
@@ -352,6 +369,10 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
} else if (T == bridge.Function) {
|
} else if (T == bridge.Function) {
|
||||||
references[idx] = @bitCast(@intFromPtr(value.func));
|
references[idx] = @bitCast(@intFromPtr(value.func));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
|
} else if (T == bridge.Property) {
|
||||||
|
if (value.template == false) {
|
||||||
|
has_non_template_property = true;
|
||||||
|
}
|
||||||
} else if (T == bridge.Iterator) {
|
} else if (T == bridge.Iterator) {
|
||||||
references[idx] = @bitCast(@intFromPtr(value.func));
|
references[idx] = @bitCast(@intFromPtr(value.func));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
@@ -373,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;
|
return references;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,6 +445,7 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
|||||||
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
||||||
|
|
||||||
const declarations = @typeInfo(JsApi).@"struct".decls;
|
const declarations = @typeInfo(JsApi).@"struct".decls;
|
||||||
|
var has_named_index_getter = false;
|
||||||
|
|
||||||
inline for (declarations) |d| {
|
inline for (declarations) |d| {
|
||||||
const name: [:0]const u8 = d.name;
|
const name: [:0]const u8 = d.name;
|
||||||
@@ -418,7 +455,7 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
|||||||
switch (definition) {
|
switch (definition) {
|
||||||
bridge.Accessor => {
|
bridge.Accessor => {
|
||||||
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
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.setter == null) {
|
||||||
if (value.static) {
|
if (value.static) {
|
||||||
v8.v8__Template__SetAccessorProperty__DEFAULT(@ptrCast(template), js_name, getter_callback);
|
v8.v8__Template__SetAccessorProperty__DEFAULT(@ptrCast(template), js_name, getter_callback);
|
||||||
@@ -429,12 +466,12 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
|||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
std.debug.assert(value.static == false);
|
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);
|
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT2(target, js_name, getter_callback, setter_callback);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bridge.Function => {
|
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));
|
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
if (value.static) {
|
if (value.static) {
|
||||||
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
|
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
|
||||||
@@ -469,9 +506,10 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
|||||||
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
||||||
};
|
};
|
||||||
v8.v8__ObjectTemplate__SetNamedHandler(instance, &configuration);
|
v8.v8__ObjectTemplate__SetNamedHandler(instance, &configuration);
|
||||||
|
has_named_index_getter = true;
|
||||||
},
|
},
|
||||||
bridge.Iterator => {
|
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)
|
const js_name = if (value.async)
|
||||||
v8.v8__Symbol__GetAsyncIterator(isolate)
|
v8.v8__Symbol__GetAsyncIterator(isolate)
|
||||||
else
|
else
|
||||||
@@ -479,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);
|
v8.v8__Template__Set(@ptrCast(target), js_name, @ptrCast(function_template), v8.None);
|
||||||
},
|
},
|
||||||
bridge.Property => {
|
bridge.Property => {
|
||||||
// simpleZigValueToJs now returns raw handle directly
|
const js_value = switch (value.value) {
|
||||||
const js_value = switch (value) {
|
.null => js.simpleZigValueToJs(.{ .handle = isolate }, null, true, false),
|
||||||
.int => |v| js.simpleZigValueToJs(.{ .handle = isolate }, v, 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));
|
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
|
if (value.template == false) {
|
||||||
v8.v8__Template__Set(@ptrCast(target), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
// 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
|
bridge.Constructor => {}, // already handled in generateConstructor
|
||||||
else => {},
|
else => {},
|
||||||
@@ -506,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));
|
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);
|
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 {
|
fn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt {
|
||||||
|
|||||||
@@ -18,36 +18,94 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
|
const SSO = @import("../../string.zig").String;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const String = @This();
|
const String = @This();
|
||||||
|
|
||||||
ctx: *js.Context,
|
local: *const js.Local,
|
||||||
handle: *const v8.String,
|
handle: *const v8.String,
|
||||||
|
|
||||||
pub const ToZigOpts = struct {
|
pub fn toSlice(self: String) ![]u8 {
|
||||||
allocator: ?Allocator = null,
|
return self._toSlice(false, self.local.call_arena);
|
||||||
};
|
|
||||||
|
|
||||||
pub fn toZig(self: String, opts: ToZigOpts) ![]u8 {
|
|
||||||
return self._toZig(false, opts);
|
|
||||||
}
|
}
|
||||||
|
pub fn toSliceZ(self: String) ![:0]u8 {
|
||||||
pub fn toZigZ(self: String, opts: ToZigOpts) ![:0]u8 {
|
return self._toSlice(true, self.local.call_arena);
|
||||||
return self._toZig(true, opts);
|
|
||||||
}
|
}
|
||||||
|
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 len = v8.v8__String__Utf8Length(handle, isolate);
|
||||||
const isolate = self.ctx.isolate.handle;
|
const buf = try (if (comptime null_terminate) allocator.allocSentinel(u8, @intCast(len), 0) else allocator.alloc(u8, @intCast(len)));
|
||||||
const allocator = opts.allocator orelse self.ctx.call_arena;
|
const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||||
const len: u32 = @intCast(v8.v8__String__Utf8Length(self.handle, isolate));
|
if (comptime IS_DEBUG) {
|
||||||
const buf = if (null_terminate) try allocator.allocSentinel(u8, len, 0) else try allocator.alloc(u8, len);
|
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;
|
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]);
|
||||||
|
}
|
||||||
|
|||||||
156
src/browser/js/TaggedOpaque.zig
Normal file
156
src/browser/js/TaggedOpaque.zig
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
// 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.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
const bridge = js.bridge;
|
||||||
|
|
||||||
|
// When we return a Zig object to V8, we put it on the heap and pass it into
|
||||||
|
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
|
||||||
|
// function parameter, we know what type it _should_ be.
|
||||||
|
//
|
||||||
|
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
|
||||||
|
// to the parameter type:
|
||||||
|
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
|
||||||
|
//
|
||||||
|
// But there are 2 reasons we can't do that.
|
||||||
|
//
|
||||||
|
// == Reason 1 ==
|
||||||
|
// The JS code might pass the wrong type:
|
||||||
|
//
|
||||||
|
// var cat = new Cat();
|
||||||
|
// cat.setOwner(new Cat());
|
||||||
|
//
|
||||||
|
// The zig_setOwner method expects the 2nd parameter to be an *Owner, but
|
||||||
|
// the JS code passed a *Cat.
|
||||||
|
//
|
||||||
|
// To solve this issue, we tag every returned value so that we can check what
|
||||||
|
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
|
||||||
|
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
|
||||||
|
//
|
||||||
|
// == Reason 2 ==
|
||||||
|
// Because of prototype inheritance, even "correct" code can be a challenge. For
|
||||||
|
// example, say the above JavaScript is fixed:
|
||||||
|
//
|
||||||
|
// var cat = new Cat();
|
||||||
|
// cat.setOwner(new Owner("Leto"));
|
||||||
|
//
|
||||||
|
// The issue is that setOwner might not expect an *Owner, but rather a
|
||||||
|
// *Person, which is the prototype for Owner. Now our Zig code is expecting
|
||||||
|
// a *Person, but it was (correctly) given an *Owner.
|
||||||
|
// For this reason, we also store the prototype chain.
|
||||||
|
const TaggedOpaque = @This();
|
||||||
|
|
||||||
|
prototype_len: u16,
|
||||||
|
prototype_chain: [*]const PrototypeChainEntry,
|
||||||
|
|
||||||
|
// Ptr to the Zig instance. Between the context where it's called (i.e.
|
||||||
|
// we have the comptime parameter info for all functions), and the index field
|
||||||
|
// we can figure out what type this is.
|
||||||
|
value: *anyopaque,
|
||||||
|
|
||||||
|
// When we're asked to describe an object via the Inspector, we _must_ include
|
||||||
|
// the proper subtype (and description) fields in the returned JSON.
|
||||||
|
// V8 will give us a Value and ask us for the subtype. From the js.Value we
|
||||||
|
// can get a js.Object, and from the js.Object, we can get out TaggedOpaque
|
||||||
|
// which is where we store the subtype.
|
||||||
|
subtype: ?bridge.SubType,
|
||||||
|
|
||||||
|
pub const PrototypeChainEntry = struct {
|
||||||
|
index: bridge.JsApiLookup.BackingInt,
|
||||||
|
offset: u16, // offset to the _proto field
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reverses the mapZigInstanceToJs, making sure that our TaggedOpaque
|
||||||
|
// contains a ptr to the correct type.
|
||||||
|
pub fn fromJS(comptime R: type, js_obj_handle: *const v8.Object) !R {
|
||||||
|
const ti = @typeInfo(R);
|
||||||
|
if (ti != .pointer) {
|
||||||
|
@compileError("non-pointer Zig parameter type: " ++ @typeName(R));
|
||||||
|
}
|
||||||
|
|
||||||
|
const T = ti.pointer.child;
|
||||||
|
const JsApi = bridge.Struct(T).JsApi;
|
||||||
|
|
||||||
|
if (@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
|
||||||
|
// Empty structs aren't stored as TOAs and there's no data
|
||||||
|
// stored in the JSObject's IntenrnalField. Why bother when
|
||||||
|
// we can just return an empty struct here?
|
||||||
|
return @constCast(@as(*const T, &.{}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const internal_field_count = v8.v8__Object__InternalFieldCount(js_obj_handle);
|
||||||
|
// Special case for Window: the global object doesn't have internal fields
|
||||||
|
// Window instance is stored in context.page.window instead
|
||||||
|
if (internal_field_count == 0) {
|
||||||
|
// Normally, this would be an error. All JsObject that map to a Zig type
|
||||||
|
// are either `empty_with_no_proto` (handled above) or have an
|
||||||
|
// interalFieldCount. The only exception to that is the Window...
|
||||||
|
const isolate = v8.v8__Object__GetIsolate(js_obj_handle).?;
|
||||||
|
const context = js.Context.fromIsolate(.{ .handle = isolate });
|
||||||
|
|
||||||
|
const Window = @import("../webapi/Window.zig");
|
||||||
|
if (T == Window) {
|
||||||
|
return context.page.window;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... Or the window's prototype.
|
||||||
|
// We could make this all comptime-fancy, but it's easier to hard-code
|
||||||
|
// the EventTarget
|
||||||
|
|
||||||
|
const EventTarget = @import("../webapi/EventTarget.zig");
|
||||||
|
if (T == EventTarget) {
|
||||||
|
return context.page.window._proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type not found in Window's prototype chain
|
||||||
|
return error.InvalidArgument;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it isn't an empty struct, then the v8.Object should have an
|
||||||
|
// InternalFieldCount > 0, since our toa pointer should be embedded
|
||||||
|
// at index 0 of the internal field count.
|
||||||
|
if (internal_field_count == 0) {
|
||||||
|
return error.InvalidArgument;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bridge.JsApiLookup.has(JsApi)) {
|
||||||
|
@compileError("unknown Zig type: " ++ @typeName(R));
|
||||||
|
}
|
||||||
|
|
||||||
|
const internal_field_handle = v8.v8__Object__GetInternalField(js_obj_handle, 0).?;
|
||||||
|
const tao: *TaggedOpaque = @ptrCast(@alignCast(v8.v8__External__Value(internal_field_handle)));
|
||||||
|
const expected_type_index = bridge.JsApiLookup.getId(JsApi);
|
||||||
|
|
||||||
|
const prototype_chain = tao.prototype_chain[0..tao.prototype_len];
|
||||||
|
if (prototype_chain[0].index == expected_type_index) {
|
||||||
|
return @ptrCast(@alignCast(tao.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ok, let's walk up the chain
|
||||||
|
var ptr = @intFromPtr(tao.value);
|
||||||
|
for (prototype_chain[1..]) |proto| {
|
||||||
|
ptr += proto.offset; // the offset to the _proto field
|
||||||
|
const proto_ptr: **anyopaque = @ptrFromInt(ptr);
|
||||||
|
if (proto.index == expected_type_index) {
|
||||||
|
return @ptrCast(@alignCast(proto_ptr.*));
|
||||||
|
}
|
||||||
|
ptr = @intFromPtr(proto_ptr.*);
|
||||||
|
}
|
||||||
|
return error.InvalidArgument;
|
||||||
|
}
|
||||||
@@ -20,47 +20,77 @@ const std = @import("std");
|
|||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const TryCatch = @This();
|
const TryCatch = @This();
|
||||||
|
|
||||||
ctx: *js.Context,
|
|
||||||
handle: v8.TryCatch,
|
handle: v8.TryCatch,
|
||||||
|
local: *const js.Local,
|
||||||
|
|
||||||
pub fn init(self: *TryCatch, ctx: *js.Context) void {
|
pub fn init(self: *TryCatch, l: *const js.Local) void {
|
||||||
self.ctx = ctx;
|
self.local = l;
|
||||||
v8.v8__TryCatch__CONSTRUCT(&self.handle, ctx.isolate.handle);
|
v8.v8__TryCatch__CONSTRUCT(&self.handle, l.isolate.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hasCaught(self: TryCatch) bool {
|
pub fn hasCaught(self: TryCatch) bool {
|
||||||
return v8.v8__TryCatch__HasCaught(&self.handle);
|
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 {
|
pub fn caught(self: TryCatch, allocator: Allocator) ?Caught {
|
||||||
if (!self.hasCaught()) {
|
if (self.hasCaught() == false) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx = self.ctx;
|
const l = self.local;
|
||||||
|
|
||||||
var hs: js.HandleScope = undefined;
|
|
||||||
hs.init(ctx.isolate);
|
|
||||||
defer hs.deinit();
|
|
||||||
|
|
||||||
const line: ?u32 = blk: {
|
const line: ?u32 = blk: {
|
||||||
const handle = v8.v8__TryCatch__Message(&self.handle) orelse return null;
|
const handle = v8.v8__TryCatch__Message(&self.handle) orelse return null;
|
||||||
const l = v8.v8__Message__GetLineNumber(handle, ctx.handle);
|
const line = v8.v8__Message__GetLineNumber(handle, l.handle);
|
||||||
break :blk if (l < 0) null else @intCast(l);
|
break :blk if (line < 0) null else @intCast(line);
|
||||||
};
|
};
|
||||||
|
|
||||||
const exception: ?[]const u8 = blk: {
|
const exception: ?[]const u8 = blk: {
|
||||||
const handle = v8.v8__TryCatch__Exception(&self.handle) orelse break :blk null;
|
const handle = v8.v8__TryCatch__Exception(&self.handle) orelse break :blk null;
|
||||||
break :blk ctx.valueToString(.{ .ctx = ctx, .handle = 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 stack: ?[]const u8 = blk: {
|
||||||
const handle = v8.v8__TryCatch__StackTrace(&self.handle, ctx.handle) orelse break :blk null;
|
const handle = v8.v8__TryCatch__StackTrace(&self.handle, l.handle) orelse break :blk null;
|
||||||
break :blk ctx.valueToString(.{ .ctx = ctx, .handle = 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 .{
|
return .{
|
||||||
@@ -85,10 +115,10 @@ pub fn deinit(self: *TryCatch) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const Caught = struct {
|
pub const Caught = struct {
|
||||||
line: ?u32,
|
line: ?u32 = null,
|
||||||
caught: bool,
|
caught: bool = false,
|
||||||
stack: ?[]const u8,
|
stack: ?[]const u8 = null,
|
||||||
exception: ?[]const u8,
|
exception: ?[]const u8 = null,
|
||||||
|
|
||||||
pub fn format(self: Caught, writer: *std.Io.Writer) !void {
|
pub fn format(self: Caught, writer: *std.Io.Writer) !void {
|
||||||
const separator = @import("../../log.zig").separator();
|
const separator = @import("../../log.zig").separator();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
|
const SSO = @import("../../string.zig").String;
|
||||||
|
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
@@ -27,15 +28,19 @@ const Allocator = std.mem.Allocator;
|
|||||||
|
|
||||||
const Value = @This();
|
const Value = @This();
|
||||||
|
|
||||||
ctx: *js.Context,
|
local: *const js.Local,
|
||||||
handle: *const v8.Value,
|
handle: *const v8.Value,
|
||||||
|
|
||||||
pub fn isObject(self: Value) bool {
|
pub fn isObject(self: Value) bool {
|
||||||
return v8.v8__Value__IsObject(self.handle);
|
return v8.v8__Value__IsObject(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isString(self: Value) bool {
|
pub fn isString(self: Value) ?js.String {
|
||||||
return v8.v8__Value__IsString(self.handle);
|
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 {
|
pub fn isArray(self: Value) bool {
|
||||||
@@ -155,12 +160,12 @@ pub fn isPromise(self: Value) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn toBool(self: Value) bool {
|
pub fn toBool(self: Value) bool {
|
||||||
return v8.v8__Value__BooleanValue(self.handle, self.ctx.isolate.handle);
|
return v8.v8__Value__BooleanValue(self.handle, self.local.isolate.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn typeOf(self: Value) js.String {
|
pub fn typeOf(self: Value) js.String {
|
||||||
const str_handle = v8.v8__Value__TypeOf(self.handle, self.ctx.isolate.handle).?;
|
const str_handle = v8.v8__Value__TypeOf(self.handle, self.local.isolate.handle).?;
|
||||||
return js.String{ .ctx = self.ctx, .handle = str_handle };
|
return js.String{ .local = self.local, .handle = str_handle };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toF32(self: Value) !f32 {
|
pub fn toF32(self: Value) !f32 {
|
||||||
@@ -169,7 +174,7 @@ pub fn toF32(self: Value) !f32 {
|
|||||||
|
|
||||||
pub fn toF64(self: Value) !f64 {
|
pub fn toF64(self: Value) !f64 {
|
||||||
var maybe: v8.MaybeF64 = undefined;
|
var maybe: v8.MaybeF64 = undefined;
|
||||||
v8.v8__Value__NumberValue(self.handle, self.ctx.handle, &maybe);
|
v8.v8__Value__NumberValue(self.handle, self.local.handle, &maybe);
|
||||||
if (!maybe.has_value) {
|
if (!maybe.has_value) {
|
||||||
return error.JsException;
|
return error.JsException;
|
||||||
}
|
}
|
||||||
@@ -178,7 +183,7 @@ pub fn toF64(self: Value) !f64 {
|
|||||||
|
|
||||||
pub fn toI32(self: Value) !i32 {
|
pub fn toI32(self: Value) !i32 {
|
||||||
var maybe: v8.MaybeI32 = undefined;
|
var maybe: v8.MaybeI32 = undefined;
|
||||||
v8.v8__Value__Int32Value(self.handle, self.ctx.handle, &maybe);
|
v8.v8__Value__Int32Value(self.handle, self.local.handle, &maybe);
|
||||||
if (!maybe.has_value) {
|
if (!maybe.has_value) {
|
||||||
return error.JsException;
|
return error.JsException;
|
||||||
}
|
}
|
||||||
@@ -187,7 +192,7 @@ pub fn toI32(self: Value) !i32 {
|
|||||||
|
|
||||||
pub fn toU32(self: Value) !u32 {
|
pub fn toU32(self: Value) !u32 {
|
||||||
var maybe: v8.MaybeU32 = undefined;
|
var maybe: v8.MaybeU32 = undefined;
|
||||||
v8.v8__Value__Uint32Value(self.handle, self.ctx.handle, &maybe);
|
v8.v8__Value__Uint32Value(self.handle, self.local.handle, &maybe);
|
||||||
if (!maybe.has_value) {
|
if (!maybe.has_value) {
|
||||||
return error.JsException;
|
return error.JsException;
|
||||||
}
|
}
|
||||||
@@ -199,66 +204,70 @@ pub fn toPromise(self: Value) js.Promise {
|
|||||||
std.debug.assert(self.isPromise());
|
std.debug.assert(self.isPromise());
|
||||||
}
|
}
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = self.local,
|
||||||
.handle = @ptrCast(self.handle),
|
.handle = @ptrCast(self.handle),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toString(self: Value, opts: js.String.ToZigOpts) ![]u8 {
|
pub fn toString(self: Value) !js.String {
|
||||||
return self._toString(false, opts);
|
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 {
|
pub fn toJson(self: Value, allocator: Allocator) ![]u8 {
|
||||||
const json_str_handle = v8.v8__JSON__Stringify(self.ctx.handle, self.handle, null) orelse return error.JsException;
|
const local = self.local;
|
||||||
return self.ctx.jsStringToZig(json_str_handle, .{ .allocator = allocator });
|
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);
|
||||||
|
|
||||||
fn _toString(self: Value, comptime null_terminate: bool, opts: js.String.ToZigOpts) !(if (null_terminate) [:0]u8 else []u8) {
|
|
||||||
const ctx = self.ctx;
|
|
||||||
|
|
||||||
if (self.isSymbol()) {
|
|
||||||
const sym_handle = v8.v8__Symbol__Description(@ptrCast(self.handle), ctx.isolate.handle).?;
|
|
||||||
return _toString(.{ .handle = @ptrCast(sym_handle), .ctx = ctx }, null_terminate, opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
const str_handle = v8.v8__Value__ToString(self.handle, ctx.handle) orelse {
|
|
||||||
return error.JsException;
|
|
||||||
};
|
|
||||||
|
|
||||||
const str = js.String{ .ctx = ctx, .handle = str_handle };
|
|
||||||
if (comptime null_terminate) {
|
|
||||||
return js.String.toZigZ(str, opts);
|
|
||||||
}
|
|
||||||
return js.String.toZig(str, opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fromJson(ctx: *js.Context, json: []const u8) !Value {
|
|
||||||
const v8_isolate = v8.Isolate{ .handle = ctx.isolate.handle };
|
|
||||||
const json_string = v8.String.initUtf8(v8_isolate, json);
|
|
||||||
const v8_context = v8.Context{ .handle = ctx.handle };
|
|
||||||
const value = try v8.Json.parse(v8_context, json_string);
|
|
||||||
return .{ .ctx = ctx, .handle = value.handle };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn persist(self: Value) !Global {
|
pub fn persist(self: Value) !Global {
|
||||||
var ctx = self.ctx;
|
return self._persist(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn temp(self: Value) !Temp {
|
||||||
|
return self._persist(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Global else Temp) {
|
||||||
|
var ctx = self.local.ctx;
|
||||||
|
|
||||||
var global: v8.Global = undefined;
|
var global: v8.Global = undefined;
|
||||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
|
if (comptime is_global) {
|
||||||
try ctx.global_values.append(ctx.arena, global);
|
try ctx.global_values.append(ctx.arena, global);
|
||||||
|
} else {
|
||||||
return .{
|
try ctx.global_values_temp.put(ctx.arena, global.data_ptr, global);
|
||||||
.handle = global,
|
}
|
||||||
.ctx = ctx,
|
return .{ .handle = global };
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toZig(self: Value, comptime T: type) !T {
|
pub fn toZig(self: Value, comptime T: type) !T {
|
||||||
return self.ctx.jsValueToZig(T, self);
|
return self.local.jsValueToZig(T, self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toObject(self: Value) js.Object {
|
pub fn toObject(self: Value) js.Object {
|
||||||
@@ -267,7 +276,7 @@ pub fn toObject(self: Value) js.Object {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = self.local,
|
||||||
.handle = @ptrCast(self.handle),
|
.handle = @ptrCast(self.handle),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -278,7 +287,7 @@ pub fn toArray(self: Value) js.Array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.ctx = self.ctx,
|
.local = self.local,
|
||||||
.handle = @ptrCast(self.handle),
|
.handle = @ptrCast(self.handle),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -295,28 +304,37 @@ pub fn toBigInt(self: Value) js.BigInt {
|
|||||||
|
|
||||||
pub fn format(self: Value, writer: *std.Io.Writer) !void {
|
pub fn format(self: Value, writer: *std.Io.Writer) !void {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
return self.ctx.debugValue(self, writer);
|
return self.local.debugValue(self, writer);
|
||||||
}
|
}
|
||||||
const str = self.toString(.{}) catch return error.WriteFailed;
|
const js_str = self.toString() catch return error.WriteFailed;
|
||||||
return writer.writeAll(str);
|
return js_str.format(writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Global = struct {
|
pub const Temp = G(0);
|
||||||
handle: v8.Global,
|
pub const Global = G(1);
|
||||||
ctx: *js.Context,
|
|
||||||
|
|
||||||
pub fn deinit(self: *Global) void {
|
fn G(comptime discriminator: u8) type {
|
||||||
v8.v8__Global__Reset(&self.handle);
|
return struct {
|
||||||
}
|
handle: v8.Global,
|
||||||
|
|
||||||
pub fn local(self: *const Global) Value {
|
// makes the types different (G(0) != G(1)), without taking up space
|
||||||
return .{
|
comptime _: u8 = discriminator,
|
||||||
.ctx = self.ctx,
|
|
||||||
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isEqual(self: *const Global, other: Value) bool {
|
const Self = @This();
|
||||||
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
|
||||||
}
|
pub fn deinit(self: *Self) void {
|
||||||
};
|
v8.v8__Global__Reset(&self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local(self: *const Self, l: *const js.Local) Value {
|
||||||
|
return .{
|
||||||
|
.local = l,
|
||||||
|
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isEqual(self: *const Self, other: Value) bool {
|
||||||
|
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,48 +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 js = @import("js.zig");
|
|
||||||
|
|
||||||
const v8 = js.v8;
|
|
||||||
|
|
||||||
pub fn Global(comptime T: type) type {
|
|
||||||
const H = @FieldType(T, "handle");
|
|
||||||
|
|
||||||
return struct {
|
|
||||||
global: v8.Global,
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
pub fn init(isolate: *v8.Isolate, handle: H) Self {
|
|
||||||
var global: v8.Global = undefined;
|
|
||||||
v8.v8__Global__New(isolate, handle, &global);
|
|
||||||
return .{
|
|
||||||
.global = global,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
|
||||||
v8.v8__Global__Reset(&self.global);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn local(self: *const Self) H {
|
|
||||||
return @ptrCast(@alignCast(@as(*const anyopaque, @ptrFromInt(self.global.data_ptr))));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -19,19 +19,19 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
pub const v8 = @import("v8").c;
|
pub const v8 = @import("v8").c;
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
const string = @import("../../string.zig");
|
||||||
|
|
||||||
pub const Env = @import("Env.zig");
|
pub const Env = @import("Env.zig");
|
||||||
pub const bridge = @import("bridge.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 Context = @import("Context.zig");
|
||||||
|
pub const Local = @import("Local.zig");
|
||||||
pub const Inspector = @import("Inspector.zig");
|
pub const Inspector = @import("Inspector.zig");
|
||||||
pub const Snapshot = @import("Snapshot.zig");
|
pub const Snapshot = @import("Snapshot.zig");
|
||||||
pub const Platform = @import("Platform.zig");
|
pub const Platform = @import("Platform.zig");
|
||||||
pub const Isolate = @import("Isolate.zig");
|
pub const Isolate = @import("Isolate.zig");
|
||||||
pub const HandleScope = @import("HandleScope.zig");
|
pub const HandleScope = @import("HandleScope.zig");
|
||||||
|
|
||||||
pub const Name = @import("Name.zig");
|
|
||||||
pub const Value = @import("Value.zig");
|
pub const Value = @import("Value.zig");
|
||||||
pub const Array = @import("Array.zig");
|
pub const Array = @import("Array.zig");
|
||||||
pub const String = @import("String.zig");
|
pub const String = @import("String.zig");
|
||||||
@@ -43,8 +43,8 @@ pub const Module = @import("Module.zig");
|
|||||||
pub const BigInt = @import("BigInt.zig");
|
pub const BigInt = @import("BigInt.zig");
|
||||||
pub const Number = @import("Number.zig");
|
pub const Number = @import("Number.zig");
|
||||||
pub const Integer = @import("Integer.zig");
|
pub const Integer = @import("Integer.zig");
|
||||||
pub const Global = @import("global.zig").Global;
|
|
||||||
pub const PromiseResolver = @import("PromiseResolver.zig");
|
pub const PromiseResolver = @import("PromiseResolver.zig");
|
||||||
|
pub const PromiseRejection = @import("PromiseRejection.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
@@ -78,12 +78,8 @@ pub const ArrayBuffer = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const Exception = struct {
|
pub const Exception = struct {
|
||||||
ctx: *const Context,
|
local: *const Local,
|
||||||
handle: *const v8.Value,
|
handle: *const v8.Value,
|
||||||
|
|
||||||
pub fn exception(self: Exception, allocator: Allocator) ![]const u8 {
|
|
||||||
return self.context.valueToString(self.inner, .{ .allocator = allocator });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// These are simple types that we can convert to JS with only an isolate. This
|
// These are simple types that we can convert to JS with only an isolate. This
|
||||||
@@ -133,6 +129,7 @@ pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool,
|
|||||||
},
|
},
|
||||||
.@"struct" => {
|
.@"struct" => {
|
||||||
switch (@TypeOf(value)) {
|
switch (@TypeOf(value)) {
|
||||||
|
string.String => return isolate.initStringHandle(value.str()),
|
||||||
ArrayBuffer => {
|
ArrayBuffer => {
|
||||||
const values = value.values;
|
const values = value.values;
|
||||||
const len = values.len;
|
const len = values.len;
|
||||||
@@ -215,61 +212,6 @@ pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool,
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// When we return a Zig object to V8, we put it on the heap and pass it into
|
|
||||||
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
|
|
||||||
// function parameter, we know what type it _should_ be.
|
|
||||||
//
|
|
||||||
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
|
|
||||||
// to the parameter type:
|
|
||||||
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
|
|
||||||
//
|
|
||||||
// But there are 2 reasons we can't do that.
|
|
||||||
//
|
|
||||||
// == Reason 1 ==
|
|
||||||
// The JS code might pass the wrong type:
|
|
||||||
//
|
|
||||||
// var cat = new Cat();
|
|
||||||
// cat.setOwner(new Cat());
|
|
||||||
//
|
|
||||||
// The zig_setOwner method expects the 2nd parameter to be an *Owner, but
|
|
||||||
// the JS code passed a *Cat.
|
|
||||||
//
|
|
||||||
// To solve this issue, we tag every returned value so that we can check what
|
|
||||||
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
|
|
||||||
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
|
|
||||||
//
|
|
||||||
// == Reason 2 ==
|
|
||||||
// Because of prototype inheritance, even "correct" code can be a challenge. For
|
|
||||||
// example, say the above JavaScript is fixed:
|
|
||||||
//
|
|
||||||
// var cat = new Cat();
|
|
||||||
// cat.setOwner(new Owner("Leto"));
|
|
||||||
//
|
|
||||||
// The issue is that setOwner might not expect an *Owner, but rather a
|
|
||||||
// *Person, which is the prototype for Owner. Now our Zig code is expecting
|
|
||||||
// a *Person, but it was (correctly) given an *Owner.
|
|
||||||
// For this reason, we also store the prototype chain.
|
|
||||||
pub const TaggedAnyOpaque = struct {
|
|
||||||
prototype_len: u16,
|
|
||||||
prototype_chain: [*]const PrototypeChainEntry,
|
|
||||||
|
|
||||||
// Ptr to the Zig instance. Between the context where it's called (i.e.
|
|
||||||
// we have the comptime parameter info for all functions), and the index field
|
|
||||||
// we can figure out what type this is.
|
|
||||||
value: *anyopaque,
|
|
||||||
|
|
||||||
// When we're asked to describe an object via the Inspector, we _must_ include
|
|
||||||
// the proper subtype (and description) fields in the returned JSON.
|
|
||||||
// V8 will give us a Value and ask us for the subtype. From the js.Value we
|
|
||||||
// can get a js.Object, and from the js.Object, we can get out TaggedAnyOpaque
|
|
||||||
// which is where we store the subtype.
|
|
||||||
subtype: ?bridge.SubType,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const PrototypeChainEntry = struct {
|
|
||||||
index: bridge.JsApiLookup.BackingInt,
|
|
||||||
offset: u16, // offset to the _proto field
|
|
||||||
};
|
|
||||||
|
|
||||||
// These are here, and not in Inspector.zig, because Inspector.zig isn't always
|
// These are here, and not in Inspector.zig, because Inspector.zig isn't always
|
||||||
// included (e.g. in the wpt build).
|
// included (e.g. in the wpt build).
|
||||||
@@ -281,7 +223,7 @@ pub export fn v8_inspector__Client__IMPL__valueSubtype(
|
|||||||
_: *v8.InspectorClientImpl,
|
_: *v8.InspectorClientImpl,
|
||||||
c_value: *const v8.Value,
|
c_value: *const v8.Value,
|
||||||
) callconv(.c) [*c]const u8 {
|
) callconv(.c) [*c]const u8 {
|
||||||
const external_entry = Inspector.getTaggedAnyOpaque(c_value) orelse return null;
|
const external_entry = Inspector.getTaggedOpaque(c_value) orelse return null;
|
||||||
return if (external_entry.subtype) |st| @tagName(st) else null;
|
return if (external_entry.subtype) |st| @tagName(st) else null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,11 +240,11 @@ pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
|
|||||||
|
|
||||||
// We _must_ include a non-null description in order for the subtype value
|
// We _must_ include a non-null description in order for the subtype value
|
||||||
// to be included. Besides that, I don't know if the value has any meaning
|
// to be included. Besides that, I don't know if the value has any meaning
|
||||||
const external_entry = Inspector.getTaggedAnyOpaque(c_value) orelse return null;
|
const external_entry = Inspector.getTaggedOpaque(c_value) orelse return null;
|
||||||
return if (external_entry.subtype == null) null else "";
|
return if (external_entry.subtype == null) null else "";
|
||||||
}
|
}
|
||||||
|
|
||||||
test "TaggedAnyOpaque" {
|
test "TaggedAnyOpaque" {
|
||||||
// If we grow this, fine, but it should be a conscious decision
|
// If we grow this, fine, but it should be a conscious decision
|
||||||
try std.testing.expectEqual(24, @sizeOf(TaggedAnyOpaque));
|
try std.testing.expectEqual(24, @sizeOf(@import("TaggedOpaque.zig")));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const Page = @import("../Page.zig");
|
|||||||
const Node = @import("../webapi/Node.zig");
|
const Node = @import("../webapi/Node.zig");
|
||||||
const Element = @import("../webapi/Element.zig");
|
const Element = @import("../webapi/Element.zig");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
pub const ParsedNode = struct {
|
pub const ParsedNode = struct {
|
||||||
node: *Node,
|
node: *Node,
|
||||||
@@ -373,6 +374,17 @@ fn _appendCallback(self: *Parser, parent: *Node, node_or_text: h5e.NodeOrText) !
|
|||||||
switch (node_or_text.toUnion()) {
|
switch (node_or_text.toUnion()) {
|
||||||
.node => |cpn| {
|
.node => |cpn| {
|
||||||
const child = getNode(cpn);
|
const child = getNode(cpn);
|
||||||
|
if (child._parent) |previous_parent| {
|
||||||
|
// html5ever says this can't happen, but we might be screwing up
|
||||||
|
// the node on our side. We shouldn't be, but we're seeing this
|
||||||
|
// in the wild, and I'm not sure why. In debug, let's crash so
|
||||||
|
// we can try to figure it out. In release, let's disconnect
|
||||||
|
// the child first.
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
unreachable;
|
||||||
|
}
|
||||||
|
self.page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() });
|
||||||
|
}
|
||||||
try self.page.appendNew(parent, .{ .node = child });
|
try self.page.appendNew(parent, .{ .node = child });
|
||||||
},
|
},
|
||||||
.text => |txt| try self.page.appendNew(parent, .{ .text = txt }),
|
.text => |txt| try self.page.appendNew(parent, .{ .text = txt }),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -16,8 +16,6 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
// Gets the Parent of child.
|
// Gets the Parent of child.
|
||||||
// HtmlElement.of(script) -> *HTMLElement
|
// HtmlElement.of(script) -> *HTMLElement
|
||||||
pub fn Struct(comptime T: type) type {
|
pub fn Struct(comptime T: type) type {
|
||||||
@@ -28,37 +26,3 @@ pub fn Struct(comptime T: type) type {
|
|||||||
else => unreachable,
|
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('\\30 abc', CSS.escape('0abc'));
|
||||||
testing.expectEqual('\\31 23', CSS.escape('123'));
|
testing.expectEqual('\\31 23', CSS.escape('123'));
|
||||||
testing.expectEqual('\\-test', CSS.escape('-test'));
|
testing.expectEqual('\\-', CSS.escape('-'));
|
||||||
testing.expectEqual('\\--test', CSS.escape('--test'));
|
testing.expectEqual('-test', CSS.escape('-test'));
|
||||||
|
testing.expectEqual('--test', CSS.escape('--test'));
|
||||||
|
testing.expectEqual('-\\33 ', CSS.escape('-3'));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
<body></body>
|
<body></body>
|
||||||
<script src="../testing.js"></script>
|
<script src="../testing.js"></script>
|
||||||
<script id=createElement>
|
<script id=createElement>
|
||||||
const div = document.createElement('div');
|
testing.expectEqual(1, document.createElement.length);
|
||||||
testing.expectEqual("DIV", div.tagName);
|
|
||||||
div.id = "hello";
|
const div1 = document.createElement('div');
|
||||||
|
testing.expectEqual(true, div1 instanceof HTMLDivElement);
|
||||||
|
testing.expectEqual("DIV", div1.tagName);
|
||||||
|
div1.id = "hello";
|
||||||
testing.expectEqual(null, $('#hello'));
|
testing.expectEqual(null, $('#hello'));
|
||||||
|
|
||||||
document.getElementsByTagName('body')[0].appendChild(div);
|
const div2 = document.createElement('DIV');
|
||||||
testing.expectEqual(div, $('#hello'));
|
testing.expectEqual(true, div2 instanceof HTMLDivElement);
|
||||||
|
|
||||||
|
document.getElementsByTagName('body')[0].appendChild(div1);
|
||||||
|
testing.expectEqual(div1, $('#hello'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,15 @@
|
|||||||
<body></body>
|
<body></body>
|
||||||
<script src="../testing.js"></script>
|
<script src="../testing.js"></script>
|
||||||
<script id=createElementNS>
|
<script id=createElementNS>
|
||||||
const htmlDiv = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
|
const htmlDiv1 = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
|
||||||
testing.expectEqual('DIV', htmlDiv.tagName);
|
testing.expectEqual('DIV', htmlDiv1.tagName);
|
||||||
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv.namespaceURI);
|
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');
|
const svgRect = document.createElementNS('http://www.w3.org/2000/svg', 'RecT');
|
||||||
testing.expectEqual('RecT', svgRect.tagName);
|
testing.expectEqual('RecT', svgRect.tagName);
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
testing.expectEqual(undefined, document.getCurrentScript);
|
testing.expectEqual(undefined, document.getCurrentScript);
|
||||||
testing.expectEqual("http://127.0.0.1:9582/src/browser/tests/document/document.html", document.URL);
|
testing.expectEqual("http://127.0.0.1:9582/src/browser/tests/document/document.html", document.URL);
|
||||||
testing.expectEqual(window, document.defaultView);
|
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>
|
||||||
|
|
||||||
<script id=headAndbody>
|
<script id=headAndbody>
|
||||||
|
|||||||
@@ -41,4 +41,53 @@
|
|||||||
testing.expectEqual("DIV", newElement.tagName);
|
testing.expectEqual("DIV", newElement.tagName);
|
||||||
testing.expectEqual("after begin", newElement.innerText);
|
testing.expectEqual("after begin", newElement.innerText);
|
||||||
testing.expectEqual("afterbegin", newElement.className);
|
testing.expectEqual("afterbegin", newElement.className);
|
||||||
|
|
||||||
|
const fuzzWrapper = document.createElement("div");
|
||||||
|
fuzzWrapper.id = "fuzz-wrapper";
|
||||||
|
document.body.appendChild(fuzzWrapper);
|
||||||
|
|
||||||
|
const fuzzCases = [
|
||||||
|
// These cases have no <body> element (or empty body), so nothing is inserted
|
||||||
|
{ name: "empty string", html: "", expectElements: 0 },
|
||||||
|
{ name: "comment only", html: "<!-- comment -->", expectElements: 0 },
|
||||||
|
{ name: "doctype only", html: "<!DOCTYPE html>", expectElements: 0 },
|
||||||
|
{ name: "full empty doc", html: "<!DOCTYPE html><html><head></head><body></body></html>", expectElements: 0 },
|
||||||
|
|
||||||
|
{ name: "whitespace only", html: " ", expectElements: 0 },
|
||||||
|
{ name: "newlines only", html: "\n\n\n", expectElements: 0 },
|
||||||
|
{ name: "just text", html: "plain text", expectElements: 0 },
|
||||||
|
// Head-only elements: Extracted from <head> container
|
||||||
|
{ name: "empty meta", html: "<meta>", expectElements: 1 },
|
||||||
|
{ name: "empty title", html: "<title></title>", expectElements: 1 },
|
||||||
|
{ name: "empty head", html: "<head></head>", expectElements: 0 }, // Container with no children
|
||||||
|
{ name: "empty body", html: "<body></body>", expectElements: 0 }, // Container with no children
|
||||||
|
{ name: "empty html", html: "<html></html>", expectElements: 0 }, // Container with no children
|
||||||
|
{ name: "meta only", html: "<meta charset='utf-8'>", expectElements: 1 },
|
||||||
|
{ name: "title only", html: "<title>Test</title>", expectElements: 1 },
|
||||||
|
{ name: "link only", html: "<link rel='stylesheet' href='test.css'>", expectElements: 1 },
|
||||||
|
{ name: "meta and title", html: "<meta charset='utf-8'><title>Test</title>", expectElements: 2 },
|
||||||
|
{ name: "script only", html: "<script>console.log('hi')<\/script>", expectElements: 1 },
|
||||||
|
{ name: "style only", html: "<style>body { color: red; }<\/style>", expectElements: 1 },
|
||||||
|
{ name: "unclosed div", html: "<div>content", expectElements: 1 },
|
||||||
|
{ name: "unclosed span", html: "<span>text", expectElements: 1 },
|
||||||
|
{ name: "invalid tag", html: "<notarealtag>content</notarealtag>", expectElements: 1 },
|
||||||
|
{ name: "malformed", html: "<<div>>test<</div>>", expectElements: 1 }, // Parser handles it
|
||||||
|
{ name: "just closing tag", html: "</div>", expectElements: 0 },
|
||||||
|
{ name: "nested empty", html: "<div><div></div></div>", expectElements: 1 },
|
||||||
|
{ name: "multiple top-level", html: "<span>1</span><span>2</span><span>3</span>", expectElements: 3 },
|
||||||
|
{ name: "mixed text and elements", html: "Text before<b>bold</b>Text after", expectElements: 1 },
|
||||||
|
{ name: "deeply nested", html: "<div><div><div><span>deep</span></div></div></div>", expectElements: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
fuzzCases.forEach((tc, idx) => {
|
||||||
|
fuzzWrapper.innerHTML = "";
|
||||||
|
fuzzWrapper.insertAdjacentHTML("beforeend", tc.html);
|
||||||
|
if (tc.expectElements !== fuzzWrapper.childElementCount) {
|
||||||
|
console.warn(`Fuzz idx: ${idx}`);
|
||||||
|
testing.expectEqual(tc.expectElements, fuzzWrapper.childElementCount);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
document.body.removeChild(fuzzWrapper);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
<main>Main content</main>
|
<main>Main content</main>
|
||||||
|
|
||||||
<script id=byId name="test1">
|
<script id=byId name="test1">
|
||||||
|
testing.expectEqual(1, document.querySelector.length);
|
||||||
testing.expectError("SyntaxError: Syntax Error", () => document.querySelector(''));
|
testing.expectError("SyntaxError: Syntax Error", () => document.querySelector(''));
|
||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(12, err.code);
|
testing.expectEqual(12, err.code);
|
||||||
|
|||||||
@@ -248,7 +248,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id=legacy></a>
|
<a id=legacy></a>
|
||||||
<script id=legacy>
|
<script id=legacy>
|
||||||
{
|
{
|
||||||
let a = document.getElementById('legacy').attributes;
|
let a = document.getElementById('legacy').attributes;
|
||||||
@@ -266,3 +266,19 @@
|
|||||||
testing.expectEqual('abc123', a[0].value);
|
testing.expectEqual('abc123', a[0].value);
|
||||||
}
|
}
|
||||||
</script>
|
</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>
|
||||||
|
|||||||
514
src/browser/tests/element/html/event_listeners.html
Normal file
514
src/browser/tests/element/html/event_listeners.html
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<!-- Test inline event listeners set via HTML attributes -->
|
||||||
|
<div id="attr-click" onclick="window.x = 1"></div>
|
||||||
|
<div id="attr-load" onload="window.x = 1"></div>
|
||||||
|
<div id="attr-error" onerror="window.x = 1"></div>
|
||||||
|
<div id="attr-focus" onfocus="window.x = 1"></div>
|
||||||
|
<div id="attr-blur" onblur="window.x = 1"></div>
|
||||||
|
<div id="attr-keydown" onkeydown="window.x = 1"></div>
|
||||||
|
<div id="attr-mousedown" onmousedown="window.x = 1"></div>
|
||||||
|
<div id="attr-submit" onsubmit="window.x = 1"></div>
|
||||||
|
<div id="attr-wheel" onwheel="window.x = 1"></div>
|
||||||
|
<div id="attr-scroll" onscroll="window.x = 1"></div>
|
||||||
|
<div id="attr-contextmenu" oncontextmenu="window.x = 1"></div>
|
||||||
|
<div id="no-listeners"></div>
|
||||||
|
|
||||||
|
<script id="attr_listener_returns_function">
|
||||||
|
{
|
||||||
|
// Inline listeners set via HTML attributes should return a function
|
||||||
|
testing.expectEqual('function', typeof $('#attr-click').onclick);
|
||||||
|
testing.expectEqual('function', typeof $('#attr-load').onload);
|
||||||
|
testing.expectEqual('function', typeof $('#attr-error').onerror);
|
||||||
|
testing.expectEqual('function', typeof $('#attr-focus').onfocus);
|
||||||
|
testing.expectEqual('function', typeof $('#attr-blur').onblur);
|
||||||
|
testing.expectEqual('function', typeof $('#attr-keydown').onkeydown);
|
||||||
|
testing.expectEqual('function', typeof $('#attr-mousedown').onmousedown);
|
||||||
|
testing.expectEqual('function', typeof $('#attr-submit').onsubmit);
|
||||||
|
testing.expectEqual('function', typeof $('#attr-wheel').onwheel);
|
||||||
|
testing.expectEqual('function', typeof $('#attr-scroll').onscroll);
|
||||||
|
testing.expectEqual('function', typeof $('#attr-contextmenu').oncontextmenu);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="no_attr_listener_returns_null">
|
||||||
|
{
|
||||||
|
// Elements without inline listeners should return null
|
||||||
|
const div = $('#no-listeners');
|
||||||
|
testing.expectEqual(null, div.onclick);
|
||||||
|
testing.expectEqual(null, div.onload);
|
||||||
|
testing.expectEqual(null, div.onerror);
|
||||||
|
testing.expectEqual(null, div.onfocus);
|
||||||
|
testing.expectEqual(null, div.onblur);
|
||||||
|
testing.expectEqual(null, div.onkeydown);
|
||||||
|
testing.expectEqual(null, div.onmousedown);
|
||||||
|
testing.expectEqual(null, div.onsubmit);
|
||||||
|
testing.expectEqual(null, div.onwheel);
|
||||||
|
testing.expectEqual(null, div.onscroll);
|
||||||
|
testing.expectEqual(null, div.oncontextmenu);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="js_setter_getter">
|
||||||
|
{
|
||||||
|
// Test setting and getting listeners via JavaScript property
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
// Initially null
|
||||||
|
testing.expectEqual(null, div.onclick);
|
||||||
|
testing.expectEqual(null, div.onload);
|
||||||
|
testing.expectEqual(null, div.onerror);
|
||||||
|
|
||||||
|
// Set listeners
|
||||||
|
const clickHandler = () => {};
|
||||||
|
const loadHandler = () => {};
|
||||||
|
const errorHandler = () => {};
|
||||||
|
|
||||||
|
div.onclick = clickHandler;
|
||||||
|
div.onload = loadHandler;
|
||||||
|
div.onerror = errorHandler;
|
||||||
|
|
||||||
|
// Verify they can be retrieved and are functions
|
||||||
|
testing.expectEqual('function', typeof div.onclick);
|
||||||
|
testing.expectEqual('function', typeof div.onload);
|
||||||
|
testing.expectEqual('function', typeof div.onerror);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="js_listener_invoke">
|
||||||
|
{
|
||||||
|
// Test that JS-set listeners can be invoked directly
|
||||||
|
const div = document.createElement('div');
|
||||||
|
window.jsInvokeResult = 0;
|
||||||
|
|
||||||
|
div.onclick = () => { window.jsInvokeResult = 100; };
|
||||||
|
div.onclick();
|
||||||
|
testing.expectEqual(100, window.jsInvokeResult);
|
||||||
|
|
||||||
|
div.onload = () => { window.jsInvokeResult = 200; };
|
||||||
|
div.onload();
|
||||||
|
testing.expectEqual(200, window.jsInvokeResult);
|
||||||
|
|
||||||
|
div.onfocus = () => { window.jsInvokeResult = 300; };
|
||||||
|
div.onfocus();
|
||||||
|
testing.expectEqual(300, window.jsInvokeResult);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="js_listener_invoke_with_return">
|
||||||
|
{
|
||||||
|
// Test that JS-set listeners return values when invoked
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
div.onclick = () => { return 'click-result'; };
|
||||||
|
testing.expectEqual('click-result', div.onclick());
|
||||||
|
|
||||||
|
div.onload = () => { return 42; };
|
||||||
|
testing.expectEqual(42, div.onload());
|
||||||
|
|
||||||
|
div.onfocus = () => { return { key: 'value' }; };
|
||||||
|
testing.expectEqual('value', div.onfocus().key);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="js_listener_invoke_with_args">
|
||||||
|
{
|
||||||
|
// Test that JS-set listeners can receive arguments when invoked
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
div.onclick = (a, b) => { return a + b; };
|
||||||
|
testing.expectEqual(15, div.onclick(10, 5));
|
||||||
|
|
||||||
|
div.onload = (msg) => { return 'Hello, ' + msg; };
|
||||||
|
testing.expectEqual('Hello, World', div.onload('World'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="js_setter_override">
|
||||||
|
{
|
||||||
|
// Test that setting a new listener overrides the old one
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
const first = () => { return 1; };
|
||||||
|
const second = () => { return 2; };
|
||||||
|
|
||||||
|
div.onclick = first;
|
||||||
|
testing.expectEqual('function', typeof div.onclick);
|
||||||
|
testing.expectEqual(1, div.onclick());
|
||||||
|
|
||||||
|
div.onclick = second;
|
||||||
|
testing.expectEqual('function', typeof div.onclick);
|
||||||
|
testing.expectEqual(2, div.onclick());
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="different_event_types_independent">
|
||||||
|
{
|
||||||
|
// Test that different event types are stored independently
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
const clickFn = () => {};
|
||||||
|
const focusFn = () => {};
|
||||||
|
const blurFn = () => {};
|
||||||
|
|
||||||
|
div.onclick = clickFn;
|
||||||
|
testing.expectEqual('function', typeof div.onclick);
|
||||||
|
testing.expectEqual(null, div.onfocus);
|
||||||
|
testing.expectEqual(null, div.onblur);
|
||||||
|
|
||||||
|
div.onfocus = focusFn;
|
||||||
|
testing.expectEqual('function', typeof div.onclick);
|
||||||
|
testing.expectEqual('function', typeof div.onfocus);
|
||||||
|
testing.expectEqual(null, div.onblur);
|
||||||
|
|
||||||
|
div.onblur = blurFn;
|
||||||
|
testing.expectEqual('function', typeof div.onclick);
|
||||||
|
testing.expectEqual('function', typeof div.onfocus);
|
||||||
|
testing.expectEqual('function', typeof div.onblur);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="keyboard_event_listeners">
|
||||||
|
{
|
||||||
|
// Test keyboard event listener getters/setters
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
testing.expectEqual(null, div.onkeydown);
|
||||||
|
testing.expectEqual(null, div.onkeyup);
|
||||||
|
testing.expectEqual(null, div.onkeypress);
|
||||||
|
|
||||||
|
div.onkeydown = () => {};
|
||||||
|
div.onkeyup = () => {};
|
||||||
|
div.onkeypress = () => {};
|
||||||
|
|
||||||
|
testing.expectEqual('function', typeof div.onkeydown);
|
||||||
|
testing.expectEqual('function', typeof div.onkeyup);
|
||||||
|
testing.expectEqual('function', typeof div.onkeypress);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="mouse_event_listeners">
|
||||||
|
{
|
||||||
|
// Test mouse event listener getters/setters
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
testing.expectEqual(null, div.onmousedown);
|
||||||
|
testing.expectEqual(null, div.onmouseup);
|
||||||
|
testing.expectEqual(null, div.onmousemove);
|
||||||
|
testing.expectEqual(null, div.onmouseover);
|
||||||
|
testing.expectEqual(null, div.onmouseout);
|
||||||
|
testing.expectEqual(null, div.ondblclick);
|
||||||
|
|
||||||
|
div.onmousedown = () => {};
|
||||||
|
div.onmouseup = () => {};
|
||||||
|
div.onmousemove = () => {};
|
||||||
|
div.onmouseover = () => {};
|
||||||
|
div.onmouseout = () => {};
|
||||||
|
div.ondblclick = () => {};
|
||||||
|
|
||||||
|
testing.expectEqual('function', typeof div.onmousedown);
|
||||||
|
testing.expectEqual('function', typeof div.onmouseup);
|
||||||
|
testing.expectEqual('function', typeof div.onmousemove);
|
||||||
|
testing.expectEqual('function', typeof div.onmouseover);
|
||||||
|
testing.expectEqual('function', typeof div.onmouseout);
|
||||||
|
testing.expectEqual('function', typeof div.ondblclick);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="pointer_event_listeners">
|
||||||
|
{
|
||||||
|
// Test pointer event listener getters/setters
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
testing.expectEqual(null, div.onpointerdown);
|
||||||
|
testing.expectEqual(null, div.onpointerup);
|
||||||
|
testing.expectEqual(null, div.onpointermove);
|
||||||
|
testing.expectEqual(null, div.onpointerover);
|
||||||
|
testing.expectEqual(null, div.onpointerout);
|
||||||
|
testing.expectEqual(null, div.onpointerenter);
|
||||||
|
testing.expectEqual(null, div.onpointerleave);
|
||||||
|
testing.expectEqual(null, div.onpointercancel);
|
||||||
|
|
||||||
|
div.onpointerdown = () => {};
|
||||||
|
div.onpointerup = () => {};
|
||||||
|
div.onpointermove = () => {};
|
||||||
|
div.onpointerover = () => {};
|
||||||
|
div.onpointerout = () => {};
|
||||||
|
div.onpointerenter = () => {};
|
||||||
|
div.onpointerleave = () => {};
|
||||||
|
div.onpointercancel = () => {};
|
||||||
|
|
||||||
|
testing.expectEqual('function', typeof div.onpointerdown);
|
||||||
|
testing.expectEqual('function', typeof div.onpointerup);
|
||||||
|
testing.expectEqual('function', typeof div.onpointermove);
|
||||||
|
testing.expectEqual('function', typeof div.onpointerover);
|
||||||
|
testing.expectEqual('function', typeof div.onpointerout);
|
||||||
|
testing.expectEqual('function', typeof div.onpointerenter);
|
||||||
|
testing.expectEqual('function', typeof div.onpointerleave);
|
||||||
|
testing.expectEqual('function', typeof div.onpointercancel);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="form_event_listeners">
|
||||||
|
{
|
||||||
|
// Test form event listener getters/setters
|
||||||
|
const form = document.createElement('form');
|
||||||
|
|
||||||
|
testing.expectEqual(null, form.onsubmit);
|
||||||
|
testing.expectEqual(null, form.onreset);
|
||||||
|
testing.expectEqual(null, form.onchange);
|
||||||
|
testing.expectEqual(null, form.oninput);
|
||||||
|
testing.expectEqual(null, form.oninvalid);
|
||||||
|
|
||||||
|
form.onsubmit = () => {};
|
||||||
|
form.onreset = () => {};
|
||||||
|
form.onchange = () => {};
|
||||||
|
form.oninput = () => {};
|
||||||
|
form.oninvalid = () => {};
|
||||||
|
|
||||||
|
testing.expectEqual('function', typeof form.onsubmit);
|
||||||
|
testing.expectEqual('function', typeof form.onreset);
|
||||||
|
testing.expectEqual('function', typeof form.onchange);
|
||||||
|
testing.expectEqual('function', typeof form.oninput);
|
||||||
|
testing.expectEqual('function', typeof form.oninvalid);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="drag_event_listeners">
|
||||||
|
{
|
||||||
|
// Test drag event listener getters/setters
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
testing.expectEqual(null, div.ondrag);
|
||||||
|
testing.expectEqual(null, div.ondragstart);
|
||||||
|
testing.expectEqual(null, div.ondragend);
|
||||||
|
testing.expectEqual(null, div.ondragenter);
|
||||||
|
testing.expectEqual(null, div.ondragleave);
|
||||||
|
testing.expectEqual(null, div.ondragover);
|
||||||
|
testing.expectEqual(null, div.ondrop);
|
||||||
|
|
||||||
|
div.ondrag = () => {};
|
||||||
|
div.ondragstart = () => {};
|
||||||
|
div.ondragend = () => {};
|
||||||
|
div.ondragenter = () => {};
|
||||||
|
div.ondragleave = () => {};
|
||||||
|
div.ondragover = () => {};
|
||||||
|
div.ondrop = () => {};
|
||||||
|
|
||||||
|
testing.expectEqual('function', typeof div.ondrag);
|
||||||
|
testing.expectEqual('function', typeof div.ondragstart);
|
||||||
|
testing.expectEqual('function', typeof div.ondragend);
|
||||||
|
testing.expectEqual('function', typeof div.ondragenter);
|
||||||
|
testing.expectEqual('function', typeof div.ondragleave);
|
||||||
|
testing.expectEqual('function', typeof div.ondragover);
|
||||||
|
testing.expectEqual('function', typeof div.ondrop);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="clipboard_event_listeners">
|
||||||
|
{
|
||||||
|
// Test clipboard event listener getters/setters
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
testing.expectEqual(null, div.oncopy);
|
||||||
|
testing.expectEqual(null, div.oncut);
|
||||||
|
testing.expectEqual(null, div.onpaste);
|
||||||
|
|
||||||
|
div.oncopy = () => {};
|
||||||
|
div.oncut = () => {};
|
||||||
|
div.onpaste = () => {};
|
||||||
|
|
||||||
|
testing.expectEqual('function', typeof div.oncopy);
|
||||||
|
testing.expectEqual('function', typeof div.oncut);
|
||||||
|
testing.expectEqual('function', typeof div.onpaste);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="scroll_event_listeners">
|
||||||
|
{
|
||||||
|
// Test scroll event listener getters/setters
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
testing.expectEqual(null, div.onscroll);
|
||||||
|
testing.expectEqual(null, div.onscrollend);
|
||||||
|
testing.expectEqual(null, div.onresize);
|
||||||
|
|
||||||
|
div.onscroll = () => {};
|
||||||
|
div.onscrollend = () => {};
|
||||||
|
div.onresize = () => {};
|
||||||
|
|
||||||
|
testing.expectEqual('function', typeof div.onscroll);
|
||||||
|
testing.expectEqual('function', typeof div.onscrollend);
|
||||||
|
testing.expectEqual('function', typeof div.onresize);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="animation_event_listeners">
|
||||||
|
{
|
||||||
|
// Test animation event listener getters/setters
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
testing.expectEqual(null, div.onanimationstart);
|
||||||
|
testing.expectEqual(null, div.onanimationend);
|
||||||
|
testing.expectEqual(null, div.onanimationiteration);
|
||||||
|
testing.expectEqual(null, div.onanimationcancel);
|
||||||
|
|
||||||
|
div.onanimationstart = () => {};
|
||||||
|
div.onanimationend = () => {};
|
||||||
|
div.onanimationiteration = () => {};
|
||||||
|
div.onanimationcancel = () => {};
|
||||||
|
|
||||||
|
testing.expectEqual('function', typeof div.onanimationstart);
|
||||||
|
testing.expectEqual('function', typeof div.onanimationend);
|
||||||
|
testing.expectEqual('function', typeof div.onanimationiteration);
|
||||||
|
testing.expectEqual('function', typeof div.onanimationcancel);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="transition_event_listeners">
|
||||||
|
{
|
||||||
|
// Test transition event listener getters/setters
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
testing.expectEqual(null, div.ontransitionstart);
|
||||||
|
testing.expectEqual(null, div.ontransitionend);
|
||||||
|
testing.expectEqual(null, div.ontransitionrun);
|
||||||
|
testing.expectEqual(null, div.ontransitioncancel);
|
||||||
|
|
||||||
|
div.ontransitionstart = () => {};
|
||||||
|
div.ontransitionend = () => {};
|
||||||
|
div.ontransitionrun = () => {};
|
||||||
|
div.ontransitioncancel = () => {};
|
||||||
|
|
||||||
|
testing.expectEqual('function', typeof div.ontransitionstart);
|
||||||
|
testing.expectEqual('function', typeof div.ontransitionend);
|
||||||
|
testing.expectEqual('function', typeof div.ontransitionrun);
|
||||||
|
testing.expectEqual('function', typeof div.ontransitioncancel);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="misc_event_listeners">
|
||||||
|
{
|
||||||
|
// Test miscellaneous event listener getters/setters
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
testing.expectEqual(null, div.onwheel);
|
||||||
|
testing.expectEqual(null, div.ontoggle);
|
||||||
|
testing.expectEqual(null, div.oncontextmenu);
|
||||||
|
testing.expectEqual(null, div.onselect);
|
||||||
|
testing.expectEqual(null, div.onabort);
|
||||||
|
testing.expectEqual(null, div.oncancel);
|
||||||
|
testing.expectEqual(null, div.onclose);
|
||||||
|
|
||||||
|
div.onwheel = () => {};
|
||||||
|
div.ontoggle = () => {};
|
||||||
|
div.oncontextmenu = () => {};
|
||||||
|
div.onselect = () => {};
|
||||||
|
div.onabort = () => {};
|
||||||
|
div.oncancel = () => {};
|
||||||
|
div.onclose = () => {};
|
||||||
|
|
||||||
|
testing.expectEqual('function', typeof div.onwheel);
|
||||||
|
testing.expectEqual('function', typeof div.ontoggle);
|
||||||
|
testing.expectEqual('function', typeof div.oncontextmenu);
|
||||||
|
testing.expectEqual('function', typeof div.onselect);
|
||||||
|
testing.expectEqual('function', typeof div.onabort);
|
||||||
|
testing.expectEqual('function', typeof div.oncancel);
|
||||||
|
testing.expectEqual('function', typeof div.onclose);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="media_event_listeners">
|
||||||
|
{
|
||||||
|
// Test media event listener getters/setters
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
testing.expectEqual(null, div.onplay);
|
||||||
|
testing.expectEqual(null, div.onpause);
|
||||||
|
testing.expectEqual(null, div.onplaying);
|
||||||
|
testing.expectEqual(null, div.onended);
|
||||||
|
testing.expectEqual(null, div.onvolumechange);
|
||||||
|
testing.expectEqual(null, div.onwaiting);
|
||||||
|
testing.expectEqual(null, div.onseeking);
|
||||||
|
testing.expectEqual(null, div.onseeked);
|
||||||
|
testing.expectEqual(null, div.ontimeupdate);
|
||||||
|
testing.expectEqual(null, div.onloadstart);
|
||||||
|
testing.expectEqual(null, div.onprogress);
|
||||||
|
testing.expectEqual(null, div.onstalled);
|
||||||
|
testing.expectEqual(null, div.onsuspend);
|
||||||
|
testing.expectEqual(null, div.oncanplay);
|
||||||
|
testing.expectEqual(null, div.oncanplaythrough);
|
||||||
|
testing.expectEqual(null, div.ondurationchange);
|
||||||
|
testing.expectEqual(null, div.onemptied);
|
||||||
|
testing.expectEqual(null, div.onloadeddata);
|
||||||
|
testing.expectEqual(null, div.onloadedmetadata);
|
||||||
|
testing.expectEqual(null, div.onratechange);
|
||||||
|
|
||||||
|
div.onplay = () => {};
|
||||||
|
div.onpause = () => {};
|
||||||
|
div.onplaying = () => {};
|
||||||
|
div.onended = () => {};
|
||||||
|
div.onvolumechange = () => {};
|
||||||
|
div.onwaiting = () => {};
|
||||||
|
div.onseeking = () => {};
|
||||||
|
div.onseeked = () => {};
|
||||||
|
div.ontimeupdate = () => {};
|
||||||
|
div.onloadstart = () => {};
|
||||||
|
div.onprogress = () => {};
|
||||||
|
div.onstalled = () => {};
|
||||||
|
div.onsuspend = () => {};
|
||||||
|
div.oncanplay = () => {};
|
||||||
|
div.oncanplaythrough = () => {};
|
||||||
|
div.ondurationchange = () => {};
|
||||||
|
div.onemptied = () => {};
|
||||||
|
div.onloadeddata = () => {};
|
||||||
|
div.onloadedmetadata = () => {};
|
||||||
|
div.onratechange = () => {};
|
||||||
|
|
||||||
|
testing.expectEqual('function', typeof div.onplay);
|
||||||
|
testing.expectEqual('function', typeof div.onpause);
|
||||||
|
testing.expectEqual('function', typeof div.onplaying);
|
||||||
|
testing.expectEqual('function', typeof div.onended);
|
||||||
|
testing.expectEqual('function', typeof div.onvolumechange);
|
||||||
|
testing.expectEqual('function', typeof div.onwaiting);
|
||||||
|
testing.expectEqual('function', typeof div.onseeking);
|
||||||
|
testing.expectEqual('function', typeof div.onseeked);
|
||||||
|
testing.expectEqual('function', typeof div.ontimeupdate);
|
||||||
|
testing.expectEqual('function', typeof div.onloadstart);
|
||||||
|
testing.expectEqual('function', typeof div.onprogress);
|
||||||
|
testing.expectEqual('function', typeof div.onstalled);
|
||||||
|
testing.expectEqual('function', typeof div.onsuspend);
|
||||||
|
testing.expectEqual('function', typeof div.oncanplay);
|
||||||
|
testing.expectEqual('function', typeof div.oncanplaythrough);
|
||||||
|
testing.expectEqual('function', typeof div.ondurationchange);
|
||||||
|
testing.expectEqual('function', typeof div.onemptied);
|
||||||
|
testing.expectEqual('function', typeof div.onloadeddata);
|
||||||
|
testing.expectEqual('function', typeof div.onloadedmetadata);
|
||||||
|
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'));
|
testing.expectEqual('lazy', img.getAttribute('loading'));
|
||||||
}
|
}
|
||||||
</script>
|
</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,7 +183,45 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id="defaultChecked">
|
<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(true, $('#check1').defaultChecked)
|
||||||
testing.expectEqual(false, $('#check2').defaultChecked)
|
testing.expectEqual(false, $('#check2').defaultChecked)
|
||||||
testing.expectEqual(true, $('#radio1').defaultChecked)
|
testing.expectEqual(true, $('#radio1').defaultChecked)
|
||||||
@@ -455,4 +493,4 @@
|
|||||||
input_checked.defaultChecked = true;
|
input_checked.defaultChecked = true;
|
||||||
testing.expectEqual(false, input_checked.checked);
|
testing.expectEqual(false, input_checked.checked);
|
||||||
}
|
}
|
||||||
</script>
|
</script> -->
|
||||||
|
|||||||
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>
|
||||||
|
-->
|
||||||
12
src/browser/tests/element/html/script/script.html
Normal file
12
src/browser/tests/element/html/script/script.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../../testing.js"></script>
|
||||||
|
|
||||||
|
<script id="script">
|
||||||
|
{
|
||||||
|
let s = document.createElement('script');
|
||||||
|
testing.expectEqual('', s.src);
|
||||||
|
|
||||||
|
s.src = '/over.9000.js';
|
||||||
|
testing.expectEqual('http://127.0.0.1:9582/over.9000.js', s.src);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -229,3 +229,41 @@
|
|||||||
testing.expectEqual('some content', clone.value)
|
testing.expectEqual('some content', clone.value)
|
||||||
}
|
}
|
||||||
</script>
|
</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>
|
||||||
165
src/browser/tests/event/pointer.html
Normal file
165
src/browser/tests/event/pointer.html
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script id=default>
|
||||||
|
{
|
||||||
|
let event = new PointerEvent('pointerdown');
|
||||||
|
testing.expectEqual('pointerdown', event.type);
|
||||||
|
testing.expectEqual(true, event instanceof PointerEvent);
|
||||||
|
testing.expectEqual(true, event instanceof MouseEvent);
|
||||||
|
testing.expectEqual(true, event instanceof UIEvent);
|
||||||
|
testing.expectEqual(true, event instanceof Event);
|
||||||
|
testing.expectEqual(0, event.pointerId);
|
||||||
|
testing.expectEqual('', event.pointerType);
|
||||||
|
testing.expectEqual(1.0, event.width);
|
||||||
|
testing.expectEqual(1.0, event.height);
|
||||||
|
testing.expectEqual(0.0, event.pressure);
|
||||||
|
testing.expectEqual(0.0, event.tangentialPressure);
|
||||||
|
testing.expectEqual(0, event.tiltX);
|
||||||
|
testing.expectEqual(0, event.tiltY);
|
||||||
|
testing.expectEqual(0, event.twist);
|
||||||
|
testing.expectEqual(false, event.isPrimary);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=parameters>
|
||||||
|
{
|
||||||
|
let new_event = new PointerEvent('pointerdown', {
|
||||||
|
pointerId: 42,
|
||||||
|
pointerType: 'pen',
|
||||||
|
width: 10.5,
|
||||||
|
height: 20.5,
|
||||||
|
pressure: 0.75,
|
||||||
|
tangentialPressure: -0.25,
|
||||||
|
tiltX: 30,
|
||||||
|
tiltY: 45,
|
||||||
|
twist: 90,
|
||||||
|
isPrimary: true,
|
||||||
|
clientX: 100,
|
||||||
|
clientY: 200,
|
||||||
|
screenX: 300,
|
||||||
|
screenY: 400
|
||||||
|
});
|
||||||
|
testing.expectEqual(42, new_event.pointerId);
|
||||||
|
testing.expectEqual('pen', new_event.pointerType);
|
||||||
|
testing.expectEqual(10.5, new_event.width);
|
||||||
|
testing.expectEqual(20.5, new_event.height);
|
||||||
|
testing.expectEqual(0.75, new_event.pressure);
|
||||||
|
testing.expectEqual(-0.25, new_event.tangentialPressure);
|
||||||
|
testing.expectEqual(30, new_event.tiltX);
|
||||||
|
testing.expectEqual(45, new_event.tiltY);
|
||||||
|
testing.expectEqual(90, new_event.twist);
|
||||||
|
testing.expectEqual(true, new_event.isPrimary);
|
||||||
|
testing.expectEqual(100, new_event.clientX);
|
||||||
|
testing.expectEqual(200, new_event.clientY);
|
||||||
|
testing.expectEqual(300, new_event.screenX);
|
||||||
|
testing.expectEqual(400, new_event.screenY);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=mousePointerType>
|
||||||
|
{
|
||||||
|
let mouse_event = new PointerEvent('pointerdown', { pointerType: 'mouse' });
|
||||||
|
testing.expectEqual('mouse', mouse_event.pointerType);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=touchPointerType>
|
||||||
|
{
|
||||||
|
let touch_event = new PointerEvent('pointerdown', { pointerType: 'touch', pointerId: 1, pressure: 0.5 });
|
||||||
|
testing.expectEqual('touch', touch_event.pointerType);
|
||||||
|
testing.expectEqual(1, touch_event.pointerId);
|
||||||
|
testing.expectEqual(0.5, touch_event.pressure);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=listener>
|
||||||
|
{
|
||||||
|
let pe = new PointerEvent('pointerdown', { pointerId: 123 });
|
||||||
|
testing.expectEqual(true, pe instanceof PointerEvent);
|
||||||
|
testing.expectEqual(true, pe instanceof MouseEvent);
|
||||||
|
testing.expectEqual(true, pe instanceof Event);
|
||||||
|
|
||||||
|
var evt = null;
|
||||||
|
document.addEventListener('pointerdown', function (e) {
|
||||||
|
evt = e;
|
||||||
|
});
|
||||||
|
document.dispatchEvent(pe);
|
||||||
|
testing.expectEqual('pointerdown', evt.type);
|
||||||
|
testing.expectEqual(true, evt instanceof PointerEvent);
|
||||||
|
testing.expectEqual(123, evt.pointerId);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=isTrusted>
|
||||||
|
{
|
||||||
|
let pointerEvent = new PointerEvent('pointerup');
|
||||||
|
testing.expectEqual(false, pointerEvent.isTrusted);
|
||||||
|
|
||||||
|
let pointerIsTrusted = null;
|
||||||
|
document.addEventListener('pointertest', (e) => {
|
||||||
|
pointerIsTrusted = e.isTrusted;
|
||||||
|
testing.expectEqual(true, e instanceof PointerEvent);
|
||||||
|
});
|
||||||
|
document.dispatchEvent(new PointerEvent('pointertest'));
|
||||||
|
testing.expectEqual(false, pointerIsTrusted);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=eventTypes>
|
||||||
|
{
|
||||||
|
let down = new PointerEvent('pointerdown');
|
||||||
|
testing.expectEqual('pointerdown', down.type);
|
||||||
|
|
||||||
|
let up = new PointerEvent('pointerup');
|
||||||
|
testing.expectEqual('pointerup', up.type);
|
||||||
|
|
||||||
|
let move = new PointerEvent('pointermove');
|
||||||
|
testing.expectEqual('pointermove', move.type);
|
||||||
|
|
||||||
|
let enter = new PointerEvent('pointerenter');
|
||||||
|
testing.expectEqual('pointerenter', enter.type);
|
||||||
|
|
||||||
|
let leave = new PointerEvent('pointerleave');
|
||||||
|
testing.expectEqual('pointerleave', leave.type);
|
||||||
|
|
||||||
|
let over = new PointerEvent('pointerover');
|
||||||
|
testing.expectEqual('pointerover', over.type);
|
||||||
|
|
||||||
|
let out = new PointerEvent('pointerout');
|
||||||
|
testing.expectEqual('pointerout', out.type);
|
||||||
|
|
||||||
|
let cancel = new PointerEvent('pointercancel');
|
||||||
|
testing.expectEqual('pointercancel', cancel.type);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=inheritedMouseProperties>
|
||||||
|
{
|
||||||
|
let pe = new PointerEvent('pointerdown', {
|
||||||
|
button: 2,
|
||||||
|
buttons: 4,
|
||||||
|
altKey: true,
|
||||||
|
ctrlKey: true,
|
||||||
|
shiftKey: true,
|
||||||
|
metaKey: true
|
||||||
|
});
|
||||||
|
testing.expectEqual(2, pe.button);
|
||||||
|
testing.expectEqual(true, pe.altKey);
|
||||||
|
testing.expectEqual(true, pe.ctrlKey);
|
||||||
|
testing.expectEqual(true, pe.shiftKey);
|
||||||
|
testing.expectEqual(true, pe.metaKey);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=inheritedUIEventProperties>
|
||||||
|
{
|
||||||
|
let pe = new PointerEvent('pointerdown', {
|
||||||
|
detail: 5,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
});
|
||||||
|
testing.expectEqual(5, pe.detail);
|
||||||
|
testing.expectEqual(true, pe.bubbles);
|
||||||
|
testing.expectEqual(true, pe.cancelable);
|
||||||
|
}
|
||||||
|
</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>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
testing.eventually(() => {
|
testing.eventually(() => {
|
||||||
testing.expectEqual(true, popstateEventFired);
|
testing.expectEqual(true, popstateEventFired);
|
||||||
testing.expectEqual(state, popstateEventState);
|
testing.expectEqual({testInProgress: true }, popstateEventState);
|
||||||
})
|
})
|
||||||
|
|
||||||
history.back();
|
history.back();
|
||||||
|
|||||||
@@ -1,6 +1 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<script src="testing.js"></script>
|
|
||||||
|
|
||||||
<script id=history-after-nav>
|
|
||||||
testing.expectEqual(true, history.state && history.state.testInProgress);
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -106,3 +106,17 @@
|
|||||||
testing.expectEqual(req5, target);
|
testing.expectEqual(req5, target);
|
||||||
})
|
})
|
||||||
</script>
|
</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"));
|
testing.expectEqual("you", request2.headers.get("target"));
|
||||||
}
|
}
|
||||||
</script>
|
</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>
|
||||||
|
|
||||||
|
<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>
|
<script id=xhr_redirect>
|
||||||
testing.async(async (restore) => {
|
testing.async(async (restore) => {
|
||||||
const req = new XMLHttpRequest();
|
const req = new XMLHttpRequest();
|
||||||
|
|||||||
@@ -58,3 +58,6 @@
|
|||||||
testing.expectEqual(true, e.toString().includes("FailedToLoad"), {script_id: 'import-404'});
|
testing.expectEqual(true, e.toString().includes("FailedToLoad"), {script_id: 'import-404'});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- this used to crash -->
|
||||||
|
<script type=module src=modules/self_async.js></script>
|
||||||
|
|||||||
1
src/browser/tests/page/modules/self_async.js
Normal file
1
src/browser/tests/page/modules/self_async.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
const c = await import('./self_async.js');
|
||||||
595
src/browser/tests/selection.html
Normal file
595
src/browser/tests/selection.html
Normal file
@@ -0,0 +1,595 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<script src="./testing.js"></script>
|
||||||
|
|
||||||
|
<div id="test-content">
|
||||||
|
<p id="p1">The quick brown fox</p>
|
||||||
|
<p id="p2">jumps over the lazy dog</p>
|
||||||
|
<div id="nested">
|
||||||
|
<span id="s1">Hello</span>
|
||||||
|
<span id="s2">World</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script id=basic>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
|
||||||
|
testing.expectEqual(0, sel.rangeCount);
|
||||||
|
testing.expectEqual("None", sel.type);
|
||||||
|
testing.expectEqual(true, sel.isCollapsed);
|
||||||
|
testing.expectEqual(null, sel.anchorNode);
|
||||||
|
testing.expectEqual(null, sel.focusNode);
|
||||||
|
testing.expectEqual(0, sel.anchorOffset);
|
||||||
|
testing.expectEqual(0, sel.focusOffset);
|
||||||
|
testing.expectEqual("none", sel.direction);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=collapse>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
const textNode = p1.firstChild;
|
||||||
|
|
||||||
|
// Collapse to a position
|
||||||
|
sel.collapse(textNode, 4);
|
||||||
|
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
testing.expectEqual("Caret", sel.type);
|
||||||
|
testing.expectEqual(true, sel.isCollapsed);
|
||||||
|
testing.expectEqual(textNode, sel.anchorNode);
|
||||||
|
testing.expectEqual(textNode, sel.focusNode);
|
||||||
|
testing.expectEqual(4, sel.anchorOffset);
|
||||||
|
testing.expectEqual(4, sel.focusOffset);
|
||||||
|
testing.expectEqual("none", sel.direction);
|
||||||
|
|
||||||
|
// Collapse to null removes all ranges
|
||||||
|
sel.collapse(null);
|
||||||
|
testing.expectEqual(0, sel.rangeCount);
|
||||||
|
testing.expectEqual("None", sel.type);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=setPosition>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
const p2 = document.getElementById("p2");
|
||||||
|
const textNode = p2.firstChild;
|
||||||
|
|
||||||
|
// setPosition is an alias for collapse
|
||||||
|
sel.setPosition(textNode, 10);
|
||||||
|
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
testing.expectEqual("Caret", sel.type);
|
||||||
|
testing.expectEqual(textNode, sel.anchorNode);
|
||||||
|
testing.expectEqual(10, sel.anchorOffset);
|
||||||
|
|
||||||
|
// Test default offset
|
||||||
|
sel.setPosition(textNode);
|
||||||
|
testing.expectEqual(0, sel.anchorOffset);
|
||||||
|
|
||||||
|
// Test null
|
||||||
|
sel.setPosition(null);
|
||||||
|
testing.expectEqual(0, sel.rangeCount);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=addRange>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
|
||||||
|
const range1 = document.createRange();
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
range1.selectNodeContents(p1);
|
||||||
|
|
||||||
|
sel.addRange(range1);
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
testing.expectEqual("Range", sel.type);
|
||||||
|
testing.expectEqual(false, sel.isCollapsed);
|
||||||
|
|
||||||
|
// Adding same range again should do nothing
|
||||||
|
sel.addRange(range1);
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
|
||||||
|
// Adding different range
|
||||||
|
const range2 = document.createRange();
|
||||||
|
const p2 = document.getElementById("p2");
|
||||||
|
range2.selectNodeContents(p2);
|
||||||
|
|
||||||
|
sel.addRange(range2);
|
||||||
|
|
||||||
|
// Firefox does support multiple ranges so it will be 2 here instead of 1.
|
||||||
|
// Chrome and Safari don't so we don't either.
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=getRangeAt>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
|
||||||
|
const range = document.createRange();
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
range.selectNodeContents(p1);
|
||||||
|
sel.addRange(range);
|
||||||
|
|
||||||
|
const retrieved = sel.getRangeAt(0);
|
||||||
|
testing.expectEqual(range, retrieved);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=removeRange>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
|
||||||
|
const range1 = document.createRange();
|
||||||
|
const range2 = document.createRange();
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
const p2 = document.getElementById("p2");
|
||||||
|
|
||||||
|
range1.selectNodeContents(p1);
|
||||||
|
range2.selectNodeContents(p2);
|
||||||
|
|
||||||
|
sel.addRange(range1);
|
||||||
|
sel.addRange(range2);
|
||||||
|
|
||||||
|
// Firefox does support multiple ranges so it will be 2 here instead of 1.
|
||||||
|
// Chrome and Safari don't so we don't either.
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
|
||||||
|
// Chrome doesn't throw an error here even though the spec defines it:
|
||||||
|
// https://w3c.github.io/selection-api/#dom-selection-removerange
|
||||||
|
testing.expectError('NotFoundError', () => { sel.removeRange(range2); });
|
||||||
|
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
testing.expectEqual(range1, sel.getRangeAt(0));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=removeAllRanges>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
|
||||||
|
const range1 = document.createRange();
|
||||||
|
const range2 = document.createRange();
|
||||||
|
|
||||||
|
range1.selectNodeContents(document.getElementById("p1"));
|
||||||
|
range2.selectNodeContents(document.getElementById("p2"));
|
||||||
|
|
||||||
|
sel.addRange(range1);
|
||||||
|
sel.addRange(range2);
|
||||||
|
|
||||||
|
// Firefox does support multiple ranges so it will be 2 here instead of 1.
|
||||||
|
// Chrome and Safari don't so we don't either.
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
|
||||||
|
sel.removeAllRanges();
|
||||||
|
testing.expectEqual(0, sel.rangeCount);
|
||||||
|
testing.expectEqual("none", sel.direction);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=empty>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(document.getElementById("p1"));
|
||||||
|
|
||||||
|
sel.addRange(range);
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
|
||||||
|
// empty() is an alias for removeAllRanges()
|
||||||
|
sel.empty();
|
||||||
|
testing.expectEqual(0, sel.rangeCount);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=collapseToStart>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
const textNode = p1.firstChild;
|
||||||
|
|
||||||
|
const range = document.createRange();
|
||||||
|
range.setStart(textNode, 4);
|
||||||
|
range.setEnd(textNode, 15);
|
||||||
|
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
|
||||||
|
testing.expectEqual(false, sel.isCollapsed);
|
||||||
|
testing.expectEqual(4, sel.anchorOffset);
|
||||||
|
testing.expectEqual(15, sel.focusOffset);
|
||||||
|
|
||||||
|
sel.collapseToStart();
|
||||||
|
|
||||||
|
testing.expectEqual(true, sel.isCollapsed);
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
testing.expectEqual(textNode, sel.anchorNode);
|
||||||
|
testing.expectEqual(4, sel.anchorOffset);
|
||||||
|
testing.expectEqual(4, sel.focusOffset);
|
||||||
|
testing.expectEqual("none", sel.direction);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=collapseToEnd>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
const textNode = p1.firstChild;
|
||||||
|
|
||||||
|
const range = document.createRange();
|
||||||
|
range.setStart(textNode, 4);
|
||||||
|
range.setEnd(textNode, 15);
|
||||||
|
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
|
||||||
|
testing.expectEqual(false, sel.isCollapsed);
|
||||||
|
|
||||||
|
sel.collapseToEnd();
|
||||||
|
|
||||||
|
testing.expectEqual(true, sel.isCollapsed);
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
testing.expectEqual(textNode, sel.anchorNode);
|
||||||
|
testing.expectEqual(15, sel.anchorOffset);
|
||||||
|
testing.expectEqual(15, sel.focusOffset);
|
||||||
|
testing.expectEqual("none", sel.direction);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=extend>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
const textNode = p1.firstChild;
|
||||||
|
|
||||||
|
// Start with collapsed selection
|
||||||
|
sel.collapse(textNode, 10);
|
||||||
|
testing.expectEqual(true, sel.isCollapsed);
|
||||||
|
testing.expectEqual(10, sel.anchorOffset);
|
||||||
|
testing.expectEqual("none", sel.direction);
|
||||||
|
|
||||||
|
// Extend forward
|
||||||
|
sel.extend(textNode, 15);
|
||||||
|
testing.expectEqual(false, sel.isCollapsed);
|
||||||
|
testing.expectEqual(10, sel.anchorOffset);
|
||||||
|
testing.expectEqual(15, sel.focusOffset);
|
||||||
|
testing.expectEqual("forward", sel.direction);
|
||||||
|
|
||||||
|
// Extend backward from anchor
|
||||||
|
sel.extend(textNode, 5);
|
||||||
|
testing.expectEqual(false, sel.isCollapsed);
|
||||||
|
testing.expectEqual(10, sel.anchorOffset);
|
||||||
|
testing.expectEqual(5, sel.focusOffset);
|
||||||
|
testing.expectEqual("backward", sel.direction);
|
||||||
|
|
||||||
|
// Extend to same position as anchor
|
||||||
|
sel.extend(textNode, 10);
|
||||||
|
testing.expectEqual(true, sel.isCollapsed);
|
||||||
|
testing.expectEqual(10, sel.anchorOffset);
|
||||||
|
testing.expectEqual(10, sel.focusOffset);
|
||||||
|
testing.expectEqual("none", sel.direction);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=direction>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
const textNode = p1.firstChild;
|
||||||
|
|
||||||
|
// Forward selection
|
||||||
|
sel.collapse(textNode, 5);
|
||||||
|
sel.extend(textNode, 10);
|
||||||
|
testing.expectEqual("forward", sel.direction);
|
||||||
|
testing.expectEqual(5, sel.anchorOffset);
|
||||||
|
testing.expectEqual(10, sel.focusOffset);
|
||||||
|
|
||||||
|
// Backward selection
|
||||||
|
sel.collapse(textNode, 10);
|
||||||
|
sel.extend(textNode, 5);
|
||||||
|
testing.expectEqual("backward", sel.direction);
|
||||||
|
testing.expectEqual(10, sel.anchorOffset);
|
||||||
|
testing.expectEqual(5, sel.focusOffset);
|
||||||
|
|
||||||
|
// None (collapsed)
|
||||||
|
sel.collapse(textNode, 7);
|
||||||
|
testing.expectEqual("none", sel.direction);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=containsNode>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
const nested = document.getElementById("nested");
|
||||||
|
const s1 = document.getElementById("s1");
|
||||||
|
const s2 = document.getElementById("s2");
|
||||||
|
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(nested);
|
||||||
|
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
|
||||||
|
// Partial containment
|
||||||
|
testing.expectEqual(true, sel.containsNode(s1, true));
|
||||||
|
testing.expectEqual(true, sel.containsNode(s2, true));
|
||||||
|
testing.expectEqual(true, sel.containsNode(nested, true));
|
||||||
|
|
||||||
|
// Node outside selection
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
testing.expectEqual(false, sel.containsNode(p1, false));
|
||||||
|
testing.expectEqual(false, sel.containsNode(p1, true));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script id=deleteFromDocument>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
const textNode = p1.firstChild;
|
||||||
|
const originalText = textNode.textContent;
|
||||||
|
|
||||||
|
const range = document.createRange();
|
||||||
|
range.setStart(textNode, 4);
|
||||||
|
range.setEnd(textNode, 15);
|
||||||
|
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
|
||||||
|
sel.deleteFromDocument();
|
||||||
|
|
||||||
|
// Text should be deleted
|
||||||
|
const expectedText = originalText.slice(0, 4) + originalText.slice(15);
|
||||||
|
testing.expectEqual(expectedText, textNode.textContent);
|
||||||
|
|
||||||
|
// Selection should be collapsed at deletion point
|
||||||
|
testing.expectEqual(true, sel.isCollapsed);
|
||||||
|
|
||||||
|
// Restore original text for other tests
|
||||||
|
textNode.textContent = originalText;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=typeProperty>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
const textNode = p1.firstChild;
|
||||||
|
|
||||||
|
// None type
|
||||||
|
sel.removeAllRanges();
|
||||||
|
testing.expectEqual("None", sel.type);
|
||||||
|
|
||||||
|
// Caret type (collapsed)
|
||||||
|
sel.collapse(textNode, 5);
|
||||||
|
testing.expectEqual("Caret", sel.type);
|
||||||
|
|
||||||
|
// Range type (not collapsed)
|
||||||
|
sel.extend(textNode, 10);
|
||||||
|
testing.expectEqual("Range", sel.type);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=selectAllChildren>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
|
||||||
|
const nested = document.getElementById("nested");
|
||||||
|
const s1 = document.getElementById("s1");
|
||||||
|
const s2 = document.getElementById("s2");
|
||||||
|
|
||||||
|
// Select all children of nested div
|
||||||
|
sel.selectAllChildren(nested);
|
||||||
|
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
testing.expectEqual("Range", sel.type);
|
||||||
|
testing.expectEqual(false, sel.isCollapsed);
|
||||||
|
|
||||||
|
// Anchor and focus should be on the parent node
|
||||||
|
testing.expectEqual(nested, sel.anchorNode);
|
||||||
|
testing.expectEqual(nested, sel.focusNode);
|
||||||
|
|
||||||
|
// Should start at offset 0 (before first child)
|
||||||
|
testing.expectEqual(0, sel.anchorOffset);
|
||||||
|
|
||||||
|
const childrenCount = nested.childNodes.length;
|
||||||
|
|
||||||
|
// Should end at offset equal to number of children (after last child)
|
||||||
|
testing.expectEqual(childrenCount, sel.focusOffset);
|
||||||
|
|
||||||
|
// Direction should be forward
|
||||||
|
testing.expectEqual("forward", sel.direction);
|
||||||
|
|
||||||
|
// Should not fully contain the parent itself
|
||||||
|
testing.expectEqual(false, sel.containsNode(nested, false));
|
||||||
|
|
||||||
|
// But should partially contain the parent
|
||||||
|
testing.expectEqual(true, sel.containsNode(nested, true));
|
||||||
|
|
||||||
|
// Verify the range
|
||||||
|
const range = sel.getRangeAt(0);
|
||||||
|
testing.expectEqual(nested, range.startContainer);
|
||||||
|
testing.expectEqual(nested, range.endContainer);
|
||||||
|
testing.expectEqual(0, range.startOffset);
|
||||||
|
testing.expectEqual(childrenCount, range.endOffset);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=selectAllChildrenEmpty>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
|
||||||
|
// Create an empty element
|
||||||
|
const empty = document.createElement("div");
|
||||||
|
document.body.appendChild(empty);
|
||||||
|
|
||||||
|
// Select all children of empty element
|
||||||
|
sel.selectAllChildren(empty);
|
||||||
|
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
testing.expectEqual("Caret", sel.type); // Collapsed because no children
|
||||||
|
testing.expectEqual(true, sel.isCollapsed);
|
||||||
|
testing.expectEqual(empty, sel.anchorNode);
|
||||||
|
testing.expectEqual(0, sel.anchorOffset);
|
||||||
|
testing.expectEqual(0, sel.focusOffset);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
document.body.removeChild(empty);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=selectAllChildrenReplacesSelection>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
|
||||||
|
// Start with an existing selection
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
sel.selectAllChildren(p1);
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
testing.expectEqual(p1, sel.anchorNode);
|
||||||
|
|
||||||
|
// selectAllChildren should replace the existing selection
|
||||||
|
const p2 = document.getElementById("p2");
|
||||||
|
sel.selectAllChildren(p2);
|
||||||
|
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
testing.expectEqual(p2, sel.anchorNode);
|
||||||
|
testing.expectEqual(p2, sel.focusNode);
|
||||||
|
|
||||||
|
// Verify old selection is gone
|
||||||
|
const range = sel.getRangeAt(0);
|
||||||
|
testing.expectEqual(p2, range.startContainer);
|
||||||
|
testing.expectEqual(false, p1 == range.startContainer);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=setBaseAndExtent>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
const textNode = p1.firstChild;
|
||||||
|
|
||||||
|
// Forward selection (anchor before focus)
|
||||||
|
sel.setBaseAndExtent(textNode, 4, textNode, 15);
|
||||||
|
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
testing.expectEqual("Range", sel.type);
|
||||||
|
testing.expectEqual(false, sel.isCollapsed);
|
||||||
|
testing.expectEqual(textNode, sel.anchorNode);
|
||||||
|
testing.expectEqual(4, sel.anchorOffset);
|
||||||
|
testing.expectEqual(textNode, sel.focusNode);
|
||||||
|
testing.expectEqual(15, sel.focusOffset);
|
||||||
|
testing.expectEqual("forward", sel.direction);
|
||||||
|
|
||||||
|
// Backward selection (anchor after focus)
|
||||||
|
sel.setBaseAndExtent(textNode, 15, textNode, 4);
|
||||||
|
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
testing.expectEqual("Range", sel.type);
|
||||||
|
testing.expectEqual(textNode, sel.anchorNode);
|
||||||
|
testing.expectEqual(15, sel.anchorOffset);
|
||||||
|
testing.expectEqual(textNode, sel.focusNode);
|
||||||
|
testing.expectEqual(4, sel.focusOffset);
|
||||||
|
testing.expectEqual("backward", sel.direction);
|
||||||
|
|
||||||
|
// Collapsed selection (anchor equals focus)
|
||||||
|
sel.setBaseAndExtent(textNode, 10, textNode, 10);
|
||||||
|
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
testing.expectEqual("Caret", sel.type);
|
||||||
|
testing.expectEqual(true, sel.isCollapsed);
|
||||||
|
testing.expectEqual(10, sel.anchorOffset);
|
||||||
|
testing.expectEqual(10, sel.focusOffset);
|
||||||
|
testing.expectEqual("none", sel.direction);
|
||||||
|
|
||||||
|
// Across different nodes
|
||||||
|
const p2 = document.getElementById("p2");
|
||||||
|
const textNode2 = p2.firstChild;
|
||||||
|
|
||||||
|
sel.setBaseAndExtent(textNode, 4, textNode2, 5);
|
||||||
|
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
testing.expectEqual(textNode, sel.anchorNode);
|
||||||
|
testing.expectEqual(4, sel.anchorOffset);
|
||||||
|
testing.expectEqual(textNode2, sel.focusNode);
|
||||||
|
testing.expectEqual(5, sel.focusOffset);
|
||||||
|
testing.expectEqual("forward", sel.direction);
|
||||||
|
|
||||||
|
// Should replace existing selection
|
||||||
|
sel.setBaseAndExtent(textNode, 0, textNode, 3);
|
||||||
|
testing.expectEqual(1, sel.rangeCount);
|
||||||
|
testing.expectEqual(0, sel.anchorOffset);
|
||||||
|
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>
|
||||||
211
src/browser/tests/window/onerror.html
Normal file
211
src/browser/tests/window/onerror.html
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=onerrorBasicCallback>
|
||||||
|
{
|
||||||
|
let callbackCalled = false;
|
||||||
|
let receivedArgs = null;
|
||||||
|
|
||||||
|
window.onerror = function(message, source, lineno, colno, error) {
|
||||||
|
callbackCalled = true;
|
||||||
|
receivedArgs = { message, source, lineno, colno, error };
|
||||||
|
};
|
||||||
|
|
||||||
|
const err = new Error('Test error');
|
||||||
|
window.reportError(err);
|
||||||
|
|
||||||
|
testing.expectEqual(true, callbackCalled);
|
||||||
|
testing.expectEqual(true, receivedArgs.message.includes('Test error'));
|
||||||
|
testing.expectEqual(err, receivedArgs.error);
|
||||||
|
|
||||||
|
window.onerror = null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=onerrorFiveArguments>
|
||||||
|
{
|
||||||
|
let argCount = 0;
|
||||||
|
|
||||||
|
window.onerror = function() {
|
||||||
|
argCount = arguments.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.reportError(new Error('Five args test'));
|
||||||
|
|
||||||
|
// Per WHATWG spec, onerror receives exactly 5 arguments
|
||||||
|
testing.expectEqual(5, argCount);
|
||||||
|
|
||||||
|
window.onerror = null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=onerrorReturnTrueCancelsEvent>
|
||||||
|
{
|
||||||
|
let listenerCalled = false;
|
||||||
|
|
||||||
|
window.onerror = function() {
|
||||||
|
return true; // Should cancel the event
|
||||||
|
};
|
||||||
|
|
||||||
|
const listener = function() {
|
||||||
|
listenerCalled = true;
|
||||||
|
};
|
||||||
|
window.addEventListener('error', listener);
|
||||||
|
|
||||||
|
window.reportError(new Error('Should be cancelled'));
|
||||||
|
|
||||||
|
// The event listener should still be called (onerror returning true
|
||||||
|
// only prevents default, not propagation)
|
||||||
|
testing.expectEqual(true, listenerCalled);
|
||||||
|
|
||||||
|
window.onerror = null;
|
||||||
|
window.removeEventListener('error', listener);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=onerrorAndEventListenerBothCalled>
|
||||||
|
{
|
||||||
|
let onerrorCalled = false;
|
||||||
|
let listenerCalled = false;
|
||||||
|
|
||||||
|
window.onerror = function() {
|
||||||
|
onerrorCalled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const listener = function() {
|
||||||
|
listenerCalled = true;
|
||||||
|
};
|
||||||
|
window.addEventListener('error', listener);
|
||||||
|
|
||||||
|
window.reportError(new Error('Both should fire'));
|
||||||
|
|
||||||
|
testing.expectEqual(true, onerrorCalled);
|
||||||
|
testing.expectEqual(true, listenerCalled);
|
||||||
|
|
||||||
|
window.onerror = null;
|
||||||
|
window.removeEventListener('error', listener);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=onerrorCalledBeforeEventListener>
|
||||||
|
{
|
||||||
|
let callOrder = [];
|
||||||
|
|
||||||
|
window.onerror = function() {
|
||||||
|
callOrder.push('onerror');
|
||||||
|
};
|
||||||
|
|
||||||
|
const listener = function() {
|
||||||
|
callOrder.push('listener');
|
||||||
|
};
|
||||||
|
window.addEventListener('error', listener);
|
||||||
|
|
||||||
|
window.reportError(new Error('Order test'));
|
||||||
|
|
||||||
|
// onerror should be called before addEventListener handlers
|
||||||
|
testing.expectEqual('onerror', callOrder[0]);
|
||||||
|
testing.expectEqual('listener', callOrder[1]);
|
||||||
|
|
||||||
|
window.onerror = null;
|
||||||
|
window.removeEventListener('error', listener);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=onerrorGetterSetter>
|
||||||
|
{
|
||||||
|
const handler = function() {};
|
||||||
|
|
||||||
|
testing.expectEqual(null, window.onerror);
|
||||||
|
|
||||||
|
window.onerror = handler;
|
||||||
|
testing.expectEqual(handler, window.onerror);
|
||||||
|
|
||||||
|
window.onerror = null;
|
||||||
|
testing.expectEqual(null, window.onerror);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=onerrorWithNonFunction>
|
||||||
|
{
|
||||||
|
// Setting onerror to a non-function should not throw
|
||||||
|
// but should not be stored as the handler
|
||||||
|
window.onerror = "not a function";
|
||||||
|
testing.expectEqual(null, window.onerror);
|
||||||
|
|
||||||
|
window.onerror = {};
|
||||||
|
testing.expectEqual(null, window.onerror);
|
||||||
|
|
||||||
|
window.onerror = 123;
|
||||||
|
testing.expectEqual(null, window.onerror);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=onerrorArgumentTypes>
|
||||||
|
{
|
||||||
|
let receivedTypes = null;
|
||||||
|
|
||||||
|
window.onerror = function(message, source, lineno, colno, error) {
|
||||||
|
receivedTypes = {
|
||||||
|
message: typeof message,
|
||||||
|
source: typeof source,
|
||||||
|
lineno: typeof lineno,
|
||||||
|
colno: typeof colno,
|
||||||
|
error: typeof error
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
window.reportError(new Error('Type check'));
|
||||||
|
|
||||||
|
testing.expectEqual('string', receivedTypes.message);
|
||||||
|
testing.expectEqual('string', receivedTypes.source);
|
||||||
|
testing.expectEqual('number', receivedTypes.lineno);
|
||||||
|
testing.expectEqual('number', receivedTypes.colno);
|
||||||
|
testing.expectEqual('object', receivedTypes.error);
|
||||||
|
|
||||||
|
window.onerror = null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=onerrorReturnFalseDoesNotCancel>
|
||||||
|
{
|
||||||
|
let eventDefaultPrevented = false;
|
||||||
|
|
||||||
|
window.onerror = function() {
|
||||||
|
return false; // Should NOT cancel the event
|
||||||
|
};
|
||||||
|
|
||||||
|
const listener = function(e) {
|
||||||
|
eventDefaultPrevented = e.defaultPrevented;
|
||||||
|
};
|
||||||
|
window.addEventListener('error', listener);
|
||||||
|
|
||||||
|
window.reportError(new Error('Return false test'));
|
||||||
|
|
||||||
|
testing.expectEqual(false, eventDefaultPrevented);
|
||||||
|
|
||||||
|
window.onerror = null;
|
||||||
|
window.removeEventListener('error', listener);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=onerrorReturnTruePreventsDefault>
|
||||||
|
{
|
||||||
|
let eventDefaultPrevented = false;
|
||||||
|
|
||||||
|
window.onerror = function() {
|
||||||
|
return true; // Should cancel (prevent default)
|
||||||
|
};
|
||||||
|
|
||||||
|
const listener = function(e) {
|
||||||
|
eventDefaultPrevented = e.defaultPrevented;
|
||||||
|
};
|
||||||
|
window.addEventListener('error', listener);
|
||||||
|
|
||||||
|
window.reportError(new Error('Return true test'));
|
||||||
|
|
||||||
|
testing.expectEqual(true, eventDefaultPrevented);
|
||||||
|
|
||||||
|
window.onerror = null;
|
||||||
|
window.removeEventListener('error', listener);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<script id=setTimeout>
|
<script id=setTimeout>
|
||||||
|
testing.expectEqual(1, window.setTimeout.length);
|
||||||
let wst2 = 1;
|
let wst2 = 1;
|
||||||
window.setTimeout((a, b) => {
|
window.setTimeout((a, b) => {
|
||||||
wst2 = 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, globalThis);
|
||||||
testing.expectEqual(window, self);
|
testing.expectEqual(window, self);
|
||||||
testing.expectEqual(window, window.self);
|
testing.expectEqual(window, window.self);
|
||||||
|
testing.expectEqual(null, window.opener);
|
||||||
|
|
||||||
testing.expectEqual(1080, innerHeight);
|
testing.expectEqual(1080, innerHeight);
|
||||||
testing.expectEqual(1920, innerWidth);
|
testing.expectEqual(1920, innerWidth);
|
||||||
@@ -51,6 +52,7 @@
|
|||||||
<script id=btoa>
|
<script id=btoa>
|
||||||
testing.expectEqual('SGVsbG8gV29ybGQh', btoa('Hello World!'));
|
testing.expectEqual('SGVsbG8gV29ybGQh', btoa('Hello World!'));
|
||||||
testing.expectEqual('', btoa(''));
|
testing.expectEqual('', btoa(''));
|
||||||
|
testing.expectEqual('IA==', btoa(' '));
|
||||||
testing.expectEqual('YQ==', btoa('a'));
|
testing.expectEqual('YQ==', btoa('a'));
|
||||||
testing.expectEqual('YWI=', btoa('ab'));
|
testing.expectEqual('YWI=', btoa('ab'));
|
||||||
testing.expectEqual('YWJj', btoa('abc'));
|
testing.expectEqual('YWJj', btoa('abc'));
|
||||||
@@ -61,6 +63,13 @@
|
|||||||
<script id=atob>
|
<script id=atob>
|
||||||
testing.expectEqual('Hello World!', atob('SGVsbG8gV29ybGQh'));
|
testing.expectEqual('Hello World!', atob('SGVsbG8gV29ybGQh'));
|
||||||
testing.expectEqual('', atob(''));
|
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('a', atob('YQ=='));
|
||||||
testing.expectEqual('ab', atob('YWI='));
|
testing.expectEqual('ab', atob('YWI='));
|
||||||
testing.expectEqual('abc', atob('YWJj'));
|
testing.expectEqual('abc', atob('YWJj'));
|
||||||
@@ -105,3 +114,30 @@
|
|||||||
testing.expectEqual(24, screen.pixelDepth);
|
testing.expectEqual(24, screen.pixelDepth);
|
||||||
testing.expectEqual(screen, window.screen);
|
testing.expectEqual(screen, window.screen);
|
||||||
</script>
|
</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>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ pub fn getSignal(self: *const AbortController) *AbortSignal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn abort(self: *AbortController, reason_: ?js.Value.Global, page: *Page) !void {
|
pub fn abort(self: *AbortController, reason_: ?js.Value.Global, page: *Page) !void {
|
||||||
try self._signal.abort(if (reason_) |r| .{ .js_val = r } else null, page);
|
try self._signal.abort(if (reason_) |r| .{ .js_val = r } else null, page.js.local.?, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ pub fn asEventTarget(self: *AbortSignal) *EventTarget {
|
|||||||
return self._proto;
|
return self._proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void {
|
pub fn abort(self: *AbortSignal, reason_: ?Reason, local: *const js.Local, page: *Page) !void {
|
||||||
if (self._aborted) {
|
if (self._aborted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -76,12 +76,13 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch abort event
|
// Dispatch abort event
|
||||||
const event = try Event.initTrusted("abort", .{}, page);
|
const event = try Event.initTrusted(comptime .wrap("abort"), .{}, page);
|
||||||
const func = if (self._on_abort) |*g| g.local() else null;
|
defer if (!event._v8_handoff) event.deinit(false);
|
||||||
|
|
||||||
try page._event_manager.dispatchWithFunction(
|
try page._event_manager.dispatchWithFunction(
|
||||||
self.asEventTarget(),
|
self.asEventTarget(),
|
||||||
event,
|
event,
|
||||||
func,
|
local.toLocal(self._on_abort),
|
||||||
.{ .context = "abort signal" },
|
.{ .context = "abort signal" },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -89,7 +90,7 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void {
|
|||||||
// Static method to create an already-aborted signal
|
// Static method to create an already-aborted signal
|
||||||
pub fn createAborted(reason_: ?js.Value.Global, page: *Page) !*AbortSignal {
|
pub fn createAborted(reason_: ?js.Value.Global, page: *Page) !*AbortSignal {
|
||||||
const signal = try init(page);
|
const signal = try init(page);
|
||||||
try signal.abort(if (reason_) |r| .{ .js_val = r } else null, page);
|
try signal.abort(if (reason_) |r| .{ .js_val = r } else null, page.js.local.?, page);
|
||||||
return signal;
|
return signal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +101,7 @@ pub fn createTimeout(delay: u32, page: *Page) !*AbortSignal {
|
|||||||
.signal = try init(page),
|
.signal = try init(page),
|
||||||
};
|
};
|
||||||
|
|
||||||
try page.scheduler.add(callback, TimeoutCallback.run, delay, .{
|
try page.js.scheduler.add(callback, TimeoutCallback.run, delay, .{
|
||||||
.name = "AbortSignal.timeout",
|
.name = "AbortSignal.timeout",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -112,11 +113,13 @@ const ThrowIfAborted = union(enum) {
|
|||||||
undefined: void,
|
undefined: void,
|
||||||
};
|
};
|
||||||
pub fn throwIfAborted(self: *const AbortSignal, page: *Page) !ThrowIfAborted {
|
pub fn throwIfAborted(self: *const AbortSignal, page: *Page) !ThrowIfAborted {
|
||||||
|
const local = page.js.local.?;
|
||||||
|
|
||||||
if (self._aborted) {
|
if (self._aborted) {
|
||||||
const exception = switch (self._reason) {
|
const exception = switch (self._reason) {
|
||||||
.string => |str| page.js.throw(str),
|
.string => |str| local.throw(str),
|
||||||
.js_val => |js_val| page.js.throw(try js_val.local().toString(.{ .allocator = page.call_arena })),
|
.js_val => |js_val| local.throw(try local.toLocal(js_val).toStringSlice()),
|
||||||
.undefined => page.js.throw("AbortError"),
|
.undefined => local.throw("AbortError"),
|
||||||
};
|
};
|
||||||
return .{ .exception = exception };
|
return .{ .exception = exception };
|
||||||
}
|
}
|
||||||
@@ -135,7 +138,11 @@ const TimeoutCallback = struct {
|
|||||||
|
|
||||||
fn run(ctx: *anyopaque) !?u32 {
|
fn run(ctx: *anyopaque) !?u32 {
|
||||||
const self: *TimeoutCallback = @ptrCast(@alignCast(ctx));
|
const self: *TimeoutCallback = @ptrCast(@alignCast(ctx));
|
||||||
self.signal.abort(.{ .string = "TimeoutError" }, self.page) catch |err| {
|
var ls: js.Local.Scope = undefined;
|
||||||
|
self.page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
|
self.signal.abort(.{ .string = "TimeoutError" }, &ls.local, self.page) catch |err| {
|
||||||
log.warn(.app, "abort signal timeout", .{ .err = err });
|
log.warn(.app, "abort signal timeout", .{ .err = err });
|
||||||
};
|
};
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ fn writeBlobParts(
|
|||||||
/// Returns a Promise that resolves with the contents of the blob
|
/// Returns a Promise that resolves with the contents of the blob
|
||||||
/// as binary data contained in an ArrayBuffer.
|
/// as binary data contained in an ArrayBuffer.
|
||||||
pub fn arrayBuffer(self: *const Blob, page: *Page) !js.Promise {
|
pub fn arrayBuffer(self: *const Blob, page: *Page) !js.Promise {
|
||||||
return page.js.resolvePromise(js.ArrayBuffer{ .values = self._slice });
|
return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = self._slice });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReadableStream = @import("streams/ReadableStream.zig");
|
const ReadableStream = @import("streams/ReadableStream.zig");
|
||||||
@@ -219,7 +219,7 @@ pub fn stream(self: *const Blob, page: *Page) !*ReadableStream {
|
|||||||
/// Returns a Promise that resolves with a string containing
|
/// Returns a Promise that resolves with a string containing
|
||||||
/// the contents of the blob, interpreted as UTF-8.
|
/// the contents of the blob, interpreted as UTF-8.
|
||||||
pub fn text(self: *const Blob, page: *Page) !js.Promise {
|
pub fn text(self: *const Blob, page: *Page) !js.Promise {
|
||||||
return page.js.resolvePromise(self._slice);
|
return page.js.local.?.resolvePromise(self._slice);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extension to Blob; works on Firefox and Safari.
|
/// Extension to Blob; works on Firefox and Safari.
|
||||||
@@ -227,7 +227,7 @@ pub fn text(self: *const Blob, page: *Page) !js.Promise {
|
|||||||
/// Returns a Promise that resolves with a Uint8Array containing
|
/// Returns a Promise that resolves with a Uint8Array containing
|
||||||
/// the contents of the blob as an array of bytes.
|
/// the contents of the blob as an array of bytes.
|
||||||
pub fn bytes(self: *const Blob, page: *Page) !js.Promise {
|
pub fn bytes(self: *const Blob, page: *Page) !js.Promise {
|
||||||
return page.js.resolvePromise(js.TypedArray(u8){ .values = self._slice });
|
return page.js.local.?.resolvePromise(js.TypedArray(u8){ .values = self._slice });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new Blob object which contains data
|
/// Returns a new Blob object which contains data
|
||||||
|
|||||||
@@ -42,15 +42,23 @@ pub fn parseDimension(value: []const u8) ?f64 {
|
|||||||
/// https://drafts.csswg.org/cssom/#the-css.escape()-method
|
/// https://drafts.csswg.org/cssom/#the-css.escape()-method
|
||||||
pub fn escape(_: *const CSS, value: []const u8, page: *Page) ![]const u8 {
|
pub fn escape(_: *const CSS, value: []const u8, page: *Page) ![]const u8 {
|
||||||
if (value.len == 0) {
|
if (value.len == 0) {
|
||||||
return error.InvalidCharacterError;
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const first = value[0];
|
const first = value[0];
|
||||||
|
if (first == '-' and value.len == 1) {
|
||||||
|
return "\\-";
|
||||||
|
}
|
||||||
|
|
||||||
// Count how many characters we need for the output
|
// Count how many characters we need for the output
|
||||||
var out_len: usize = escapeLen(true, first);
|
var out_len: usize = escapeLen(true, first);
|
||||||
for (value[1..]) |c| {
|
for (value[1..], 0..) |c, i| {
|
||||||
out_len += escapeLen(false, c);
|
// 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) {
|
if (out_len == value.len) {
|
||||||
@@ -67,8 +75,13 @@ pub fn escape(_: *const CSS, value: []const u8, page: *Page) ![]const u8 {
|
|||||||
pos = 1;
|
pos = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (value[1..]) |c| {
|
for (value[1..], 0..) |c, i| {
|
||||||
if (!needsEscape(false, c)) {
|
// 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;
|
result[pos] = c;
|
||||||
pos += 1;
|
pos += 1;
|
||||||
} else {
|
} else {
|
||||||
@@ -105,9 +118,6 @@ fn needsEscape(comptime is_first: bool, c: u8) bool {
|
|||||||
if (c >= '0' and c <= '9') {
|
if (c >= '0' and c <= '9') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (c == '-') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Characters that need escaping
|
// Characters that need escaping
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ pub const init: Console = .{};
|
|||||||
|
|
||||||
pub fn trace(_: *const Console, values: []js.Value, page: *Page) !void {
|
pub fn trace(_: *const Console, values: []js.Value, page: *Page) !void {
|
||||||
logger.debug(.js, "console.trace", .{
|
logger.debug(.js, "console.trace", .{
|
||||||
.stack = page.js.stackTrace() catch "???",
|
.stack = page.js.local.?.stackTrace() catch "???",
|
||||||
.args = ValueWriter{ .page = page, .values = values },
|
.args = ValueWriter{ .page = page, .values = values },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -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 }});
|
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 {
|
pub fn count(self: *Console, label_: ?[]const u8, page: *Page) !void {
|
||||||
const label = label_ orelse "default";
|
const label = label_ orelse "default";
|
||||||
const gop = try self._counts.getOrPut(page.arena, label);
|
const gop = try self._counts.getOrPut(page.arena, label);
|
||||||
@@ -138,7 +142,7 @@ const ValueWriter = struct {
|
|||||||
try writer.print("\n arg({d}): {f}", .{ i, value });
|
try writer.print("\n arg({d}): {f}", .{ i, value });
|
||||||
}
|
}
|
||||||
if (self.include_stack) {
|
if (self.include_stack) {
|
||||||
try writer.print("\n stack: {s}", .{self.page.js.stackTrace() catch |err| @errorName(err) orelse "???"});
|
try writer.print("\n stack: {s}", .{self.page.js.local.?.stackTrace() catch |err| @errorName(err) orelse "???"});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +150,7 @@ const ValueWriter = struct {
|
|||||||
var buf: [32]u8 = undefined;
|
var buf: [32]u8 = undefined;
|
||||||
for (self.values, 0..) |value, i| {
|
for (self.values, 0..) |value, i| {
|
||||||
const name = try std.fmt.bufPrint(&buf, "param.{d}", .{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 const prototype_chain = bridge.prototypeChain();
|
||||||
pub var class_id: bridge.ClassId = undefined;
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
pub const empty_with_no_proto = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const trace = bridge.function(Console.trace, .{});
|
pub const trace = bridge.function(Console.trace, .{});
|
||||||
@@ -179,9 +182,15 @@ pub const JsApi = struct {
|
|||||||
pub const assert = bridge.function(Console.assert, .{});
|
pub const assert = bridge.function(Console.assert, .{});
|
||||||
pub const @"error" = bridge.function(Console.@"error", .{});
|
pub const @"error" = bridge.function(Console.@"error", .{});
|
||||||
pub const exception = 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 count = bridge.function(Console.count, .{});
|
||||||
pub const countReset = bridge.function(Console.countReset, .{});
|
pub const countReset = bridge.function(Console.countReset, .{});
|
||||||
pub const time = bridge.function(Console.time, .{});
|
pub const time = bridge.function(Console.time, .{});
|
||||||
pub const timeLog = bridge.function(Console.timeLog, .{});
|
pub const timeLog = bridge.function(Console.timeLog, .{});
|
||||||
pub const timeEnd = bridge.function(Console.timeEnd, .{});
|
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 std = @import("std");
|
||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
|
|
||||||
const Page = @import("../Page.zig");
|
|
||||||
|
|
||||||
const SubtleCrypto = @import("SubtleCrypto.zig");
|
const SubtleCrypto = @import("SubtleCrypto.zig");
|
||||||
|
|
||||||
const Crypto = @This();
|
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>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -17,21 +17,25 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const String = @import("../../string.zig").String;
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
const Page = @import("../Page.zig");
|
|
||||||
const Element = @import("Element.zig");
|
const Element = @import("Element.zig");
|
||||||
|
|
||||||
const CustomElementDefinition = @This();
|
const CustomElementDefinition = @This();
|
||||||
|
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
constructor: js.Function.Global,
|
constructor: js.Function.Global,
|
||||||
|
|
||||||
|
// TODO: Make this a Map<String>
|
||||||
observed_attributes: std.StringHashMapUnmanaged(void) = .{},
|
observed_attributes: std.StringHashMapUnmanaged(void) = .{},
|
||||||
|
|
||||||
// For customized built-in elements, this is the element tag they extend (e.g., .button)
|
// For customized built-in elements, this is the element tag they extend (e.g., .button)
|
||||||
// For autonomous custom elements, this is null
|
// For autonomous custom elements, this is null
|
||||||
extends: ?Element.Tag = null,
|
extends: ?Element.Tag = null,
|
||||||
|
|
||||||
pub fn isAttributeObserved(self: *const CustomElementDefinition, name: []const u8) bool {
|
pub fn isAttributeObserved(self: *const CustomElementDefinition, name: String) bool {
|
||||||
return self.observed_attributes.contains(name);
|
return self.observed_attributes.contains(name.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isAutonomous(self: *const CustomElementDefinition) bool {
|
pub fn isAutonomous(self: *const CustomElementDefinition) bool {
|
||||||
|
|||||||
@@ -73,9 +73,8 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
|
|||||||
var js_arr = observed_attrs.toArray();
|
var js_arr = observed_attrs.toArray();
|
||||||
for (0..js_arr.len()) |i| {
|
for (0..js_arr.len()) |i| {
|
||||||
const attr_val = js_arr.get(@intCast(i)) catch continue;
|
const attr_val = js_arr.get(@intCast(i)) catch continue;
|
||||||
const attr_name = attr_val.toString(.{ .allocator = page.arena }) catch continue;
|
const attr_name = attr_val.toStringSliceWithAlloc(page.arena) catch continue;
|
||||||
const owned_attr = page.dupeString(attr_name) catch continue;
|
definition.observed_attributes.put(page.arena, attr_name, {}) catch continue;
|
||||||
definition.observed_attributes.put(page.arena, owned_attr, {}) catch continue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,7 +105,7 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (self._when_defined.fetchRemove(name)) |entry| {
|
if (self._when_defined.fetchRemove(name)) |entry| {
|
||||||
entry.value.local().resolve("whenDefined", constructor);
|
page.js.toLocal(entry.value).resolve("whenDefined", constructor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,22 +119,23 @@ pub fn upgrade(self: *CustomElementRegistry, root: *Node, page: *Page) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn whenDefined(self: *CustomElementRegistry, name: []const u8, page: *Page) !js.Promise {
|
pub fn whenDefined(self: *CustomElementRegistry, name: []const u8, page: *Page) !js.Promise {
|
||||||
|
const local = page.js.local.?;
|
||||||
if (self._definitions.get(name)) |definition| {
|
if (self._definitions.get(name)) |definition| {
|
||||||
return page.js.resolvePromise(definition.constructor);
|
return local.resolvePromise(definition.constructor);
|
||||||
}
|
}
|
||||||
|
|
||||||
const gop = try self._when_defined.getOrPut(page.arena, name);
|
const gop = try self._when_defined.getOrPut(page.arena, name);
|
||||||
if (gop.found_existing) {
|
if (gop.found_existing) {
|
||||||
return gop.value_ptr.local().promise();
|
return local.toLocal(gop.value_ptr.*).promise();
|
||||||
}
|
}
|
||||||
errdefer _ = self._when_defined.remove(name);
|
errdefer _ = self._when_defined.remove(name);
|
||||||
const owned_name = try page.dupeString(name);
|
const owned_name = try page.dupeString(name);
|
||||||
|
|
||||||
const resolver = try page.js.createPromiseResolver().persist();
|
const resolver = local.createPromiseResolver();
|
||||||
gop.key_ptr.* = owned_name;
|
gop.key_ptr.* = owned_name;
|
||||||
gop.value_ptr.* = resolver;
|
gop.value_ptr.* = try resolver.persist();
|
||||||
|
|
||||||
return resolver.local().promise();
|
return resolver.promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn upgradeNode(self: *CustomElementRegistry, node: *Node, page: *Page) !void {
|
fn upgradeNode(self: *CustomElementRegistry, node: *Node, page: *Page) !void {
|
||||||
@@ -174,8 +174,12 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio
|
|||||||
page._upgrading_element = node;
|
page._upgrading_element = node;
|
||||||
defer page._upgrading_element = prev_upgrading;
|
defer page._upgrading_element = prev_upgrading;
|
||||||
|
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
var caught: js.TryCatch.Caught = undefined;
|
var caught: js.TryCatch.Caught = undefined;
|
||||||
_ = definition.constructor.local().newInstance(&caught) catch |err| {
|
_ = ls.toLocal(definition.constructor).newInstance(&caught) catch |err| {
|
||||||
log.warn(.js, "custom element upgrade", .{ .name = definition.name, .err = err, .caught = caught });
|
log.warn(.js, "custom element upgrade", .{ .name = definition.name, .err = err, .caught = caught });
|
||||||
return error.CustomElementUpgradeFailed;
|
return error.CustomElementUpgradeFailed;
|
||||||
};
|
};
|
||||||
@@ -183,9 +187,9 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio
|
|||||||
// Invoke attributeChangedCallback for existing observed attributes
|
// Invoke attributeChangedCallback for existing observed attributes
|
||||||
var attr_it = custom.asElement().attributeIterator();
|
var attr_it = custom.asElement().attributeIterator();
|
||||||
while (attr_it.next()) |attr| {
|
while (attr_it.next()) |attr| {
|
||||||
const name = attr._name.str();
|
const name = attr._name;
|
||||||
if (definition.isAttributeObserved(name)) {
|
if (definition.isAttributeObserved(name)) {
|
||||||
custom.invokeAttributeChangedCallback(name, null, attr._value.str(), page);
|
custom.invokeAttributeChangedCallback(name, null, attr._value, page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ _what_to_show: u32,
|
|||||||
_filter: NodeFilter,
|
_filter: NodeFilter,
|
||||||
_reference_node: *Node,
|
_reference_node: *Node,
|
||||||
_pointer_before_reference_node: bool,
|
_pointer_before_reference_node: bool,
|
||||||
|
_active: bool = false,
|
||||||
|
|
||||||
pub fn init(root: *Node, what_to_show: u32, filter: ?FilterOpts, page: *Page) !*DOMNodeIterator {
|
pub fn init(root: *Node, what_to_show: u32, filter: ?FilterOpts, page: *Page) !*DOMNodeIterator {
|
||||||
const node_filter = try NodeFilter.init(filter);
|
const node_filter = try NodeFilter.init(filter);
|
||||||
@@ -63,14 +64,21 @@ pub fn getFilter(self: *const DOMNodeIterator) ?FilterOpts {
|
|||||||
return self._filter._original_filter;
|
return self._filter._original_filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nextNode(self: *DOMNodeIterator) !?*Node {
|
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 node = self._reference_node;
|
||||||
var before_node = self._pointer_before_reference_node;
|
var before_node = self._pointer_before_reference_node;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (before_node) {
|
if (before_node) {
|
||||||
before_node = false;
|
before_node = false;
|
||||||
const result = try self.filterNode(node);
|
const result = try self.filterNode(node, page);
|
||||||
if (result == NodeFilter.FILTER_ACCEPT) {
|
if (result == NodeFilter.FILTER_ACCEPT) {
|
||||||
self._reference_node = node;
|
self._reference_node = node;
|
||||||
self._pointer_before_reference_node = false;
|
self._pointer_before_reference_node = false;
|
||||||
@@ -84,7 +92,7 @@ pub fn nextNode(self: *DOMNodeIterator) !?*Node {
|
|||||||
}
|
}
|
||||||
node = next.?;
|
node = next.?;
|
||||||
|
|
||||||
const result = try self.filterNode(node);
|
const result = try self.filterNode(node, page);
|
||||||
if (result == NodeFilter.FILTER_ACCEPT) {
|
if (result == NodeFilter.FILTER_ACCEPT) {
|
||||||
self._reference_node = node;
|
self._reference_node = node;
|
||||||
self._pointer_before_reference_node = false;
|
self._pointer_before_reference_node = false;
|
||||||
@@ -94,13 +102,20 @@ pub fn nextNode(self: *DOMNodeIterator) !?*Node {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn previousNode(self: *DOMNodeIterator) !?*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 node = self._reference_node;
|
||||||
var before_node = self._pointer_before_reference_node;
|
var before_node = self._pointer_before_reference_node;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (!before_node) {
|
if (!before_node) {
|
||||||
const result = try self.filterNode(node);
|
const result = try self.filterNode(node, page);
|
||||||
if (result == NodeFilter.FILTER_ACCEPT) {
|
if (result == NodeFilter.FILTER_ACCEPT) {
|
||||||
self._reference_node = node;
|
self._reference_node = node;
|
||||||
self._pointer_before_reference_node = true;
|
self._pointer_before_reference_node = true;
|
||||||
@@ -119,7 +134,11 @@ pub fn previousNode(self: *DOMNodeIterator) !?*Node {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filterNode(self: *const DOMNodeIterator, node: *Node) !i32 {
|
pub fn detach(_: *const DOMNodeIterator) void {
|
||||||
|
// no-op legacy
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filterNode(self: *const DOMNodeIterator, node: *Node, page: *Page) !i32 {
|
||||||
// First check whatToShow
|
// First check whatToShow
|
||||||
if (!NodeFilter.shouldShow(node, self._what_to_show)) {
|
if (!NodeFilter.shouldShow(node, self._what_to_show)) {
|
||||||
return NodeFilter.FILTER_SKIP;
|
return NodeFilter.FILTER_SKIP;
|
||||||
@@ -128,7 +147,7 @@ fn filterNode(self: *const DOMNodeIterator, node: *Node) !i32 {
|
|||||||
// Then check the filter callback
|
// Then check the filter callback
|
||||||
// For NodeIterator, REJECT and SKIP are equivalent - both skip the node
|
// For NodeIterator, REJECT and SKIP are equivalent - both skip the node
|
||||||
// but continue with its descendants
|
// but continue with its descendants
|
||||||
const result = try self._filter.acceptNode(node);
|
const result = try self._filter.acceptNode(node, page.js.local.?);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +200,7 @@ pub const JsApi = struct {
|
|||||||
pub const whatToShow = bridge.accessor(DOMNodeIterator.getWhatToShow, null, .{});
|
pub const whatToShow = bridge.accessor(DOMNodeIterator.getWhatToShow, null, .{});
|
||||||
pub const filter = bridge.accessor(DOMNodeIterator.getFilter, null, .{});
|
pub const filter = bridge.accessor(DOMNodeIterator.getFilter, null, .{});
|
||||||
|
|
||||||
pub const nextNode = bridge.function(DOMNodeIterator.nextNode, .{});
|
pub const nextNode = bridge.function(DOMNodeIterator.nextNode, .{ .dom_exception = true });
|
||||||
pub const previousNode = bridge.function(DOMNodeIterator.previousNode, .{});
|
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>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -26,7 +26,6 @@ const Parser = @import("../parser/Parser.zig");
|
|||||||
const HTMLDocument = @import("HTMLDocument.zig");
|
const HTMLDocument = @import("HTMLDocument.zig");
|
||||||
const XMLDocument = @import("XMLDocument.zig");
|
const XMLDocument = @import("XMLDocument.zig");
|
||||||
const Document = @import("Document.zig");
|
const Document = @import("Document.zig");
|
||||||
const ProcessingInstruction = @import("../webapi/cdata/ProcessingInstruction.zig");
|
|
||||||
|
|
||||||
const DOMParser = @This();
|
const DOMParser = @This();
|
||||||
|
|
||||||
@@ -48,6 +47,9 @@ pub fn parseFromString(
|
|||||||
@"image/svg+xml",
|
@"image/svg+xml",
|
||||||
}, mime_type) orelse return error.NotSupported;
|
}, mime_type) orelse return error.NotSupported;
|
||||||
|
|
||||||
|
const arena = try page.getArena(.{ .debug = "DOMParser.parseFromString" });
|
||||||
|
defer page.releaseArena(arena);
|
||||||
|
|
||||||
return switch (target_mime) {
|
return switch (target_mime) {
|
||||||
.@"text/html" => {
|
.@"text/html" => {
|
||||||
// Create a new HTMLDocument
|
// Create a new HTMLDocument
|
||||||
@@ -61,7 +63,7 @@ pub fn parseFromString(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse HTML into the document
|
// Parse HTML into the document
|
||||||
var parser = Parser.init(page.arena, doc.asNode(), page);
|
var parser = Parser.init(arena, doc.asNode(), page);
|
||||||
parser.parse(normalized);
|
parser.parse(normalized);
|
||||||
|
|
||||||
if (parser.err) |pe| {
|
if (parser.err) |pe| {
|
||||||
@@ -78,7 +80,7 @@ pub fn parseFromString(
|
|||||||
|
|
||||||
// Parse XML into XMLDocument.
|
// Parse XML into XMLDocument.
|
||||||
const doc_node = doc.asNode();
|
const doc_node = doc.asNode();
|
||||||
var parser = Parser.init(page.arena, doc_node, page);
|
var parser = Parser.init(arena, doc_node, page);
|
||||||
parser.parseXML(html);
|
parser.parseXML(html);
|
||||||
|
|
||||||
if (parser.err) |pe| {
|
if (parser.err) |pe| {
|
||||||
|
|||||||
@@ -62,13 +62,13 @@ pub fn setCurrentNode(self: *DOMTreeWalker, node: *Node) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Navigation methods
|
// Navigation methods
|
||||||
pub fn parentNode(self: *DOMTreeWalker) !?*Node {
|
pub fn parentNode(self: *DOMTreeWalker, page: *Page) !?*Node {
|
||||||
var node = self._current._parent;
|
var node = self._current._parent;
|
||||||
while (node) |n| {
|
while (node) |n| {
|
||||||
if (n == self._root._parent) {
|
if (n == self._root._parent) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) {
|
if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) {
|
||||||
self._current = n;
|
self._current = n;
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
@@ -77,11 +77,11 @@ pub fn parentNode(self: *DOMTreeWalker) !?*Node {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn firstChild(self: *DOMTreeWalker) !?*Node {
|
pub fn firstChild(self: *DOMTreeWalker, page: *Page) !?*Node {
|
||||||
var node = self._current.firstChild();
|
var node = self._current.firstChild();
|
||||||
|
|
||||||
while (node) |n| {
|
while (node) |n| {
|
||||||
const filter_result = try self.acceptNode(n);
|
const filter_result = try self.acceptNode(n, page);
|
||||||
|
|
||||||
if (filter_result == NodeFilter.FILTER_ACCEPT) {
|
if (filter_result == NodeFilter.FILTER_ACCEPT) {
|
||||||
self._current = n;
|
self._current = n;
|
||||||
@@ -117,11 +117,11 @@ pub fn firstChild(self: *DOMTreeWalker) !?*Node {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lastChild(self: *DOMTreeWalker) !?*Node {
|
pub fn lastChild(self: *DOMTreeWalker, page: *Page) !?*Node {
|
||||||
var node = self._current.lastChild();
|
var node = self._current.lastChild();
|
||||||
|
|
||||||
while (node) |n| {
|
while (node) |n| {
|
||||||
const filter_result = try self.acceptNode(n);
|
const filter_result = try self.acceptNode(n, page);
|
||||||
|
|
||||||
if (filter_result == NodeFilter.FILTER_ACCEPT) {
|
if (filter_result == NodeFilter.FILTER_ACCEPT) {
|
||||||
self._current = n;
|
self._current = n;
|
||||||
@@ -157,10 +157,10 @@ pub fn lastChild(self: *DOMTreeWalker) !?*Node {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn previousSibling(self: *DOMTreeWalker) !?*Node {
|
pub fn previousSibling(self: *DOMTreeWalker, page: *Page) !?*Node {
|
||||||
var node = self.previousSiblingOrNull(self._current);
|
var node = self.previousSiblingOrNull(self._current);
|
||||||
while (node) |n| {
|
while (node) |n| {
|
||||||
if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) {
|
if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) {
|
||||||
self._current = n;
|
self._current = n;
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
@@ -169,10 +169,10 @@ pub fn previousSibling(self: *DOMTreeWalker) !?*Node {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nextSibling(self: *DOMTreeWalker) !?*Node {
|
pub fn nextSibling(self: *DOMTreeWalker, page: *Page) !?*Node {
|
||||||
var node = self.nextSiblingOrNull(self._current);
|
var node = self.nextSiblingOrNull(self._current);
|
||||||
while (node) |n| {
|
while (node) |n| {
|
||||||
if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) {
|
if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) {
|
||||||
self._current = n;
|
self._current = n;
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
@@ -181,7 +181,7 @@ pub fn nextSibling(self: *DOMTreeWalker) !?*Node {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn previousNode(self: *DOMTreeWalker) !?*Node {
|
pub fn previousNode(self: *DOMTreeWalker, page: *Page) !?*Node {
|
||||||
var node = self._current;
|
var node = self._current;
|
||||||
while (node != self._root) {
|
while (node != self._root) {
|
||||||
var sibling = self.previousSiblingOrNull(node);
|
var sibling = self.previousSiblingOrNull(node);
|
||||||
@@ -189,7 +189,7 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
|
|||||||
node = sib;
|
node = sib;
|
||||||
|
|
||||||
// Check if this sibling is rejected before descending into it
|
// Check if this sibling is rejected before descending into it
|
||||||
const sib_result = try self.acceptNode(node);
|
const sib_result = try self.acceptNode(node, page);
|
||||||
if (sib_result == NodeFilter.FILTER_REJECT) {
|
if (sib_result == NodeFilter.FILTER_REJECT) {
|
||||||
// Skip this sibling and its descendants entirely
|
// Skip this sibling and its descendants entirely
|
||||||
sibling = self.previousSiblingOrNull(node);
|
sibling = self.previousSiblingOrNull(node);
|
||||||
@@ -204,7 +204,7 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
|
|||||||
while (child) |c| {
|
while (child) |c| {
|
||||||
if (!self.isInSubtree(c)) break;
|
if (!self.isInSubtree(c)) break;
|
||||||
|
|
||||||
const filter_result = try self.acceptNode(c);
|
const filter_result = try self.acceptNode(c, page);
|
||||||
if (filter_result == NodeFilter.FILTER_REJECT) {
|
if (filter_result == NodeFilter.FILTER_REJECT) {
|
||||||
// Skip this child and try its previous sibling
|
// Skip this child and try its previous sibling
|
||||||
child = self.previousSiblingOrNull(c);
|
child = self.previousSiblingOrNull(c);
|
||||||
@@ -220,7 +220,7 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
|
|||||||
node = child.?;
|
node = child.?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (try self.acceptNode(node) == NodeFilter.FILTER_ACCEPT) {
|
if (try self.acceptNode(node, page) == NodeFilter.FILTER_ACCEPT) {
|
||||||
self._current = node;
|
self._current = node;
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
@@ -232,7 +232,7 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parent = node._parent orelse return null;
|
const parent = node._parent orelse return null;
|
||||||
if (try self.acceptNode(parent) == NodeFilter.FILTER_ACCEPT) {
|
if (try self.acceptNode(parent, page) == NodeFilter.FILTER_ACCEPT) {
|
||||||
self._current = parent;
|
self._current = parent;
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
@@ -241,14 +241,14 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nextNode(self: *DOMTreeWalker) !?*Node {
|
pub fn nextNode(self: *DOMTreeWalker, page: *Page) !?*Node {
|
||||||
var node = self._current;
|
var node = self._current;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
// Try children first (depth-first)
|
// Try children first (depth-first)
|
||||||
if (node.firstChild()) |child| {
|
if (node.firstChild()) |child| {
|
||||||
node = child;
|
node = child;
|
||||||
const filter_result = try self.acceptNode(node);
|
const filter_result = try self.acceptNode(node, page);
|
||||||
if (filter_result == NodeFilter.FILTER_ACCEPT) {
|
if (filter_result == NodeFilter.FILTER_ACCEPT) {
|
||||||
self._current = node;
|
self._current = node;
|
||||||
return node;
|
return node;
|
||||||
@@ -271,7 +271,7 @@ pub fn nextNode(self: *DOMTreeWalker) !?*Node {
|
|||||||
|
|
||||||
if (node.nextSibling()) |sibling| {
|
if (node.nextSibling()) |sibling| {
|
||||||
node = sibling;
|
node = sibling;
|
||||||
const filter_result = try self.acceptNode(node);
|
const filter_result = try self.acceptNode(node, page);
|
||||||
if (filter_result == NodeFilter.FILTER_ACCEPT) {
|
if (filter_result == NodeFilter.FILTER_ACCEPT) {
|
||||||
self._current = node;
|
self._current = node;
|
||||||
return node;
|
return node;
|
||||||
@@ -293,7 +293,7 @@ pub fn nextNode(self: *DOMTreeWalker) !?*Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
fn acceptNode(self: *const DOMTreeWalker, node: *Node) !i32 {
|
fn acceptNode(self: *const DOMTreeWalker, node: *Node, page: *Page) !i32 {
|
||||||
// First check whatToShow
|
// First check whatToShow
|
||||||
if (!NodeFilter.shouldShow(node, self._what_to_show)) {
|
if (!NodeFilter.shouldShow(node, self._what_to_show)) {
|
||||||
return NodeFilter.FILTER_SKIP;
|
return NodeFilter.FILTER_SKIP;
|
||||||
@@ -303,7 +303,7 @@ fn acceptNode(self: *const DOMTreeWalker, node: *Node) !i32 {
|
|||||||
// For TreeWalker, REJECT means reject node and its descendants
|
// For TreeWalker, REJECT means reject node and its descendants
|
||||||
// SKIP means skip node but check its descendants
|
// SKIP means skip node but check its descendants
|
||||||
// ACCEPT means accept the node
|
// ACCEPT means accept the node
|
||||||
return try self._filter.acceptNode(node);
|
return try self._filter.acceptNode(node, page.js.local.?);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isInSubtree(self: *const DOMTreeWalker, node: *Node) bool {
|
fn isInSubtree(self: *const DOMTreeWalker, node: *Node) bool {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -30,12 +30,11 @@ const Location = @import("Location.zig");
|
|||||||
const Parser = @import("../parser/Parser.zig");
|
const Parser = @import("../parser/Parser.zig");
|
||||||
const collections = @import("collections.zig");
|
const collections = @import("collections.zig");
|
||||||
const Selector = @import("selector/Selector.zig");
|
const Selector = @import("selector/Selector.zig");
|
||||||
const NodeFilter = @import("NodeFilter.zig");
|
|
||||||
const DocumentType = @import("DocumentType.zig");
|
|
||||||
const DOMTreeWalker = @import("DOMTreeWalker.zig");
|
const DOMTreeWalker = @import("DOMTreeWalker.zig");
|
||||||
const DOMNodeIterator = @import("DOMNodeIterator.zig");
|
const DOMNodeIterator = @import("DOMNodeIterator.zig");
|
||||||
const DOMImplementation = @import("DOMImplementation.zig");
|
const DOMImplementation = @import("DOMImplementation.zig");
|
||||||
const StyleSheetList = @import("css/StyleSheetList.zig");
|
const StyleSheetList = @import("css/StyleSheetList.zig");
|
||||||
|
const Selection = @import("Selection.zig");
|
||||||
|
|
||||||
pub const XMLDocument = @import("XMLDocument.zig");
|
pub const XMLDocument = @import("XMLDocument.zig");
|
||||||
pub const HTMLDocument = @import("HTMLDocument.zig");
|
pub const HTMLDocument = @import("HTMLDocument.zig");
|
||||||
@@ -55,6 +54,21 @@ _style_sheets: ?*StyleSheetList = null,
|
|||||||
_write_insertion_point: ?*Node = null,
|
_write_insertion_point: ?*Node = null,
|
||||||
_script_created_parser: ?Parser.Streaming = null,
|
_script_created_parser: ?Parser.Streaming = null,
|
||||||
_adopted_style_sheets: ?js.Object.Global = 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) {
|
pub const Type = union(enum) {
|
||||||
generic,
|
generic,
|
||||||
@@ -103,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 {
|
pub fn getDomain(_: *const Document, page: *const Page) []const u8 {
|
||||||
return URL.getHostname(page.url);
|
return URL.getHostname(page.url);
|
||||||
}
|
}
|
||||||
@@ -125,14 +127,16 @@ const CreateElementOptions = struct {
|
|||||||
|
|
||||||
pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element {
|
pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element {
|
||||||
try validateElementName(name);
|
try validateElementName(name);
|
||||||
const namespace: Element.Namespace = blk: {
|
const ns: Element.Namespace, const normalized_name = blk: {
|
||||||
if (self._type == .html) {
|
if (self._type == .html) {
|
||||||
break :blk .html;
|
break :blk .{ .html, std.ascii.lowerString(&page.buf, name) };
|
||||||
}
|
}
|
||||||
// Generic and XML documents create XML elements
|
// 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);
|
const element = node.as(Element);
|
||||||
|
|
||||||
// Track owner document if it's not the main document
|
// Track owner document if it's not the main document
|
||||||
@@ -142,7 +146,7 @@ pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElement
|
|||||||
|
|
||||||
const options = options_ orelse return element;
|
const options = options_ orelse return element;
|
||||||
if (options.is) |is_value| {
|
if (options.is) |is_value| {
|
||||||
try element.setAttribute("is", is_value, page);
|
try element.setAttribute(comptime .wrap("is"), .wrap(is_value), page);
|
||||||
try Element.Html.Custom.checkAndAttachBuiltIn(element, page);
|
try Element.Html.Custom.checkAndAttachBuiltIn(element, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,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 {
|
pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element {
|
||||||
try validateElementName(name);
|
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
|
// Track owner document if it's not the main document
|
||||||
if (self != page.document) {
|
if (self != page.document) {
|
||||||
@@ -160,26 +166,26 @@ pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8
|
|||||||
return node.as(Element);
|
return node.as(Element);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn createAttribute(_: *const Document, name: []const u8, page: *Page) !?*Element.Attribute {
|
pub fn createAttribute(_: *const Document, name: String.Global, page: *Page) !?*Element.Attribute {
|
||||||
try Element.Attribute.validateAttributeName(name);
|
try Element.Attribute.validateAttributeName(name.str);
|
||||||
return page._factory.node(Element.Attribute{
|
return page._factory.node(Element.Attribute{
|
||||||
._proto = undefined,
|
._proto = undefined,
|
||||||
._name = try page.dupeString(name),
|
._name = name.str,
|
||||||
._value = "",
|
._value = String.empty,
|
||||||
._element = null,
|
._element = null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn createAttributeNS(_: *const Document, namespace: []const u8, name: []const u8, page: *Page) !?*Element.Attribute {
|
pub fn createAttributeNS(_: *const Document, namespace: []const u8, name: String.Global, page: *Page) !?*Element.Attribute {
|
||||||
if (std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml") == false) {
|
if (std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml") == false) {
|
||||||
log.warn(.not_implemented, "document.createAttributeNS", .{ .namespace = namespace });
|
log.warn(.not_implemented, "document.createAttributeNS", .{ .namespace = namespace });
|
||||||
}
|
}
|
||||||
|
|
||||||
try Element.Attribute.validateAttributeName(name);
|
try Element.Attribute.validateAttributeName(name.str);
|
||||||
return page._factory.node(Element.Attribute{
|
return page._factory.node(Element.Attribute{
|
||||||
._proto = undefined,
|
._proto = undefined,
|
||||||
._name = try page.dupeString(name),
|
._name = name.str,
|
||||||
._value = "",
|
._value = String.empty,
|
||||||
._element = null,
|
._element = null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -197,7 +203,7 @@ pub fn getElementById(self: *Document, id: []const u8, page: *Page) ?*Element {
|
|||||||
if (self._removed_ids.remove(id)) {
|
if (self._removed_ids.remove(id)) {
|
||||||
var tw = @import("TreeWalker.zig").Full.Elements.init(self.asNode(), .{});
|
var tw = @import("TreeWalker.zig").Full.Elements.init(self.asNode(), .{});
|
||||||
while (tw.next()) |el| {
|
while (tw.next()) |el| {
|
||||||
const element_id = el.getAttributeSafe("id") orelse continue;
|
const element_id = el.getAttributeSafe(comptime .wrap("id")) orelse continue;
|
||||||
if (std.mem.eql(u8, element_id, id)) {
|
if (std.mem.eql(u8, element_id, id)) {
|
||||||
// we ignore this error to keep getElementById easy to call
|
// we ignore this error to keep getElementById easy to call
|
||||||
// if it really failed, then we're out of memory and nothing's
|
// if it really failed, then we're out of memory and nothing's
|
||||||
@@ -247,7 +253,7 @@ pub fn getElementsByClassName(self: *Document, class_name: []const u8, page: *Pa
|
|||||||
|
|
||||||
// Parse space-separated class names
|
// Parse space-separated class names
|
||||||
var class_names: std.ArrayList([]const u8) = .empty;
|
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| {
|
while (it.next()) |name| {
|
||||||
try class_names.append(arena, try page.dupeString(name));
|
try class_names.append(arena, try page.dupeString(name));
|
||||||
}
|
}
|
||||||
@@ -276,12 +282,16 @@ pub fn getDocumentElement(self: *Document) ?*Element {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn querySelector(self: *Document, input: []const u8, page: *Page) !?*Element {
|
pub fn getSelection(self: *Document) *Selection {
|
||||||
return Selector.querySelector(self.asNode(), input, page);
|
return &self._selection;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn querySelectorAll(self: *Document, input: []const u8, page: *Page) !*Selector.List {
|
pub fn querySelector(self: *Document, input: String, page: *Page) !?*Element {
|
||||||
return Selector.querySelectorAll(self.asNode(), input, page);
|
return Selector.querySelector(self.asNode(), input.str(), page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn querySelectorAll(self: *Document, input: String, page: *Page) !*Selector.List {
|
||||||
|
return Selector.querySelectorAll(self.asNode(), input.str(), page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getImplementation(_: *const Document) DOMImplementation {
|
pub fn getImplementation(_: *const Document) DOMImplementation {
|
||||||
@@ -642,7 +652,10 @@ pub fn write(self: *Document, text: []const []const u8, page: *Page) !void {
|
|||||||
page._parse_mode = .document_write;
|
page._parse_mode = .document_write;
|
||||||
defer page._parse_mode = previous_parse_mode;
|
defer page._parse_mode = previous_parse_mode;
|
||||||
|
|
||||||
var parser = Parser.init(page.call_arena, fragment_node, page);
|
const arena = try page.getArena(.{ .debug = "Document.write" });
|
||||||
|
defer page.releaseArena(arena);
|
||||||
|
|
||||||
|
var parser = Parser.init(arena, fragment_node, page);
|
||||||
parser.parseFragment(html);
|
parser.parseFragment(html);
|
||||||
|
|
||||||
// Extract children from wrapper HTML element (html5ever wraps fragments)
|
// Extract children from wrapper HTML element (html5ever wraps fragments)
|
||||||
@@ -655,7 +668,7 @@ pub fn write(self: *Document, text: []const []const u8, page: *Page) !void {
|
|||||||
|
|
||||||
var it = if (first.is(Element.Html.Html) == null) fragment_node.childrenIterator() else first.childrenIterator();
|
var it = if (first.is(Element.Html.Html) == null) fragment_node.childrenIterator() else first.childrenIterator();
|
||||||
while (it.next()) |child| {
|
while (it.next()) |child| {
|
||||||
try children_to_insert.append(page.call_arena, child);
|
try children_to_insert.append(arena, child);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (children_to_insert.items.len == 0) {
|
if (children_to_insert.items.len == 0) {
|
||||||
@@ -770,7 +783,7 @@ pub fn getAdoptedStyleSheets(self: *Document, page: *Page) !js.Object.Global {
|
|||||||
if (self._adopted_style_sheets) |ass| {
|
if (self._adopted_style_sheets) |ass| {
|
||||||
return ass;
|
return ass;
|
||||||
}
|
}
|
||||||
const js_arr = page.js.newArray(0);
|
const js_arr = page.js.local.?.newArray(0);
|
||||||
const js_obj = js_arr.toObject();
|
const js_obj = js_arr.toObject();
|
||||||
self._adopted_style_sheets = try js_obj.persist();
|
self._adopted_style_sheets = try js_obj.persist();
|
||||||
return self._adopted_style_sheets.?;
|
return self._adopted_style_sheets.?;
|
||||||
@@ -785,6 +798,17 @@ pub fn setAdoptedStyleSheets(self: *Document, sheets: js.Object) !void {
|
|||||||
self._adopted_style_sheets = try sheets.persist();
|
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:
|
// Validates that nodes can be inserted into a Document, respecting Document constraints:
|
||||||
// - At most one Element child
|
// - At most one Element child
|
||||||
// - At most one DocumentType child
|
// - At most one DocumentType child
|
||||||
@@ -920,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 URL = bridge.accessor(Document.getURL, null, .{});
|
||||||
pub const documentURI = bridge.accessor(Document.getURL, null, .{});
|
pub const documentURI = bridge.accessor(Document.getURL, null, .{});
|
||||||
pub const documentElement = bridge.accessor(Document.getDocumentElement, null, .{});
|
pub const documentElement = bridge.accessor(Document.getDocumentElement, null, .{});
|
||||||
@@ -929,11 +954,6 @@ pub const JsApi = struct {
|
|||||||
pub const activeElement = bridge.accessor(Document.getActiveElement, null, .{});
|
pub const activeElement = bridge.accessor(Document.getActiveElement, null, .{});
|
||||||
pub const styleSheets = bridge.accessor(Document.getStyleSheets, null, .{});
|
pub const styleSheets = bridge.accessor(Document.getStyleSheets, null, .{});
|
||||||
pub const contentType = bridge.accessor(Document.getContentType, 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 domain = bridge.accessor(Document.getDomain, null, .{});
|
||||||
pub const createElement = bridge.function(Document.createElement, .{ .dom_exception = true });
|
pub const createElement = bridge.function(Document.createElement, .{ .dom_exception = true });
|
||||||
pub const createElementNS = bridge.function(Document.createElementNS, .{ .dom_exception = true });
|
pub const createElementNS = bridge.function(Document.createElementNS, .{ .dom_exception = true });
|
||||||
@@ -962,6 +982,7 @@ pub const JsApi = struct {
|
|||||||
pub const querySelector = bridge.function(Document.querySelector, .{ .dom_exception = true });
|
pub const querySelector = bridge.function(Document.querySelector, .{ .dom_exception = true });
|
||||||
pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true });
|
pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true });
|
||||||
pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{});
|
pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{});
|
||||||
|
pub const getSelection = bridge.function(Document.getSelection, .{});
|
||||||
pub const getElementsByClassName = bridge.function(Document.getElementsByClassName, .{});
|
pub const getElementsByClassName = bridge.function(Document.getElementsByClassName, .{});
|
||||||
pub const getElementsByName = bridge.function(Document.getElementsByName, .{});
|
pub const getElementsByName = bridge.function(Document.getElementsByName, .{});
|
||||||
pub const adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true });
|
pub const adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true });
|
||||||
@@ -979,13 +1000,21 @@ pub const JsApi = struct {
|
|||||||
pub const lastElementChild = bridge.accessor(Document.getLastElementChild, null, .{});
|
pub const lastElementChild = bridge.accessor(Document.getLastElementChild, null, .{});
|
||||||
pub const childElementCount = bridge.accessor(Document.getChildElementCount, null, .{});
|
pub const childElementCount = bridge.accessor(Document.getChildElementCount, null, .{});
|
||||||
pub const adoptedStyleSheets = bridge.accessor(Document.getAdoptedStyleSheets, Document.setAdoptedStyleSheets, .{});
|
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 {
|
pub const defaultView = bridge.accessor(struct {
|
||||||
fn defaultView(_: *const Document, page: *Page) *@import("Window.zig") {
|
fn defaultView(_: *const Document, page: *Page) *@import("Window.zig") {
|
||||||
return page.window;
|
return page.window;
|
||||||
}
|
}
|
||||||
}.defaultView, null, .{ .cache = "defaultView" });
|
}.defaultView, null, .{});
|
||||||
pub const hasFocus = bridge.function(Document.hasFocus, .{});
|
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");
|
const testing = @import("../../testing.zig");
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ pub fn getElementById(self: *DocumentFragment, id: []const u8) ?*Element {
|
|||||||
|
|
||||||
var tw = @import("TreeWalker.zig").Full.Elements.init(self.asNode(), .{});
|
var tw = @import("TreeWalker.zig").Full.Elements.init(self.asNode(), .{});
|
||||||
while (tw.next()) |el| {
|
while (tw.next()) |el| {
|
||||||
if (el.getAttributeSafe("id")) |element_id| {
|
if (el.getAttributeSafe(comptime .wrap("id"))) |element_id| {
|
||||||
if (std.mem.eql(u8, element_id, id)) {
|
if (std.mem.eql(u8, element_id, id)) {
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ pub fn getTagNameLower(self: *const Element) []const u8 {
|
|||||||
.option => "option",
|
.option => "option",
|
||||||
.output => "output",
|
.output => "output",
|
||||||
.p => "p",
|
.p => "p",
|
||||||
|
.picture => "picture",
|
||||||
.param => "param",
|
.param => "param",
|
||||||
.pre => "pre",
|
.pre => "pre",
|
||||||
.progress => "progress",
|
.progress => "progress",
|
||||||
@@ -311,6 +312,7 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {
|
|||||||
.option => "OPTION",
|
.option => "OPTION",
|
||||||
.output => "OUTPUT",
|
.output => "OUTPUT",
|
||||||
.p => "P",
|
.p => "P",
|
||||||
|
.picture => "PICTURE",
|
||||||
.param => "PARAM",
|
.param => "PARAM",
|
||||||
.pre => "PRE",
|
.pre => "PRE",
|
||||||
.progress => "PROGRESS",
|
.progress => "PROGRESS",
|
||||||
@@ -427,35 +429,35 @@ pub fn setInnerHTML(self: *Element, html: []const u8, page: *Page) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getId(self: *const Element) []const u8 {
|
pub fn getId(self: *const Element) []const u8 {
|
||||||
return self.getAttributeSafe("id") orelse "";
|
return self.getAttributeSafe(comptime .wrap("id")) orelse "";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setId(self: *Element, value: []const u8, page: *Page) !void {
|
pub fn setId(self: *Element, value: []const u8, page: *Page) !void {
|
||||||
return self.setAttributeSafe("id", value, page);
|
return self.setAttributeSafe(comptime .wrap("id"), .wrap(value), page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getSlot(self: *const Element) []const u8 {
|
pub fn getSlot(self: *const Element) []const u8 {
|
||||||
return self.getAttributeSafe("slot") orelse "";
|
return self.getAttributeSafe(comptime .wrap("slot")) orelse "";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setSlot(self: *Element, value: []const u8, page: *Page) !void {
|
pub fn setSlot(self: *Element, value: []const u8, page: *Page) !void {
|
||||||
return self.setAttributeSafe("slot", value, page);
|
return self.setAttributeSafe(comptime .wrap("slot"), .wrap(value), page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getDir(self: *const Element) []const u8 {
|
pub fn getDir(self: *const Element) []const u8 {
|
||||||
return self.getAttributeSafe("dir") orelse "";
|
return self.getAttributeSafe(comptime .wrap("dir")) orelse "";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setDir(self: *Element, value: []const u8, page: *Page) !void {
|
pub fn setDir(self: *Element, value: []const u8, page: *Page) !void {
|
||||||
return self.setAttributeSafe("dir", value, page);
|
return self.setAttributeSafe(comptime .wrap("dir"), .wrap(value), page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getClassName(self: *const Element) []const u8 {
|
pub fn getClassName(self: *const Element) []const u8 {
|
||||||
return self.getAttributeSafe("class") orelse "";
|
return self.getAttributeSafe(comptime .wrap("class")) orelse "";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setClassName(self: *Element, value: []const u8, page: *Page) !void {
|
pub fn setClassName(self: *Element, value: []const u8, page: *Page) !void {
|
||||||
return self.setAttributeSafe("class", value, page);
|
return self.setAttributeSafe(comptime .wrap("class"), .wrap(value), page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn attributeIterator(self: *Element) Attribute.InnerIterator {
|
pub fn attributeIterator(self: *Element) Attribute.InnerIterator {
|
||||||
@@ -463,7 +465,7 @@ pub fn attributeIterator(self: *Element) Attribute.InnerIterator {
|
|||||||
return attributes.iterator();
|
return attributes.iterator();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getAttribute(self: *const Element, name: []const u8, page: *Page) !?[]const u8 {
|
pub fn getAttribute(self: *const Element, name: String, page: *Page) !?String {
|
||||||
const attributes = self._attributes orelse return null;
|
const attributes = self._attributes orelse return null;
|
||||||
return attributes.get(name, page);
|
return attributes.get(name, page);
|
||||||
}
|
}
|
||||||
@@ -472,9 +474,9 @@ pub fn getAttribute(self: *const Element, name: []const u8, page: *Page) !?[]con
|
|||||||
pub fn getAttributeNS(
|
pub fn getAttributeNS(
|
||||||
self: *const Element,
|
self: *const Element,
|
||||||
maybe_namespace: ?[]const u8,
|
maybe_namespace: ?[]const u8,
|
||||||
local_name: []const u8,
|
local_name: String,
|
||||||
page: *Page,
|
page: *Page,
|
||||||
) !?[]const u8 {
|
) !?String {
|
||||||
if (maybe_namespace) |namespace| {
|
if (maybe_namespace) |namespace| {
|
||||||
if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
|
if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
|
||||||
log.warn(.not_implemented, "Element.getAttributeNS", .{ .namespace = namespace });
|
log.warn(.not_implemented, "Element.getAttributeNS", .{ .namespace = namespace });
|
||||||
@@ -484,18 +486,18 @@ pub fn getAttributeNS(
|
|||||||
return self.getAttribute(local_name, page);
|
return self.getAttribute(local_name, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getAttributeSafe(self: *const Element, name: []const u8) ?[]const u8 {
|
pub fn getAttributeSafe(self: *const Element, name: String) ?[]const u8 {
|
||||||
const attributes = self._attributes orelse return null;
|
const attributes = self._attributes orelse return null;
|
||||||
return attributes.getSafe(name);
|
return attributes.getSafe(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hasAttribute(self: *const Element, name: []const u8, page: *Page) !bool {
|
pub fn hasAttribute(self: *const Element, name: String, page: *Page) !bool {
|
||||||
const attributes = self._attributes orelse return false;
|
const attributes = self._attributes orelse return false;
|
||||||
const value = try attributes.get(name, page);
|
const value = try attributes.get(name, page);
|
||||||
return value != null;
|
return value != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hasAttributeSafe(self: *const Element, name: []const u8) bool {
|
pub fn hasAttributeSafe(self: *const Element, name: String) bool {
|
||||||
const attributes = self._attributes orelse return false;
|
const attributes = self._attributes orelse return false;
|
||||||
return attributes.hasSafe(name);
|
return attributes.hasSafe(name);
|
||||||
}
|
}
|
||||||
@@ -505,12 +507,12 @@ pub fn hasAttributes(self: *const Element) bool {
|
|||||||
return attributes.isEmpty() == false;
|
return attributes.isEmpty() == false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getAttributeNode(self: *Element, name: []const u8, page: *Page) !?*Attribute {
|
pub fn getAttributeNode(self: *Element, name: String, page: *Page) !?*Attribute {
|
||||||
const attributes = self._attributes orelse return null;
|
const attributes = self._attributes orelse return null;
|
||||||
return attributes.getAttribute(name, self, page);
|
return attributes.getAttribute(name, self, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setAttribute(self: *Element, name: []const u8, value: []const u8, page: *Page) !void {
|
pub fn setAttribute(self: *Element, name: String, value: String, page: *Page) !void {
|
||||||
try Attribute.validateAttributeName(name);
|
try Attribute.validateAttributeName(name);
|
||||||
const attributes = try self.getOrCreateAttributeList(page);
|
const attributes = try self.getOrCreateAttributeList(page);
|
||||||
_ = try attributes.put(name, value, self, page);
|
_ = try attributes.put(name, value, self, page);
|
||||||
@@ -520,7 +522,7 @@ pub fn setAttributeNS(
|
|||||||
self: *Element,
|
self: *Element,
|
||||||
maybe_namespace: ?[]const u8,
|
maybe_namespace: ?[]const u8,
|
||||||
qualified_name: []const u8,
|
qualified_name: []const u8,
|
||||||
value: []const u8,
|
value: String,
|
||||||
page: *Page,
|
page: *Page,
|
||||||
) !void {
|
) !void {
|
||||||
if (maybe_namespace) |namespace| {
|
if (maybe_namespace) |namespace| {
|
||||||
@@ -533,10 +535,10 @@ pub fn setAttributeNS(
|
|||||||
qualified_name[idx + 1 ..]
|
qualified_name[idx + 1 ..]
|
||||||
else
|
else
|
||||||
qualified_name;
|
qualified_name;
|
||||||
return self.setAttribute(local_name, value, page);
|
return self.setAttribute(.wrap(local_name), value, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setAttributeSafe(self: *Element, name: []const u8, value: []const u8, page: *Page) !void {
|
pub fn setAttributeSafe(self: *Element, name: String, value: String, page: *Page) !void {
|
||||||
const attributes = try self.getOrCreateAttributeList(page);
|
const attributes = try self.getOrCreateAttributeList(page);
|
||||||
_ = try attributes.putSafe(name, value, self, page);
|
_ = try attributes.putSafe(name, value, self, page);
|
||||||
}
|
}
|
||||||
@@ -607,19 +609,19 @@ pub fn setAttributeNode(self: *Element, attr: *Attribute, page: *Page) !?*Attrib
|
|||||||
return attributes.putAttribute(attr, self, page);
|
return attributes.putAttribute(attr, self, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn removeAttribute(self: *Element, name: []const u8, page: *Page) !void {
|
pub fn removeAttribute(self: *Element, name: String, page: *Page) !void {
|
||||||
const attributes = self._attributes orelse return;
|
const attributes = self._attributes orelse return;
|
||||||
return attributes.delete(name, self, page);
|
return attributes.delete(name, self, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggleAttribute(self: *Element, name: []const u8, force: ?bool, page: *Page) !bool {
|
pub fn toggleAttribute(self: *Element, name: String, force: ?bool, page: *Page) !bool {
|
||||||
try Attribute.validateAttributeName(name);
|
try Attribute.validateAttributeName(name);
|
||||||
const has = try self.hasAttribute(name, page);
|
const has = try self.hasAttribute(name, page);
|
||||||
|
|
||||||
const should_add = force orelse !has;
|
const should_add = force orelse !has;
|
||||||
|
|
||||||
if (should_add and !has) {
|
if (should_add and !has) {
|
||||||
try self.setAttribute(name, "", page);
|
try self.setAttribute(name, String.empty, page);
|
||||||
return true;
|
return true;
|
||||||
} else if (!should_add and has) {
|
} else if (!should_add and has) {
|
||||||
try self.removeAttribute(name, page);
|
try self.removeAttribute(name, page);
|
||||||
@@ -666,7 +668,7 @@ pub fn getClassList(self: *Element, page: *Page) !*collections.DOMTokenList {
|
|||||||
if (!gop.found_existing) {
|
if (!gop.found_existing) {
|
||||||
gop.value_ptr.* = try page._factory.create(collections.DOMTokenList{
|
gop.value_ptr.* = try page._factory.create(collections.DOMTokenList{
|
||||||
._element = self,
|
._element = self,
|
||||||
._attribute_name = "class",
|
._attribute_name = comptime .wrap("class"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return gop.value_ptr.*;
|
return gop.value_ptr.*;
|
||||||
@@ -677,7 +679,7 @@ pub fn getRelList(self: *Element, page: *Page) !*collections.DOMTokenList {
|
|||||||
if (!gop.found_existing) {
|
if (!gop.found_existing) {
|
||||||
gop.value_ptr.* = try page._factory.create(collections.DOMTokenList{
|
gop.value_ptr.* = try page._factory.create(collections.DOMTokenList{
|
||||||
._element = self,
|
._element = self,
|
||||||
._attribute_name = "rel",
|
._attribute_name = comptime .wrap("rel"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return gop.value_ptr.*;
|
return gop.value_ptr.*;
|
||||||
@@ -714,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 {
|
pub fn remove(self: *Element, page: *Page) void {
|
||||||
page.domChanged();
|
page.domChanged();
|
||||||
const node = self.asNode();
|
const node = self.asNode();
|
||||||
@@ -729,7 +769,8 @@ pub fn focus(self: *Element, page: *Page) !void {
|
|||||||
return;
|
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);
|
try page._event_manager.dispatch(old.asEventTarget(), blur_event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -737,7 +778,8 @@ pub fn focus(self: *Element, page: *Page) !void {
|
|||||||
page.document._active_element = self;
|
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);
|
try page._event_manager.dispatch(self.asEventTarget(), focus_event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -747,7 +789,8 @@ pub fn blur(self: *Element, page: *Page) !void {
|
|||||||
page.document._active_element = null;
|
page.document._active_element = null;
|
||||||
|
|
||||||
const Event = @import("Event.zig");
|
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);
|
try page._event_manager.dispatch(self.asEventTarget(), blur_event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -919,10 +962,10 @@ fn getElementDimensions(self: *Element, page: *Page) !struct { width: f64, heigh
|
|||||||
if (width == 5.0) width = 1920.0;
|
if (width == 5.0) width = 1920.0;
|
||||||
if (height == 5.0) height = 100_000_000.0;
|
if (height == 5.0) height = 100_000_000.0;
|
||||||
} else if (tag == .img or tag == .iframe) {
|
} else if (tag == .img or tag == .iframe) {
|
||||||
if (self.getAttributeSafe("width")) |w| {
|
if (self.getAttributeSafe(comptime .wrap("width"))) |w| {
|
||||||
width = std.fmt.parseFloat(f64, w) catch width;
|
width = std.fmt.parseFloat(f64, w) catch width;
|
||||||
}
|
}
|
||||||
if (self.getAttributeSafe("height")) |h| {
|
if (self.getAttributeSafe(comptime .wrap("height"))) |h| {
|
||||||
height = std.fmt.parseFloat(f64, h) catch height;
|
height = std.fmt.parseFloat(f64, h) catch height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1097,7 +1140,7 @@ pub fn getElementsByClassName(self: *Element, class_name: []const u8, page: *Pag
|
|||||||
|
|
||||||
// Parse space-separated class names
|
// Parse space-separated class names
|
||||||
var class_names: std.ArrayList([]const u8) = .empty;
|
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| {
|
while (it.next()) |name| {
|
||||||
try class_names.append(arena, try page.dupeString(name));
|
try class_names.append(arena, try page.dupeString(name));
|
||||||
}
|
}
|
||||||
@@ -1132,6 +1175,14 @@ pub fn scrollIntoViewIfNeeded(_: *const Element, center_if_needed: ?bool) void {
|
|||||||
_ = center_if_needed;
|
_ = 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 {
|
pub fn format(self: *Element, writer: *std.Io.Writer) !void {
|
||||||
try writer.writeByte('<');
|
try writer.writeByte('<');
|
||||||
try writer.writeAll(self.getTagNameDump());
|
try writer.writeAll(self.getTagNameDump());
|
||||||
@@ -1175,26 +1226,27 @@ pub fn getTag(self: *const Element) Tag {
|
|||||||
.data => .data,
|
.data => .data,
|
||||||
.datalist => .datalist,
|
.datalist => .datalist,
|
||||||
.dialog => .dialog,
|
.dialog => .dialog,
|
||||||
.directory => .unknown,
|
.directory => .directory,
|
||||||
.iframe => .iframe,
|
.iframe => .iframe,
|
||||||
.img => .img,
|
.img => .img,
|
||||||
.br => .br,
|
.br => .br,
|
||||||
.button => .button,
|
.button => .button,
|
||||||
.canvas => .canvas,
|
.canvas => .canvas,
|
||||||
.fieldset => .fieldset,
|
.fieldset => .fieldset,
|
||||||
.font => .unknown,
|
.font => .font,
|
||||||
.heading => |h| h._tag,
|
.heading => |h| h._tag,
|
||||||
.label => .unknown,
|
.label => .label,
|
||||||
.legend => .unknown,
|
.legend => .legend,
|
||||||
.li => .li,
|
.li => .li,
|
||||||
.map => .unknown,
|
.map => .map,
|
||||||
.ul => .ul,
|
.ul => .ul,
|
||||||
.ol => .ol,
|
.ol => .ol,
|
||||||
.object => .unknown,
|
.object => .object,
|
||||||
.optgroup => .optgroup,
|
.optgroup => .optgroup,
|
||||||
.output => .unknown,
|
.output => .output,
|
||||||
.param => .unknown,
|
.picture => .picture,
|
||||||
.pre => .unknown,
|
.param => .param,
|
||||||
|
.pre => .pre,
|
||||||
.generic => |g| g._tag,
|
.generic => |g| g._tag,
|
||||||
.media => |m| switch (m._type) {
|
.media => |m| switch (m._type) {
|
||||||
.audio => .audio,
|
.audio => .audio,
|
||||||
@@ -1208,7 +1260,7 @@ pub fn getTag(self: *const Element) Tag {
|
|||||||
.script => .script,
|
.script => .script,
|
||||||
.select => .select,
|
.select => .select,
|
||||||
.slot => .slot,
|
.slot => .slot,
|
||||||
.source => .unknown,
|
.source => .source,
|
||||||
.span => .span,
|
.span => .span,
|
||||||
.option => .option,
|
.option => .option,
|
||||||
.table => .table,
|
.table => .table,
|
||||||
@@ -1220,7 +1272,7 @@ pub fn getTag(self: *const Element) Tag {
|
|||||||
.template => .template,
|
.template => .template,
|
||||||
.textarea => .textarea,
|
.textarea => .textarea,
|
||||||
.time => .time,
|
.time => .time,
|
||||||
.track => .unknown,
|
.track => .track,
|
||||||
.input => .input,
|
.input => .input,
|
||||||
.link => .link,
|
.link => .link,
|
||||||
.meta => .meta,
|
.meta => .meta,
|
||||||
@@ -1267,6 +1319,7 @@ pub const Tag = enum {
|
|||||||
dfn,
|
dfn,
|
||||||
dialog,
|
dialog,
|
||||||
div,
|
div,
|
||||||
|
directory,
|
||||||
dl,
|
dl,
|
||||||
dt,
|
dt,
|
||||||
embed,
|
embed,
|
||||||
@@ -1275,6 +1328,7 @@ pub const Tag = enum {
|
|||||||
fieldset,
|
fieldset,
|
||||||
figure,
|
figure,
|
||||||
form,
|
form,
|
||||||
|
font,
|
||||||
footer,
|
footer,
|
||||||
g,
|
g,
|
||||||
h1,
|
h1,
|
||||||
@@ -1294,10 +1348,13 @@ pub const Tag = enum {
|
|||||||
img,
|
img,
|
||||||
input,
|
input,
|
||||||
ins,
|
ins,
|
||||||
|
label,
|
||||||
|
legend,
|
||||||
li,
|
li,
|
||||||
line,
|
line,
|
||||||
link,
|
link,
|
||||||
main,
|
main,
|
||||||
|
map,
|
||||||
marquee,
|
marquee,
|
||||||
media,
|
media,
|
||||||
menu,
|
menu,
|
||||||
@@ -1313,8 +1370,10 @@ pub const Tag = enum {
|
|||||||
p,
|
p,
|
||||||
path,
|
path,
|
||||||
param,
|
param,
|
||||||
|
picture,
|
||||||
polygon,
|
polygon,
|
||||||
polyline,
|
polyline,
|
||||||
|
pre,
|
||||||
progress,
|
progress,
|
||||||
quote,
|
quote,
|
||||||
rect,
|
rect,
|
||||||
@@ -1405,6 +1464,16 @@ pub const JsApi = struct {
|
|||||||
return null;
|
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 localName = bridge.accessor(Element.getLocalName, null, .{});
|
||||||
pub const id = bridge.accessor(Element.getId, Element.setId, .{});
|
pub const id = bridge.accessor(Element.getId, Element.setId, .{});
|
||||||
pub const slot = bridge.accessor(Element.getSlot, Element.setSlot, .{});
|
pub const slot = bridge.accessor(Element.getSlot, Element.setSlot, .{});
|
||||||
@@ -1419,8 +1488,6 @@ pub const JsApi = struct {
|
|||||||
pub const getAttribute = bridge.function(Element.getAttribute, .{});
|
pub const getAttribute = bridge.function(Element.getAttribute, .{});
|
||||||
pub const getAttributeNS = bridge.function(Element.getAttributeNS, .{});
|
pub const getAttributeNS = bridge.function(Element.getAttributeNS, .{});
|
||||||
pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{});
|
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 setAttributeNode = bridge.function(Element.setAttributeNode, .{});
|
||||||
pub const removeAttribute = bridge.function(Element.removeAttribute, .{});
|
pub const removeAttribute = bridge.function(Element.removeAttribute, .{});
|
||||||
pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true });
|
pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true });
|
||||||
@@ -1440,6 +1507,7 @@ pub const JsApi = struct {
|
|||||||
return self.attachShadow(init.mode, page);
|
return self.attachShadow(init.mode, page);
|
||||||
}
|
}
|
||||||
pub const replaceChildren = bridge.function(Element.replaceChildren, .{});
|
pub const replaceChildren = bridge.function(Element.replaceChildren, .{});
|
||||||
|
pub const replaceWith = bridge.function(Element.replaceWith, .{});
|
||||||
pub const remove = bridge.function(Element.remove, .{});
|
pub const remove = bridge.function(Element.remove, .{});
|
||||||
pub const append = bridge.function(Element.append, .{});
|
pub const append = bridge.function(Element.append, .{});
|
||||||
pub const prepend = bridge.function(Element.prepend, .{});
|
pub const prepend = bridge.function(Element.prepend, .{});
|
||||||
@@ -1466,6 +1534,7 @@ pub const JsApi = struct {
|
|||||||
pub const children = bridge.accessor(Element.getChildren, null, .{});
|
pub const children = bridge.accessor(Element.getChildren, null, .{});
|
||||||
pub const focus = bridge.function(Element.focus, .{});
|
pub const focus = bridge.function(Element.focus, .{});
|
||||||
pub const blur = bridge.function(Element.blur, .{});
|
pub const blur = bridge.function(Element.blur, .{});
|
||||||
|
pub const scrollIntoView = bridge.function(Element.scrollIntoView, .{});
|
||||||
pub const scrollIntoViewIfNeeded = bridge.function(Element.scrollIntoViewIfNeeded, .{});
|
pub const scrollIntoViewIfNeeded = bridge.function(Element.scrollIntoViewIfNeeded, .{});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,14 @@ const EventTarget = @import("EventTarget.zig");
|
|||||||
const Node = @import("Node.zig");
|
const Node = @import("Node.zig");
|
||||||
const String = @import("../../string.zig").String;
|
const String = @import("../../string.zig").String;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
pub const Event = @This();
|
pub const Event = @This();
|
||||||
|
|
||||||
pub const _prototype_root = true;
|
pub const _prototype_root = true;
|
||||||
_type: Type,
|
_type: Type,
|
||||||
|
_page: *Page,
|
||||||
|
_arena: Allocator,
|
||||||
_bubbles: bool = false,
|
_bubbles: bool = false,
|
||||||
_cancelable: bool = false,
|
_cancelable: bool = false,
|
||||||
_composed: bool = false,
|
_composed: bool = false,
|
||||||
@@ -44,6 +47,12 @@ _time_stamp: u64,
|
|||||||
_needs_retargeting: bool = false,
|
_needs_retargeting: bool = false,
|
||||||
_isTrusted: 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) {
|
pub const EventPhase = enum(u8) {
|
||||||
none = 0,
|
none = 0,
|
||||||
capturing_phase = 1,
|
capturing_phase = 1,
|
||||||
@@ -62,6 +71,7 @@ pub const Type = union(enum) {
|
|||||||
page_transition_event: *@import("event/PageTransitionEvent.zig"),
|
page_transition_event: *@import("event/PageTransitionEvent.zig"),
|
||||||
pop_state_event: *@import("event/PopStateEvent.zig"),
|
pop_state_event: *@import("event/PopStateEvent.zig"),
|
||||||
ui_event: *@import("event/UIEvent.zig"),
|
ui_event: *@import("event/UIEvent.zig"),
|
||||||
|
promise_rejection_event: *@import("event/PromiseRejectionEvent.zig"),
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Options = struct {
|
pub const Options = struct {
|
||||||
@@ -70,31 +80,38 @@ pub const Options = struct {
|
|||||||
composed: bool = false,
|
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 {
|
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{};
|
const opts = opts_ orelse Options{};
|
||||||
|
|
||||||
// Round to 2ms for privacy (browsers do this)
|
// Round to 2ms for privacy (browsers do this)
|
||||||
const raw_timestamp = @import("../../datetime.zig").milliTimestamp(.monotonic);
|
const raw_timestamp = @import("../../datetime.zig").milliTimestamp(.monotonic);
|
||||||
const time_stamp = (raw_timestamp / 2) * 2;
|
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,
|
._type = .generic,
|
||||||
._bubbles = opts.bubbles,
|
._bubbles = opts.bubbles,
|
||||||
._time_stamp = time_stamp,
|
._time_stamp = time_stamp,
|
||||||
._cancelable = opts.cancelable,
|
._cancelable = opts.cancelable,
|
||||||
._composed = opts.composed,
|
._composed = opts.composed,
|
||||||
._type_string = try String.init(page.arena, typ, .{}),
|
._type_string = typ,
|
||||||
});
|
._isTrusted = trusted,
|
||||||
|
};
|
||||||
event._isTrusted = trusted;
|
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,18 +120,22 @@ pub fn initEvent(
|
|||||||
event_string: []const u8,
|
event_string: []const u8,
|
||||||
bubbles: ?bool,
|
bubbles: ?bool,
|
||||||
cancelable: ?bool,
|
cancelable: ?bool,
|
||||||
page: *Page,
|
|
||||||
) !void {
|
) !void {
|
||||||
if (self._event_phase != .none) {
|
if (self._event_phase != .none) {
|
||||||
return;
|
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._bubbles = bubbles orelse false;
|
||||||
self._cancelable = cancelable orelse false;
|
self._cancelable = cancelable orelse false;
|
||||||
self._stop_propagation = 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 {
|
pub fn as(self: *Event, comptime T: type) *T {
|
||||||
return self.is(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,
|
.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,
|
.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,
|
.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| {
|
.ui_event => |e| {
|
||||||
if (T == @import("event/UIEvent.zig")) {
|
if (T == @import("event/UIEvent.zig")) {
|
||||||
return e;
|
return e;
|
||||||
@@ -385,6 +407,8 @@ pub const JsApi = struct {
|
|||||||
|
|
||||||
pub const prototype_chain = bridge.prototypeChain();
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
pub var class_id: bridge.ClassId = undefined;
|
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, .{});
|
pub const constructor = bridge.constructor(Event.init, .{});
|
||||||
@@ -410,10 +434,10 @@ pub const JsApi = struct {
|
|||||||
pub const cancelBubble = bridge.accessor(Event.getCancelBubble, Event.setCancelBubble, .{});
|
pub const cancelBubble = bridge.accessor(Event.getCancelBubble, Event.setCancelBubble, .{});
|
||||||
|
|
||||||
// Event phase constants
|
// Event phase constants
|
||||||
pub const NONE = bridge.property(@intFromEnum(EventPhase.none));
|
pub const NONE = bridge.property(@intFromEnum(EventPhase.none), .{ .template = true });
|
||||||
pub const CAPTURING_PHASE = bridge.property(@intFromEnum(EventPhase.capturing_phase));
|
pub const CAPTURING_PHASE = bridge.property(@intFromEnum(EventPhase.capturing_phase), .{ .template = true });
|
||||||
pub const AT_TARGET = bridge.property(@intFromEnum(EventPhase.at_target));
|
pub const AT_TARGET = bridge.property(@intFromEnum(EventPhase.at_target), .{ .template = true });
|
||||||
pub const BUBBLING_PHASE = bridge.property(@intFromEnum(EventPhase.bubbling_phase));
|
pub const BUBBLING_PHASE = bridge.property(@intFromEnum(EventPhase.bubbling_phase), .{ .template = true });
|
||||||
};
|
};
|
||||||
|
|
||||||
// tested in event_target
|
// tested in event_target
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ pub const Type = union(enum) {
|
|||||||
navigation: *@import("navigation/NavigationEventTarget.zig"),
|
navigation: *@import("navigation/NavigationEventTarget.zig"),
|
||||||
screen: *@import("Screen.zig"),
|
screen: *@import("Screen.zig"),
|
||||||
screen_orientation: *@import("Screen.zig").Orientation,
|
screen_orientation: *@import("Screen.zig").Orientation,
|
||||||
|
visual_viewport: *@import("VisualViewport.zig"),
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(page: *Page) !*EventTarget {
|
pub fn init(page: *Page) !*EventTarget {
|
||||||
@@ -132,12 +133,13 @@ pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void {
|
|||||||
.navigation => writer.writeAll("<Navigation>"),
|
.navigation => writer.writeAll("<Navigation>"),
|
||||||
.screen => writer.writeAll("<Screen>"),
|
.screen => writer.writeAll("<Screen>"),
|
||||||
.screen_orientation => writer.writeAll("<ScreenOrientation>"),
|
.screen_orientation => writer.writeAll("<ScreenOrientation>"),
|
||||||
|
.visual_viewport => writer.writeAll("<VisualViewport>"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toString(self: *EventTarget) []const u8 {
|
pub fn toString(self: *EventTarget) []const u8 {
|
||||||
return switch (self._type) {
|
return switch (self._type) {
|
||||||
.node => |n| return n.className(),
|
.node => return "[object Node]",
|
||||||
.generic => return "[object EventTarget]",
|
.generic => return "[object EventTarget]",
|
||||||
.window => return "[object Window]",
|
.window => return "[object Window]",
|
||||||
.xhr => return "[object XMLHttpRequestEventTarget]",
|
.xhr => return "[object XMLHttpRequestEventTarget]",
|
||||||
@@ -148,6 +150,7 @@ pub fn toString(self: *EventTarget) []const u8 {
|
|||||||
.navigation => return "[object Navigation]",
|
.navigation => return "[object Navigation]",
|
||||||
.screen => return "[object Screen]",
|
.screen => return "[object Screen]",
|
||||||
.screen_orientation => return "[object ScreenOrientation]",
|
.screen_orientation => return "[object ScreenOrientation]",
|
||||||
|
.visual_viewport => return "[object VisualViewport]",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ pub const JsApi = struct {
|
|||||||
pub const applets = bridge.accessor(HTMLDocument.getApplets, null, .{});
|
pub const applets = bridge.accessor(HTMLDocument.getApplets, null, .{});
|
||||||
pub const plugins = bridge.accessor(HTMLDocument.getEmbeds, null, .{});
|
pub const plugins = bridge.accessor(HTMLDocument.getEmbeds, null, .{});
|
||||||
pub const currentScript = bridge.accessor(HTMLDocument.getCurrentScript, null, .{});
|
pub const currentScript = bridge.accessor(HTMLDocument.getCurrentScript, null, .{});
|
||||||
pub const location = bridge.accessor(HTMLDocument.getLocation, HTMLDocument.setLocation, .{ .cache = "location" });
|
pub const location = bridge.accessor(HTMLDocument.getLocation, HTMLDocument.setLocation, .{});
|
||||||
pub const all = bridge.accessor(HTMLDocument.getAll, null, .{});
|
pub const all = bridge.accessor(HTMLDocument.getAll, null, .{});
|
||||||
pub const cookie = bridge.accessor(HTMLDocument.getCookie, HTMLDocument.setCookie, .{});
|
pub const cookie = bridge.accessor(HTMLDocument.getCookie, HTMLDocument.setCookie, .{});
|
||||||
pub const doctype = bridge.accessor(HTMLDocument.getDocType, null, .{});
|
pub const doctype = bridge.accessor(HTMLDocument.getDocType, null, .{});
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ pub fn getLength(_: *const History, page: *Page) u32 {
|
|||||||
|
|
||||||
pub fn getState(_: *const History, page: *Page) !?js.Value {
|
pub fn getState(_: *const History, page: *Page) !?js.Value {
|
||||||
if (page._session.navigation.getCurrentEntry()._state.value) |state| {
|
if (page._session.navigation.getCurrentEntry()._state.value) |state| {
|
||||||
const value = try page.js.parseJSON(state);
|
const value = try page.js.local.?.parseJSON(state);
|
||||||
return value;
|
return value;
|
||||||
} else return null;
|
} else return null;
|
||||||
}
|
}
|
||||||
@@ -79,13 +79,13 @@ fn goInner(delta: i32, page: *Page) !void {
|
|||||||
|
|
||||||
if (entry._url) |url| {
|
if (entry._url) |url| {
|
||||||
if (try page.isSameOrigin(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);
|
||||||
|
|
||||||
const func = if (page.window._on_popstate) |*g| g.local() else null;
|
|
||||||
try page._event_manager.dispatchWithFunction(
|
try page._event_manager.dispatchWithFunction(
|
||||||
page.window.asEventTarget(),
|
page.window.asEventTarget(),
|
||||||
event.asEvent(),
|
event,
|
||||||
func,
|
page.js.toLocal(page.window._on_popstate),
|
||||||
.{ .context = "Pop State" },
|
.{ .context = "Pop State" },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,6 @@ pub fn init() IdleDeadline {
|
|||||||
return .{};
|
return .{};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getDidTimeout(_: *const IdleDeadline) bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn timeRemaining(_: *const IdleDeadline) f64 {
|
pub fn timeRemaining(_: *const IdleDeadline) f64 {
|
||||||
// Return a fixed 50ms.
|
// Return a fixed 50ms.
|
||||||
// This allows idle callbacks to perform work without complex
|
// 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 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>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -19,6 +19,10 @@ const std = @import("std");
|
|||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
const Element = @import("Element.zig");
|
const Element = @import("Element.zig");
|
||||||
const DOMRect = @import("DOMRect.zig");
|
const DOMRect = @import("DOMRect.zig");
|
||||||
@@ -32,7 +36,9 @@ pub fn registerTypes() []const type {
|
|||||||
|
|
||||||
const IntersectionObserver = @This();
|
const IntersectionObserver = @This();
|
||||||
|
|
||||||
_callback: js.Function.Global,
|
_page: *Page,
|
||||||
|
_arena: Allocator,
|
||||||
|
_callback: js.Function.Temp,
|
||||||
_observing: std.ArrayList(*Element) = .{},
|
_observing: std.ArrayList(*Element) = .{},
|
||||||
_root: ?*Element = null,
|
_root: ?*Element = null,
|
||||||
_root_margin: []const u8 = "0px",
|
_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 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) {
|
const threshold = switch (opts.threshold) {
|
||||||
.scalar => |s| blk: {
|
.scalar => |s| blk: {
|
||||||
const arr = try page.arena.alloc(f64, 1);
|
const arr = try arena.alloc(f64, 1);
|
||||||
arr[0] = s;
|
arr[0] = s;
|
||||||
break :blk arr;
|
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,
|
._callback = callback,
|
||||||
._root = opts.root,
|
._root = opts.root,
|
||||||
._root_margin = root_margin,
|
._root_margin = root_margin,
|
||||||
._threshold = threshold,
|
._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 {
|
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
|
// Register with page if this is our first observation
|
||||||
if (self._observing.items.len == 0) {
|
if (self._observing.items.len == 0) {
|
||||||
|
page.js.strongRef(self);
|
||||||
try page.registerIntersectionObserver(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
|
// Don't initialize previous state yet - let checkIntersection do it
|
||||||
// This ensures we get an entry on first observation
|
// 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| {
|
for (self._observing.items, 0..) |elem, i| {
|
||||||
if (elem == target) {
|
if (elem == target) {
|
||||||
_ = self._observing.swapRemove(i);
|
_ = self._observing.swapRemove(i);
|
||||||
@@ -115,21 +139,31 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element) void {
|
|||||||
var j: usize = 0;
|
var j: usize = 0;
|
||||||
while (j < self._pending_entries.items.len) {
|
while (j < self._pending_entries.items.len) {
|
||||||
if (self._pending_entries.items[j]._target == target) {
|
if (self._pending_entries.items[j]._target == target) {
|
||||||
_ = self._pending_entries.swapRemove(j);
|
const entry = self._pending_entries.swapRemove(j);
|
||||||
|
entry.deinit(false);
|
||||||
} else {
|
} else {
|
||||||
j += 1;
|
j += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self._observing.items.len == 0) {
|
||||||
|
page.js.safeWeakRef(self);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
|
pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
|
||||||
page.unregisterIntersectionObserver(self);
|
page.unregisterIntersectionObserver(self);
|
||||||
self._observing.clearRetainingCapacity();
|
self._observing.clearRetainingCapacity();
|
||||||
self._previous_states.clearRetainingCapacity();
|
self._previous_states.clearRetainingCapacity();
|
||||||
|
|
||||||
|
for (self._pending_entries.items) |entry| {
|
||||||
|
entry.deinit(false);
|
||||||
|
}
|
||||||
self._pending_entries.clearRetainingCapacity();
|
self._pending_entries.clearRetainingCapacity();
|
||||||
|
page.js.safeWeakRef(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {
|
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);
|
(was_intersecting_opt != null and was_intersecting_opt.? != is_now_intersecting);
|
||||||
|
|
||||||
if (should_report) {
|
if (should_report) {
|
||||||
const entry = try page.arena.create(IntersectionObserverEntry);
|
const arena = try page.getArena(.{ .debug = "IntersectionObserverEntry" });
|
||||||
|
const entry = try arena.create(IntersectionObserverEntry);
|
||||||
entry.* = .{
|
entry.* = .{
|
||||||
|
._page = page,
|
||||||
|
._arena = arena,
|
||||||
._target = target,
|
._target = target,
|
||||||
._time = 0.0, // TODO: Get actual timestamp
|
._time = 0.0, // TODO: Get actual timestamp
|
||||||
._bounding_client_rect = data.bounding_client_rect,
|
._bounding_client_rect = data.bounding_client_rect,
|
||||||
@@ -217,12 +254,12 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page)
|
|||||||
._is_intersecting = is_now_intersecting,
|
._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
|
// Always update the previous state, even if we didn't report
|
||||||
// This ensures we can detect state changes on subsequent checks
|
// 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 {
|
pub fn checkIntersections(self: *IntersectionObserver, page: *Page) !void {
|
||||||
@@ -246,21 +283,32 @@ pub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void {
|
|||||||
|
|
||||||
const entries = try self.takeRecords(page);
|
const entries = try self.takeRecords(page);
|
||||||
var caught: js.TryCatch.Caught = undefined;
|
var caught: js.TryCatch.Caught = undefined;
|
||||||
self._callback.local().tryCall(void, .{ entries, self }, &caught) catch |err| {
|
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
|
ls.toLocal(self._callback).tryCall(void, .{ entries, self }, &caught) catch |err| {
|
||||||
log.err(.page, "IntsctObserver.deliverEntries", .{ .err = err, .caught = caught });
|
log.err(.page, "IntsctObserver.deliverEntries", .{ .err = err, .caught = caught });
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const IntersectionObserverEntry = struct {
|
pub const IntersectionObserverEntry = struct {
|
||||||
_target: *Element,
|
_page: *Page,
|
||||||
|
_arena: Allocator,
|
||||||
_time: f64,
|
_time: f64,
|
||||||
|
_target: *Element,
|
||||||
_bounding_client_rect: *DOMRect,
|
_bounding_client_rect: *DOMRect,
|
||||||
_intersection_rect: *DOMRect,
|
_intersection_rect: *DOMRect,
|
||||||
_root_bounds: *DOMRect,
|
_root_bounds: *DOMRect,
|
||||||
_intersection_ratio: f64,
|
_intersection_ratio: f64,
|
||||||
_is_intersecting: bool,
|
_is_intersecting: bool,
|
||||||
|
|
||||||
|
pub fn deinit(self: *const IntersectionObserverEntry, _: bool) void {
|
||||||
|
self._page.releaseArena(self._arena);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getTarget(self: *const IntersectionObserverEntry) *Element {
|
pub fn getTarget(self: *const IntersectionObserverEntry) *Element {
|
||||||
return self._target;
|
return self._target;
|
||||||
}
|
}
|
||||||
@@ -296,6 +344,8 @@ pub const IntersectionObserverEntry = struct {
|
|||||||
pub const name = "IntersectionObserverEntry";
|
pub const name = "IntersectionObserverEntry";
|
||||||
pub const prototype_chain = bridge.prototypeChain();
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
pub var class_id: bridge.ClassId = undefined;
|
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, .{});
|
pub const target = bridge.accessor(IntersectionObserverEntry.getTarget, null, .{});
|
||||||
@@ -315,6 +365,8 @@ pub const JsApi = struct {
|
|||||||
pub const name = "IntersectionObserver";
|
pub const name = "IntersectionObserver";
|
||||||
pub const prototype_chain = bridge.prototypeChain();
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
pub var class_id: bridge.ClassId = undefined;
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
pub const weak = true;
|
||||||
|
pub const finalizer = bridge.finalizer(IntersectionObserver.deinit);
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const constructor = bridge.constructor(init, .{});
|
pub const constructor = bridge.constructor(init, .{});
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ pub const Entry = struct {
|
|||||||
|
|
||||||
pub const KeyValueList = @This();
|
pub const KeyValueList = @This();
|
||||||
|
|
||||||
_entries: std.ArrayListUnmanaged(Entry) = .empty,
|
_entries: std.ArrayList(Entry) = .empty,
|
||||||
|
|
||||||
pub const empty: KeyValueList = .{
|
pub const empty: KeyValueList = .{
|
||||||
._entries = .empty,
|
._entries = .empty,
|
||||||
@@ -68,12 +68,11 @@ pub fn fromJsObject(arena: Allocator, js_obj: js.Object, comptime normalizer: ?N
|
|||||||
|
|
||||||
while (try it.next()) |name| {
|
while (try it.next()) |name| {
|
||||||
const js_value = try js_obj.get(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;
|
const normalized = if (comptime normalizer) |n| n(name, page) else name;
|
||||||
|
|
||||||
list._entries.appendAssumeCapacity(.{
|
list._entries.appendAssumeCapacity(.{
|
||||||
.name = try String.init(arena, normalized, .{}),
|
.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;
|
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) {
|
if (self._closed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ pub fn postMessage(self: *MessagePort, message: js.Value.Global, page: *Page) !v
|
|||||||
.message = message,
|
.message = message,
|
||||||
});
|
});
|
||||||
|
|
||||||
try page.scheduler.add(callback, PostMessageCallback.run, 0, .{
|
try page.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
|
||||||
.name = "MessagePort.postMessage",
|
.name = "MessagePort.postMessage",
|
||||||
.low_priority = false,
|
.low_priority = false,
|
||||||
});
|
});
|
||||||
@@ -106,7 +106,7 @@ pub fn setOnMessageError(self: *MessagePort, cb: ?js.Function.Global) !void {
|
|||||||
|
|
||||||
const PostMessageCallback = struct {
|
const PostMessageCallback = struct {
|
||||||
port: *MessagePort,
|
port: *MessagePort,
|
||||||
message: js.Value.Global,
|
message: js.Value.Temp,
|
||||||
page: *Page,
|
page: *Page,
|
||||||
|
|
||||||
fn deinit(self: *PostMessageCallback) void {
|
fn deinit(self: *PostMessageCallback) void {
|
||||||
@@ -116,25 +116,30 @@ const PostMessageCallback = struct {
|
|||||||
fn run(ctx: *anyopaque) !?u32 {
|
fn run(ctx: *anyopaque) !?u32 {
|
||||||
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
|
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
|
||||||
defer self.deinit();
|
defer self.deinit();
|
||||||
|
const page = self.page;
|
||||||
|
|
||||||
if (self.port._closed) {
|
if (self.port._closed) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = MessageEvent.initTrusted("message", .{
|
const event = (MessageEvent.initTrusted(comptime .wrap("message"), .{
|
||||||
.data = self.message,
|
.data = self.message,
|
||||||
.origin = "",
|
.origin = "",
|
||||||
.source = null,
|
.source = null,
|
||||||
}, self.page) catch |err| {
|
}, page) catch |err| {
|
||||||
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
|
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
|
||||||
return null;
|
return null;
|
||||||
};
|
}).asEvent();
|
||||||
|
defer if (!event._v8_handoff) event.deinit(false);
|
||||||
|
|
||||||
const func = if (self.port._on_message) |*g| g.local() else null;
|
var ls: js.Local.Scope = undefined;
|
||||||
self.page._event_manager.dispatchWithFunction(
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
|
page._event_manager.dispatchWithFunction(
|
||||||
self.port.asEventTarget(),
|
self.port.asEventTarget(),
|
||||||
event.asEvent(),
|
event,
|
||||||
func,
|
ls.toLocal(self.port._on_message),
|
||||||
.{ .context = "MessagePort message" },
|
.{ .context = "MessagePort message" },
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
|
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user