mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-28 15:40:04 +00:00
Compare commits
223 Commits
http-cache
...
finalizers
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad54437ca3 | ||
|
|
01ecb296e5 | ||
|
|
67bd555e75 | ||
|
|
a10e533701 | ||
|
|
0065677273 | ||
|
|
226d9bfc6f | ||
|
|
2e65ae632e | ||
|
|
ea422075c7 | ||
|
|
1d54e6944b | ||
|
|
de32e5cf34 | ||
|
|
c8d8ca5e94 | ||
|
|
da0828620f | ||
|
|
cdd33621e3 | ||
|
|
8001709506 | ||
|
|
a0ae6b4c92 | ||
|
|
fdf7f5267a | ||
|
|
88e0b39d6b | ||
|
|
f95396a487 | ||
|
|
d02d05b246 | ||
|
|
7b2d817d0e | ||
|
|
7e778a17d6 | ||
|
|
a0dd14aaad | ||
|
|
d447d1e3c7 | ||
|
|
8684d35394 | ||
|
|
e243f96988 | ||
|
|
7ea8f3f766 | ||
|
|
5e6082b5e9 | ||
|
|
1befd9a5e8 | ||
|
|
e103ce0f39 | ||
|
|
14fa2da2ad | ||
|
|
28cc60adb0 | ||
|
|
96d24b5dc6 | ||
|
|
c14a9ad986 | ||
|
|
679f2104f4 | ||
|
|
c6b0c75106 | ||
|
|
93485c1ef3 | ||
|
|
0324d5c232 | ||
|
|
0588cc374d | ||
|
|
a75c0cf08d | ||
|
|
2812b8f07c | ||
|
|
e2afbec29d | ||
|
|
a45f9cb810 | ||
|
|
cf641ed458 | ||
|
|
0fc959dcc5 | ||
|
|
077376ea04 | ||
|
|
6ed8d1d201 | ||
|
|
5207bd4202 | ||
|
|
11ed95290b | ||
|
|
a876275828 | ||
|
|
e83b8aa36d | ||
|
|
179f9c1169 | ||
|
|
ca41bb5fa2 | ||
|
|
9c37961042 | ||
|
|
0dd0495ab8 | ||
|
|
c9fa76da0c | ||
|
|
7718184e22 | ||
|
|
b81b41cbf0 | ||
|
|
3a0cead03a | ||
|
|
92ce6a916a | ||
|
|
130bf7ba11 | ||
|
|
2e40354a3a | ||
|
|
3074bde2f3 | ||
|
|
ed9f5aae2e | ||
|
|
8e315e551a | ||
|
|
bad690da65 | ||
|
|
ae080f32eb | ||
|
|
c5c1d1f2f8 | ||
|
|
eb18dc89f6 | ||
|
|
afb0c29243 | ||
|
|
267eee9693 | ||
|
|
39352a6bda | ||
|
|
0838b510f8 | ||
|
|
b19f30d865 | ||
|
|
35be9f897f | ||
|
|
d517488158 | ||
|
|
fee8fe7830 | ||
|
|
428190aecc | ||
|
|
61dabdedec | ||
|
|
dfd9f216bd | ||
|
|
567cd97312 | ||
|
|
0bfe00bbb7 | ||
|
|
260768463b | ||
|
|
fd96cd6eb9 | ||
|
|
25a7b5b778 | ||
|
|
d4bcfa974f | ||
|
|
c91eac17d0 | ||
|
|
5c79961bb7 | ||
|
|
a0c200bc49 | ||
|
|
9ea39e1c34 | ||
|
|
f7125d2bf3 | ||
|
|
b163d9709b | ||
|
|
5453630955 | ||
|
|
8ada67637f | ||
|
|
5972630e95 | ||
|
|
58c18114a5 | ||
|
|
a94b0bec93 | ||
|
|
ff0fbb6b41 | ||
|
|
797cae2ef8 | ||
|
|
433c03c709 | ||
|
|
4d3e9feaf4 | ||
|
|
5700e214bf | ||
|
|
88d40a7dcd | ||
|
|
ff209f5adf | ||
|
|
8ad092a960 | ||
|
|
0fcdc1d194 | ||
|
|
60c2359fdd | ||
|
|
08c8ba72f5 | ||
|
|
cfa4201532 | ||
|
|
cb02eb000e | ||
|
|
23334edc05 | ||
|
|
8dbe22a01a | ||
|
|
80235e2ddd | ||
|
|
2abed9fe75 | ||
|
|
35551ac84e | ||
|
|
c3a2318eca | ||
|
|
a6e801be59 | ||
|
|
0bbe25ab5e | ||
|
|
c37286f845 | ||
|
|
34079913a3 | ||
|
|
4f1b499d0f | ||
|
|
c9bc370d6a | ||
|
|
4b29823a5b | ||
|
|
a69a22ccd7 | ||
|
|
a6d2ec7610 | ||
|
|
ad83c6e70b | ||
|
|
c2a0d4c0b2 | ||
|
|
9e7f0b4776 | ||
|
|
e3085cb0f1 | ||
|
|
4e2e895cd9 | ||
|
|
c1fc2b1301 | ||
|
|
324e5eb152 | ||
|
|
df4df64066 | ||
|
|
c557a0fd87 | ||
|
|
a869f92e9a | ||
|
|
4d28265839 | ||
|
|
78c6def2b1 | ||
|
|
87a0690776 | ||
|
|
fbc71d6ff7 | ||
|
|
e10ccd846d | ||
|
|
384b2f7614 | ||
|
|
fdc79af55c | ||
|
|
e9bed18cd8 | ||
|
|
30f387d361 | ||
|
|
e7d272eaf6 | ||
|
|
00d06dbe8c | ||
|
|
7b104789aa | ||
|
|
2107ade3a5 | ||
|
|
e60424a402 | ||
|
|
107da49f81 | ||
|
|
3e309da69f | ||
|
|
370ae2b85c | ||
|
|
6008187c78 | ||
|
|
598fa254cf | ||
|
|
8526770e9f | ||
|
|
21325ca9be | ||
|
|
b5b012bd5d | ||
|
|
b4b7a7d58a | ||
|
|
a5378feb1d | ||
|
|
b5d3d37f16 | ||
|
|
9b02e4963b | ||
|
|
a865b86fa5 | ||
|
|
de28d14aff | ||
|
|
2d91acbd14 | ||
|
|
88681b1fdb | ||
|
|
1feb121ba7 | ||
|
|
35cdc3c348 | ||
|
|
1353f76bf1 | ||
|
|
3e2be5b317 | ||
|
|
448eca0c32 | ||
|
|
5404ca723c | ||
|
|
e56ffe4b60 | ||
|
|
02d05ae464 | ||
|
|
a74e97854d | ||
|
|
6925fc3f70 | ||
|
|
84557cb4e6 | ||
|
|
4cdc24326a | ||
|
|
f1293b7346 | ||
|
|
94190f93af | ||
|
|
93e239f682 | ||
|
|
a4cb5031d1 | ||
|
|
f70865e174 | ||
|
|
38e9f86088 | ||
|
|
d9c5f56500 | ||
|
|
6c5733bba3 | ||
|
|
b8f1622b52 | ||
|
|
2b9d5fd4d9 | ||
|
|
2dbd32d120 | ||
|
|
1695ea81d2 | ||
|
|
b7bf86fd85 | ||
|
|
94d8f90a96 | ||
|
|
b9bef22bbf | ||
|
|
b2a996e5c7 | ||
|
|
e2be8525c4 | ||
|
|
c15afa23ca | ||
|
|
f594b033bf | ||
|
|
10e379e4fb | ||
|
|
c1bb27c450 | ||
|
|
dda5e2c542 | ||
|
|
e29778d72b | ||
|
|
09327c3897 | ||
|
|
43a70272c5 | ||
|
|
f0c9c262ca | ||
|
|
3fde349b9f | ||
|
|
55a9976d46 | ||
|
|
66a86541d1 | ||
|
|
bc19079dad | ||
|
|
351e44343d | ||
|
|
e362a9cbc3 | ||
|
|
e2563e57f2 | ||
|
|
df5e978247 | ||
|
|
f37862a25d | ||
|
|
84d76cf90d | ||
|
|
e12f28fb70 | ||
|
|
dfe04960c0 | ||
|
|
de2b1cc6fe | ||
|
|
2aef4ab677 | ||
|
|
798f68d0ce | ||
|
|
e0343a3f6d | ||
|
|
d918ec694b | ||
|
|
b2b609a309 | ||
|
|
48dd80867b | ||
|
|
f58f6e8d65 | ||
|
|
ee034943b6 |
2
.github/actions/install/action.yml
vendored
2
.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.3.4'
|
default: 'v0.3.7'
|
||||||
v8:
|
v8:
|
||||||
description: 'v8 version to install'
|
description: 'v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
4
.github/workflows/e2e-integration-test.yml
vendored
4
.github/workflows/e2e-integration-test.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
- 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
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
@@ -61,6 +61,6 @@ jobs:
|
|||||||
|
|
||||||
- name: run end to end integration tests
|
- name: run end to end integration tests
|
||||||
run: |
|
run: |
|
||||||
./lightpanda serve --log_level error & echo $! > LPD.pid
|
./lightpanda serve --log-level error & echo $! > LPD.pid
|
||||||
go run integration/main.go
|
go run integration/main.go
|
||||||
kill `cat LPD.pid`
|
kill `cat LPD.pid`
|
||||||
|
|||||||
27
.github/workflows/e2e-test.yml
vendored
27
.github/workflows/e2e-test.yml
vendored
@@ -52,7 +52,7 @@ jobs:
|
|||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
- 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
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
@@ -98,7 +98,7 @@ jobs:
|
|||||||
- name: run end to end tests through proxy
|
- name: run end to end tests through proxy
|
||||||
run: |
|
run: |
|
||||||
./proxy/proxy & echo $! > PROXY.id
|
./proxy/proxy & echo $! > PROXY.id
|
||||||
./lightpanda serve --http_proxy 'http://127.0.0.1:3000' & echo $! > LPD.pid
|
./lightpanda serve --http-proxy 'http://127.0.0.1:3000' & echo $! > LPD.pid
|
||||||
go run runner/main.go
|
go run runner/main.go
|
||||||
kill `cat LPD.pid` `cat PROXY.id`
|
kill `cat LPD.pid` `cat PROXY.id`
|
||||||
|
|
||||||
@@ -139,9 +139,9 @@ jobs:
|
|||||||
- name: run end to end tests
|
- name: run end to end tests
|
||||||
run: |
|
run: |
|
||||||
./lightpanda serve \
|
./lightpanda serve \
|
||||||
--web_bot_auth_key_file private_key.pem \
|
--web-bot-auth-key-file private_key.pem \
|
||||||
--web_bot_auth_keyid ${{ vars.WBA_KEY_ID }} \
|
--web-bot-auth-keyid ${{ vars.WBA_KEY_ID }} \
|
||||||
--web_bot_auth_domain ${{ vars.WBA_DOMAIN }} \
|
--web-bot-auth-domain ${{ vars.WBA_DOMAIN }} \
|
||||||
& echo $! > LPD.pid
|
& echo $! > LPD.pid
|
||||||
go run runner/main.go
|
go run runner/main.go
|
||||||
kill `cat LPD.pid`
|
kill `cat LPD.pid`
|
||||||
@@ -155,10 +155,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
./proxy/proxy & echo $! > PROXY.id
|
./proxy/proxy & echo $! > PROXY.id
|
||||||
./lightpanda serve \
|
./lightpanda serve \
|
||||||
--web_bot_auth_key_file private_key.pem \
|
--web-bot-auth-key-file private_key.pem \
|
||||||
--web_bot_auth_keyid ${{ vars.WBA_KEY_ID }} \
|
--web-bot-auth-keyid ${{ vars.WBA_KEY_ID }} \
|
||||||
--web_bot_auth_domain ${{ vars.WBA_DOMAIN }} \
|
--web-bot-auth-domain ${{ vars.WBA_DOMAIN }} \
|
||||||
--http_proxy 'http://127.0.0.1:3000' \
|
--http-proxy 'http://127.0.0.1:3000' \
|
||||||
& echo $! > LPD.pid
|
& echo $! > LPD.pid
|
||||||
go run runner/main.go
|
go run runner/main.go
|
||||||
kill `cat LPD.pid` `cat PROXY.id`
|
kill `cat LPD.pid` `cat PROXY.id`
|
||||||
@@ -179,6 +179,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
|
|
||||||
|
# Don't execute on PR
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
@@ -205,9 +208,9 @@ jobs:
|
|||||||
exec 3<<< "${{ secrets.WBA_PRIVATE_KEY_PEM }}"
|
exec 3<<< "${{ secrets.WBA_PRIVATE_KEY_PEM }}"
|
||||||
|
|
||||||
./lightpanda fetch --dump http://127.0.0.1:8989/ \
|
./lightpanda fetch --dump http://127.0.0.1:8989/ \
|
||||||
--web_bot_auth_key_file /proc/self/fd/3 \
|
--web-bot-auth-key-file /proc/self/fd/3 \
|
||||||
--web_bot_auth_keyid ${{ vars.WBA_KEY_ID }} \
|
--web-bot-auth-keyid ${{ vars.WBA_KEY_ID }} \
|
||||||
--web_bot_auth_domain ${{ vars.WBA_DOMAIN }}
|
--web-bot-auth-domain ${{ vars.WBA_DOMAIN }}
|
||||||
|
|
||||||
wait $VALIDATOR_PID
|
wait $VALIDATOR_PID
|
||||||
exec 3>&-
|
exec 3>&-
|
||||||
|
|||||||
10
.github/workflows/nightly.yml
vendored
10
.github/workflows/nightly.yml
vendored
@@ -7,7 +7,7 @@ env:
|
|||||||
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
|
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
|
||||||
|
|
||||||
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
|
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
|
||||||
GIT_VERSION_FLAG: ${{ github.ref_type == 'tag' && format('-Dgit_version={0}', github.ref_name) || '' }}
|
VERSION_FLAG: ${{ github.ref_type == 'tag' && format('-Dversion_string={0}', github.ref_name) || format('-Dpre_version={0}', 'nightly') }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
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
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) ${{ env.GIT_VERSION_FLAG }}
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 ${{ env.VERSION_FLAG }}
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
@@ -85,7 +85,7 @@ jobs:
|
|||||||
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
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) ${{ env.GIT_VERSION_FLAG }}
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic ${{ env.VERSION_FLAG }}
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
@@ -127,7 +127,7 @@ jobs:
|
|||||||
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
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) ${{ env.GIT_VERSION_FLAG }}
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast ${{ env.VERSION_FLAG }}
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
@@ -167,7 +167,7 @@ jobs:
|
|||||||
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
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) ${{ env.GIT_VERSION_FLAG }}
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast ${{ env.VERSION_FLAG }}
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
|
|||||||
2
.github/workflows/wpt.yml
vendored
2
.github/workflows/wpt.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
|||||||
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
|
||||||
|
|
||||||
- name: zig build release
|
- name: zig build release
|
||||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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.3.4
|
ARG ZIG_V8=v0.3.7
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
RUN apt-get update -yq && \
|
RUN apt-get update -yq && \
|
||||||
@@ -53,8 +53,7 @@ RUN zig build -Doptimize=ReleaseFast \
|
|||||||
# build release
|
# build release
|
||||||
RUN zig build -Doptimize=ReleaseFast \
|
RUN zig build -Doptimize=ReleaseFast \
|
||||||
-Dsnapshot_path=../../snapshot.bin \
|
-Dsnapshot_path=../../snapshot.bin \
|
||||||
-Dprebuilt_v8_path=v8/libc_v8.a \
|
-Dprebuilt_v8_path=v8/libc_v8.a
|
||||||
-Dgit_commit=$(git rev-parse --short HEAD)
|
|
||||||
|
|
||||||
FROM debian:stable-slim
|
FROM debian:stable-slim
|
||||||
|
|
||||||
@@ -75,4 +74,4 @@ EXPOSE 9222/tcp
|
|||||||
# Using "tini" as PID1 ensures that signals work as expected, so e.g. "docker stop" will not hang.
|
# Using "tini" as PID1 ensures that signals work as expected, so e.g. "docker stop" will not hang.
|
||||||
# (See https://github.com/krallin/tini#why-tini).
|
# (See https://github.com/krallin/tini#why-tini).
|
||||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
CMD ["/bin/lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222", "--log_level", "info"]
|
CMD ["/bin/lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222", "--log-level", "info"]
|
||||||
|
|||||||
@@ -4,11 +4,3 @@ License names used in this document are as per [SPDX License
|
|||||||
List](https://spdx.org/licenses/).
|
List](https://spdx.org/licenses/).
|
||||||
|
|
||||||
The default license for this project is [AGPL-3.0-only](LICENSE).
|
The default license for this project is [AGPL-3.0-only](LICENSE).
|
||||||
|
|
||||||
The following directories and their subdirectories are licensed under their
|
|
||||||
original upstream licenses:
|
|
||||||
|
|
||||||
```
|
|
||||||
vendor/
|
|
||||||
tests/wpt/
|
|
||||||
```
|
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -58,13 +58,13 @@ build-v8-snapshot:
|
|||||||
## Build in release-fast mode
|
## Build in release-fast mode
|
||||||
build: build-v8-snapshot
|
build: build-v8-snapshot
|
||||||
@printf "\033[36mBuilding (release fast)...\033[0m\n"
|
@printf "\033[36mBuilding (release fast)...\033[0m\n"
|
||||||
@$(ZIG) build -Doptimize=ReleaseFast -Dsnapshot_path=../../snapshot.bin -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
@$(ZIG) build -Doptimize=ReleaseFast -Dsnapshot_path=../../snapshot.bin || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||||
@printf "\033[33mBuild OK\033[0m\n"
|
@printf "\033[33mBuild OK\033[0m\n"
|
||||||
|
|
||||||
## Build in debug mode
|
## Build in debug mode
|
||||||
build-dev:
|
build-dev:
|
||||||
@printf "\033[36mBuilding (debug)...\033[0m\n"
|
@printf "\033[36mBuilding (debug)...\033[0m\n"
|
||||||
@$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
@$(ZIG) build || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||||
@printf "\033[33mBuild OK\033[0m\n"
|
@printf "\033[33mBuild OK\033[0m\n"
|
||||||
|
|
||||||
## Run the server in release mode
|
## Run the server in release mode
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -18,15 +18,15 @@ Not a Chromium fork. Not a WebKit patch. A new browser, written in Zig.
|
|||||||
</div>
|
</div>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/execution-time.svg">
|
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/execution-time-v2.svg">
|
||||||
](https://github.com/lightpanda-io/demo)
|
](https://github.com/lightpanda-io/demo)
|
||||||
 
|
 
|
||||||
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/memory-frame.svg">
|
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/memory-frame-v2.svg">
|
||||||
](https://github.com/lightpanda-io/demo)
|
](https://github.com/lightpanda-io/demo)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
_Puppeteer requesting 100 pages from a local website on a AWS EC2 m5.large instance.
|
_chromedp requesting 933 real web pages over the network on a AWS EC2 m5.large instance.
|
||||||
See [benchmark details](https://github.com/lightpanda-io/demo)._
|
See [benchmark details](https://github.com/lightpanda-io/demo/blob/main/BENCHMARKS.md#crawler-benchmark)._
|
||||||
|
|
||||||
Lightpanda is the open-source browser made for headless usage:
|
Lightpanda is the open-source browser made for headless usage:
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ docker run -d --name lightpanda -p 9222:9222 lightpanda/browser:nightly
|
|||||||
### Dump a URL
|
### Dump a URL
|
||||||
|
|
||||||
```console
|
```console
|
||||||
./lightpanda fetch --obey_robots --log_format pretty --log_level info https://demo-browser.lightpanda.io/campfire-commerce/
|
./lightpanda fetch --obey-robots --log-format pretty --log-level info https://demo-browser.lightpanda.io/campfire-commerce/
|
||||||
```
|
```
|
||||||
```console
|
```console
|
||||||
INFO telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
INFO telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
||||||
@@ -117,7 +117,7 @@ INFO http : request complete . . . . . . . . . . . . . . . . [+141ms]
|
|||||||
### Start a CDP server
|
### Start a CDP server
|
||||||
|
|
||||||
```console
|
```console
|
||||||
./lightpanda serve --obey_robots --log_format pretty --log_level info --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 telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
INFO telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
||||||
@@ -186,7 +186,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`
|
- [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.
|
||||||
|
|
||||||
@@ -317,7 +317,7 @@ First start the WPT's HTTP server from your `wpt/` clone dir.
|
|||||||
Run a Lightpanda browser
|
Run a Lightpanda browser
|
||||||
|
|
||||||
```
|
```
|
||||||
zig build run -- --insecure_disable_tls_host_verification
|
zig build run -- --insecure-disable-tls-host-verification
|
||||||
```
|
```
|
||||||
|
|
||||||
Then you can start the wptrunner from the Demo's clone dir:
|
Then you can start the wptrunner from the Demo's clone dir:
|
||||||
|
|||||||
101
build.zig
101
build.zig
@@ -17,24 +17,37 @@
|
|||||||
// 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 builtin = @import("builtin");
|
||||||
|
|
||||||
const Build = std.Build;
|
const lightpanda_version = std.SemanticVersion.parse(@import("build.zig.zon").version) catch unreachable;
|
||||||
|
const min_zig_version = std.SemanticVersion.parse(@import("build.zig.zon").minimum_zig_version) catch unreachable;
|
||||||
|
|
||||||
|
const Build = blk: {
|
||||||
|
if (builtin.zig_version.order(min_zig_version) == .lt) {
|
||||||
|
const message = std.fmt.comptimePrint(
|
||||||
|
\\Zig version is too old:
|
||||||
|
\\ current Zig version: {f}
|
||||||
|
\\ minimum Zig version: {f}
|
||||||
|
, .{ builtin.zig_version, min_zig_version });
|
||||||
|
@compileError(message);
|
||||||
|
} else {
|
||||||
|
break :blk std.Build;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub fn build(b: *Build) !void {
|
pub fn build(b: *Build) !void {
|
||||||
const target = b.standardTargetOptions(.{});
|
const target = b.standardTargetOptions(.{});
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
const manifest = Manifest.init(b);
|
|
||||||
|
|
||||||
const git_commit = b.option([]const u8, "git_commit", "Current git commit");
|
|
||||||
const git_version = b.option([]const u8, "git_version", "Current git version (from tag)");
|
|
||||||
const prebuilt_v8_path = b.option([]const u8, "prebuilt_v8_path", "Path to prebuilt libc_v8.a");
|
const prebuilt_v8_path = b.option([]const u8, "prebuilt_v8_path", "Path to prebuilt libc_v8.a");
|
||||||
const snapshot_path = b.option([]const u8, "snapshot_path", "Path to v8 snapshot");
|
const snapshot_path = b.option([]const u8, "snapshot_path", "Path to v8 snapshot");
|
||||||
|
|
||||||
|
const version = resolveVersion(b);
|
||||||
|
var stdout = std.fs.File.stdout().writer(&.{});
|
||||||
|
try stdout.interface.print("Lightpanda {f}\n", .{version});
|
||||||
|
|
||||||
var opts = b.addOptions();
|
var opts = b.addOptions();
|
||||||
opts.addOption([]const u8, "version", manifest.version);
|
opts.addOption([]const u8, "version", b.fmt("{f}", .{version}));
|
||||||
opts.addOption([]const u8, "git_commit", git_commit orelse "dev");
|
|
||||||
opts.addOption(?[]const u8, "git_version", git_version orelse null);
|
|
||||||
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") orelse false;
|
const enable_tsan = b.option(bool, "tsan", "Enable Thread Sanitizer") orelse false;
|
||||||
@@ -96,6 +109,11 @@ pub fn build(b: *Build) !void {
|
|||||||
}
|
}
|
||||||
const run_step = b.step("run", "Run the app");
|
const run_step = b.step("run", "Run the app");
|
||||||
run_step.dependOn(&run_cmd.step);
|
run_step.dependOn(&run_cmd.step);
|
||||||
|
|
||||||
|
const version_info_step = b.step("version", "Print the resolved version information");
|
||||||
|
const version_info_run = b.addRunArtifact(exe);
|
||||||
|
version_info_run.addArg("version");
|
||||||
|
version_info_step.dependOn(&version_info_run.step);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -701,27 +719,50 @@ fn buildCurl(
|
|||||||
return lib;
|
return lib;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Manifest = struct {
|
/// Returns `MAJOR.MINOR.PATCH-dev` when `git describe` fails.
|
||||||
version: []const u8,
|
fn resolveVersion(b: *std.Build) std.SemanticVersion {
|
||||||
minimum_zig_version: []const u8,
|
const version_string = b.option([]const u8, "version_string", "Override the version of this build");
|
||||||
|
if (version_string) |semver_string| {
|
||||||
fn init(b: *std.Build) Manifest {
|
return std.SemanticVersion.parse(semver_string) catch |err| {
|
||||||
const input = @embedFile("build.zig.zon");
|
std.debug.panic("Expected -Dversion-string={s} to be a semantic version: {}", .{ semver_string, err });
|
||||||
|
|
||||||
var diagnostics: std.zon.parse.Diagnostics = .{};
|
|
||||||
defer diagnostics.deinit(b.allocator);
|
|
||||||
|
|
||||||
return std.zon.parse.fromSlice(Manifest, b.allocator, input, &diagnostics, .{
|
|
||||||
.free_on_error = true,
|
|
||||||
.ignore_unknown_fields = true,
|
|
||||||
}) catch |err| {
|
|
||||||
switch (err) {
|
|
||||||
error.OutOfMemory => @panic("OOM"),
|
|
||||||
error.ParseZon => {
|
|
||||||
std.debug.print("Parse diagnostics:\n{f}\n", .{diagnostics});
|
|
||||||
std.process.exit(1);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
const pre_version = b.option([]const u8, "pre_version", "Override the pre version of this build");
|
||||||
|
const pre = blk: {
|
||||||
|
if (pre_version) |pre| {
|
||||||
|
break :blk pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk lightpanda_version.pre;
|
||||||
|
};
|
||||||
|
|
||||||
|
// If it's a stable release (no pre or build metadata in build.zig.zon), use it as is
|
||||||
|
if (pre == null and lightpanda_version.build == null) return lightpanda_version;
|
||||||
|
|
||||||
|
// For dev/nightly versions, calculate the commit count and hash
|
||||||
|
const git_hash_raw = runGit(b, &.{ "rev-parse", "--short", "HEAD" }) catch return lightpanda_version;
|
||||||
|
const commit_hash = std.mem.trim(u8, git_hash_raw, " \n\r");
|
||||||
|
|
||||||
|
const git_count_raw = runGit(b, &.{ "rev-list", "--count", "HEAD" }) catch return lightpanda_version;
|
||||||
|
const commit_count = std.mem.trim(u8, git_count_raw, " \n\r");
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.major = lightpanda_version.major,
|
||||||
|
.minor = lightpanda_version.minor,
|
||||||
|
.patch = lightpanda_version.patch,
|
||||||
|
.pre = b.fmt("{s}.{s}", .{ pre.?, commit_count }),
|
||||||
|
.build = commit_hash,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to run git commands and return stdout
|
||||||
|
fn runGit(b: *std.Build, args: []const []const u8) ![]const u8 {
|
||||||
|
var code: u8 = undefined;
|
||||||
|
const dir = b.pathFromRoot(".");
|
||||||
|
var command: std.ArrayList([]const u8) = .empty;
|
||||||
|
defer command.deinit(b.allocator);
|
||||||
|
try command.appendSlice(b.allocator, &.{ "git", "-C", dir });
|
||||||
|
try command.appendSlice(b.allocator, args);
|
||||||
|
return b.runAllowFail(command.items, &code, .Ignore);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
.{
|
.{
|
||||||
.name = .browser,
|
.name = .browser,
|
||||||
.version = "0.0.0",
|
.version = "1.0.0-dev",
|
||||||
.fingerprint = 0xda130f3af836cea0, // Changing this has security and trust implications.
|
.fingerprint = 0xda130f3af836cea0, // Changing this has security and trust implications.
|
||||||
.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/refs/tags/v0.3.4.tar.gz",
|
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.7.tar.gz",
|
||||||
.hash = "v8-0.0.0-xddH6_F3BAAiFvKY6R1H-gkuQlk19BkDQ0--uZuTrSup",
|
.hash = "v8-0.0.0-xddH67uBBAD95hWsPQz3Ni1PlZjdywtPXrGUAp8rSKco",
|
||||||
},
|
},
|
||||||
// .v8 = .{ .path = "../zig-v8-fork" },
|
// .v8 = .{ .path = "../zig-v8-fork" },
|
||||||
.brotli = .{
|
.brotli = .{
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ pub fn init(allocator: Allocator, config: *const Config) !*App {
|
|||||||
.arena_pool = undefined,
|
.arena_pool = undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
app.network = try Network.init(allocator, app, config);
|
app.network = try Network.init(allocator, config);
|
||||||
errdefer app.network.deinit();
|
errdefer app.network.deinit();
|
||||||
|
|
||||||
app.platform = try Platform.init();
|
app.platform = try Platform.init();
|
||||||
|
|||||||
@@ -17,12 +17,16 @@
|
|||||||
// 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 builtin = @import("builtin");
|
||||||
|
const log = @import("log.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
const ArenaPool = @This();
|
const ArenaPool = @This();
|
||||||
|
|
||||||
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
retain_bytes: usize,
|
retain_bytes: usize,
|
||||||
free_list_len: u16 = 0,
|
free_list_len: u16 = 0,
|
||||||
@@ -30,10 +34,17 @@ free_list: ?*Entry = null,
|
|||||||
free_list_max: u16,
|
free_list_max: u16,
|
||||||
entry_pool: std.heap.MemoryPool(Entry),
|
entry_pool: std.heap.MemoryPool(Entry),
|
||||||
mutex: std.Thread.Mutex = .{},
|
mutex: std.Thread.Mutex = .{},
|
||||||
|
// Debug mode: track acquire/release counts per debug name to detect leaks and double-frees
|
||||||
|
_leak_track: if (IS_DEBUG) std.StringHashMapUnmanaged(isize) else void = if (IS_DEBUG) .empty else {},
|
||||||
|
|
||||||
const Entry = struct {
|
const Entry = struct {
|
||||||
next: ?*Entry,
|
next: ?*Entry,
|
||||||
arena: ArenaAllocator,
|
arena: ArenaAllocator,
|
||||||
|
debug: if (IS_DEBUG) []const u8 else void = if (IS_DEBUG) "" else {},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const DebugInfo = struct {
|
||||||
|
debug: []const u8 = "",
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, free_list_max: u16, retain_bytes: usize) ArenaPool {
|
pub fn init(allocator: Allocator, free_list_max: u16, retain_bytes: usize) ArenaPool {
|
||||||
@@ -42,10 +53,26 @@ pub fn init(allocator: Allocator, free_list_max: u16, retain_bytes: usize) Arena
|
|||||||
.free_list_max = free_list_max,
|
.free_list_max = free_list_max,
|
||||||
.retain_bytes = retain_bytes,
|
.retain_bytes = retain_bytes,
|
||||||
.entry_pool = .init(allocator),
|
.entry_pool = .init(allocator),
|
||||||
|
._leak_track = if (IS_DEBUG) .empty else {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *ArenaPool) void {
|
pub fn deinit(self: *ArenaPool) void {
|
||||||
|
if (IS_DEBUG) {
|
||||||
|
var has_leaks = false;
|
||||||
|
var it = self._leak_track.iterator();
|
||||||
|
while (it.next()) |kv| {
|
||||||
|
if (kv.value_ptr.* != 0) {
|
||||||
|
log.err(.bug, "ArenaPool leak", .{ .name = kv.key_ptr.*, .count = kv.value_ptr.* });
|
||||||
|
has_leaks = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (has_leaks) {
|
||||||
|
@panic("ArenaPool: leaked arenas detected");
|
||||||
|
}
|
||||||
|
self._leak_track.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
var entry = self.free_list;
|
var entry = self.free_list;
|
||||||
while (entry) |e| {
|
while (entry) |e| {
|
||||||
entry = e.next;
|
entry = e.next;
|
||||||
@@ -54,13 +81,21 @@ pub fn deinit(self: *ArenaPool) void {
|
|||||||
self.entry_pool.deinit();
|
self.entry_pool.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn acquire(self: *ArenaPool) !Allocator {
|
pub fn acquire(self: *ArenaPool, dbg: DebugInfo) !Allocator {
|
||||||
self.mutex.lock();
|
self.mutex.lock();
|
||||||
defer self.mutex.unlock();
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
if (self.free_list) |entry| {
|
if (self.free_list) |entry| {
|
||||||
self.free_list = entry.next;
|
self.free_list = entry.next;
|
||||||
self.free_list_len -= 1;
|
self.free_list_len -= 1;
|
||||||
|
if (IS_DEBUG) {
|
||||||
|
entry.debug = dbg.debug;
|
||||||
|
const gop = try self._leak_track.getOrPut(self.allocator, dbg.debug);
|
||||||
|
if (!gop.found_existing) {
|
||||||
|
gop.value_ptr.* = 0;
|
||||||
|
}
|
||||||
|
gop.value_ptr.* += 1;
|
||||||
|
}
|
||||||
return entry.arena.allocator();
|
return entry.arena.allocator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,8 +103,16 @@ pub fn acquire(self: *ArenaPool) !Allocator {
|
|||||||
entry.* = .{
|
entry.* = .{
|
||||||
.next = null,
|
.next = null,
|
||||||
.arena = ArenaAllocator.init(self.allocator),
|
.arena = ArenaAllocator.init(self.allocator),
|
||||||
|
.debug = if (IS_DEBUG) dbg.debug else {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (IS_DEBUG) {
|
||||||
|
const gop = try self._leak_track.getOrPut(self.allocator, dbg.debug);
|
||||||
|
if (!gop.found_existing) {
|
||||||
|
gop.value_ptr.* = 0;
|
||||||
|
}
|
||||||
|
gop.value_ptr.* += 1;
|
||||||
|
}
|
||||||
return entry.arena.allocator();
|
return entry.arena.allocator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +126,19 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void {
|
|||||||
self.mutex.lock();
|
self.mutex.lock();
|
||||||
defer self.mutex.unlock();
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
if (IS_DEBUG) {
|
||||||
|
if (self._leak_track.getPtr(entry.debug)) |count| {
|
||||||
|
count.* -= 1;
|
||||||
|
if (count.* < 0) {
|
||||||
|
log.err(.bug, "ArenaPool double-free", .{ .name = entry.debug });
|
||||||
|
@panic("ArenaPool: double-free detected");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.err(.bug, "ArenaPool release unknown", .{ .name = entry.debug });
|
||||||
|
@panic("ArenaPool: release of untracked arena");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const free_list_len = self.free_list_len;
|
const free_list_len = self.free_list_len;
|
||||||
if (free_list_len == self.free_list_max) {
|
if (free_list_len == self.free_list_max) {
|
||||||
arena.deinit();
|
arena.deinit();
|
||||||
@@ -100,13 +156,18 @@ pub fn reset(_: *const ArenaPool, allocator: Allocator, retain: usize) void {
|
|||||||
_ = arena.reset(.{ .retain_with_limit = retain });
|
_ = arena.reset(.{ .retain_with_limit = retain });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn resetRetain(_: *const ArenaPool, allocator: Allocator) void {
|
||||||
|
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
||||||
|
_ = arena.reset(.retain_capacity);
|
||||||
|
}
|
||||||
|
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
|
||||||
test "arena pool - basic acquire and use" {
|
test "arena pool - basic acquire and use" {
|
||||||
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
defer pool.deinit();
|
defer pool.deinit();
|
||||||
|
|
||||||
const alloc = try pool.acquire();
|
const alloc = try pool.acquire(.{ .debug = "test" });
|
||||||
const buf = try alloc.alloc(u8, 64);
|
const buf = try alloc.alloc(u8, 64);
|
||||||
@memset(buf, 0xAB);
|
@memset(buf, 0xAB);
|
||||||
try testing.expectEqual(@as(u8, 0xAB), buf[0]);
|
try testing.expectEqual(@as(u8, 0xAB), buf[0]);
|
||||||
@@ -118,14 +179,14 @@ test "arena pool - reuse entry after release" {
|
|||||||
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
defer pool.deinit();
|
defer pool.deinit();
|
||||||
|
|
||||||
const alloc1 = try pool.acquire();
|
const alloc1 = try pool.acquire(.{ .debug = "test" });
|
||||||
try testing.expectEqual(@as(u16, 0), pool.free_list_len);
|
try testing.expectEqual(@as(u16, 0), pool.free_list_len);
|
||||||
|
|
||||||
pool.release(alloc1);
|
pool.release(alloc1);
|
||||||
try testing.expectEqual(@as(u16, 1), pool.free_list_len);
|
try testing.expectEqual(@as(u16, 1), pool.free_list_len);
|
||||||
|
|
||||||
// The same entry should be returned from the free list.
|
// The same entry should be returned from the free list.
|
||||||
const alloc2 = try pool.acquire();
|
const alloc2 = try pool.acquire(.{ .debug = "test" });
|
||||||
try testing.expectEqual(@as(u16, 0), pool.free_list_len);
|
try testing.expectEqual(@as(u16, 0), pool.free_list_len);
|
||||||
try testing.expectEqual(alloc1.ptr, alloc2.ptr);
|
try testing.expectEqual(alloc1.ptr, alloc2.ptr);
|
||||||
|
|
||||||
@@ -136,9 +197,9 @@ test "arena pool - multiple concurrent arenas" {
|
|||||||
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
defer pool.deinit();
|
defer pool.deinit();
|
||||||
|
|
||||||
const a1 = try pool.acquire();
|
const a1 = try pool.acquire(.{ .debug = "test1" });
|
||||||
const a2 = try pool.acquire();
|
const a2 = try pool.acquire(.{ .debug = "test2" });
|
||||||
const a3 = try pool.acquire();
|
const a3 = try pool.acquire(.{ .debug = "test3" });
|
||||||
|
|
||||||
// All three must be distinct arenas.
|
// All three must be distinct arenas.
|
||||||
try testing.expect(a1.ptr != a2.ptr);
|
try testing.expect(a1.ptr != a2.ptr);
|
||||||
@@ -161,8 +222,8 @@ test "arena pool - free list respects max limit" {
|
|||||||
var pool = ArenaPool.init(testing.allocator, 1, 1024 * 16);
|
var pool = ArenaPool.init(testing.allocator, 1, 1024 * 16);
|
||||||
defer pool.deinit();
|
defer pool.deinit();
|
||||||
|
|
||||||
const a1 = try pool.acquire();
|
const a1 = try pool.acquire(.{ .debug = "test1" });
|
||||||
const a2 = try pool.acquire();
|
const a2 = try pool.acquire(.{ .debug = "test2" });
|
||||||
|
|
||||||
pool.release(a1);
|
pool.release(a1);
|
||||||
try testing.expectEqual(@as(u16, 1), pool.free_list_len);
|
try testing.expectEqual(@as(u16, 1), pool.free_list_len);
|
||||||
@@ -176,7 +237,7 @@ test "arena pool - reset clears memory without releasing" {
|
|||||||
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
defer pool.deinit();
|
defer pool.deinit();
|
||||||
|
|
||||||
const alloc = try pool.acquire();
|
const alloc = try pool.acquire(.{ .debug = "test" });
|
||||||
|
|
||||||
const buf = try alloc.alloc(u8, 128);
|
const buf = try alloc.alloc(u8, 128);
|
||||||
@memset(buf, 0xFF);
|
@memset(buf, 0xFF);
|
||||||
@@ -200,8 +261,8 @@ test "arena pool - deinit with entries in free list" {
|
|||||||
// detected by the test allocator).
|
// detected by the test allocator).
|
||||||
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
|
|
||||||
const a1 = try pool.acquire();
|
const a1 = try pool.acquire(.{ .debug = "test1" });
|
||||||
const a2 = try pool.acquire();
|
const a2 = try pool.acquire(.{ .debug = "test2" });
|
||||||
_ = try a1.alloc(u8, 256);
|
_ = try a1.alloc(u8, 256);
|
||||||
_ = try a2.alloc(u8, 512);
|
_ = try a2.alloc(u8, 512);
|
||||||
pool.release(a1);
|
pool.release(a1);
|
||||||
|
|||||||
240
src/Config.zig
240
src/Config.zig
@@ -156,13 +156,6 @@ pub fn userAgentSuffix(self: *const Config) ?[]const u8 {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cacheDir(self: *const Config) ?[]const u8 {
|
|
||||||
return switch (self.mode) {
|
|
||||||
inline .serve, .fetch, .mcp => |opts| opts.common.cache_dir,
|
|
||||||
else => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cdpTimeout(self: *const Config) usize {
|
pub fn cdpTimeout(self: *const Config) usize {
|
||||||
return switch (self.mode) {
|
return switch (self.mode) {
|
||||||
.serve => |opts| if (opts.timeout > 604_800) 604_800_000 else @as(usize, opts.timeout) * 1000,
|
.serve => |opts| if (opts.timeout > 604_800) 604_800_000 else @as(usize, opts.timeout) * 1000,
|
||||||
@@ -170,6 +163,20 @@ pub fn cdpTimeout(self: *const Config) usize {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn port(self: *const Config) u16 {
|
||||||
|
return switch (self.mode) {
|
||||||
|
.serve => |opts| opts.port,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn advertiseHost(self: *const Config) []const u8 {
|
||||||
|
return switch (self.mode) {
|
||||||
|
.serve => |opts| opts.advertise_host orelse opts.host,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn webBotAuth(self: *const Config) ?WebBotAuthConfig {
|
pub fn webBotAuth(self: *const Config) ?WebBotAuthConfig {
|
||||||
return switch (self.mode) {
|
return switch (self.mode) {
|
||||||
inline .serve, .fetch, .mcp => |opts| WebBotAuthConfig{
|
inline .serve, .fetch, .mcp => |opts| WebBotAuthConfig{
|
||||||
@@ -206,6 +213,7 @@ pub const Mode = union(RunMode) {
|
|||||||
pub const Serve = struct {
|
pub const Serve = struct {
|
||||||
host: []const u8 = "127.0.0.1",
|
host: []const u8 = "127.0.0.1",
|
||||||
port: u16 = 9222,
|
port: u16 = 9222,
|
||||||
|
advertise_host: ?[]const u8 = null,
|
||||||
timeout: u31 = 10,
|
timeout: u31 = 10,
|
||||||
cdp_max_connections: u16 = 16,
|
cdp_max_connections: u16 = 16,
|
||||||
cdp_max_pending_connections: u16 = 128,
|
cdp_max_pending_connections: u16 = 128,
|
||||||
@@ -224,6 +232,13 @@ pub const DumpFormat = enum {
|
|||||||
semantic_tree_text,
|
semantic_tree_text,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const WaitUntil = enum {
|
||||||
|
load,
|
||||||
|
domcontentloaded,
|
||||||
|
networkidle,
|
||||||
|
done,
|
||||||
|
};
|
||||||
|
|
||||||
pub const Fetch = struct {
|
pub const Fetch = struct {
|
||||||
url: [:0]const u8,
|
url: [:0]const u8,
|
||||||
dump_mode: ?DumpFormat = null,
|
dump_mode: ?DumpFormat = null,
|
||||||
@@ -231,6 +246,8 @@ pub const Fetch = struct {
|
|||||||
with_base: bool = false,
|
with_base: bool = false,
|
||||||
with_frames: bool = false,
|
with_frames: bool = false,
|
||||||
strip: dump.Opts.Strip = .{},
|
strip: dump.Opts.Strip = .{},
|
||||||
|
wait_ms: u32 = 5000,
|
||||||
|
wait_until: WaitUntil = .done,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Common = struct {
|
pub const Common = struct {
|
||||||
@@ -247,7 +264,6 @@ pub const Common = struct {
|
|||||||
log_format: ?log.Format = null,
|
log_format: ?log.Format = null,
|
||||||
log_filter_scopes: ?[]log.Scope = null,
|
log_filter_scopes: ?[]log.Scope = null,
|
||||||
user_agent_suffix: ?[]const u8 = null,
|
user_agent_suffix: ?[]const u8 = null,
|
||||||
cache_dir: ?[]const u8 = null,
|
|
||||||
|
|
||||||
web_bot_auth_key_file: ?[]const u8 = null,
|
web_bot_auth_key_file: ?[]const u8 = null,
|
||||||
web_bot_auth_keyid: ?[]const u8 = null,
|
web_bot_auth_keyid: ?[]const u8 = null,
|
||||||
@@ -301,71 +317,71 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
|||||||
// MAX_HELP_LEN|
|
// MAX_HELP_LEN|
|
||||||
const common_options =
|
const common_options =
|
||||||
\\
|
\\
|
||||||
\\--insecure_disable_tls_host_verification
|
\\--insecure-disable-tls-host-verification
|
||||||
\\ Disables host verification on all HTTP requests. This is an
|
\\ Disables host verification on all HTTP requests. This is an
|
||||||
\\ advanced option which should only be set if you understand
|
\\ advanced option which should only be set if you understand
|
||||||
\\ and accept the risk of disabling host verification.
|
\\ and accept the risk of disabling host verification.
|
||||||
\\
|
\\
|
||||||
\\--obey_robots
|
\\--obey-robots
|
||||||
\\ Fetches and obeys the robots.txt (if available) of the web pages
|
\\ Fetches and obeys the robots.txt (if available) of the web pages
|
||||||
\\ we make requests towards.
|
\\ we make requests towards.
|
||||||
\\ Defaults to false.
|
\\ Defaults to false.
|
||||||
\\
|
\\
|
||||||
\\--http_proxy The HTTP proxy to use for all HTTP requests.
|
\\--http-proxy The HTTP proxy to use for all HTTP requests.
|
||||||
\\ A username:password can be included for basic authentication.
|
\\ A username:password can be included for basic authentication.
|
||||||
\\ Defaults to none.
|
\\ Defaults to none.
|
||||||
\\
|
\\
|
||||||
\\--proxy_bearer_token
|
\\--proxy-bearer-token
|
||||||
\\ The <token> to send for bearer authentication with the proxy
|
\\ The <token> to send for bearer authentication with the proxy
|
||||||
\\ Proxy-Authorization: Bearer <token>
|
\\ Proxy-Authorization: Bearer <token>
|
||||||
\\
|
\\
|
||||||
\\--http_max_concurrent
|
\\--http-max-concurrent
|
||||||
\\ The maximum number of concurrent HTTP requests.
|
\\ The maximum number of concurrent HTTP requests.
|
||||||
\\ Defaults to 10.
|
\\ Defaults to 10.
|
||||||
\\
|
\\
|
||||||
\\--http_max_host_open
|
\\--http-max-host-open
|
||||||
\\ The maximum number of open connection to a given host:port.
|
\\ The maximum number of open connection to a given host:port.
|
||||||
\\ Defaults to 4.
|
\\ Defaults to 4.
|
||||||
\\
|
\\
|
||||||
\\--http_connect_timeout
|
\\--http-connect-timeout
|
||||||
\\ The time, in milliseconds, for establishing an HTTP connection
|
\\ The time, in milliseconds, for establishing an HTTP connection
|
||||||
\\ before timing out. 0 means it never times out.
|
\\ before timing out. 0 means it never times out.
|
||||||
\\ Defaults to 0.
|
\\ Defaults to 0.
|
||||||
\\
|
\\
|
||||||
\\--http_timeout
|
\\--http-timeout
|
||||||
\\ The maximum time, in milliseconds, the transfer is allowed
|
\\ The maximum time, in milliseconds, the transfer is allowed
|
||||||
\\ to complete. 0 means it never times out.
|
\\ to complete. 0 means it never times out.
|
||||||
\\ Defaults to 10000.
|
\\ Defaults to 10000.
|
||||||
\\
|
\\
|
||||||
\\--http_max_response_size
|
\\--http-max-response-size
|
||||||
\\ Limits the acceptable response size for any request
|
\\ Limits the acceptable response size for any request
|
||||||
\\ (e.g. XHR, fetch, script loading, ...).
|
\\ (e.g. XHR, fetch, script loading, ...).
|
||||||
\\ Defaults to no limit.
|
\\ Defaults to no limit.
|
||||||
\\
|
\\
|
||||||
\\--log_level The log level: debug, info, warn, error or fatal.
|
\\--log-level The log level: debug, info, warn, error or fatal.
|
||||||
\\ Defaults to
|
\\ Defaults to
|
||||||
++ (if (builtin.mode == .Debug) " info." else "warn.") ++
|
++ (if (builtin.mode == .Debug) " info." else "warn.") ++
|
||||||
\\
|
\\
|
||||||
\\
|
\\
|
||||||
\\--log_format The log format: pretty or logfmt.
|
\\--log-format The log format: pretty or logfmt.
|
||||||
\\ Defaults to
|
\\ Defaults to
|
||||||
++ (if (builtin.mode == .Debug) " pretty." else " logfmt.") ++
|
++ (if (builtin.mode == .Debug) " pretty." else " logfmt.") ++
|
||||||
\\
|
\\
|
||||||
\\
|
\\
|
||||||
\\--log_filter_scopes
|
\\--log-filter-scopes
|
||||||
\\ Filter out too verbose logs per scope:
|
\\ Filter out too verbose logs per scope:
|
||||||
\\ http, unknown_prop, event, ...
|
\\ http, unknown_prop, event, ...
|
||||||
\\
|
\\
|
||||||
\\--user_agent_suffix
|
\\--user-agent-suffix
|
||||||
\\ Suffix to append to the Lightpanda/X.Y User-Agent
|
\\ Suffix to append to the Lightpanda/X.Y User-Agent
|
||||||
\\
|
\\
|
||||||
\\--web_bot_auth_key_file
|
\\--web-bot-auth-key-file
|
||||||
\\ Path to the Ed25519 private key PEM file.
|
\\ Path to the Ed25519 private key PEM file.
|
||||||
\\
|
\\
|
||||||
\\--web_bot_auth_keyid
|
\\--web-bot-auth-keyid
|
||||||
\\ The JWK thumbprint of your public key.
|
\\ The JWK thumbprint of your public key.
|
||||||
\\
|
\\
|
||||||
\\--web_bot_auth_domain
|
\\--web-bot-auth-domain
|
||||||
\\ Your domain e.g. yourdomain.com
|
\\ Your domain e.g. yourdomain.com
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -384,16 +400,23 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
|||||||
\\ Argument must be 'html', 'markdown', 'semantic_tree', or 'semantic_tree_text'.
|
\\ Argument must be 'html', 'markdown', 'semantic_tree', or 'semantic_tree_text'.
|
||||||
\\ Defaults to no dump.
|
\\ Defaults to no dump.
|
||||||
\\
|
\\
|
||||||
\\--strip_mode Comma separated list of tag groups to remove from dump
|
\\--strip-mode Comma separated list of tag groups to remove from dump
|
||||||
\\ the dump. e.g. --strip_mode js,css
|
\\ the dump. e.g. --strip-mode js,css
|
||||||
\\ - "js" script and link[as=script, rel=preload]
|
\\ - "js" script and link[as=script, rel=preload]
|
||||||
\\ - "ui" includes img, picture, video, css and svg
|
\\ - "ui" includes img, picture, video, css and svg
|
||||||
\\ - "css" includes style and link[rel=stylesheet]
|
\\ - "css" includes style and link[rel=stylesheet]
|
||||||
\\ - "full" includes js, ui and css
|
\\ - "full" includes js, ui and css
|
||||||
\\
|
\\
|
||||||
\\--with_base Add a <base> tag in dump. Defaults to false.
|
\\--with-base Add a <base> tag in dump. Defaults to false.
|
||||||
\\
|
\\
|
||||||
\\--with_frames Includes the contents of iframes. Defaults to false.
|
\\--with-frames Includes the contents of iframes. Defaults to false.
|
||||||
|
\\
|
||||||
|
\\--wait-ms Wait time in milliseconds.
|
||||||
|
\\ Defaults to 5000.
|
||||||
|
\\
|
||||||
|
\\--wait-until Wait until the specified event.
|
||||||
|
\\ Supported events: load, domcontentloaded, networkidle, done.
|
||||||
|
\\ Defaults to 'done'.
|
||||||
\\
|
\\
|
||||||
++ common_options ++
|
++ common_options ++
|
||||||
\\
|
\\
|
||||||
@@ -408,14 +431,19 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
|||||||
\\--port Port of the CDP server
|
\\--port Port of the CDP server
|
||||||
\\ Defaults to 9222
|
\\ Defaults to 9222
|
||||||
\\
|
\\
|
||||||
|
\\--advertise-host
|
||||||
|
\\ The host to advertise, e.g. in the /json/version response.
|
||||||
|
\\ Useful, for example, when --host is 0.0.0.0.
|
||||||
|
\\ Defaults to --host value
|
||||||
|
\\
|
||||||
\\--timeout Inactivity timeout in seconds before disconnecting clients
|
\\--timeout Inactivity timeout in seconds before disconnecting clients
|
||||||
\\ Defaults to 10 (seconds). Limited to 604800 (1 week).
|
\\ Defaults to 10 (seconds). Limited to 604800 (1 week).
|
||||||
\\
|
\\
|
||||||
\\--cdp_max_connections
|
\\--cdp-max-connections
|
||||||
\\ Maximum number of simultaneous CDP connections.
|
\\ Maximum number of simultaneous CDP connections.
|
||||||
\\ Defaults to 16.
|
\\ Defaults to 16.
|
||||||
\\
|
\\
|
||||||
\\--cdp_max_pending_connections
|
\\--cdp-max-pending-connections
|
||||||
\\ Maximum pending connections in the accept queue.
|
\\ Maximum pending connections in the accept queue.
|
||||||
\\ Defaults to 128.
|
\\ Defaults to 128.
|
||||||
\\
|
\\
|
||||||
@@ -493,15 +521,15 @@ fn inferMode(opt: []const u8) ?RunMode {
|
|||||||
return .fetch;
|
return .fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, opt, "--strip_mode")) {
|
if (std.mem.eql(u8, opt, "--strip-mode") or std.mem.eql(u8, opt, "--strip_mode")) {
|
||||||
return .fetch;
|
return .fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, opt, "--with_base")) {
|
if (std.mem.eql(u8, opt, "--with-base") or std.mem.eql(u8, opt, "--with_base")) {
|
||||||
return .fetch;
|
return .fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, opt, "--with_frames")) {
|
if (std.mem.eql(u8, opt, "--with-frames") or std.mem.eql(u8, opt, "--with_frames")) {
|
||||||
return .fetch;
|
return .fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,6 +577,15 @@ fn parseServeArgs(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--advertise-host", opt) or std.mem.eql(u8, "--advertise_host", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
serve.advertise_host = try allocator.dupe(u8, str);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--timeout", opt)) {
|
if (std.mem.eql(u8, "--timeout", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--timeout" });
|
log.fatal(.app, "missing argument value", .{ .arg = "--timeout" });
|
||||||
@@ -562,27 +599,27 @@ fn parseServeArgs(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--cdp_max_connections", opt)) {
|
if (std.mem.eql(u8, "--cdp-max-connections", opt) or std.mem.eql(u8, "--cdp_max_connections", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--cdp_max_connections" });
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
|
|
||||||
serve.cdp_max_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
serve.cdp_max_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||||
log.fatal(.app, "invalid argument value", .{ .arg = "--cdp_max_connections", .err = err });
|
log.fatal(.app, "invalid argument value", .{ .arg = opt, .err = err });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--cdp_max_pending_connections", opt)) {
|
if (std.mem.eql(u8, "--cdp-max-pending-connections", opt) or std.mem.eql(u8, "--cdp_max_pending_connections", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--cdp_max_pending_connections" });
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
|
|
||||||
serve.cdp_max_pending_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
serve.cdp_max_pending_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||||
log.fatal(.app, "invalid argument value", .{ .arg = "--cdp_max_pending_connections", .err = err });
|
log.fatal(.app, "invalid argument value", .{ .arg = opt, .err = err });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
continue;
|
continue;
|
||||||
@@ -627,8 +664,34 @@ fn parseFetchArgs(
|
|||||||
var url: ?[:0]const u8 = null;
|
var url: ?[:0]const u8 = null;
|
||||||
var common: Common = .{};
|
var common: Common = .{};
|
||||||
var strip: dump.Opts.Strip = .{};
|
var strip: dump.Opts.Strip = .{};
|
||||||
|
var wait_ms: u32 = 5000;
|
||||||
|
var wait_until: WaitUntil = .done;
|
||||||
|
|
||||||
while (args.next()) |opt| {
|
while (args.next()) |opt| {
|
||||||
|
if (std.mem.eql(u8, "--wait-ms", opt) or std.mem.eql(u8, "--wait_ms", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
wait_ms = std.fmt.parseInt(u32, str, 10) catch |err| {
|
||||||
|
log.fatal(.app, "invalid argument value", .{ .arg = opt, .err = err });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--wait-until", opt) or std.mem.eql(u8, "--wait_until", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
wait_until = std.meta.stringToEnum(WaitUntil, str) orelse {
|
||||||
|
log.fatal(.app, "invalid argument value", .{ .arg = opt, .val = str });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--dump", opt)) {
|
if (std.mem.eql(u8, "--dump", opt)) {
|
||||||
var peek_args = args.*;
|
var peek_args = args.*;
|
||||||
if (peek_args.next()) |next_arg| {
|
if (peek_args.next()) |next_arg| {
|
||||||
@@ -647,25 +710,25 @@ fn parseFetchArgs(
|
|||||||
if (std.mem.eql(u8, "--noscript", opt)) {
|
if (std.mem.eql(u8, "--noscript", opt)) {
|
||||||
log.warn(.app, "deprecation warning", .{
|
log.warn(.app, "deprecation warning", .{
|
||||||
.feature = "--noscript argument",
|
.feature = "--noscript argument",
|
||||||
.hint = "use '--strip_mode js' instead",
|
.hint = "use '--strip-mode js' instead",
|
||||||
});
|
});
|
||||||
strip.js = true;
|
strip.js = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--with_base", opt)) {
|
if (std.mem.eql(u8, "--with-base", opt) or std.mem.eql(u8, "--with_base", opt)) {
|
||||||
with_base = true;
|
with_base = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--with_frames", opt)) {
|
if (std.mem.eql(u8, "--with-frames", opt) or std.mem.eql(u8, "--with_frames", opt)) {
|
||||||
with_frames = true;
|
with_frames = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--strip_mode", opt)) {
|
if (std.mem.eql(u8, "--strip-mode", opt) or std.mem.eql(u8, "--strip_mode", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--strip_mode" });
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -683,7 +746,7 @@ fn parseFetchArgs(
|
|||||||
strip.ui = true;
|
strip.ui = true;
|
||||||
strip.css = true;
|
strip.css = true;
|
||||||
} else {
|
} else {
|
||||||
log.fatal(.app, "invalid option choice", .{ .arg = "--strip_mode", .value = trimmed });
|
log.fatal(.app, "invalid option choice", .{ .arg = opt, .value = trimmed });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -717,6 +780,8 @@ fn parseFetchArgs(
|
|||||||
.common = common,
|
.common = common,
|
||||||
.with_base = with_base,
|
.with_base = with_base,
|
||||||
.with_frames = with_frames,
|
.with_frames = with_frames,
|
||||||
|
.wait_ms = wait_ms,
|
||||||
|
.wait_until = wait_until,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,102 +791,102 @@ fn parseCommonArg(
|
|||||||
args: *std.process.ArgIterator,
|
args: *std.process.ArgIterator,
|
||||||
common: *Common,
|
common: *Common,
|
||||||
) !bool {
|
) !bool {
|
||||||
if (std.mem.eql(u8, "--insecure_disable_tls_host_verification", opt)) {
|
if (std.mem.eql(u8, "--insecure-disable-tls-host-verification", opt) or std.mem.eql(u8, "--insecure_disable_tls_host_verification", opt)) {
|
||||||
common.tls_verify_host = false;
|
common.tls_verify_host = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--obey_robots", opt)) {
|
if (std.mem.eql(u8, "--obey-robots", opt) or std.mem.eql(u8, "--obey_robots", opt)) {
|
||||||
common.obey_robots = true;
|
common.obey_robots = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--http_proxy", opt)) {
|
if (std.mem.eql(u8, "--http-proxy", opt) or std.mem.eql(u8, "--http_proxy", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_proxy" });
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
common.http_proxy = try allocator.dupeZ(u8, str);
|
common.http_proxy = try allocator.dupeZ(u8, str);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--proxy_bearer_token", opt)) {
|
if (std.mem.eql(u8, "--proxy-bearer-token", opt) or std.mem.eql(u8, "--proxy_bearer_token", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--proxy_bearer_token" });
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
common.proxy_bearer_token = try allocator.dupeZ(u8, str);
|
common.proxy_bearer_token = try allocator.dupeZ(u8, str);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--http_max_concurrent", opt)) {
|
if (std.mem.eql(u8, "--http-max-concurrent", opt) or std.mem.eql(u8, "--http_max_concurrent", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_max_concurrent" });
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
|
|
||||||
common.http_max_concurrent = std.fmt.parseInt(u8, str, 10) catch |err| {
|
common.http_max_concurrent = std.fmt.parseInt(u8, str, 10) catch |err| {
|
||||||
log.fatal(.app, "invalid argument value", .{ .arg = "--http_max_concurrent", .err = err });
|
log.fatal(.app, "invalid argument value", .{ .arg = opt, .err = err });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--http_max_host_open", opt)) {
|
if (std.mem.eql(u8, "--http-max-host-open", opt) or std.mem.eql(u8, "--http_max_host_open", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_max_host_open" });
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
|
|
||||||
common.http_max_host_open = std.fmt.parseInt(u8, str, 10) catch |err| {
|
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 });
|
log.fatal(.app, "invalid argument value", .{ .arg = opt, .err = err });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--http_connect_timeout", opt)) {
|
if (std.mem.eql(u8, "--http-connect-timeout", opt) or std.mem.eql(u8, "--http_connect_timeout", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_connect_timeout" });
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
|
|
||||||
common.http_connect_timeout = std.fmt.parseInt(u31, str, 10) catch |err| {
|
common.http_connect_timeout = std.fmt.parseInt(u31, str, 10) catch |err| {
|
||||||
log.fatal(.app, "invalid argument value", .{ .arg = "--http_connect_timeout", .err = err });
|
log.fatal(.app, "invalid argument value", .{ .arg = opt, .err = err });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--http_timeout", opt)) {
|
if (std.mem.eql(u8, "--http-timeout", opt) or std.mem.eql(u8, "--http_timeout", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_timeout" });
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
|
|
||||||
common.http_timeout = std.fmt.parseInt(u31, str, 10) catch |err| {
|
common.http_timeout = std.fmt.parseInt(u31, str, 10) catch |err| {
|
||||||
log.fatal(.app, "invalid argument value", .{ .arg = "--http_timeout", .err = err });
|
log.fatal(.app, "invalid argument value", .{ .arg = opt, .err = err });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--http_max_response_size", opt)) {
|
if (std.mem.eql(u8, "--http-max-response-size", opt) or std.mem.eql(u8, "--http_max_response_size", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_max_response_size" });
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
|
|
||||||
common.http_max_response_size = std.fmt.parseInt(usize, str, 10) catch |err| {
|
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 });
|
log.fatal(.app, "invalid argument value", .{ .arg = opt, .err = err });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--log_level", opt)) {
|
if (std.mem.eql(u8, "--log-level", opt) or std.mem.eql(u8, "--log_level", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--log_level" });
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -829,26 +894,26 @@ fn parseCommonArg(
|
|||||||
if (std.mem.eql(u8, str, "error")) {
|
if (std.mem.eql(u8, str, "error")) {
|
||||||
break :blk .err;
|
break :blk .err;
|
||||||
}
|
}
|
||||||
log.fatal(.app, "invalid option choice", .{ .arg = "--log_level", .value = str });
|
log.fatal(.app, "invalid option choice", .{ .arg = opt, .value = str });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--log_format", opt)) {
|
if (std.mem.eql(u8, "--log-format", opt) or std.mem.eql(u8, "--log_format", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--log_format" });
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
|
|
||||||
common.log_format = std.meta.stringToEnum(log.Format, str) orelse {
|
common.log_format = std.meta.stringToEnum(log.Format, str) orelse {
|
||||||
log.fatal(.app, "invalid option choice", .{ .arg = "--log_format", .value = str });
|
log.fatal(.app, "invalid option choice", .{ .arg = opt, .value = str });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--log_filter_scopes", opt)) {
|
if (std.mem.eql(u8, "--log-filter-scopes", opt) or std.mem.eql(u8, "--log_filter_scopes", opt)) {
|
||||||
if (builtin.mode != .Debug) {
|
if (builtin.mode != .Debug) {
|
||||||
log.fatal(.app, "experimental", .{ .help = "log scope filtering is only available in debug builds" });
|
log.fatal(.app, "experimental", .{ .help = "log scope filtering is only available in debug builds" });
|
||||||
return false;
|
return false;
|
||||||
@@ -865,7 +930,7 @@ fn parseCommonArg(
|
|||||||
var it = std.mem.splitScalar(u8, str, ',');
|
var it = std.mem.splitScalar(u8, str, ',');
|
||||||
while (it.next()) |part| {
|
while (it.next()) |part| {
|
||||||
try arr.append(allocator, std.meta.stringToEnum(log.Scope, part) orelse {
|
try arr.append(allocator, std.meta.stringToEnum(log.Scope, part) orelse {
|
||||||
log.fatal(.app, "invalid option choice", .{ .arg = "--log_filter_scopes", .value = part });
|
log.fatal(.app, "invalid option choice", .{ .arg = opt, .value = part });
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -873,14 +938,14 @@ fn parseCommonArg(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--user_agent_suffix", opt)) {
|
if (std.mem.eql(u8, "--user-agent-suffix", opt) or std.mem.eql(u8, "--user_agent_suffix", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--user_agent_suffix" });
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
for (str) |c| {
|
for (str) |c| {
|
||||||
if (!std.ascii.isPrint(c)) {
|
if (!std.ascii.isPrint(c)) {
|
||||||
log.fatal(.app, "not printable character", .{ .arg = "--user_agent_suffix" });
|
log.fatal(.app, "not printable character", .{ .arg = opt });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -888,41 +953,32 @@ fn parseCommonArg(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--web_bot_auth_key_file", opt)) {
|
if (std.mem.eql(u8, "--web-bot-auth-key-file", opt) or std.mem.eql(u8, "--web_bot_auth_key_file", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--web_bot_auth_key_file" });
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
common.web_bot_auth_key_file = try allocator.dupe(u8, str);
|
common.web_bot_auth_key_file = try allocator.dupe(u8, str);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--web_bot_auth_keyid", opt)) {
|
if (std.mem.eql(u8, "--web-bot-auth-keyid", opt) or std.mem.eql(u8, "--web_bot_auth_keyid", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--web_bot_auth_keyid" });
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
common.web_bot_auth_keyid = try allocator.dupe(u8, str);
|
common.web_bot_auth_keyid = try allocator.dupe(u8, str);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--web_bot_auth_domain", opt)) {
|
if (std.mem.eql(u8, "--web-bot-auth-domain", opt) or std.mem.eql(u8, "--web_bot_auth_domain", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--web_bot_auth_domain" });
|
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
common.web_bot_auth_domain = try allocator.dupe(u8, str);
|
common.web_bot_auth_domain = try allocator.dupe(u8, str);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--cache_dir", opt)) {
|
|
||||||
const str = args.next() orelse {
|
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--cache_dir" });
|
|
||||||
return error.InvalidArgument;
|
|
||||||
};
|
|
||||||
common.cache_dir = try allocator.dupe(u8, str);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,15 @@ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!
|
|||||||
log.err(.app, "listener map failed", .{ .err = err });
|
log.err(.app, "listener map failed", .{ .err = err });
|
||||||
return error.WriteFailed;
|
return error.WriteFailed;
|
||||||
};
|
};
|
||||||
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, 0) catch |err| {
|
var visibility_cache: Element.VisibilityCache = .empty;
|
||||||
|
var pointer_events_cache: Element.PointerEventsCache = .empty;
|
||||||
|
var ctx: WalkContext = .{
|
||||||
|
.xpath_buffer = &xpath_buffer,
|
||||||
|
.listener_targets = listener_targets,
|
||||||
|
.visibility_cache = &visibility_cache,
|
||||||
|
.pointer_events_cache = &pointer_events_cache,
|
||||||
|
};
|
||||||
|
self.walk(&ctx, self.dom_node, null, &visitor, 1, 0) catch |err| {
|
||||||
log.err(.app, "semantic tree json dump failed", .{ .err = err });
|
log.err(.app, "semantic tree json dump failed", .{ .err = err });
|
||||||
return error.WriteFailed;
|
return error.WriteFailed;
|
||||||
};
|
};
|
||||||
@@ -60,7 +68,15 @@ pub fn textStringify(self: @This(), writer: *std.Io.Writer) error{WriteFailed}!v
|
|||||||
log.err(.app, "listener map failed", .{ .err = err });
|
log.err(.app, "listener map failed", .{ .err = err });
|
||||||
return error.WriteFailed;
|
return error.WriteFailed;
|
||||||
};
|
};
|
||||||
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, 0) catch |err| {
|
var visibility_cache: Element.VisibilityCache = .empty;
|
||||||
|
var pointer_events_cache: Element.PointerEventsCache = .empty;
|
||||||
|
var ctx: WalkContext = .{
|
||||||
|
.xpath_buffer = &xpath_buffer,
|
||||||
|
.listener_targets = listener_targets,
|
||||||
|
.visibility_cache = &visibility_cache,
|
||||||
|
.pointer_events_cache = &pointer_events_cache,
|
||||||
|
};
|
||||||
|
self.walk(&ctx, self.dom_node, null, &visitor, 1, 0) catch |err| {
|
||||||
log.err(.app, "semantic tree text dump failed", .{ .err = err });
|
log.err(.app, "semantic tree text dump failed", .{ .err = err });
|
||||||
return error.WriteFailed;
|
return error.WriteFailed;
|
||||||
};
|
};
|
||||||
@@ -84,7 +100,22 @@ const NodeData = struct {
|
|||||||
node_name: []const u8,
|
node_name: []const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_name: ?[]const u8, visitor: anytype, index: usize, listener_targets: interactive.ListenerTargetMap, current_depth: u32) !void {
|
const WalkContext = struct {
|
||||||
|
xpath_buffer: *std.ArrayList(u8),
|
||||||
|
listener_targets: interactive.ListenerTargetMap,
|
||||||
|
visibility_cache: *Element.VisibilityCache,
|
||||||
|
pointer_events_cache: *Element.PointerEventsCache,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn walk(
|
||||||
|
self: @This(),
|
||||||
|
ctx: *WalkContext,
|
||||||
|
node: *Node,
|
||||||
|
parent_name: ?[]const u8,
|
||||||
|
visitor: anytype,
|
||||||
|
index: usize,
|
||||||
|
current_depth: u32,
|
||||||
|
) !void {
|
||||||
if (current_depth > self.max_depth) return;
|
if (current_depth > self.max_depth) return;
|
||||||
|
|
||||||
// 1. Skip non-content nodes
|
// 1. Skip non-content nodes
|
||||||
@@ -96,7 +127,7 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
|||||||
if (tag == .datalist or tag == .option or tag == .optgroup) return;
|
if (tag == .datalist or tag == .option or tag == .optgroup) return;
|
||||||
|
|
||||||
// Check visibility using the engine's checkVisibility which handles CSS display: none
|
// Check visibility using the engine's checkVisibility which handles CSS display: none
|
||||||
if (!el.checkVisibility(self.page)) {
|
if (!el.checkVisibilityCached(ctx.visibility_cache, self.page)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +168,7 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (el.is(Element.Html)) |html_el| {
|
if (el.is(Element.Html)) |html_el| {
|
||||||
if (interactive.classifyInteractivity(el, html_el, listener_targets) != null) {
|
if (interactive.classifyInteractivity(self.page, el, html_el, ctx.listener_targets, ctx.pointer_events_cache) != null) {
|
||||||
is_interactive = true;
|
is_interactive = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,9 +176,9 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
|||||||
node_name = "root";
|
node_name = "root";
|
||||||
}
|
}
|
||||||
|
|
||||||
const initial_xpath_len = xpath_buffer.items.len;
|
const initial_xpath_len = ctx.xpath_buffer.items.len;
|
||||||
try appendXPathSegment(node, xpath_buffer.writer(self.arena), index);
|
try appendXPathSegment(node, ctx.xpath_buffer.writer(self.arena), index);
|
||||||
const xpath = xpath_buffer.items;
|
const xpath = ctx.xpath_buffer.items;
|
||||||
|
|
||||||
var name = try axn.getName(self.page, self.arena);
|
var name = try axn.getName(self.page, self.arena);
|
||||||
|
|
||||||
@@ -165,18 +196,6 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
|||||||
name = null;
|
name = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = NodeData{
|
|
||||||
.id = cdp_node.id,
|
|
||||||
.axn = axn,
|
|
||||||
.role = role,
|
|
||||||
.name = name,
|
|
||||||
.value = value,
|
|
||||||
.options = options,
|
|
||||||
.xpath = xpath,
|
|
||||||
.is_interactive = is_interactive,
|
|
||||||
.node_name = node_name,
|
|
||||||
};
|
|
||||||
|
|
||||||
var should_visit = true;
|
var should_visit = true;
|
||||||
if (self.interactive_only) {
|
if (self.interactive_only) {
|
||||||
var keep = false;
|
var keep = false;
|
||||||
@@ -208,6 +227,18 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
|||||||
|
|
||||||
var did_visit = false;
|
var did_visit = false;
|
||||||
var should_walk_children = true;
|
var should_walk_children = true;
|
||||||
|
var data: NodeData = .{
|
||||||
|
.id = cdp_node.id,
|
||||||
|
.axn = axn,
|
||||||
|
.role = role,
|
||||||
|
.name = name,
|
||||||
|
.value = value,
|
||||||
|
.options = options,
|
||||||
|
.xpath = xpath,
|
||||||
|
.is_interactive = is_interactive,
|
||||||
|
.node_name = node_name,
|
||||||
|
};
|
||||||
|
|
||||||
if (should_visit) {
|
if (should_visit) {
|
||||||
should_walk_children = try visitor.visit(node, &data);
|
should_walk_children = try visitor.visit(node, &data);
|
||||||
did_visit = true; // Always true if should_visit was true, because visit() executed and opened structures
|
did_visit = true; // Always true if should_visit was true, because visit() executed and opened structures
|
||||||
@@ -233,7 +264,7 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
|||||||
}
|
}
|
||||||
gop.value_ptr.* += 1;
|
gop.value_ptr.* += 1;
|
||||||
|
|
||||||
try self.walk(child, xpath_buffer, name, visitor, gop.value_ptr.*, listener_targets, current_depth + 1);
|
try self.walk(ctx, child, name, visitor, gop.value_ptr.*, current_depth + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,11 +272,11 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
|||||||
try visitor.leave();
|
try visitor.leave();
|
||||||
}
|
}
|
||||||
|
|
||||||
xpath_buffer.shrinkRetainingCapacity(initial_xpath_len);
|
ctx.xpath_buffer.shrinkRetainingCapacity(initial_xpath_len);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extractSelectOptions(node: *Node, page: *Page, arena: std.mem.Allocator) ![]OptionData {
|
fn extractSelectOptions(node: *Node, page: *Page, arena: std.mem.Allocator) ![]OptionData {
|
||||||
var options = std.ArrayListUnmanaged(OptionData){};
|
var options: std.ArrayList(OptionData) = .empty;
|
||||||
var it = node.childrenIterator();
|
var it = node.childrenIterator();
|
||||||
while (it.next()) |child| {
|
while (it.next()) |child| {
|
||||||
if (child.is(Element)) |el| {
|
if (child.is(Element)) |el| {
|
||||||
|
|||||||
@@ -22,12 +22,11 @@ const net = std.net;
|
|||||||
const posix = std.posix;
|
const posix = std.posix;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
|
||||||
|
|
||||||
const log = @import("log.zig");
|
const log = @import("log.zig");
|
||||||
const App = @import("App.zig");
|
const App = @import("App.zig");
|
||||||
const Config = @import("Config.zig");
|
const Config = @import("Config.zig");
|
||||||
const CDP = @import("cdp/cdp.zig").CDP;
|
const CDP = @import("cdp/CDP.zig");
|
||||||
const Net = @import("network/websocket.zig");
|
const Net = @import("network/websocket.zig");
|
||||||
const HttpClient = @import("browser/HttpClient.zig");
|
const HttpClient = @import("browser/HttpClient.zig");
|
||||||
|
|
||||||
@@ -45,7 +44,7 @@ clients_pool: std.heap.MemoryPool(Client),
|
|||||||
|
|
||||||
pub fn init(app: *App, address: net.Address) !*Server {
|
pub fn init(app: *App, address: net.Address) !*Server {
|
||||||
const allocator = app.allocator;
|
const allocator = app.allocator;
|
||||||
const json_version_response = try buildJSONVersionResponse(allocator, address);
|
const json_version_response = try buildJSONVersionResponse(app);
|
||||||
errdefer allocator.free(json_version_response);
|
errdefer allocator.free(json_version_response);
|
||||||
|
|
||||||
const self = try allocator.create(Server);
|
const self = try allocator.create(Server);
|
||||||
@@ -212,7 +211,7 @@ pub const Client = struct {
|
|||||||
http: *HttpClient,
|
http: *HttpClient,
|
||||||
ws: Net.WsConnection,
|
ws: Net.WsConnection,
|
||||||
|
|
||||||
fn init(
|
pub fn init(
|
||||||
socket: posix.socket_t,
|
socket: posix.socket_t,
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
app: *App,
|
app: *App,
|
||||||
@@ -250,7 +249,7 @@ pub const Client = struct {
|
|||||||
self.ws.shutdown();
|
self.ws.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deinit(self: *Client) void {
|
pub fn deinit(self: *Client) void {
|
||||||
switch (self.mode) {
|
switch (self.mode) {
|
||||||
.cdp => |*cdp| cdp.deinit(),
|
.cdp => |*cdp| cdp.deinit(),
|
||||||
.http => {},
|
.http => {},
|
||||||
@@ -302,15 +301,8 @@ pub const Client = struct {
|
|||||||
var ms_remaining = self.ws.timeout_ms;
|
var ms_remaining = self.ws.timeout_ms;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
switch (cdp.pageWait(ms_remaining)) {
|
const result = cdp.pageWait(ms_remaining) catch |wait_err| switch (wait_err) {
|
||||||
.cdp_socket => {
|
error.NoPage => {
|
||||||
if (self.readSocket() == false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
last_message = milliTimestamp(.monotonic);
|
|
||||||
ms_remaining = self.ws.timeout_ms;
|
|
||||||
},
|
|
||||||
.no_page => {
|
|
||||||
const status = http.tick(ms_remaining) catch |err| {
|
const status = http.tick(ms_remaining) catch |err| {
|
||||||
log.err(.app, "http tick", .{ .err = err });
|
log.err(.app, "http tick", .{ .err = err });
|
||||||
return;
|
return;
|
||||||
@@ -324,6 +316,18 @@ pub const Client = struct {
|
|||||||
}
|
}
|
||||||
last_message = milliTimestamp(.monotonic);
|
last_message = milliTimestamp(.monotonic);
|
||||||
ms_remaining = self.ws.timeout_ms;
|
ms_remaining = self.ws.timeout_ms;
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
else => return wait_err,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (result) {
|
||||||
|
.cdp_socket => {
|
||||||
|
if (self.readSocket() == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
last_message = milliTimestamp(.monotonic);
|
||||||
|
ms_remaining = self.ws.timeout_ms;
|
||||||
},
|
},
|
||||||
.done => {
|
.done => {
|
||||||
const now = milliTimestamp(.monotonic);
|
const now = milliTimestamp(.monotonic);
|
||||||
@@ -456,7 +460,7 @@ pub const Client = struct {
|
|||||||
|
|
||||||
fn upgradeConnection(self: *Client, request: []u8) !void {
|
fn upgradeConnection(self: *Client, request: []u8) !void {
|
||||||
try self.ws.upgrade(request);
|
try self.ws.upgrade(request);
|
||||||
self.mode = .{ .cdp = try CDP.init(self.app, self.http, self) };
|
self.mode = .{ .cdp = try CDP.init(self) };
|
||||||
}
|
}
|
||||||
|
|
||||||
fn writeHTTPErrorResponse(self: *Client, comptime status: u16, comptime body: []const u8) void {
|
fn writeHTTPErrorResponse(self: *Client, comptime status: u16, comptime body: []const u8) void {
|
||||||
@@ -484,11 +488,17 @@ pub const Client = struct {
|
|||||||
// --------
|
// --------
|
||||||
|
|
||||||
fn buildJSONVersionResponse(
|
fn buildJSONVersionResponse(
|
||||||
allocator: Allocator,
|
app: *const App,
|
||||||
address: net.Address,
|
|
||||||
) ![]const u8 {
|
) ![]const u8 {
|
||||||
const body_format = "{{\"webSocketDebuggerUrl\": \"ws://{f}/\"}}";
|
const port = app.config.port();
|
||||||
const body_len = std.fmt.count(body_format, .{address});
|
const host = app.config.advertiseHost();
|
||||||
|
if (std.mem.eql(u8, host, "0.0.0.0")) {
|
||||||
|
log.info(.cdp, "unreachable advertised host", .{
|
||||||
|
.message = "when --host is set to 0.0.0.0 consider setting --advertise-host to a reachable address",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const body_format = "{{\"webSocketDebuggerUrl\": \"ws://{s}:{d}/\"}}";
|
||||||
|
const body_len = std.fmt.count(body_format, .{ host, port });
|
||||||
|
|
||||||
// We send a Connection: Close (and actually close the connection)
|
// We send a Connection: Close (and actually close the connection)
|
||||||
// because chromedp (Go driver) sends a request to /json/version and then
|
// because chromedp (Go driver) sends a request to /json/version and then
|
||||||
@@ -502,23 +512,22 @@ fn buildJSONVersionResponse(
|
|||||||
"Connection: Close\r\n" ++
|
"Connection: Close\r\n" ++
|
||||||
"Content-Type: application/json; charset=UTF-8\r\n\r\n" ++
|
"Content-Type: application/json; charset=UTF-8\r\n\r\n" ++
|
||||||
body_format;
|
body_format;
|
||||||
return try std.fmt.allocPrint(allocator, response_format, .{ body_len, address });
|
return try std.fmt.allocPrint(app.allocator, response_format, .{ body_len, host, port });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const timestamp = @import("datetime.zig").timestamp;
|
pub const timestamp = @import("datetime.zig").timestamp;
|
||||||
pub const milliTimestamp = @import("datetime.zig").milliTimestamp;
|
pub const milliTimestamp = @import("datetime.zig").milliTimestamp;
|
||||||
|
|
||||||
const testing = std.testing;
|
const testing = @import("testing.zig");
|
||||||
test "server: buildJSONVersionResponse" {
|
test "server: buildJSONVersionResponse" {
|
||||||
const address = try net.Address.parseIp4("127.0.0.1", 9001);
|
const res = try buildJSONVersionResponse(testing.test_app);
|
||||||
const res = try buildJSONVersionResponse(testing.allocator, address);
|
defer testing.test_app.allocator.free(res);
|
||||||
defer testing.allocator.free(res);
|
|
||||||
|
|
||||||
try testing.expectEqualStrings("HTTP/1.1 200 OK\r\n" ++
|
try testing.expectEqual("HTTP/1.1 200 OK\r\n" ++
|
||||||
"Content-Length: 48\r\n" ++
|
"Content-Length: 48\r\n" ++
|
||||||
"Connection: Close\r\n" ++
|
"Connection: Close\r\n" ++
|
||||||
"Content-Type: application/json; charset=UTF-8\r\n\r\n" ++
|
"Content-Type: application/json; charset=UTF-8\r\n\r\n" ++
|
||||||
"{\"webSocketDebuggerUrl\": \"ws://127.0.0.1:9001/\"}", res);
|
"{\"webSocketDebuggerUrl\": \"ws://127.0.0.1:9222/\"}", res);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Client: http invalid request" {
|
test "Client: http invalid request" {
|
||||||
@@ -526,7 +535,7 @@ test "Client: http invalid request" {
|
|||||||
defer c.deinit();
|
defer c.deinit();
|
||||||
|
|
||||||
const res = try c.httpRequest("GET /over/9000 HTTP/1.1\r\n" ++ "Header: " ++ ("a" ** 4100) ++ "\r\n\r\n");
|
const res = try c.httpRequest("GET /over/9000 HTTP/1.1\r\n" ++ "Header: " ++ ("a" ** 4100) ++ "\r\n\r\n");
|
||||||
try testing.expectEqualStrings("HTTP/1.1 413 \r\n" ++
|
try testing.expectEqual("HTTP/1.1 413 \r\n" ++
|
||||||
"Connection: Close\r\n" ++
|
"Connection: Close\r\n" ++
|
||||||
"Content-Length: 17\r\n\r\n" ++
|
"Content-Length: 17\r\n\r\n" ++
|
||||||
"Request too large", res);
|
"Request too large", res);
|
||||||
@@ -595,7 +604,7 @@ test "Client: http valid handshake" {
|
|||||||
"Custom: Header-Value\r\n\r\n";
|
"Custom: Header-Value\r\n\r\n";
|
||||||
|
|
||||||
const res = try c.httpRequest(request);
|
const res = try c.httpRequest(request);
|
||||||
try testing.expectEqualStrings("HTTP/1.1 101 Switching Protocols\r\n" ++
|
try testing.expectEqual("HTTP/1.1 101 Switching Protocols\r\n" ++
|
||||||
"Upgrade: websocket\r\n" ++
|
"Upgrade: websocket\r\n" ++
|
||||||
"Connection: upgrade\r\n" ++
|
"Connection: upgrade\r\n" ++
|
||||||
"Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\r\n\r\n", res);
|
"Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\r\n\r\n", res);
|
||||||
@@ -723,7 +732,7 @@ test "server: 404" {
|
|||||||
defer c.deinit();
|
defer c.deinit();
|
||||||
|
|
||||||
const res = try c.httpRequest("GET /unknown HTTP/1.1\r\n\r\n");
|
const res = try c.httpRequest("GET /unknown HTTP/1.1\r\n\r\n");
|
||||||
try testing.expectEqualStrings("HTTP/1.1 404 \r\n" ++
|
try testing.expectEqual("HTTP/1.1 404 \r\n" ++
|
||||||
"Connection: Close\r\n" ++
|
"Connection: Close\r\n" ++
|
||||||
"Content-Length: 9\r\n\r\n" ++
|
"Content-Length: 9\r\n\r\n" ++
|
||||||
"Not found", res);
|
"Not found", res);
|
||||||
@@ -735,7 +744,7 @@ test "server: get /json/version" {
|
|||||||
"Content-Length: 48\r\n" ++
|
"Content-Length: 48\r\n" ++
|
||||||
"Connection: Close\r\n" ++
|
"Connection: Close\r\n" ++
|
||||||
"Content-Type: application/json; charset=UTF-8\r\n\r\n" ++
|
"Content-Type: application/json; charset=UTF-8\r\n\r\n" ++
|
||||||
"{\"webSocketDebuggerUrl\": \"ws://127.0.0.1:9583/\"}";
|
"{\"webSocketDebuggerUrl\": \"ws://127.0.0.1:9222/\"}";
|
||||||
|
|
||||||
{
|
{
|
||||||
// twice on the same connection
|
// twice on the same connection
|
||||||
@@ -743,7 +752,7 @@ test "server: get /json/version" {
|
|||||||
defer c.deinit();
|
defer c.deinit();
|
||||||
|
|
||||||
const res1 = try c.httpRequest("GET /json/version HTTP/1.1\r\n\r\n");
|
const res1 = try c.httpRequest("GET /json/version HTTP/1.1\r\n\r\n");
|
||||||
try testing.expectEqualStrings(expected_response, res1);
|
try testing.expectEqual(expected_response, res1);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -752,7 +761,7 @@ test "server: get /json/version" {
|
|||||||
defer c.deinit();
|
defer c.deinit();
|
||||||
|
|
||||||
const res1 = try c.httpRequest("GET /json/version HTTP/1.1\r\n\r\n");
|
const res1 = try c.httpRequest("GET /json/version HTTP/1.1\r\n\r\n");
|
||||||
try testing.expectEqualStrings(expected_response, res1);
|
try testing.expectEqual(expected_response, res1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,7 +779,7 @@ fn assertHTTPError(
|
|||||||
.{ expected_status, expected_body.len, expected_body },
|
.{ expected_status, expected_body.len, expected_body },
|
||||||
);
|
);
|
||||||
|
|
||||||
try testing.expectEqualStrings(expected_response, res);
|
try testing.expectEqual(expected_response, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assertWebSocketError(close_code: u16, input: []const u8) !void {
|
fn assertWebSocketError(close_code: u16, input: []const u8) !void {
|
||||||
@@ -914,7 +923,7 @@ const TestClient = struct {
|
|||||||
"Custom: Header-Value\r\n\r\n";
|
"Custom: Header-Value\r\n\r\n";
|
||||||
|
|
||||||
const res = try self.httpRequest(request);
|
const res = try self.httpRequest(request);
|
||||||
try testing.expectEqualStrings("HTTP/1.1 101 Switching Protocols\r\n" ++
|
try testing.expectEqual("HTTP/1.1 101 Switching Protocols\r\n" ++
|
||||||
"Upgrade: websocket\r\n" ++
|
"Upgrade: websocket\r\n" ++
|
||||||
"Connection: upgrade\r\n" ++
|
"Connection: upgrade\r\n" ++
|
||||||
"Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\r\n\r\n", res);
|
"Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\r\n\r\n", res);
|
||||||
|
|||||||
@@ -19,17 +19,13 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
|
||||||
|
|
||||||
const js = @import("js/js.zig");
|
const js = @import("js/js.zig");
|
||||||
const log = @import("../log.zig");
|
|
||||||
const App = @import("../App.zig");
|
const App = @import("../App.zig");
|
||||||
const HttpClient = @import("HttpClient.zig");
|
const HttpClient = @import("HttpClient.zig");
|
||||||
|
|
||||||
const ArenaPool = App.ArenaPool;
|
const ArenaPool = App.ArenaPool;
|
||||||
|
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
|
||||||
|
|
||||||
const Session = @import("Session.zig");
|
const Session = @import("Session.zig");
|
||||||
const Notification = @import("../Notification.zig");
|
const Notification = @import("../Notification.zig");
|
||||||
|
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat
|
|||||||
|
|
||||||
pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, comptime opts: DispatchOpts) DispatchError!void {
|
pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, comptime opts: DispatchOpts) DispatchError!void {
|
||||||
event.acquireRef();
|
event.acquireRef();
|
||||||
defer event.deinit(false, self.page._session);
|
defer _ = event.releaseRef(self.page._session);
|
||||||
|
|
||||||
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 });
|
||||||
@@ -240,7 +240,7 @@ pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event,
|
|||||||
defer window._current_event = prev_event;
|
defer window._current_event = prev_event;
|
||||||
|
|
||||||
event.acquireRef();
|
event.acquireRef();
|
||||||
defer event.deinit(false, page._session);
|
defer _ = event.releaseRef(page._session);
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.event, "dispatchDirect", .{ .type = event._type_string, .context = opts.context });
|
log.debug(.event, "dispatchDirect", .{ .type = event._type_string, .context = opts.context });
|
||||||
@@ -425,7 +425,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
|
|||||||
ls.deinit();
|
ls.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
const activation_state = ActivationState.create(event, target, page);
|
const activation_state = try ActivationState.create(event, target, page);
|
||||||
|
|
||||||
// Defer runs even on early return - ensures event phase is reset
|
// Defer runs even on early return - ensures event phase is reset
|
||||||
// and default actions execute (unless prevented)
|
// and default actions execute (unless prevented)
|
||||||
@@ -820,7 +820,7 @@ const ActivationState = struct {
|
|||||||
|
|
||||||
const Input = Element.Html.Input;
|
const Input = Element.Html.Input;
|
||||||
|
|
||||||
fn create(event: *const Event, target: *Node, page: *Page) ?ActivationState {
|
fn create(event: *const Event, target: *Node, page: *Page) !?ActivationState {
|
||||||
if (event._type_string.eql(comptime .wrap("click")) == false) {
|
if (event._type_string.eql(comptime .wrap("click")) == false) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ fn eventInit(arena: Allocator, typ: String, value: anytype) !Event {
|
|||||||
const time_stamp = (raw_timestamp / 2) * 2;
|
const time_stamp = (raw_timestamp / 2) * 2;
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
._rc = 0,
|
._rc = .{},
|
||||||
._arena = arena,
|
._arena = arena,
|
||||||
._type = unionInit(Event.Type, value),
|
._type = unionInit(Event.Type, value),
|
||||||
._type_string = typ,
|
._type_string = typ,
|
||||||
@@ -255,6 +255,7 @@ pub fn blob(_: *const Factory, arena: Allocator, child: anytype) !*@TypeOf(child
|
|||||||
|
|
||||||
const blob_ptr = chain.get(0);
|
const blob_ptr = chain.get(0);
|
||||||
blob_ptr.* = .{
|
blob_ptr.* = .{
|
||||||
|
._rc = .{},
|
||||||
._arena = arena,
|
._arena = arena,
|
||||||
._type = unionInit(Blob.Type, chain.get(1)),
|
._type = unionInit(Blob.Type, chain.get(1)),
|
||||||
._slice = "",
|
._slice = "",
|
||||||
@@ -271,7 +272,7 @@ pub fn abstractRange(_: *const Factory, arena: Allocator, child: anytype, page:
|
|||||||
const doc = page.document.asNode();
|
const doc = page.document.asNode();
|
||||||
const abstract_range = chain.get(0);
|
const abstract_range = chain.get(0);
|
||||||
abstract_range.* = AbstractRange{
|
abstract_range.* = AbstractRange{
|
||||||
._rc = 0,
|
._rc = .{},
|
||||||
._arena = arena,
|
._arena = arena,
|
||||||
._page_id = page.id,
|
._page_id = page.id,
|
||||||
._type = unionInit(AbstractRange.Type, chain.get(1)),
|
._type = unionInit(AbstractRange.Type, chain.get(1)),
|
||||||
|
|||||||
@@ -24,17 +24,10 @@ const lp = @import("lightpanda");
|
|||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
const Net = @import("../network/http.zig");
|
const Net = @import("../network/http.zig");
|
||||||
const Network = @import("../network/Runtime.zig");
|
const Network = @import("../network/Runtime.zig");
|
||||||
const Config = @import("../Config.zig");
|
|
||||||
const URL = @import("../browser/URL.zig");
|
const URL = @import("../browser/URL.zig");
|
||||||
const Notification = @import("../Notification.zig");
|
const Notification = @import("../Notification.zig");
|
||||||
const CookieJar = @import("../browser/webapi/storage/Cookie.zig").Jar;
|
const CookieJar = @import("../browser/webapi/storage/Cookie.zig").Jar;
|
||||||
const Robots = @import("../network/Robots.zig");
|
const Robots = @import("../network/Robots.zig");
|
||||||
const RobotStore = Robots.RobotStore;
|
|
||||||
const WebBotAuth = @import("../network/WebBotAuth.zig");
|
|
||||||
|
|
||||||
const Cache = @import("../network/cache/Cache.zig");
|
|
||||||
const CacheMetadata = Cache.CachedMetadata;
|
|
||||||
const CachedResponse = Cache.CachedResponse;
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
@@ -293,69 +286,7 @@ pub fn request(self: *Client, req: Request) !void {
|
|||||||
return self.fetchRobotsThenProcessRequest(robots_url, req);
|
return self.fetchRobotsThenProcessRequest(robots_url, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serveFromCache(req: Request, cached: *const CachedResponse) !void {
|
|
||||||
const response = Response.fromCached(req.ctx, cached);
|
|
||||||
|
|
||||||
if (req.start_callback) |cb| {
|
|
||||||
try cb(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
const proceed = try req.header_callback(response);
|
|
||||||
if (!proceed) {
|
|
||||||
switch (cached.data) {
|
|
||||||
.buffer => |_| {},
|
|
||||||
.file => |file| file.close(),
|
|
||||||
}
|
|
||||||
req.error_callback(req.ctx, error.Abort);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (cached.data) {
|
|
||||||
.buffer => |data| {
|
|
||||||
if (data.len > 0) {
|
|
||||||
try req.data_callback(response, data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.file => |file| {
|
|
||||||
defer file.close();
|
|
||||||
var buf: [1024]u8 = undefined;
|
|
||||||
var file_reader = file.reader(&buf);
|
|
||||||
|
|
||||||
const reader = &file_reader.interface;
|
|
||||||
var read_buf: [1024]u8 = undefined;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const curr = try reader.readSliceShort(&read_buf);
|
|
||||||
if (curr == 0) break;
|
|
||||||
try req.data_callback(response, read_buf[0..curr]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
try req.done_callback(req.ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn processRequest(self: *Client, req: Request) !void {
|
fn processRequest(self: *Client, req: Request) !void {
|
||||||
if (self.network.cache) |*cache| {
|
|
||||||
if (req.method == .GET) {
|
|
||||||
const arena = try self.network.app.arena_pool.acquire();
|
|
||||||
defer self.network.app.arena_pool.release(arena);
|
|
||||||
|
|
||||||
if (cache.get(arena, .{ .url = req.url, .timestamp = std.time.timestamp() })) |cached| {
|
|
||||||
log.debug(.browser, "http.cache.get", .{
|
|
||||||
.url = req.url,
|
|
||||||
.found = true,
|
|
||||||
.metadata = cached.metadata,
|
|
||||||
});
|
|
||||||
|
|
||||||
defer req.headers.deinit();
|
|
||||||
return serveFromCache(req, &cached);
|
|
||||||
} else {
|
|
||||||
log.debug(.browser, "http.cache.get", .{ .url = req.url, .found = false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const transfer = try self.makeTransfer(req);
|
const transfer = try self.makeTransfer(req);
|
||||||
|
|
||||||
transfer.req.notification.dispatch(.http_request_start, &.{ .transfer = transfer });
|
transfer.req.notification.dispatch(.http_request_start, &.{ .transfer = transfer });
|
||||||
@@ -442,10 +373,8 @@ fn fetchRobotsThenProcessRequest(self: *Client, robots_url: [:0]const u8, req: R
|
|||||||
try entry.value_ptr.append(self.allocator, req);
|
try entry.value_ptr.append(self.allocator, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn robotsHeaderCallback(response: Response) !bool {
|
fn robotsHeaderCallback(transfer: *Transfer) !bool {
|
||||||
const ctx: *RobotsRequestContext = @ptrCast(@alignCast(response.ctx));
|
const ctx: *RobotsRequestContext = @ptrCast(@alignCast(transfer.ctx));
|
||||||
// Robots callbacks only happen on real live requests.
|
|
||||||
const transfer = response.inner.live;
|
|
||||||
|
|
||||||
if (transfer.response_header) |hdr| {
|
if (transfer.response_header) |hdr| {
|
||||||
log.debug(.browser, "robots status", .{ .status = hdr.status, .robots_url = ctx.robots_url });
|
log.debug(.browser, "robots status", .{ .status = hdr.status, .robots_url = ctx.robots_url });
|
||||||
@@ -459,8 +388,8 @@ fn robotsHeaderCallback(response: Response) !bool {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn robotsDataCallback(response: Response, data: []const u8) !void {
|
fn robotsDataCallback(transfer: *Transfer, data: []const u8) !void {
|
||||||
const ctx: *RobotsRequestContext = @ptrCast(@alignCast(response.ctx));
|
const ctx: *RobotsRequestContext = @ptrCast(@alignCast(transfer.ctx));
|
||||||
try ctx.buffer.appendSlice(ctx.client.allocator, data);
|
try ctx.buffer.appendSlice(ctx.client.allocator, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -679,6 +608,7 @@ fn makeTransfer(self: *Client, req: Request) !*Transfer {
|
|||||||
.id = id,
|
.id = id,
|
||||||
.url = req.url,
|
.url = req.url,
|
||||||
.req = req,
|
.req = req,
|
||||||
|
.ctx = req.ctx,
|
||||||
.client = self,
|
.client = self,
|
||||||
.max_response_size = self.network.config.httpMaxResponseSize(),
|
.max_response_size = self.network.config.httpMaxResponseSize(),
|
||||||
};
|
};
|
||||||
@@ -701,9 +631,9 @@ fn requestFailed(transfer: *Transfer, err: anyerror, comptime execute_callback:
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (execute_callback) {
|
if (execute_callback) {
|
||||||
transfer.req.error_callback(transfer.req.ctx, err);
|
transfer.req.error_callback(transfer.ctx, err);
|
||||||
} else if (transfer.req.shutdown_callback) |cb| {
|
} else if (transfer.req.shutdown_callback) |cb| {
|
||||||
cb(transfer.req.ctx);
|
cb(transfer.ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,7 +740,7 @@ fn makeRequest(self: *Client, conn: *Net.Connection, transfer: *Transfer) anyerr
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (req.start_callback) |cb| {
|
if (req.start_callback) |cb| {
|
||||||
cb(Response.fromLive(transfer)) catch |err| {
|
cb(transfer) catch |err| {
|
||||||
transfer.deinit();
|
transfer.deinit();
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
@@ -915,14 +845,28 @@ fn processMessages(self: *Client) !bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the server sends "Connection: close" and closes the TLS
|
||||||
|
// connection without a close_notify alert, BoringSSL reports
|
||||||
|
// RecvError. If we already received valid HTTP headers, this is
|
||||||
|
// a normal end-of-body (the connection closure signals the end
|
||||||
|
// of the response per HTTP/1.1 when there is no Content-Length).
|
||||||
|
// We must check this before endTransfer, which may reset the
|
||||||
|
// easy handle.
|
||||||
|
const is_conn_close_recv = blk: {
|
||||||
|
const err = msg.err orelse break :blk false;
|
||||||
|
if (err != error.RecvError) break :blk false;
|
||||||
|
const hdr = msg.conn.getResponseHeader("connection", 0) orelse break :blk false;
|
||||||
|
break :blk std.ascii.eqlIgnoreCase(hdr.value, "close");
|
||||||
|
};
|
||||||
|
|
||||||
// release it ASAP so that it's available; some done_callbacks
|
// release it ASAP so that it's available; some done_callbacks
|
||||||
// will load more resources.
|
// will load more resources.
|
||||||
self.endTransfer(transfer);
|
self.endTransfer(transfer);
|
||||||
|
|
||||||
defer transfer.deinit();
|
defer transfer.deinit();
|
||||||
|
|
||||||
if (msg.err) |err| {
|
if (msg.err != null and !is_conn_close_recv) {
|
||||||
requestFailed(transfer, err, true);
|
requestFailed(transfer, msg.err.?, true);
|
||||||
} else blk: {
|
} else blk: {
|
||||||
// make sure the transfer can't be immediately aborted from a callback
|
// make sure the transfer can't be immediately aborted from a callback
|
||||||
// since we still need it here.
|
// since we still need it here.
|
||||||
@@ -942,31 +886,18 @@ fn processMessages(self: *Client) !bool {
|
|||||||
break :blk;
|
break :blk;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
transfer.req.done_callback(transfer.req.ctx) catch |err| {
|
transfer.req.done_callback(transfer.ctx) catch |err| {
|
||||||
// transfer isn't valid at this point, don't use it.
|
// transfer isn't valid at this point, don't use it.
|
||||||
log.err(.http, "done_callback", .{ .err = err });
|
log.err(.http, "done_callback", .{ .err = err });
|
||||||
requestFailed(transfer, err, true);
|
requestFailed(transfer, err, true);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (transfer.pending_cache_metadata) |metadata| {
|
transfer.req.notification.dispatch(.http_request_done, &.{
|
||||||
const cache = &self.network.cache.?;
|
.transfer = transfer,
|
||||||
|
});
|
||||||
// TODO: Support Vary Keying
|
processed = true;
|
||||||
const cache_key = transfer.req.url;
|
|
||||||
|
|
||||||
log.debug(.browser, "http cache", .{ .key = cache_key, .metadata = metadata });
|
|
||||||
cache.put(metadata, transfer.pending_cache_body.items) catch |err| {
|
|
||||||
log.warn(.http, "cache put failed", .{ .err = err });
|
|
||||||
};
|
|
||||||
log.debug(.browser, "http.cache.put", .{ .url = transfer.req.url });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transfer.req.notification.dispatch(.http_request_done, &.{
|
|
||||||
.transfer = transfer,
|
|
||||||
});
|
|
||||||
processed = true;
|
|
||||||
}
|
}
|
||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
@@ -1042,9 +973,9 @@ pub const Request = struct {
|
|||||||
// arbitrary data that can be associated with this request
|
// arbitrary data that can be associated with this request
|
||||||
ctx: *anyopaque = undefined,
|
ctx: *anyopaque = undefined,
|
||||||
|
|
||||||
start_callback: ?*const fn (response: Response) anyerror!void = null,
|
start_callback: ?*const fn (transfer: *Transfer) anyerror!void = null,
|
||||||
header_callback: *const fn (response: Response) anyerror!bool,
|
header_callback: *const fn (transfer: *Transfer) anyerror!bool,
|
||||||
data_callback: *const fn (response: Response, data: []const u8) anyerror!void,
|
data_callback: *const fn (transfer: *Transfer, data: []const u8) anyerror!void,
|
||||||
done_callback: *const fn (ctx: *anyopaque) anyerror!void,
|
done_callback: *const fn (ctx: *anyopaque) anyerror!void,
|
||||||
error_callback: *const fn (ctx: *anyopaque, err: anyerror) void,
|
error_callback: *const fn (ctx: *anyopaque, err: anyerror) void,
|
||||||
shutdown_callback: ?*const fn (ctx: *anyopaque) void = null,
|
shutdown_callback: ?*const fn (ctx: *anyopaque) void = null,
|
||||||
@@ -1072,92 +1003,16 @@ pub const Request = struct {
|
|||||||
|
|
||||||
const AuthChallenge = Net.AuthChallenge;
|
const AuthChallenge = Net.AuthChallenge;
|
||||||
|
|
||||||
pub const Response = struct {
|
|
||||||
ctx: *anyopaque,
|
|
||||||
inner: union(enum) {
|
|
||||||
live: *Transfer,
|
|
||||||
cached: *const CachedResponse,
|
|
||||||
},
|
|
||||||
|
|
||||||
pub fn fromLive(transfer: *Transfer) Response {
|
|
||||||
return .{ .ctx = transfer.req.ctx, .inner = .{ .live = transfer } };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fromCached(ctx: *anyopaque, resp: *const CachedResponse) Response {
|
|
||||||
return .{ .ctx = ctx, .inner = .{ .cached = resp } };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn status(self: Response) ?u16 {
|
|
||||||
return switch (self.inner) {
|
|
||||||
.live => |live| if (live.response_header) |rh| rh.status else null,
|
|
||||||
.cached => |c| c.metadata.status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn contentType(self: Response) ?[]const u8 {
|
|
||||||
return switch (self.inner) {
|
|
||||||
.live => |live| if (live.response_header) |*rh| rh.contentType() else null,
|
|
||||||
.cached => |c| c.metadata.content_type,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn contentLength(self: Response) ?u32 {
|
|
||||||
return switch (self.inner) {
|
|
||||||
.live => |live| live.getContentLength(),
|
|
||||||
.cached => |c| switch (c.data) {
|
|
||||||
.buffer => |buf| @intCast(buf.len),
|
|
||||||
.file => |f| @intCast(f.getEndPos() catch 0),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn redirectCount(self: Response) ?u32 {
|
|
||||||
return switch (self.inner) {
|
|
||||||
.live => |live| if (live.response_header) |rh| rh.redirect_count else null,
|
|
||||||
.cached => 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn url(self: Response) [:0]const u8 {
|
|
||||||
return switch (self.inner) {
|
|
||||||
.live => |live| live.url,
|
|
||||||
.cached => |c| c.metadata.url,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn headerIterator(self: Response) HeaderIterator {
|
|
||||||
return switch (self.inner) {
|
|
||||||
.live => |live| live.responseHeaderIterator(),
|
|
||||||
.cached => |c| HeaderIterator{ .list = .{ .list = c.metadata.headers } },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn abort(self: Response, err: anyerror) void {
|
|
||||||
switch (self.inner) {
|
|
||||||
.live => |live| live.abort(err),
|
|
||||||
.cached => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn terminate(self: Response) void {
|
|
||||||
switch (self.inner) {
|
|
||||||
.live => |live| live.terminate(),
|
|
||||||
.cached => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Transfer = struct {
|
pub const Transfer = struct {
|
||||||
arena: ArenaAllocator,
|
arena: ArenaAllocator,
|
||||||
id: u32 = 0,
|
id: u32 = 0,
|
||||||
req: Request,
|
req: Request,
|
||||||
url: [:0]const u8,
|
url: [:0]const u8,
|
||||||
|
ctx: *anyopaque, // copied from req.ctx to make it easier for callback handlers
|
||||||
client: *Client,
|
client: *Client,
|
||||||
// total bytes received in the response, including the response status line,
|
// total bytes received in the response, including the response status line,
|
||||||
// the headers, and the [encoded] body.
|
// the headers, and the [encoded] body.
|
||||||
bytes_received: usize = 0,
|
bytes_received: usize = 0,
|
||||||
pending_cache_body: std.ArrayList(u8) = .empty,
|
|
||||||
pending_cache_metadata: ?CacheMetadata = null,
|
|
||||||
|
|
||||||
aborted: bool = false,
|
aborted: bool = false,
|
||||||
|
|
||||||
@@ -1209,8 +1064,6 @@ pub const Transfer = struct {
|
|||||||
self._notified_fail = false;
|
self._notified_fail = false;
|
||||||
self.response_header = null;
|
self.response_header = null;
|
||||||
self.bytes_received = 0;
|
self.bytes_received = 0;
|
||||||
self.pending_cache_metadata = null;
|
|
||||||
self.pending_cache_body = .empty;
|
|
||||||
|
|
||||||
self._tries += 1;
|
self._tries += 1;
|
||||||
}
|
}
|
||||||
@@ -1319,7 +1172,7 @@ pub const Transfer = struct {
|
|||||||
self.client.endTransfer(self);
|
self.client.endTransfer(self);
|
||||||
}
|
}
|
||||||
if (self.req.shutdown_callback) |cb| {
|
if (self.req.shutdown_callback) |cb| {
|
||||||
cb(self.req.ctx);
|
cb(self.ctx);
|
||||||
}
|
}
|
||||||
self.deinit();
|
self.deinit();
|
||||||
}
|
}
|
||||||
@@ -1421,48 +1274,11 @@ pub const Transfer = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const proceed = transfer.req.header_callback(Response.fromLive(transfer)) catch |err| {
|
const proceed = transfer.req.header_callback(transfer) catch |err| {
|
||||||
log.err(.http, "header_callback", .{ .err = err, .req = transfer });
|
log.err(.http, "header_callback", .{ .err = err, .req = transfer });
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (transfer.client.network.cache != null and transfer.req.method == .GET) {
|
|
||||||
const rh = &transfer.response_header.?;
|
|
||||||
const allocator = transfer.arena.allocator();
|
|
||||||
|
|
||||||
const maybe_cm = try Cache.tryCache(
|
|
||||||
allocator,
|
|
||||||
std.time.timestamp(),
|
|
||||||
transfer.url,
|
|
||||||
rh.status,
|
|
||||||
rh.contentType(),
|
|
||||||
if (conn.getResponseHeader("cache-control", 0)) |h| h.value else null,
|
|
||||||
if (conn.getResponseHeader("vary", 0)) |h| h.value else null,
|
|
||||||
if (conn.getResponseHeader("etag", 0)) |h| h.value else null,
|
|
||||||
if (conn.getResponseHeader("last-modified", 0)) |h| h.value else null,
|
|
||||||
if (conn.getResponseHeader("age", 0)) |h| h.value else null,
|
|
||||||
conn.getResponseHeader("set-cookie", 0) != null,
|
|
||||||
conn.getResponseHeader("authorization", 0) != null,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (maybe_cm) |cm| {
|
|
||||||
var header_list: std.ArrayList(Net.Header) = .empty;
|
|
||||||
var it = transfer.responseHeaderIterator();
|
|
||||||
while (it.next()) |hdr| {
|
|
||||||
try header_list.append(allocator, .{
|
|
||||||
.name = try allocator.dupe(u8, hdr.name),
|
|
||||||
.value = try allocator.dupe(u8, hdr.value),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
transfer.pending_cache_metadata = cm;
|
|
||||||
transfer.pending_cache_metadata.?.headers = header_list.items;
|
|
||||||
if (transfer.getContentLength()) |cl| {
|
|
||||||
try transfer.pending_cache_body.ensureTotalCapacity(allocator, cl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transfer.req.notification.dispatch(.http_response_header_done, &.{
|
transfer.req.notification.dispatch(.http_response_header_done, &.{
|
||||||
.transfer = transfer,
|
.transfer = transfer,
|
||||||
});
|
});
|
||||||
@@ -1623,14 +1439,7 @@ pub const Transfer = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chunk = buffer[0..chunk_len];
|
const chunk = buffer[0..chunk_len];
|
||||||
if (transfer.pending_cache_metadata != null) {
|
transfer.req.data_callback(transfer, chunk) catch |err| {
|
||||||
transfer.pending_cache_body.appendSlice(transfer.arena.allocator(), chunk) catch |err| {
|
|
||||||
log.err(.http, "cache body append", .{ .err = err, .req = transfer });
|
|
||||||
return Net.writefunc_error;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
transfer.req.data_callback(Response.fromLive(transfer), chunk) catch |err| {
|
|
||||||
log.err(.http, "data_callback", .{ .err = err, .req = transfer });
|
log.err(.http, "data_callback", .{ .err = err, .req = transfer });
|
||||||
return Net.writefunc_error;
|
return Net.writefunc_error;
|
||||||
};
|
};
|
||||||
@@ -1682,7 +1491,7 @@ pub const Transfer = struct {
|
|||||||
fn _fulfill(transfer: *Transfer, status: u16, headers: []const Net.Header, body: ?[]const u8) !void {
|
fn _fulfill(transfer: *Transfer, status: u16, headers: []const Net.Header, body: ?[]const u8) !void {
|
||||||
const req = &transfer.req;
|
const req = &transfer.req;
|
||||||
if (req.start_callback) |cb| {
|
if (req.start_callback) |cb| {
|
||||||
try cb(Response.fromLive(transfer));
|
try cb(transfer);
|
||||||
}
|
}
|
||||||
|
|
||||||
transfer.response_header = .{
|
transfer.response_header = .{
|
||||||
@@ -1701,13 +1510,13 @@ pub const Transfer = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lp.assert(transfer._header_done_called == false, "Transfer.fulfill header_done_called", .{});
|
lp.assert(transfer._header_done_called == false, "Transfer.fulfill header_done_called", .{});
|
||||||
if (try req.header_callback(Response.fromLive(transfer)) == false) {
|
if (try req.header_callback(transfer) == false) {
|
||||||
transfer.abort(error.Abort);
|
transfer.abort(error.Abort);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body) |b| {
|
if (body) |b| {
|
||||||
try req.data_callback(Response.fromLive(transfer), b);
|
try req.data_callback(transfer, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
try req.done_callback(req.ctx);
|
try req.done_callback(req.ctx);
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ charset: [41]u8 = default_charset,
|
|||||||
charset_len: usize = default_charset_len,
|
charset_len: usize = default_charset_len,
|
||||||
is_default_charset: bool = true,
|
is_default_charset: bool = true,
|
||||||
|
|
||||||
type_buf: [127]u8 = @splat(0),
|
|
||||||
sub_type_buf: [127]u8 = @splat(0),
|
|
||||||
|
|
||||||
/// String "UTF-8" continued by null characters.
|
/// String "UTF-8" continued by null characters.
|
||||||
const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
|
const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
|
||||||
const default_charset_len = 5;
|
const default_charset_len = 5;
|
||||||
@@ -64,10 +61,7 @@ pub const ContentType = union(ContentTypeEnum) {
|
|||||||
image_webp: void,
|
image_webp: void,
|
||||||
application_json: void,
|
application_json: void,
|
||||||
unknown: void,
|
unknown: void,
|
||||||
other: struct {
|
other: struct { type: []const u8, sub_type: []const u8 },
|
||||||
type: []const u8,
|
|
||||||
sub_type: []const u8,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn contentTypeString(mime: *const Mime) []const u8 {
|
pub fn contentTypeString(mime: *const Mime) []const u8 {
|
||||||
@@ -118,18 +112,17 @@ fn parseCharset(value: []const u8) error{ CharsetTooBig, Invalid }![]const u8 {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(input: []const u8) !Mime {
|
pub fn parse(input: []u8) !Mime {
|
||||||
if (input.len > 255) {
|
if (input.len > 255) {
|
||||||
return error.TooBig;
|
return error.TooBig;
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf: [255]u8 = undefined;
|
// Zig's trim API is broken. The return type is always `[]const u8`,
|
||||||
const normalized = std.ascii.lowerString(&buf, std.mem.trim(u8, input, &std.ascii.whitespace));
|
// even if the input type is `[]u8`. @constCast is safe here.
|
||||||
|
var normalized = @constCast(std.mem.trim(u8, input, &std.ascii.whitespace));
|
||||||
_ = std.ascii.lowerString(normalized, normalized);
|
_ = std.ascii.lowerString(normalized, normalized);
|
||||||
|
|
||||||
var mime = Mime{ .content_type = undefined };
|
const content_type, const type_len = try parseContentType(normalized);
|
||||||
|
|
||||||
const content_type, const type_len = try parseContentType(normalized, &mime.type_buf, &mime.sub_type_buf);
|
|
||||||
if (type_len >= normalized.len) {
|
if (type_len >= normalized.len) {
|
||||||
return .{ .content_type = content_type };
|
return .{ .content_type = content_type };
|
||||||
}
|
}
|
||||||
@@ -170,12 +163,13 @@ pub fn parse(input: []const u8) !Mime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mime.params = params;
|
return .{
|
||||||
mime.charset = charset;
|
.params = params,
|
||||||
mime.charset_len = charset_len;
|
.charset = charset,
|
||||||
mime.content_type = content_type;
|
.charset_len = charset_len,
|
||||||
mime.is_default_charset = !has_explicit_charset;
|
.content_type = content_type,
|
||||||
return mime;
|
.is_default_charset = !has_explicit_charset,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prescan the first 1024 bytes of an HTML document for a charset declaration.
|
/// Prescan the first 1024 bytes of an HTML document for a charset declaration.
|
||||||
@@ -392,8 +386,16 @@ pub fn isHTML(self: *const Mime) bool {
|
|||||||
return self.content_type == .text_html;
|
return self.content_type == .text_html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn isText(mime: *const Mime) bool {
|
||||||
|
return switch (mime.content_type) {
|
||||||
|
.text_xml, .text_html, .text_javascript, .text_plain, .text_css => true,
|
||||||
|
.application_json => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// we expect value to be lowercase
|
// we expect value to be lowercase
|
||||||
fn parseContentType(value: []const u8, type_buf: []u8, sub_type_buf: []u8) !struct { ContentType, usize } {
|
fn parseContentType(value: []const u8) !struct { ContentType, usize } {
|
||||||
const end = std.mem.indexOfScalarPos(u8, value, 0, ';') orelse value.len;
|
const end = std.mem.indexOfScalarPos(u8, value, 0, ';') orelse value.len;
|
||||||
const type_name = trimRight(value[0..end]);
|
const type_name = trimRight(value[0..end]);
|
||||||
const attribute_start = end + 1;
|
const attribute_start = end + 1;
|
||||||
@@ -442,18 +444,10 @@ fn parseContentType(value: []const u8, type_buf: []u8, sub_type_buf: []u8) !stru
|
|||||||
return error.Invalid;
|
return error.Invalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@memcpy(type_buf[0..main_type.len], main_type);
|
return .{ .{ .other = .{
|
||||||
@memcpy(sub_type_buf[0..sub_type.len], sub_type);
|
.type = main_type,
|
||||||
|
.sub_type = sub_type,
|
||||||
return .{
|
} }, attribute_start };
|
||||||
.{
|
|
||||||
.other = .{
|
|
||||||
.type = type_buf[0..main_type.len],
|
|
||||||
.sub_type = sub_type_buf[0..sub_type.len],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
attribute_start,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const VALID_CODEPOINTS = blk: {
|
const VALID_CODEPOINTS = blk: {
|
||||||
@@ -467,13 +461,6 @@ const VALID_CODEPOINTS = blk: {
|
|||||||
break :blk v;
|
break :blk v;
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn typeString(self: *const Mime) []const u8 {
|
|
||||||
return switch (self.content_type) {
|
|
||||||
.other => |o| o.type[0..o.type_len],
|
|
||||||
else => "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validType(value: []const u8) bool {
|
fn validType(value: []const u8) bool {
|
||||||
for (value) |b| {
|
for (value) |b| {
|
||||||
if (VALID_CODEPOINTS[b] == false) {
|
if (VALID_CODEPOINTS[b] == false) {
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ const IS_DEBUG = builtin.mode == .Debug;
|
|||||||
|
|
||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
|
|
||||||
const App = @import("../App.zig");
|
|
||||||
const String = @import("../string.zig").String;
|
const String = @import("../string.zig").String;
|
||||||
|
|
||||||
const Mime = @import("Mime.zig");
|
const Mime = @import("Mime.zig");
|
||||||
@@ -35,6 +34,7 @@ const Factory = @import("Factory.zig");
|
|||||||
const Session = @import("Session.zig");
|
const Session = @import("Session.zig");
|
||||||
const EventManager = @import("EventManager.zig");
|
const EventManager = @import("EventManager.zig");
|
||||||
const ScriptManager = @import("ScriptManager.zig");
|
const ScriptManager = @import("ScriptManager.zig");
|
||||||
|
const StyleManager = @import("StyleManager.zig");
|
||||||
|
|
||||||
const Parser = @import("parser/Parser.zig");
|
const Parser = @import("parser/Parser.zig");
|
||||||
|
|
||||||
@@ -42,7 +42,6 @@ const URL = @import("URL.zig");
|
|||||||
const Blob = @import("webapi/Blob.zig");
|
const Blob = @import("webapi/Blob.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 EventTarget = @import("webapi/EventTarget.zig");
|
|
||||||
const CData = @import("webapi/CData.zig");
|
const CData = @import("webapi/CData.zig");
|
||||||
const Element = @import("webapi/Element.zig");
|
const Element = @import("webapi/Element.zig");
|
||||||
const HtmlElement = @import("webapi/element/Html.zig");
|
const HtmlElement = @import("webapi/element/Html.zig");
|
||||||
@@ -58,14 +57,13 @@ const AbstractRange = @import("webapi/AbstractRange.zig");
|
|||||||
const MutationObserver = @import("webapi/MutationObserver.zig");
|
const MutationObserver = @import("webapi/MutationObserver.zig");
|
||||||
const IntersectionObserver = @import("webapi/IntersectionObserver.zig");
|
const IntersectionObserver = @import("webapi/IntersectionObserver.zig");
|
||||||
const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig");
|
const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig");
|
||||||
const storage = @import("webapi/storage/storage.zig");
|
|
||||||
const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
|
const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
|
||||||
|
const SubmitEvent = @import("webapi/event/SubmitEvent.zig");
|
||||||
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
|
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
|
||||||
const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
|
const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
|
||||||
const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
||||||
|
|
||||||
const HttpClient = @import("HttpClient.zig");
|
const HttpClient = @import("HttpClient.zig");
|
||||||
const ArenaPool = App.ArenaPool;
|
|
||||||
|
|
||||||
const timestamp = @import("../datetime.zig").timestamp;
|
const timestamp = @import("../datetime.zig").timestamp;
|
||||||
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
||||||
@@ -144,6 +142,7 @@ _blob_urls: std.StringHashMapUnmanaged(*Blob) = .{},
|
|||||||
/// A call to `documentIsComplete` (which calls `_documentIsComplete`) resets it.
|
/// A call to `documentIsComplete` (which calls `_documentIsComplete`) resets it.
|
||||||
_to_load: std.ArrayList(*Element.Html) = .{},
|
_to_load: std.ArrayList(*Element.Html) = .{},
|
||||||
|
|
||||||
|
_style_manager: StyleManager,
|
||||||
_script_manager: ScriptManager,
|
_script_manager: ScriptManager,
|
||||||
|
|
||||||
// List of active live ranges (for mutation updates per DOM spec)
|
// List of active live ranges (for mutation updates per DOM spec)
|
||||||
@@ -269,6 +268,7 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void
|
|||||||
._factory = factory,
|
._factory = factory,
|
||||||
._pending_loads = 1, // always 1 for the ScriptManager
|
._pending_loads = 1, // always 1 for the ScriptManager
|
||||||
._type = if (parent == null) .root else .frame,
|
._type = if (parent == null) .root else .frame,
|
||||||
|
._style_manager = undefined,
|
||||||
._script_manager = undefined,
|
._script_manager = undefined,
|
||||||
._event_manager = EventManager.init(session.page_arena, self),
|
._event_manager = EventManager.init(session.page_arena, self),
|
||||||
};
|
};
|
||||||
@@ -296,13 +296,22 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void
|
|||||||
._performance = Performance.init(),
|
._performance = Performance.init(),
|
||||||
._screen = screen,
|
._screen = screen,
|
||||||
._visual_viewport = visual_viewport,
|
._visual_viewport = visual_viewport,
|
||||||
|
._cross_origin_wrapper = undefined,
|
||||||
});
|
});
|
||||||
|
self.window._cross_origin_wrapper = .{ .window = self.window };
|
||||||
|
|
||||||
|
self._style_manager = try StyleManager.init(self);
|
||||||
|
errdefer self._style_manager.deinit();
|
||||||
|
|
||||||
const browser = session.browser;
|
const browser = session.browser;
|
||||||
self._script_manager = ScriptManager.init(browser.allocator, browser.http_client, self);
|
self._script_manager = ScriptManager.init(browser.allocator, browser.http_client, self);
|
||||||
errdefer self._script_manager.deinit();
|
errdefer self._script_manager.deinit();
|
||||||
|
|
||||||
self.js = try browser.env.createContext(self);
|
self.js = try browser.env.createContext(self, .{
|
||||||
|
.identity = &session.identity,
|
||||||
|
.identity_arena = session.page_arena,
|
||||||
|
.call_arena = self.call_arena,
|
||||||
|
});
|
||||||
errdefer self.js.deinit();
|
errdefer self.js.deinit();
|
||||||
|
|
||||||
document._page = self;
|
document._page = self;
|
||||||
@@ -356,6 +365,7 @@ pub fn deinit(self: *Page, abort_http: bool) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self._script_manager.deinit();
|
self._script_manager.deinit();
|
||||||
|
self._style_manager.deinit();
|
||||||
|
|
||||||
session.releaseArena(self.call_arena);
|
session.releaseArena(self.call_arena);
|
||||||
}
|
}
|
||||||
@@ -437,6 +447,12 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
|||||||
if (is_about_blank or is_blob) {
|
if (is_about_blank or is_blob) {
|
||||||
self.url = if (is_about_blank) "about:blank" else try self.arena.dupeZ(u8, request_url);
|
self.url = if (is_about_blank) "about:blank" else try self.arena.dupeZ(u8, request_url);
|
||||||
|
|
||||||
|
// even though this might be the same _data_ as `default_location`, we
|
||||||
|
// have to do this to make sure window.location is at a unique _address_.
|
||||||
|
// If we don't do this, mulitple window._location will have the same
|
||||||
|
// address and thus be mapped to the same v8::Object in the identity map.
|
||||||
|
self.window._location = try Location.init(self.url, self);
|
||||||
|
|
||||||
if (is_blob) {
|
if (is_blob) {
|
||||||
// strip out blob:
|
// strip out blob:
|
||||||
self.origin = try URL.getOrigin(self.arena, request_url[5.. :0]);
|
self.origin = try URL.getOrigin(self.arena, request_url[5.. :0]);
|
||||||
@@ -583,13 +599,34 @@ pub fn scheduleNavigation(self: *Page, request_url: []const u8, opts: NavigateOp
|
|||||||
// page that it's acting on.
|
// page that it's acting on.
|
||||||
fn scheduleNavigationWithArena(originator: *Page, arena: Allocator, request_url: []const u8, opts: NavigateOpts, nt: Navigation) !void {
|
fn scheduleNavigationWithArena(originator: *Page, arena: Allocator, request_url: []const u8, opts: NavigateOpts, nt: Navigation) !void {
|
||||||
const resolved_url, const is_about_blank = blk: {
|
const resolved_url, const is_about_blank = blk: {
|
||||||
|
if (URL.isCompleteHTTPUrl(request_url)) {
|
||||||
|
break :blk .{ try arena.dupeZ(u8, request_url), false };
|
||||||
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, request_url, "about:blank")) {
|
if (std.mem.eql(u8, request_url, "about:blank")) {
|
||||||
// navigate will handle this special case
|
// navigate will handle this special case
|
||||||
break :blk .{ "about:blank", true };
|
break :blk .{ "about:blank", true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// request_url isn't a "complete" URL, so it has to be resolved with the
|
||||||
|
// originator's base. Unless, originator's base is "about:blank", in which
|
||||||
|
// case we have to walk up the parents and find a real base.
|
||||||
|
const page_base = base_blk: {
|
||||||
|
var maybe_not_blank_page = originator;
|
||||||
|
while (true) {
|
||||||
|
const maybe_base = maybe_not_blank_page.base();
|
||||||
|
if (std.mem.eql(u8, maybe_base, "about:blank") == false) {
|
||||||
|
break :base_blk maybe_base;
|
||||||
|
}
|
||||||
|
// The orelse here is probably an invalid case, but there isn't
|
||||||
|
// anything we can do about it. It should never happen?
|
||||||
|
maybe_not_blank_page = maybe_not_blank_page.parent orelse break :base_blk "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const u = try URL.resolve(
|
const u = try URL.resolve(
|
||||||
arena,
|
arena,
|
||||||
originator.base(),
|
page_base,
|
||||||
request_url,
|
request_url,
|
||||||
.{ .always_dupe = true, .encode = true },
|
.{ .always_dupe = true, .encode = true },
|
||||||
);
|
);
|
||||||
@@ -821,10 +858,12 @@ fn notifyParentLoadComplete(self: *Page) void {
|
|||||||
parent.iframeCompletedLoading(self.iframe.?);
|
parent.iframeCompletedLoading(self.iframe.?);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pageHeaderDoneCallback(response: HttpClient.Response) !bool {
|
fn pageHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {
|
||||||
var self: *Page = @ptrCast(@alignCast(response.ctx));
|
var self: *Page = @ptrCast(@alignCast(transfer.ctx));
|
||||||
|
|
||||||
const response_url = response.url();
|
const header = &transfer.response_header.?;
|
||||||
|
|
||||||
|
const response_url = std.mem.span(header.url);
|
||||||
if (std.mem.eql(u8, response_url, self.url) == false) {
|
if (std.mem.eql(u8, response_url, self.url) == false) {
|
||||||
// would be different than self.url in the case of a redirect
|
// would be different than self.url in the case of a redirect
|
||||||
self.url = try self.arena.dupeZ(u8, response_url);
|
self.url = try self.arena.dupeZ(u8, response_url);
|
||||||
@@ -838,8 +877,8 @@ fn pageHeaderDoneCallback(response: HttpClient.Response) !bool {
|
|||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.page, "navigate header", .{
|
log.debug(.page, "navigate header", .{
|
||||||
.url = self.url,
|
.url = self.url,
|
||||||
.status = response.status(),
|
.status = header.status,
|
||||||
.content_type = response.contentType(),
|
.content_type = header.contentType(),
|
||||||
.type = self._type,
|
.type = self._type,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -847,14 +886,14 @@ fn pageHeaderDoneCallback(response: HttpClient.Response) !bool {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pageDataCallback(response: HttpClient.Response, data: []const u8) !void {
|
fn pageDataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
|
||||||
var self: *Page = @ptrCast(@alignCast(response.ctx));
|
var self: *Page = @ptrCast(@alignCast(transfer.ctx));
|
||||||
|
|
||||||
if (self._parse_state == .pre) {
|
if (self._parse_state == .pre) {
|
||||||
// we lazily do this, because we might need the first chunk of data
|
// we lazily do this, because we might need the first chunk of data
|
||||||
// to sniff the content type
|
// to sniff the content type
|
||||||
var mime: Mime = blk: {
|
var mime: Mime = blk: {
|
||||||
if (response.contentType()) |ct| {
|
if (transfer.response_header.?.contentType()) |ct| {
|
||||||
break :blk try Mime.parse(ct);
|
break :blk try Mime.parse(ct);
|
||||||
}
|
}
|
||||||
break :blk Mime.sniff(data);
|
break :blk Mime.sniff(data);
|
||||||
@@ -2555,6 +2594,17 @@ pub fn removeNode(self: *Page, parent: *Node, child: *Node, opts: RemoveNodeOpts
|
|||||||
}
|
}
|
||||||
|
|
||||||
Element.Html.Custom.invokeDisconnectedCallbackOnElement(el, self);
|
Element.Html.Custom.invokeDisconnectedCallbackOnElement(el, self);
|
||||||
|
|
||||||
|
// If a <style> element is being removed, remove its sheet from the list
|
||||||
|
if (el.is(Element.Html.Style)) |style| {
|
||||||
|
if (style._sheet) |sheet| {
|
||||||
|
if (self.document._style_sheets) |sheets| {
|
||||||
|
sheets.remove(sheet);
|
||||||
|
}
|
||||||
|
style._sheet = null;
|
||||||
|
}
|
||||||
|
self._style_manager.sheetModified();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2566,8 +2616,10 @@ pub fn appendAllChildren(self: *Page, parent: *Node, target: *Node) !void {
|
|||||||
self.domChanged();
|
self.domChanged();
|
||||||
const dest_connected = target.isConnected();
|
const dest_connected = target.isConnected();
|
||||||
|
|
||||||
var it = parent.childrenIterator();
|
// Use firstChild() instead of iterator to handle cases where callbacks
|
||||||
while (it.next()) |child| {
|
// (like custom element connectedCallback) modify the parent during iteration.
|
||||||
|
// The iterator captures "next" pointers that can become stale.
|
||||||
|
while (parent.firstChild()) |child| {
|
||||||
// Check if child was connected BEFORE removing it from parent
|
// Check if child was connected BEFORE removing it from parent
|
||||||
const child_was_connected = child.isConnected();
|
const child_was_connected = child.isConnected();
|
||||||
self.removeNode(parent, child, .{ .will_be_reconnected = dest_connected });
|
self.removeNode(parent, child, .{ .will_be_reconnected = dest_connected });
|
||||||
@@ -2579,8 +2631,10 @@ pub fn insertAllChildrenBefore(self: *Page, fragment: *Node, parent: *Node, ref_
|
|||||||
self.domChanged();
|
self.domChanged();
|
||||||
const dest_connected = parent.isConnected();
|
const dest_connected = parent.isConnected();
|
||||||
|
|
||||||
var it = fragment.childrenIterator();
|
// Use firstChild() instead of iterator to handle cases where callbacks
|
||||||
while (it.next()) |child| {
|
// (like custom element connectedCallback) modify the fragment during iteration.
|
||||||
|
// The iterator captures "next" pointers that can become stale.
|
||||||
|
while (fragment.firstChild()) |child| {
|
||||||
// Check if child was connected BEFORE removing it from fragment
|
// Check if child was connected BEFORE removing it from fragment
|
||||||
const child_was_connected = child.isConnected();
|
const child_was_connected = child.isConnected();
|
||||||
self.removeNode(fragment, child, .{ .will_be_reconnected = dest_connected });
|
self.removeNode(fragment, child, .{ .will_be_reconnected = dest_connected });
|
||||||
@@ -3341,7 +3395,7 @@ pub fn handleClick(self: *Page, target: *Node) !void {
|
|||||||
pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
|
pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
|
||||||
const event = keyboard_event.asEvent();
|
const event = keyboard_event.asEvent();
|
||||||
const element = self.window._document._active_element orelse {
|
const element = self.window._document._active_element orelse {
|
||||||
keyboard_event.deinit(false, self._session);
|
_ = event.releaseRef(self._session);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3432,11 +3486,12 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (submit_opts.fire_event) {
|
if (submit_opts.fire_event) {
|
||||||
const submit_event = try Event.initTrusted(comptime .wrap("submit"), .{ .bubbles = true, .cancelable = true }, self);
|
const submitter_html: ?*HtmlElement = if (submitter_) |s| s.is(HtmlElement) else null;
|
||||||
|
const submit_event = (try SubmitEvent.initTrusted(comptime .wrap("submit"), .{ .bubbles = true, .cancelable = true, .submitter = submitter_html }, self)).asEvent();
|
||||||
|
|
||||||
// so submit_event is still valid when we check _prevent_default
|
// so submit_event is still valid when we check _prevent_default
|
||||||
submit_event.acquireRef();
|
submit_event.acquireRef();
|
||||||
defer submit_event.deinit(false, self._session);
|
defer _ = submit_event.releaseRef(self._session);
|
||||||
|
|
||||||
try self._event_manager.dispatch(form_element.asEventTarget(), submit_event);
|
try self._event_manager.dispatch(form_element.asEventTarget(), submit_event);
|
||||||
// If the submit event was prevented, don't submit the form
|
// If the submit event was prevented, don't submit the form
|
||||||
@@ -3530,9 +3585,6 @@ test "WebApi: Page" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test "WebApi: Frames" {
|
test "WebApi: Frames" {
|
||||||
const filter: testing.LogFilter = .init(&.{.js});
|
|
||||||
defer filter.deinit();
|
|
||||||
|
|
||||||
try testing.htmlRunner("frames", .{});
|
try testing.htmlRunner("frames", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
238
src/browser/Runner.zig
Normal file
238
src/browser/Runner.zig
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
// 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 builtin = @import("builtin");
|
||||||
|
|
||||||
|
const log = @import("../log.zig");
|
||||||
|
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
const Session = @import("Session.zig");
|
||||||
|
const HttpClient = @import("HttpClient.zig");
|
||||||
|
|
||||||
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
|
const Runner = @This();
|
||||||
|
|
||||||
|
page: *Page,
|
||||||
|
session: *Session,
|
||||||
|
http_client: *HttpClient,
|
||||||
|
|
||||||
|
pub const Opts = struct {};
|
||||||
|
|
||||||
|
pub fn init(session: *Session, _: Opts) !Runner {
|
||||||
|
const page = &(session.page orelse return error.NoPage);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.page = page,
|
||||||
|
.session = session,
|
||||||
|
.http_client = session.browser.http_client,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const WaitOpts = struct {
|
||||||
|
ms: u32,
|
||||||
|
until: lp.Config.WaitUntil = .done,
|
||||||
|
};
|
||||||
|
pub fn wait(self: *Runner, opts: WaitOpts) !void {
|
||||||
|
_ = try self._wait(false, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const CDPWaitResult = enum {
|
||||||
|
done,
|
||||||
|
cdp_socket,
|
||||||
|
};
|
||||||
|
pub fn waitCDP(self: *Runner, opts: WaitOpts) !CDPWaitResult {
|
||||||
|
return self._wait(true, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _wait(self: *Runner, comptime is_cdp: bool, opts: WaitOpts) !CDPWaitResult {
|
||||||
|
var timer = try std.time.Timer.start();
|
||||||
|
var ms_remaining = opts.ms;
|
||||||
|
|
||||||
|
const tick_opts = TickOpts{
|
||||||
|
.ms = 200,
|
||||||
|
.until = opts.until,
|
||||||
|
};
|
||||||
|
while (true) {
|
||||||
|
const tick_result = self._tick(is_cdp, tick_opts) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.JsError => {}, // already logged (with hopefully more context)
|
||||||
|
else => log.err(.browser, "session wait", .{
|
||||||
|
.err = err,
|
||||||
|
.url = self.page.url,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
|
const next_ms = switch (tick_result) {
|
||||||
|
.ok => |next_ms| next_ms,
|
||||||
|
.done => return .done,
|
||||||
|
.cdp_socket => if (comptime is_cdp) return .cdp_socket else unreachable,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ms_elapsed = timer.lap() / 1_000_000;
|
||||||
|
if (ms_elapsed >= ms_remaining) {
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
ms_remaining -= @intCast(ms_elapsed);
|
||||||
|
if (next_ms > 0) {
|
||||||
|
std.Thread.sleep(std.time.ns_per_ms * next_ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const TickOpts = struct {
|
||||||
|
ms: u32,
|
||||||
|
until: lp.Config.WaitUntil = .done,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const TickResult = union(enum) {
|
||||||
|
done,
|
||||||
|
ok: u32,
|
||||||
|
};
|
||||||
|
pub fn tick(self: *Runner, opts: TickOpts) !TickResult {
|
||||||
|
return switch (try self._tick(false, opts)) {
|
||||||
|
.ok => |ms| .{ .ok = ms },
|
||||||
|
.done => .done,
|
||||||
|
.cdp_socket => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const CDPTickResult = union(enum) {
|
||||||
|
done,
|
||||||
|
cdp_socket,
|
||||||
|
ok: u32,
|
||||||
|
};
|
||||||
|
pub fn tickCDP(self: *Runner, opts: TickOpts) !CDPTickResult {
|
||||||
|
return self._tick(true, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !CDPTickResult {
|
||||||
|
const page = self.page;
|
||||||
|
const http_client = self.http_client;
|
||||||
|
|
||||||
|
switch (page._parse_state) {
|
||||||
|
.pre, .raw, .text, .image => {
|
||||||
|
// The main page hasn't started/finished navigating.
|
||||||
|
// There's no JS to run, and no reason to run the scheduler.
|
||||||
|
if (http_client.active == 0 and (comptime is_cdp) == false) {
|
||||||
|
// haven't started navigating, I guess.
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either we have active http connections, or we're in CDP
|
||||||
|
// mode with an extra socket. Either way, we're waiting
|
||||||
|
// for http traffic
|
||||||
|
const http_result = try http_client.tick(@intCast(opts.ms));
|
||||||
|
if ((comptime is_cdp) and http_result == .cdp_socket) {
|
||||||
|
return .cdp_socket;
|
||||||
|
}
|
||||||
|
return .{ .ok = 0 };
|
||||||
|
},
|
||||||
|
.html, .complete => {
|
||||||
|
const session = self.session;
|
||||||
|
if (session.queued_navigation.items.len != 0) {
|
||||||
|
try session.processQueuedNavigation();
|
||||||
|
self.page = &session.page.?; // might have changed
|
||||||
|
return .{ .ok = 0 };
|
||||||
|
}
|
||||||
|
const browser = session.browser;
|
||||||
|
|
||||||
|
// The HTML page was parsed. We now either have JS scripts to
|
||||||
|
// download, or scheduled tasks to execute, or both.
|
||||||
|
|
||||||
|
// scheduler.run could trigger new http transfers, so do not
|
||||||
|
// store http_client.active BEFORE this call and then use
|
||||||
|
// it AFTER.
|
||||||
|
try browser.runMacrotasks();
|
||||||
|
|
||||||
|
// Each call to this runs scheduled load events.
|
||||||
|
try page.dispatchLoad();
|
||||||
|
|
||||||
|
const http_active = http_client.active;
|
||||||
|
const total_network_activity = http_active + http_client.intercepted;
|
||||||
|
if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
|
||||||
|
page.notifyNetworkAlmostIdle();
|
||||||
|
}
|
||||||
|
if (page._notified_network_idle.check(total_network_activity == 0)) {
|
||||||
|
page.notifyNetworkIdle();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (http_active == 0 and (comptime is_cdp == false)) {
|
||||||
|
// we don't need to consider http_client.intercepted here
|
||||||
|
// because is_cdp is true, and that can only be
|
||||||
|
// the case when interception isn't possible.
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(http_client.intercepted == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (browser.hasBackgroundTasks()) {
|
||||||
|
// _we_ have nothing to run, but v8 is working on
|
||||||
|
// background tasks. We'll wait for them.
|
||||||
|
browser.waitForBackgroundTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (opts.until) {
|
||||||
|
.done => {},
|
||||||
|
.domcontentloaded => if (page._load_state == .load or page._load_state == .complete) {
|
||||||
|
return .done;
|
||||||
|
},
|
||||||
|
.load => if (page._load_state == .complete) {
|
||||||
|
return .done;
|
||||||
|
},
|
||||||
|
.networkidle => if (page._notified_network_idle == .done) {
|
||||||
|
return .done;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// We never advertise a wait time of more than 20, there can
|
||||||
|
// always be new background tasks to run.
|
||||||
|
if (browser.msToNextMacrotask()) |ms_to_next_task| {
|
||||||
|
return .{ .ok = @min(ms_to_next_task, 20) };
|
||||||
|
}
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're here because we either have active HTTP
|
||||||
|
// connections, or is_cdp == false (aka, there's
|
||||||
|
// an cdp_socket registered with the http client).
|
||||||
|
// We should continue to run tasks, so we minimize how long
|
||||||
|
// we'll poll for network I/O.
|
||||||
|
var ms_to_wait = @min(opts.ms, browser.msToNextMacrotask() orelse 200);
|
||||||
|
if (ms_to_wait > 10 and browser.hasBackgroundTasks()) {
|
||||||
|
// if we have background tasks, we don't want to wait too
|
||||||
|
// long for a message from the client. We want to go back
|
||||||
|
// to the top of the loop and run macrotasks.
|
||||||
|
ms_to_wait = 10;
|
||||||
|
}
|
||||||
|
const http_result = try http_client.tick(@intCast(@min(opts.ms, ms_to_wait)));
|
||||||
|
if ((comptime is_cdp) and http_result == .cdp_socket) {
|
||||||
|
return .cdp_socket;
|
||||||
|
}
|
||||||
|
return .{ .ok = 0 };
|
||||||
|
},
|
||||||
|
.err => |err| {
|
||||||
|
page._parse_state = .{ .raw_done = @errorName(err) };
|
||||||
|
return err;
|
||||||
|
},
|
||||||
|
.raw_done => return .done,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,12 +28,10 @@ const String = @import("../string.zig").String;
|
|||||||
const js = @import("js/js.zig");
|
const js = @import("js/js.zig");
|
||||||
const URL = @import("URL.zig");
|
const URL = @import("URL.zig");
|
||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
const Browser = @import("Browser.zig");
|
|
||||||
|
|
||||||
const Element = @import("webapi/Element.zig");
|
const Element = @import("webapi/Element.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArrayList = std.ArrayList;
|
|
||||||
|
|
||||||
const IS_DEBUG = builtin.mode == .Debug;
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
@@ -694,86 +692,85 @@ pub const Script = struct {
|
|||||||
self.manager.page.releaseArena(self.arena);
|
self.manager.page.releaseArena(self.arena);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn startCallback(response: HttpClient.Response) !void {
|
fn startCallback(transfer: *HttpClient.Transfer) !void {
|
||||||
log.debug(.http, "script fetch start", .{ .req = response });
|
log.debug(.http, "script fetch start", .{ .req = transfer });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn headerCallback(response: HttpClient.Response) !bool {
|
fn headerCallback(transfer: *HttpClient.Transfer) !bool {
|
||||||
const self: *Script = @ptrCast(@alignCast(response.ctx));
|
const self: *Script = @ptrCast(@alignCast(transfer.ctx));
|
||||||
|
const header = &transfer.response_header.?;
|
||||||
self.status = response.status().?;
|
self.status = header.status;
|
||||||
if (response.status() != 200) {
|
if (header.status != 200) {
|
||||||
log.info(.http, "script header", .{
|
log.info(.http, "script header", .{
|
||||||
.req = response,
|
.req = transfer,
|
||||||
.status = response.status(),
|
.status = header.status,
|
||||||
.content_type = response.contentType(),
|
.content_type = header.contentType(),
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.http, "script header", .{
|
log.debug(.http, "script header", .{
|
||||||
.req = response,
|
.req = transfer,
|
||||||
.status = response.status(),
|
.status = header.status,
|
||||||
.content_type = response.contentType(),
|
.content_type = header.contentType(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// {
|
{
|
||||||
// // temp debug, trying to figure out why the next assert sometimes
|
// temp debug, trying to figure out why the next assert sometimes
|
||||||
// // fails. Is the buffer just corrupt or is headerCallback really
|
// fails. Is the buffer just corrupt or is headerCallback really
|
||||||
// // being called twice?
|
// being called twice?
|
||||||
// lp.assert(self.header_callback_called == false, "ScriptManager.Header recall", .{
|
lp.assert(self.header_callback_called == false, "ScriptManager.Header recall", .{
|
||||||
// .m = @tagName(std.meta.activeTag(self.mode)),
|
.m = @tagName(std.meta.activeTag(self.mode)),
|
||||||
// .a1 = self.debug_transfer_id,
|
.a1 = self.debug_transfer_id,
|
||||||
// .a2 = self.debug_transfer_tries,
|
.a2 = self.debug_transfer_tries,
|
||||||
// .a3 = self.debug_transfer_aborted,
|
.a3 = self.debug_transfer_aborted,
|
||||||
// .a4 = self.debug_transfer_bytes_received,
|
.a4 = self.debug_transfer_bytes_received,
|
||||||
// .a5 = self.debug_transfer_notified_fail,
|
.a5 = self.debug_transfer_notified_fail,
|
||||||
// .a6 = self.debug_transfer_redirecting,
|
.a6 = self.debug_transfer_redirecting,
|
||||||
// .a7 = self.debug_transfer_intercept_state,
|
.a7 = self.debug_transfer_intercept_state,
|
||||||
// .a8 = self.debug_transfer_auth_challenge,
|
.a8 = self.debug_transfer_auth_challenge,
|
||||||
// .a9 = self.debug_transfer_easy_id,
|
.a9 = self.debug_transfer_easy_id,
|
||||||
// .b1 = transfer.id,
|
.b1 = transfer.id,
|
||||||
// .b2 = transfer._tries,
|
.b2 = transfer._tries,
|
||||||
// .b3 = transfer.aborted,
|
.b3 = transfer.aborted,
|
||||||
// .b4 = transfer.bytes_received,
|
.b4 = transfer.bytes_received,
|
||||||
// .b5 = transfer._notified_fail,
|
.b5 = transfer._notified_fail,
|
||||||
// .b6 = transfer._redirecting,
|
.b6 = transfer._redirecting,
|
||||||
// .b7 = @intFromEnum(transfer._intercept_state),
|
.b7 = @intFromEnum(transfer._intercept_state),
|
||||||
// .b8 = transfer._auth_challenge != null,
|
.b8 = transfer._auth_challenge != null,
|
||||||
// .b9 = if (transfer._conn) |c| @intFromPtr(c.easy) else 0,
|
.b9 = if (transfer._conn) |c| @intFromPtr(c.easy) else 0,
|
||||||
// });
|
});
|
||||||
// self.header_callback_called = true;
|
self.header_callback_called = true;
|
||||||
// self.debug_transfer_id = transfer.id;
|
self.debug_transfer_id = transfer.id;
|
||||||
// self.debug_transfer_tries = transfer._tries;
|
self.debug_transfer_tries = transfer._tries;
|
||||||
// self.debug_transfer_aborted = transfer.aborted;
|
self.debug_transfer_aborted = transfer.aborted;
|
||||||
// self.debug_transfer_bytes_received = transfer.bytes_received;
|
self.debug_transfer_bytes_received = transfer.bytes_received;
|
||||||
// self.debug_transfer_notified_fail = transfer._notified_fail;
|
self.debug_transfer_notified_fail = transfer._notified_fail;
|
||||||
// self.debug_transfer_redirecting = transfer._redirecting;
|
self.debug_transfer_redirecting = transfer._redirecting;
|
||||||
// self.debug_transfer_intercept_state = @intFromEnum(transfer._intercept_state);
|
self.debug_transfer_intercept_state = @intFromEnum(transfer._intercept_state);
|
||||||
// self.debug_transfer_auth_challenge = transfer._auth_challenge != null;
|
self.debug_transfer_auth_challenge = transfer._auth_challenge != null;
|
||||||
// self.debug_transfer_easy_id = if (transfer._conn) |c| @intFromPtr(c.easy) else 0;
|
self.debug_transfer_easy_id = if (transfer._conn) |c| @intFromPtr(c.easy) else 0;
|
||||||
// }
|
}
|
||||||
|
|
||||||
lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity });
|
lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity });
|
||||||
var buffer: std.ArrayList(u8) = .empty;
|
var buffer: std.ArrayList(u8) = .empty;
|
||||||
if (response.contentLength()) |cl| {
|
if (transfer.getContentLength()) |cl| {
|
||||||
try buffer.ensureTotalCapacity(self.arena, cl);
|
try buffer.ensureTotalCapacity(self.arena, cl);
|
||||||
}
|
}
|
||||||
self.source = .{ .remote = buffer };
|
self.source = .{ .remote = buffer };
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dataCallback(response: HttpClient.Response, data: []const u8) !void {
|
fn dataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
|
||||||
const self: *Script = @ptrCast(@alignCast(response.ctx));
|
const self: *Script = @ptrCast(@alignCast(transfer.ctx));
|
||||||
self._dataCallback(response, data) catch |err| {
|
self._dataCallback(transfer, data) catch |err| {
|
||||||
log.err(.http, "SM.dataCallback", .{ .err = err, .transfer = response, .len = data.len });
|
log.err(.http, "SM.dataCallback", .{ .err = err, .transfer = transfer, .len = data.len });
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
fn _dataCallback(self: *Script, _: *HttpClient.Transfer, data: []const u8) !void {
|
||||||
fn _dataCallback(self: *Script, _: HttpClient.Response, data: []const u8) !void {
|
|
||||||
try self.source.remote.appendSlice(self.arena, data);
|
try self.source.remote.appendSlice(self.arena, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,13 @@ const log = @import("../log.zig");
|
|||||||
const App = @import("../App.zig");
|
const App = @import("../App.zig");
|
||||||
|
|
||||||
const js = @import("js/js.zig");
|
const js = @import("js/js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
const storage = @import("webapi/storage/storage.zig");
|
const storage = @import("webapi/storage/storage.zig");
|
||||||
const Navigation = @import("webapi/navigation/Navigation.zig");
|
const Navigation = @import("webapi/navigation/Navigation.zig");
|
||||||
const History = @import("webapi/History.zig");
|
const History = @import("webapi/History.zig");
|
||||||
|
|
||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
|
pub const Runner = @import("Runner.zig");
|
||||||
const Browser = @import("Browser.zig");
|
const Browser = @import("Browser.zig");
|
||||||
const Factory = @import("Factory.zig");
|
const Factory = @import("Factory.zig");
|
||||||
const Notification = @import("../Notification.zig");
|
const Notification = @import("../Notification.zig");
|
||||||
@@ -65,36 +67,53 @@ page_arena: Allocator,
|
|||||||
// Origin map for same-origin context sharing. Scoped to the root page lifetime.
|
// Origin map for same-origin context sharing. Scoped to the root page lifetime.
|
||||||
origins: std.StringHashMapUnmanaged(*js.Origin) = .empty,
|
origins: std.StringHashMapUnmanaged(*js.Origin) = .empty,
|
||||||
|
|
||||||
|
// Identity tracking for the main world. All main world contexts share this,
|
||||||
|
// ensuring object identity works across same-origin frames.
|
||||||
|
identity: js.Identity = .{},
|
||||||
|
|
||||||
|
// Shared finalizer callbacks across all Identities. Keyed by Zig instance ptr.
|
||||||
|
// This ensures objects are only freed when ALL v8 wrappers are gone.
|
||||||
|
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
|
||||||
|
|
||||||
|
// Tracked global v8 objects that need to be released on cleanup.
|
||||||
|
// Lives at Session level so objects can outlive individual Identities.
|
||||||
|
globals: std.ArrayList(v8.Global) = .empty,
|
||||||
|
|
||||||
|
// Temporary v8 globals that can be released early. Key is global.data_ptr.
|
||||||
|
// Lives at Session level so objects holding Temps can outlive individual Identities.
|
||||||
|
temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
||||||
|
|
||||||
// Shared resources for all pages in this session.
|
// Shared resources for all pages in this session.
|
||||||
// These live for the duration of the page tree (root + frames).
|
// These live for the duration of the page tree (root + frames).
|
||||||
arena_pool: *ArenaPool,
|
arena_pool: *ArenaPool,
|
||||||
|
|
||||||
// In Debug, we use this to see if anything fails to release an arena back to
|
|
||||||
// the pool.
|
|
||||||
_arena_pool_leak_track: if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, struct {
|
|
||||||
owner: []const u8,
|
|
||||||
count: usize,
|
|
||||||
}) else void = if (IS_DEBUG) .empty else {},
|
|
||||||
|
|
||||||
page: ?Page,
|
page: ?Page,
|
||||||
|
|
||||||
queued_navigation: std.ArrayList(*Page),
|
// Double buffer so that, as we process one list of queued navigations, new entries
|
||||||
|
// are added to the separate buffer. This ensures that we don't end up with
|
||||||
|
// endless navigation loops AND that we don't invalidate the list while iterating
|
||||||
|
// if a new entry gets appended
|
||||||
|
queued_navigation_1: std.ArrayList(*Page),
|
||||||
|
queued_navigation_2: std.ArrayList(*Page),
|
||||||
|
// pointer to either queued_navigation_1 or queued_navigation_2
|
||||||
|
queued_navigation: *std.ArrayList(*Page),
|
||||||
|
|
||||||
// Temporary buffer for about:blank navigations during processing.
|
// Temporary buffer for about:blank navigations during processing.
|
||||||
// We process async navigations first (safe from re-entrance), then sync
|
// We process async navigations first (safe from re-entrance), then sync
|
||||||
// about:blank navigations (which may add to queued_navigation).
|
// about:blank navigations (which may add to queued_navigation).
|
||||||
queued_queued_navigation: std.ArrayList(*Page),
|
queued_queued_navigation: std.ArrayList(*Page),
|
||||||
|
|
||||||
page_id_gen: u32,
|
page_id_gen: u32 = 0,
|
||||||
frame_id_gen: u32,
|
frame_id_gen: u32 = 0,
|
||||||
|
|
||||||
pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void {
|
pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void {
|
||||||
const allocator = browser.app.allocator;
|
const allocator = browser.app.allocator;
|
||||||
const arena_pool = browser.arena_pool;
|
const arena_pool = browser.arena_pool;
|
||||||
|
|
||||||
const arena = try arena_pool.acquire();
|
const arena = try arena_pool.acquire(.{ .debug = "Session" });
|
||||||
errdefer arena_pool.release(arena);
|
errdefer arena_pool.release(arena);
|
||||||
|
|
||||||
const page_arena = try arena_pool.acquire();
|
const page_arena = try arena_pool.acquire(.{ .debug = "Session.page_arena" });
|
||||||
errdefer arena_pool.release(page_arena);
|
errdefer arena_pool.release(page_arena);
|
||||||
|
|
||||||
self.* = .{
|
self.* = .{
|
||||||
@@ -104,17 +123,18 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
|
|||||||
.page_arena = page_arena,
|
.page_arena = page_arena,
|
||||||
.factory = Factory.init(page_arena),
|
.factory = Factory.init(page_arena),
|
||||||
.history = .{},
|
.history = .{},
|
||||||
.page_id_gen = 0,
|
|
||||||
.frame_id_gen = 0,
|
|
||||||
// The prototype (EventTarget) for Navigation is created when a Page is created.
|
// The prototype (EventTarget) for Navigation is created when a Page is created.
|
||||||
.navigation = .{ ._proto = undefined },
|
.navigation = .{ ._proto = undefined },
|
||||||
.storage_shed = .{},
|
.storage_shed = .{},
|
||||||
.browser = browser,
|
.browser = browser,
|
||||||
.queued_navigation = .{},
|
.queued_navigation = undefined,
|
||||||
|
.queued_navigation_1 = .{},
|
||||||
|
.queued_navigation_2 = .{},
|
||||||
.queued_queued_navigation = .{},
|
.queued_queued_navigation = .{},
|
||||||
.notification = notification,
|
.notification = notification,
|
||||||
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
||||||
};
|
};
|
||||||
|
self.queued_navigation = &self.queued_navigation_1;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Session) void {
|
pub fn deinit(self: *Session) void {
|
||||||
@@ -171,32 +191,11 @@ pub const GetArenaOpts = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn getArena(self: *Session, opts: GetArenaOpts) !Allocator {
|
pub fn getArena(self: *Session, opts: GetArenaOpts) !Allocator {
|
||||||
const allocator = try self.arena_pool.acquire();
|
return self.arena_pool.acquire(.{ .debug = opts.debug });
|
||||||
if (comptime IS_DEBUG) {
|
|
||||||
// Use session's arena (not page_arena) since page_arena gets reset between pages
|
|
||||||
const gop = try self._arena_pool_leak_track.getOrPut(self.arena, @intFromPtr(allocator.ptr));
|
|
||||||
if (gop.found_existing and gop.value_ptr.count != 0) {
|
|
||||||
log.err(.bug, "ArenaPool Double Use", .{ .owner = gop.value_ptr.*.owner });
|
|
||||||
@panic("ArenaPool Double Use");
|
|
||||||
}
|
|
||||||
gop.value_ptr.* = .{ .owner = opts.debug, .count = 1 };
|
|
||||||
}
|
|
||||||
return allocator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn releaseArena(self: *Session, allocator: Allocator) void {
|
pub fn releaseArena(self: *Session, allocator: Allocator) void {
|
||||||
if (comptime IS_DEBUG) {
|
self.arena_pool.release(allocator);
|
||||||
const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?;
|
|
||||||
if (found.count != 1) {
|
|
||||||
log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count });
|
|
||||||
if (comptime builtin.is_test) {
|
|
||||||
@panic("ArenaPool Double Free");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
found.count = 0;
|
|
||||||
}
|
|
||||||
return self.arena_pool.release(allocator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getOrCreateOrigin(self: *Session, key_: ?[]const u8) !*js.Origin {
|
pub fn getOrCreateOrigin(self: *Session, key_: ?[]const u8) !*js.Origin {
|
||||||
@@ -237,18 +236,33 @@ pub fn releaseOrigin(self: *Session, origin: *js.Origin) void {
|
|||||||
/// Reset page_arena and factory for a clean slate.
|
/// Reset page_arena and factory for a clean slate.
|
||||||
/// Called when root page is removed.
|
/// Called when root page is removed.
|
||||||
fn resetPageResources(self: *Session) void {
|
fn resetPageResources(self: *Session) void {
|
||||||
// Check for arena leaks before releasing
|
// Force cleanup all remaining finalized objects
|
||||||
if (comptime IS_DEBUG) {
|
{
|
||||||
var it = self._arena_pool_leak_track.valueIterator();
|
var it = self.finalizer_callbacks.valueIterator();
|
||||||
while (it.next()) |value_ptr| {
|
while (it.next()) |fc| {
|
||||||
if (value_ptr.count > 0) {
|
fc.*.deinit(self);
|
||||||
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self._arena_pool_leak_track.clearRetainingCapacity();
|
self.finalizer_callbacks = .empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
// All origins should have been released when contexts were destroyed
|
{
|
||||||
|
for (self.globals.items) |*global| {
|
||||||
|
v8.v8__Global__Reset(global);
|
||||||
|
}
|
||||||
|
self.globals = .empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var it = self.temps.valueIterator();
|
||||||
|
while (it.next()) |global| {
|
||||||
|
v8.v8__Global__Reset(global);
|
||||||
|
}
|
||||||
|
self.temps = .empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.identity.deinit();
|
||||||
|
self.identity = .{};
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
std.debug.assert(self.origins.count() == 0);
|
std.debug.assert(self.origins.count() == 0);
|
||||||
}
|
}
|
||||||
@@ -259,10 +273,9 @@ fn resetPageResources(self: *Session) void {
|
|||||||
while (it.next()) |value| {
|
while (it.next()) |value| {
|
||||||
value.*.deinit(app);
|
value.*.deinit(app);
|
||||||
}
|
}
|
||||||
self.origins.clearRetainingCapacity();
|
self.origins = .empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Release old page_arena and acquire fresh one
|
|
||||||
self.frame_id_gen = 0;
|
self.frame_id_gen = 0;
|
||||||
self.arena_pool.reset(self.page_arena, 64 * 1024);
|
self.arena_pool.reset(self.page_arena, 64 * 1024);
|
||||||
self.factory = Factory.init(self.page_arena);
|
self.factory = Factory.init(self.page_arena);
|
||||||
@@ -293,12 +306,6 @@ pub fn currentPage(self: *Session) ?*Page {
|
|||||||
return &(self.page orelse return null);
|
return &(self.page orelse return null);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const WaitResult = enum {
|
|
||||||
done,
|
|
||||||
no_page,
|
|
||||||
cdp_socket,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn findPageByFrameId(self: *Session, frame_id: u32) ?*Page {
|
pub fn findPageByFrameId(self: *Session, frame_id: u32) ?*Page {
|
||||||
const page = self.currentPage() orelse return null;
|
const page = self.currentPage() orelse return null;
|
||||||
return findPageBy(page, "_frame_id", frame_id);
|
return findPageBy(page, "_frame_id", frame_id);
|
||||||
@@ -319,194 +326,12 @@ fn findPageBy(page: *Page, comptime field: []const u8, id: u32) ?*Page {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
pub fn runner(self: *Session, opts: Runner.Opts) !Runner {
|
||||||
var page = &(self.page orelse return .no_page);
|
return Runner.init(self, opts);
|
||||||
while (true) {
|
|
||||||
const wait_result = self._wait(page, wait_ms) catch |err| {
|
|
||||||
switch (err) {
|
|
||||||
error.JsError => {}, // already logged (with hopefully more context)
|
|
||||||
else => log.err(.browser, "session wait", .{
|
|
||||||
.err = err,
|
|
||||||
.url = page.url,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
return .done;
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (wait_result) {
|
|
||||||
.done => {
|
|
||||||
if (self.queued_navigation.items.len == 0) {
|
|
||||||
return .done;
|
|
||||||
}
|
|
||||||
self.processQueuedNavigation() catch return .done;
|
|
||||||
page = &self.page.?; // might have changed
|
|
||||||
},
|
|
||||||
else => |result| return result,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
|
||||||
var timer = try std.time.Timer.start();
|
|
||||||
var ms_remaining = wait_ms;
|
|
||||||
|
|
||||||
const browser = self.browser;
|
|
||||||
var http_client = browser.http_client;
|
|
||||||
|
|
||||||
// I'd like the page to know NOTHING about cdp_socket / CDP, but the
|
|
||||||
// fact is that the behavior of wait changes depending on whether or
|
|
||||||
// not we're using CDP.
|
|
||||||
// If we aren't using CDP, as soon as we think there's nothing left
|
|
||||||
// to do, we can exit - we'de done.
|
|
||||||
// But if we are using CDP, we should wait for the whole `wait_ms`
|
|
||||||
// because the http_click.tick() also monitors the CDP socket. And while
|
|
||||||
// we could let CDP poll http (like it does for HTTP requests), the fact
|
|
||||||
// is that we know more about the timing of stuff (e.g. how long to
|
|
||||||
// poll/sleep) in the page.
|
|
||||||
const exit_when_done = http_client.cdp_client == null;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
switch (page._parse_state) {
|
|
||||||
.pre, .raw, .text, .image => {
|
|
||||||
// The main page hasn't started/finished navigating.
|
|
||||||
// There's no JS to run, and no reason to run the scheduler.
|
|
||||||
if (http_client.active == 0 and exit_when_done) {
|
|
||||||
// haven't started navigating, I guess.
|
|
||||||
return .done;
|
|
||||||
}
|
|
||||||
// Either we have active http connections, or we're in CDP
|
|
||||||
// mode with an extra socket. Either way, we're waiting
|
|
||||||
// for http traffic
|
|
||||||
if (try http_client.tick(@intCast(ms_remaining)) == .cdp_socket) {
|
|
||||||
// exit_when_done is explicitly set when there isn't
|
|
||||||
// an extra socket, so it should not be possibl to
|
|
||||||
// get an cdp_socket message when exit_when_done
|
|
||||||
// is true.
|
|
||||||
if (IS_DEBUG) {
|
|
||||||
std.debug.assert(exit_when_done == false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// data on a socket we aren't handling, return to caller
|
|
||||||
return .cdp_socket;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.html, .complete => {
|
|
||||||
if (self.queued_navigation.items.len != 0) {
|
|
||||||
return .done;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The HTML page was parsed. We now either have JS scripts to
|
|
||||||
// download, or scheduled tasks to execute, or both.
|
|
||||||
|
|
||||||
// scheduler.run could trigger new http transfers, so do not
|
|
||||||
// store http_client.active BEFORE this call and then use
|
|
||||||
// it AFTER.
|
|
||||||
try browser.runMacrotasks();
|
|
||||||
|
|
||||||
// Each call to this runs scheduled load events.
|
|
||||||
try page.dispatchLoad();
|
|
||||||
|
|
||||||
const http_active = http_client.active;
|
|
||||||
const total_network_activity = http_active + http_client.intercepted;
|
|
||||||
if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
|
|
||||||
page.notifyNetworkAlmostIdle();
|
|
||||||
}
|
|
||||||
if (page._notified_network_idle.check(total_network_activity == 0)) {
|
|
||||||
page.notifyNetworkIdle();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (http_active == 0 and exit_when_done) {
|
|
||||||
// we don't need to consider http_client.intercepted here
|
|
||||||
// because exit_when_done is true, and that can only be
|
|
||||||
// the case when interception isn't possible.
|
|
||||||
if (comptime IS_DEBUG) {
|
|
||||||
std.debug.assert(http_client.intercepted == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
var ms = blk: {
|
|
||||||
// if (wait_ms - ms_remaining < 100) {
|
|
||||||
// if (comptime builtin.is_test) {
|
|
||||||
// return .done;
|
|
||||||
// }
|
|
||||||
// // Look, we want to exit ASAP, but we don't want
|
|
||||||
// // to exit so fast that we've run none of the
|
|
||||||
// // background jobs.
|
|
||||||
// break :blk 50;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (browser.hasBackgroundTasks()) {
|
|
||||||
// _we_ have nothing to run, but v8 is working on
|
|
||||||
// background tasks. We'll wait for them.
|
|
||||||
browser.waitForBackgroundTasks();
|
|
||||||
break :blk 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
break :blk browser.msToNextMacrotask() orelse return .done;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ms > ms_remaining) {
|
|
||||||
// Same as above, except we have a scheduled task,
|
|
||||||
// it just happens to be too far into the future
|
|
||||||
// compared to how long we were told to wait.
|
|
||||||
if (!browser.hasBackgroundTasks()) {
|
|
||||||
return .done;
|
|
||||||
}
|
|
||||||
// _we_ have nothing to run, but v8 is working on
|
|
||||||
// background tasks. We'll wait for them.
|
|
||||||
browser.waitForBackgroundTasks();
|
|
||||||
ms = 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have a task to run in the not-so-distant future.
|
|
||||||
// You might think we can just sleep until that task is
|
|
||||||
// ready, but we should continue to run lowPriority tasks
|
|
||||||
// in the meantime, and that could unblock things. So
|
|
||||||
// we'll just sleep for a bit, and then restart our wait
|
|
||||||
// loop to see if anything new can be processed.
|
|
||||||
std.Thread.sleep(std.time.ns_per_ms * @as(u64, @intCast(@min(ms, 20))));
|
|
||||||
} else {
|
|
||||||
// We're here because we either have active HTTP
|
|
||||||
// connections, or exit_when_done == false (aka, there's
|
|
||||||
// an cdp_socket registered with the http client).
|
|
||||||
// We should continue to run tasks, so we minimize how long
|
|
||||||
// we'll poll for network I/O.
|
|
||||||
var ms_to_wait = @min(200, browser.msToNextMacrotask() orelse 200);
|
|
||||||
if (ms_to_wait > 10 and browser.hasBackgroundTasks()) {
|
|
||||||
// if we have background tasks, we don't want to wait too
|
|
||||||
// long for a message from the client. We want to go back
|
|
||||||
// to the top of the loop and run macrotasks.
|
|
||||||
ms_to_wait = 10;
|
|
||||||
}
|
|
||||||
if (try http_client.tick(@min(ms_remaining, ms_to_wait)) == .cdp_socket) {
|
|
||||||
// data on a socket we aren't handling, return to caller
|
|
||||||
return .cdp_socket;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.err => |err| {
|
|
||||||
page._parse_state = .{ .raw_done = @errorName(err) };
|
|
||||||
return err;
|
|
||||||
},
|
|
||||||
.raw_done => {
|
|
||||||
if (exit_when_done) {
|
|
||||||
return .done;
|
|
||||||
}
|
|
||||||
// we _could_ http_client.tick(ms_to_wait), but this has
|
|
||||||
// the same result, and I feel is more correct.
|
|
||||||
return .no_page;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const ms_elapsed = timer.lap() / 1_000_000;
|
|
||||||
if (ms_elapsed >= ms_remaining) {
|
|
||||||
return .done;
|
|
||||||
}
|
|
||||||
ms_remaining -= @intCast(ms_elapsed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scheduleNavigation(self: *Session, page: *Page) !void {
|
pub fn scheduleNavigation(self: *Session, page: *Page) !void {
|
||||||
const list = &self.queued_navigation;
|
const list = self.queued_navigation;
|
||||||
|
|
||||||
// Check if page is already queued
|
// Check if page is already queued
|
||||||
for (list.items) |existing| {
|
for (list.items) |existing| {
|
||||||
@@ -519,8 +344,13 @@ pub fn scheduleNavigation(self: *Session, page: *Page) !void {
|
|||||||
return list.append(self.arena, page);
|
return list.append(self.arena, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn processQueuedNavigation(self: *Session) !void {
|
pub fn processQueuedNavigation(self: *Session) !void {
|
||||||
const navigations = &self.queued_navigation;
|
const navigations = self.queued_navigation;
|
||||||
|
if (self.queued_navigation == &self.queued_navigation_1) {
|
||||||
|
self.queued_navigation = &self.queued_navigation_2;
|
||||||
|
} else {
|
||||||
|
self.queued_navigation = &self.queued_navigation_1;
|
||||||
|
}
|
||||||
|
|
||||||
if (self.page.?._queued_navigation != null) {
|
if (self.page.?._queued_navigation != null) {
|
||||||
// This is both an optimization and a simplification of sorts. If the
|
// This is both an optimization and a simplification of sorts. If the
|
||||||
@@ -536,7 +366,6 @@ fn processQueuedNavigation(self: *Session) !void {
|
|||||||
defer about_blank_queue.clearRetainingCapacity();
|
defer about_blank_queue.clearRetainingCapacity();
|
||||||
|
|
||||||
// First pass: process async navigations (non-about:blank)
|
// First pass: process async navigations (non-about:blank)
|
||||||
// These cannot cause re-entrant navigation scheduling
|
|
||||||
for (navigations.items) |page| {
|
for (navigations.items) |page| {
|
||||||
const qn = page._queued_navigation.?;
|
const qn = page._queued_navigation.?;
|
||||||
|
|
||||||
@@ -551,7 +380,6 @@ fn processQueuedNavigation(self: *Session) !void {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the queue after first pass
|
|
||||||
navigations.clearRetainingCapacity();
|
navigations.clearRetainingCapacity();
|
||||||
|
|
||||||
// Second pass: process synchronous navigations (about:blank)
|
// Second pass: process synchronous navigations (about:blank)
|
||||||
@@ -561,15 +389,17 @@ fn processQueuedNavigation(self: *Session) !void {
|
|||||||
try self.processFrameNavigation(page, qn);
|
try self.processFrameNavigation(page, qn);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safety: Remove any about:blank navigations that were queued during the
|
// Safety: Remove any about:blank navigations that were queued during
|
||||||
// second pass to prevent infinite loops
|
// processing to prevent infinite loops. New navigations have been queued
|
||||||
|
// in the other buffer.
|
||||||
|
const new_navigations = self.queued_navigation;
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
while (i < navigations.items.len) {
|
while (i < new_navigations.items.len) {
|
||||||
const page = navigations.items[i];
|
const page = new_navigations.items[i];
|
||||||
if (page._queued_navigation) |qn| {
|
if (page._queued_navigation) |qn| {
|
||||||
if (qn.is_about_blank) {
|
if (qn.is_about_blank) {
|
||||||
log.warn(.page, "recursive about blank", .{});
|
log.warn(.page, "recursive about blank", .{});
|
||||||
_ = navigations.swapRemove(i);
|
_ = self.queued_navigation.swapRemove(i);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -632,16 +462,6 @@ fn processRootQueuedNavigation(self: *Session) !void {
|
|||||||
|
|
||||||
defer self.arena_pool.release(qn.arena);
|
defer self.arena_pool.release(qn.arena);
|
||||||
|
|
||||||
// HACK
|
|
||||||
// Mark as released in tracking BEFORE removePage clears the map.
|
|
||||||
// We can't call releaseArena() because that would also return the arena
|
|
||||||
// to the pool, making the memory invalid before we use qn.url/qn.opts.
|
|
||||||
if (comptime IS_DEBUG) {
|
|
||||||
if (self._arena_pool_leak_track.getPtr(@intFromPtr(qn.arena.ptr))) |found| {
|
|
||||||
found.count = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.removePage();
|
self.removePage();
|
||||||
|
|
||||||
self.page = @as(Page, undefined);
|
self.page = @as(Page, undefined);
|
||||||
@@ -672,3 +492,26 @@ pub fn nextPageId(self: *Session) u32 {
|
|||||||
self.page_id_gen = id;
|
self.page_id_gen = id;
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every finalizable instance of Zig gets 1 FinalizerCallback registered in the
|
||||||
|
// session. This is to ensure that, if v8 doesn't finalize the value, we can
|
||||||
|
// release on page reset.
|
||||||
|
pub const FinalizerCallback = struct {
|
||||||
|
arena: Allocator,
|
||||||
|
session: *Session,
|
||||||
|
resolved_ptr_id: usize,
|
||||||
|
finalizer_ptr_id: usize,
|
||||||
|
_deinit: *const fn (ptr_id: usize, session: *Session) void,
|
||||||
|
|
||||||
|
// For every FinalizerCallback we'll have 1+ FinalizerCallback.Identity: one
|
||||||
|
// for every identity that gets the instance. In most cases, that'l be 1.
|
||||||
|
pub const Identity = struct {
|
||||||
|
identity: *js.Identity,
|
||||||
|
fc: *Session.FinalizerCallback,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn deinit(self: *FinalizerCallback, session: *Session) void {
|
||||||
|
self._deinit(self.finalizer_ptr_id, session);
|
||||||
|
session.releaseArena(self.arena);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
855
src/browser/StyleManager.zig
Normal file
855
src/browser/StyleManager.zig
Normal file
@@ -0,0 +1,855 @@
|
|||||||
|
// 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").String;
|
||||||
|
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
|
||||||
|
const CssParser = @import("css/Parser.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
|
||||||
|
const Selector = @import("webapi/selector/Selector.zig");
|
||||||
|
const SelectorParser = @import("webapi/selector/Parser.zig");
|
||||||
|
const SelectorList = @import("webapi/selector/List.zig");
|
||||||
|
|
||||||
|
const CSSStyleRule = @import("webapi/css/CSSStyleRule.zig");
|
||||||
|
const CSSStyleSheet = @import("webapi/css/CSSStyleSheet.zig");
|
||||||
|
const CSSStyleProperties = @import("webapi/css/CSSStyleProperties.zig");
|
||||||
|
const CSSStyleProperty = @import("webapi/css/CSSStyleDeclaration.zig").Property;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
pub const VisibilityCache = std.AutoHashMapUnmanaged(*Element, bool);
|
||||||
|
pub const PointerEventsCache = std.AutoHashMapUnmanaged(*Element, bool);
|
||||||
|
|
||||||
|
// Tracks visibility-relevant CSS rules from <style> elements.
|
||||||
|
// Rules are bucketed by their rightmost selector part for fast lookup.
|
||||||
|
const StyleManager = @This();
|
||||||
|
|
||||||
|
const Tag = Element.Tag;
|
||||||
|
const RuleList = std.MultiArrayList(VisibilityRule);
|
||||||
|
|
||||||
|
page: *Page,
|
||||||
|
|
||||||
|
arena: Allocator,
|
||||||
|
|
||||||
|
// Bucketed rules for fast lookup - keyed by rightmost selector part
|
||||||
|
id_rules: std.StringHashMapUnmanaged(RuleList) = .empty,
|
||||||
|
class_rules: std.StringHashMapUnmanaged(RuleList) = .empty,
|
||||||
|
tag_rules: std.AutoHashMapUnmanaged(Tag, RuleList) = .empty,
|
||||||
|
other_rules: RuleList = .empty, // universal, attribute, pseudo-class endings
|
||||||
|
|
||||||
|
// Document order counter for tie-breaking equal specificity
|
||||||
|
next_doc_order: u32 = 0,
|
||||||
|
|
||||||
|
// When true, rules need to be rebuilt
|
||||||
|
dirty: bool = false,
|
||||||
|
|
||||||
|
pub fn init(page: *Page) !StyleManager {
|
||||||
|
return .{
|
||||||
|
.page = page,
|
||||||
|
.arena = try page.getArena(.{ .debug = "StyleManager" }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *StyleManager) void {
|
||||||
|
self.page.releaseArena(self.arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseSheet(self: *StyleManager, sheet: *CSSStyleSheet) !void {
|
||||||
|
if (sheet._css_rules) |css_rules| {
|
||||||
|
for (css_rules._rules.items) |rule| {
|
||||||
|
const style_rule = rule.is(CSSStyleRule) orelse continue;
|
||||||
|
try self.addRule(style_rule);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const owner_node = sheet.getOwnerNode() orelse return;
|
||||||
|
if (owner_node.is(Element.Html.Style)) |style| {
|
||||||
|
const text = try style.asNode().getTextContentAlloc(self.arena);
|
||||||
|
var it = CssParser.parseStylesheet(text);
|
||||||
|
while (it.next()) |parsed_rule| {
|
||||||
|
try self.addRawRule(parsed_rule.selector, parsed_rule.block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn addRawRule(self: *StyleManager, selector_text: []const u8, block_text: []const u8) !void {
|
||||||
|
if (selector_text.len == 0) return;
|
||||||
|
|
||||||
|
var props = VisibilityProperties{};
|
||||||
|
var it = CssParser.parseDeclarationsList(block_text);
|
||||||
|
while (it.next()) |decl| {
|
||||||
|
const name = decl.name;
|
||||||
|
const val = decl.value;
|
||||||
|
if (std.ascii.eqlIgnoreCase(name, "display")) {
|
||||||
|
props.display_none = std.ascii.eqlIgnoreCase(val, "none");
|
||||||
|
} else if (std.ascii.eqlIgnoreCase(name, "visibility")) {
|
||||||
|
props.visibility_hidden = std.ascii.eqlIgnoreCase(val, "hidden") or std.ascii.eqlIgnoreCase(val, "collapse");
|
||||||
|
} else if (std.ascii.eqlIgnoreCase(name, "opacity")) {
|
||||||
|
props.opacity_zero = std.ascii.eqlIgnoreCase(val, "0");
|
||||||
|
} else if (std.ascii.eqlIgnoreCase(name, "pointer-events")) {
|
||||||
|
props.pointer_events_none = std.ascii.eqlIgnoreCase(val, "none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.isRelevant()) return;
|
||||||
|
|
||||||
|
const selectors = SelectorParser.parseList(self.arena, selector_text, self.page) catch return;
|
||||||
|
for (selectors) |selector| {
|
||||||
|
const rightmost = if (selector.segments.len > 0) selector.segments[selector.segments.len - 1].compound else selector.first;
|
||||||
|
const bucket_key = getBucketKey(rightmost) orelse continue;
|
||||||
|
const rule = VisibilityRule{
|
||||||
|
.props = props,
|
||||||
|
.selector = selector,
|
||||||
|
.priority = (@as(u64, computeSpecificity(selector)) << 32) | @as(u64, self.next_doc_order),
|
||||||
|
};
|
||||||
|
self.next_doc_order += 1;
|
||||||
|
|
||||||
|
switch (bucket_key) {
|
||||||
|
.id => |id| {
|
||||||
|
const gop = try self.id_rules.getOrPut(self.arena, id);
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
.class => |class| {
|
||||||
|
const gop = try self.class_rules.getOrPut(self.arena, class);
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
.tag => |tag| {
|
||||||
|
const gop = try self.tag_rules.getOrPut(self.arena, tag);
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
.other => {
|
||||||
|
try self.other_rules.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sheetRemoved(self: *StyleManager) void {
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sheetModified(self: *StyleManager) void {
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rebuilds the rule list from all document stylesheets.
|
||||||
|
/// Called lazily when dirty flag is set and rules are needed.
|
||||||
|
fn rebuildIfDirty(self: *StyleManager) !void {
|
||||||
|
if (!self.dirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dirty = false;
|
||||||
|
errdefer self.dirty = true;
|
||||||
|
const id_rules_count = self.id_rules.count();
|
||||||
|
const class_rules_count = self.class_rules.count();
|
||||||
|
const tag_rules_count = self.tag_rules.count();
|
||||||
|
const other_rules_count = self.other_rules.len;
|
||||||
|
|
||||||
|
self.page._session.arena_pool.resetRetain(self.arena);
|
||||||
|
|
||||||
|
self.next_doc_order = 0;
|
||||||
|
|
||||||
|
self.id_rules = .empty;
|
||||||
|
try self.id_rules.ensureTotalCapacity(self.arena, id_rules_count);
|
||||||
|
|
||||||
|
self.class_rules = .empty;
|
||||||
|
try self.class_rules.ensureTotalCapacity(self.arena, class_rules_count);
|
||||||
|
|
||||||
|
self.tag_rules = .empty;
|
||||||
|
try self.tag_rules.ensureTotalCapacity(self.arena, tag_rules_count);
|
||||||
|
|
||||||
|
self.other_rules = .{};
|
||||||
|
try self.other_rules.ensureTotalCapacity(self.arena, other_rules_count);
|
||||||
|
|
||||||
|
const sheets = self.page.document._style_sheets orelse return;
|
||||||
|
for (sheets._sheets.items) |sheet| {
|
||||||
|
self.parseSheet(sheet) catch |err| {
|
||||||
|
log.err(.browser, "StyleManager parseSheet", .{ .err = err });
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an element is hidden based on options.
|
||||||
|
// By default only checks display:none.
|
||||||
|
// Walks up the tree to check ancestors.
|
||||||
|
pub fn isHidden(self: *StyleManager, el: *Element, cache: ?*VisibilityCache, options: CheckVisibilityOptions) bool {
|
||||||
|
self.rebuildIfDirty() catch return false;
|
||||||
|
|
||||||
|
var current: ?*Element = el;
|
||||||
|
|
||||||
|
while (current) |elem| {
|
||||||
|
// Check cache first (only when checking all properties for caching consistency)
|
||||||
|
if (cache) |c| {
|
||||||
|
if (c.get(elem)) |hidden| {
|
||||||
|
if (hidden) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current = elem.parentElement();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hidden = self.isElementHidden(elem, options);
|
||||||
|
|
||||||
|
// Store in cache
|
||||||
|
if (cache) |c| {
|
||||||
|
c.put(self.page.call_arena, elem, hidden) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current = elem.parentElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a single element (not ancestors) is hidden.
|
||||||
|
fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOptions) bool {
|
||||||
|
// Track best match per property (value + priority)
|
||||||
|
// Initialize priority to INLINE_PRIORITY for properties we don't care about - this makes
|
||||||
|
// the loop naturally skip them since no stylesheet rule can have priority >= INLINE_PRIORITY
|
||||||
|
var display_none: ?bool = null;
|
||||||
|
var display_priority: u64 = 0;
|
||||||
|
|
||||||
|
var visibility_hidden: ?bool = null;
|
||||||
|
var visibility_priority: u64 = 0;
|
||||||
|
|
||||||
|
var opacity_zero: ?bool = null;
|
||||||
|
var opacity_priority: u64 = 0;
|
||||||
|
|
||||||
|
// Check inline styles FIRST - they use INLINE_PRIORITY so no stylesheet can beat them
|
||||||
|
if (getInlineStyleProperty(el, comptime .wrap("display"), self.page)) |property| {
|
||||||
|
if (property._value.eql(comptime .wrap("none"))) {
|
||||||
|
return true; // Early exit for hiding value
|
||||||
|
}
|
||||||
|
display_none = false;
|
||||||
|
display_priority = INLINE_PRIORITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.check_visibility) {
|
||||||
|
if (getInlineStyleProperty(el, comptime .wrap("visibility"), self.page)) |property| {
|
||||||
|
if (property._value.eql(comptime .wrap("hidden")) or property._value.eql(comptime .wrap("collapse"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
visibility_hidden = false;
|
||||||
|
visibility_priority = INLINE_PRIORITY;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This can't be beat. Setting this means that, when checking rules
|
||||||
|
// we no longer have to check if options.check_visibility is enabled.
|
||||||
|
// We can just compare the priority.
|
||||||
|
visibility_priority = INLINE_PRIORITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.check_opacity) {
|
||||||
|
if (getInlineStyleProperty(el, comptime .wrap("opacity"), self.page)) |property| {
|
||||||
|
if (property._value.eql(comptime .wrap("0"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
opacity_zero = false;
|
||||||
|
opacity_priority = INLINE_PRIORITY;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
opacity_priority = INLINE_PRIORITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (display_priority == INLINE_PRIORITY and visibility_priority == INLINE_PRIORITY and opacity_priority == INLINE_PRIORITY) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to check a single rule
|
||||||
|
const Ctx = struct {
|
||||||
|
display_none: *?bool,
|
||||||
|
display_priority: *u64,
|
||||||
|
visibility_hidden: *?bool,
|
||||||
|
visibility_priority: *u64,
|
||||||
|
opacity_zero: *?bool,
|
||||||
|
opacity_priority: *u64,
|
||||||
|
el: *Element,
|
||||||
|
page: *Page,
|
||||||
|
|
||||||
|
fn checkRules(ctx: @This(), rules: *const RuleList) void {
|
||||||
|
if (ctx.display_priority.* == INLINE_PRIORITY and
|
||||||
|
ctx.visibility_priority.* == INLINE_PRIORITY and
|
||||||
|
ctx.opacity_priority.* == INLINE_PRIORITY)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const priorities = rules.items(.priority);
|
||||||
|
const props_list = rules.items(.props);
|
||||||
|
const selectors = rules.items(.selector);
|
||||||
|
|
||||||
|
for (priorities, props_list, selectors) |p, props, selector| {
|
||||||
|
// Fast skip using packed u64 priority
|
||||||
|
if (p <= ctx.display_priority.* and p <= ctx.visibility_priority.* and p <= ctx.opacity_priority.*) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic for property dominance
|
||||||
|
const dominated = (props.display_none == null or p <= ctx.display_priority.*) and
|
||||||
|
(props.visibility_hidden == null or p <= ctx.visibility_priority.*) and
|
||||||
|
(props.opacity_zero == null or p <= ctx.opacity_priority.*);
|
||||||
|
|
||||||
|
if (dominated) continue;
|
||||||
|
|
||||||
|
if (matchesSelector(ctx.el, selector, ctx.page)) {
|
||||||
|
// Update best priorities
|
||||||
|
if (props.display_none != null and p > ctx.display_priority.*) {
|
||||||
|
ctx.display_none.* = props.display_none;
|
||||||
|
ctx.display_priority.* = p;
|
||||||
|
}
|
||||||
|
if (props.visibility_hidden != null and p > ctx.visibility_priority.*) {
|
||||||
|
ctx.visibility_hidden.* = props.visibility_hidden;
|
||||||
|
ctx.visibility_priority.* = p;
|
||||||
|
}
|
||||||
|
if (props.opacity_zero != null and p > ctx.opacity_priority.*) {
|
||||||
|
ctx.opacity_zero.* = props.opacity_zero;
|
||||||
|
ctx.opacity_priority.* = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const ctx = Ctx{
|
||||||
|
.display_none = &display_none,
|
||||||
|
.display_priority = &display_priority,
|
||||||
|
.visibility_hidden = &visibility_hidden,
|
||||||
|
.visibility_priority = &visibility_priority,
|
||||||
|
.opacity_zero = &opacity_zero,
|
||||||
|
.opacity_priority = &opacity_priority,
|
||||||
|
.el = el,
|
||||||
|
.page = self.page,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
|
||||||
|
if (self.id_rules.get(id)) |rules| {
|
||||||
|
ctx.checkRules(&rules);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("class"))) |class_attr| {
|
||||||
|
var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace);
|
||||||
|
while (it.next()) |class| {
|
||||||
|
if (self.class_rules.get(class)) |rules| {
|
||||||
|
ctx.checkRules(&rules);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.tag_rules.get(el.getTag())) |rules| {
|
||||||
|
ctx.checkRules(&rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.checkRules(&self.other_rules);
|
||||||
|
|
||||||
|
return (display_none orelse false) or (visibility_hidden orelse false) or (opacity_zero orelse false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if an element has pointer-events:none.
|
||||||
|
/// Checks inline style first - if set, skips stylesheet lookup.
|
||||||
|
/// Walks up the tree to check ancestors.
|
||||||
|
pub fn hasPointerEventsNone(self: *StyleManager, el: *Element, cache: ?*PointerEventsCache) bool {
|
||||||
|
self.rebuildIfDirty() catch return false;
|
||||||
|
|
||||||
|
var current: ?*Element = el;
|
||||||
|
|
||||||
|
while (current) |elem| {
|
||||||
|
// Check cache first
|
||||||
|
if (cache) |c| {
|
||||||
|
if (c.get(elem)) |pe_none| {
|
||||||
|
if (pe_none) return true;
|
||||||
|
current = elem.parentElement();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pe_none = self.elementHasPointerEventsNone(elem);
|
||||||
|
|
||||||
|
if (cache) |c| {
|
||||||
|
c.put(self.page.call_arena, elem, pe_none) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pe_none) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current = elem.parentElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a single element (not ancestors) has pointer-events:none.
|
||||||
|
fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool {
|
||||||
|
const page = self.page;
|
||||||
|
|
||||||
|
// Check inline style first
|
||||||
|
if (getInlineStyleProperty(el, .wrap("pointer-events"), page)) |property| {
|
||||||
|
if (property._value.eql(comptime .wrap("none"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result: ?bool = null;
|
||||||
|
var best_priority: u64 = 0;
|
||||||
|
|
||||||
|
// Helper to check a single rule
|
||||||
|
const checkRules = struct {
|
||||||
|
fn check(rules: *const RuleList, res: *?bool, current_priority: *u64, elem: *Element, p: *Page) void {
|
||||||
|
if (current_priority.* == INLINE_PRIORITY) return;
|
||||||
|
|
||||||
|
const priorities = rules.items(.priority);
|
||||||
|
const props_list = rules.items(.props);
|
||||||
|
const selectors = rules.items(.selector);
|
||||||
|
|
||||||
|
for (priorities, props_list, selectors) |priority, props, selector| {
|
||||||
|
if (priority <= current_priority.*) continue;
|
||||||
|
if (props.pointer_events_none == null) continue;
|
||||||
|
|
||||||
|
if (matchesSelector(elem, selector, p)) {
|
||||||
|
res.* = props.pointer_events_none;
|
||||||
|
current_priority.* = priority;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.check;
|
||||||
|
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
|
||||||
|
if (self.id_rules.get(id)) |rules| {
|
||||||
|
checkRules(&rules, &result, &best_priority, el, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("class"))) |class_attr| {
|
||||||
|
var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace);
|
||||||
|
while (it.next()) |class| {
|
||||||
|
if (self.class_rules.get(class)) |rules| {
|
||||||
|
checkRules(&rules, &result, &best_priority, el, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.tag_rules.get(el.getTag())) |rules| {
|
||||||
|
checkRules(&rules, &result, &best_priority, el, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkRules(&self.other_rules, &result, &best_priority, el, page);
|
||||||
|
|
||||||
|
return result orelse false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts visibility-relevant rules from a CSS rule.
|
||||||
|
// Creates one VisibilityRule per selector (not per selector list) so each has correct specificity.
|
||||||
|
// Buckets rules by their rightmost selector part for fast lookup.
|
||||||
|
fn addRule(self: *StyleManager, style_rule: *CSSStyleRule) !void {
|
||||||
|
const selector_text = style_rule._selector_text;
|
||||||
|
if (selector_text.len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the rule has visibility-relevant properties
|
||||||
|
const style = style_rule._style orelse return;
|
||||||
|
const props = extractVisibilityProperties(style);
|
||||||
|
if (!props.isRelevant()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the selector list
|
||||||
|
const selectors = SelectorParser.parseList(self.arena, selector_text, self.page) catch return;
|
||||||
|
if (selectors.len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create one rule per selector - each has its own specificity
|
||||||
|
// e.g., "#id, .class { display: none }" becomes two rules with different specificities
|
||||||
|
for (selectors) |selector| {
|
||||||
|
// Get the rightmost compound (last segment, or first if no segments)
|
||||||
|
const rightmost = if (selector.segments.len > 0)
|
||||||
|
selector.segments[selector.segments.len - 1].compound
|
||||||
|
else
|
||||||
|
selector.first;
|
||||||
|
|
||||||
|
// Find the bucketing key from rightmost compound
|
||||||
|
const bucket_key = getBucketKey(rightmost) orelse continue; // skip if dynamic pseudo-class
|
||||||
|
|
||||||
|
const rule = VisibilityRule{
|
||||||
|
.props = props,
|
||||||
|
.selector = selector,
|
||||||
|
.priority = (@as(u64, computeSpecificity(selector)) << 32) | @as(u64, self.next_doc_order),
|
||||||
|
};
|
||||||
|
self.next_doc_order += 1;
|
||||||
|
|
||||||
|
// Add to appropriate bucket
|
||||||
|
switch (bucket_key) {
|
||||||
|
.id => |id| {
|
||||||
|
const gop = try self.id_rules.getOrPut(self.arena, id);
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
.class => |class| {
|
||||||
|
const gop = try self.class_rules.getOrPut(self.arena, class);
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
.tag => |tag| {
|
||||||
|
const gop = try self.tag_rules.getOrPut(self.arena, tag);
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
.other => {
|
||||||
|
try self.other_rules.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const BucketKey = union(enum) {
|
||||||
|
id: []const u8,
|
||||||
|
class: []const u8,
|
||||||
|
tag: Tag,
|
||||||
|
other,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Returns the best bucket key for a compound selector, or null if it contains
|
||||||
|
/// a dynamic pseudo-class we should skip (hover, active, focus, etc.)
|
||||||
|
/// Priority: id > class > tag > other
|
||||||
|
fn getBucketKey(compound: Selector.Compound) ?BucketKey {
|
||||||
|
var best_key: BucketKey = .other;
|
||||||
|
|
||||||
|
for (compound.parts) |part| {
|
||||||
|
switch (part) {
|
||||||
|
.id => |id| {
|
||||||
|
best_key = .{ .id = id };
|
||||||
|
},
|
||||||
|
.class => |class| {
|
||||||
|
if (best_key != .id) {
|
||||||
|
best_key = .{ .class = class };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.tag => |tag| {
|
||||||
|
if (best_key == .other) {
|
||||||
|
best_key = .{ .tag = tag };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.tag_name => {
|
||||||
|
// Custom tag - put in other bucket (can't efficiently look up)
|
||||||
|
// Keep current best_key if we have something better
|
||||||
|
},
|
||||||
|
.pseudo_class => |pc| {
|
||||||
|
// Skip dynamic pseudo-classes - they depend on interaction state
|
||||||
|
switch (pc) {
|
||||||
|
.hover, .active, .focus, .focus_within, .focus_visible, .visited, .target => {
|
||||||
|
return null; // Skip this selector entirely
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.universal, .attribute => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts visibility-relevant properties from a style declaration.
|
||||||
|
fn extractVisibilityProperties(style: *CSSStyleProperties) VisibilityProperties {
|
||||||
|
var props = VisibilityProperties{};
|
||||||
|
const decl = style.asCSSStyleDeclaration();
|
||||||
|
|
||||||
|
if (decl.findProperty(comptime .wrap("display"))) |property| {
|
||||||
|
props.display_none = property._value.eql(comptime .wrap("none"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decl.findProperty(comptime .wrap("visibility"))) |property| {
|
||||||
|
props.visibility_hidden = property._value.eql(comptime .wrap("hidden")) or property._value.eql(comptime .wrap("collapse"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decl.findProperty(comptime .wrap("opacity"))) |property| {
|
||||||
|
props.opacity_zero = property._value.eql(comptime .wrap("0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decl.findProperty(.wrap("pointer-events"))) |property| {
|
||||||
|
props.pointer_events_none = property._value.eql(comptime .wrap("none"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes CSS specificity for a selector.
|
||||||
|
// Returns packed value: (id_count << 20) | (class_count << 10) | element_count
|
||||||
|
pub fn computeSpecificity(selector: Selector.Selector) u32 {
|
||||||
|
var ids: u32 = 0;
|
||||||
|
var classes: u32 = 0; // includes classes, attributes, pseudo-classes
|
||||||
|
var elements: u32 = 0; // includes elements, pseudo-elements
|
||||||
|
|
||||||
|
// Count specificity for first compound
|
||||||
|
countCompoundSpecificity(selector.first, &ids, &classes, &elements);
|
||||||
|
|
||||||
|
// Count specificity for subsequent segments
|
||||||
|
for (selector.segments) |segment| {
|
||||||
|
countCompoundSpecificity(segment.compound, &ids, &classes, &elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack into single u32: (ids << 20) | (classes << 10) | elements
|
||||||
|
// This gives us 10 bits each, supporting up to 1023 of each type
|
||||||
|
return (@as(u32, @min(ids, 1023)) << 20) | (@as(u32, @min(classes, 1023)) << 10) | @min(elements, 1023);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn countCompoundSpecificity(compound: Selector.Compound, ids: *u32, classes: *u32, elements: *u32) void {
|
||||||
|
for (compound.parts) |part| {
|
||||||
|
switch (part) {
|
||||||
|
.id => ids.* += 1,
|
||||||
|
.class => classes.* += 1,
|
||||||
|
.tag, .tag_name => elements.* += 1,
|
||||||
|
.universal => {}, // zero specificity
|
||||||
|
.attribute => classes.* += 1,
|
||||||
|
.pseudo_class => |pc| {
|
||||||
|
switch (pc) {
|
||||||
|
// :where() has zero specificity
|
||||||
|
.where => {},
|
||||||
|
// :not(), :is(), :has() take specificity of their most specific argument
|
||||||
|
.not, .is, .has => |nested| {
|
||||||
|
var max_nested: u32 = 0;
|
||||||
|
for (nested) |nested_sel| {
|
||||||
|
const spec = computeSpecificity(nested_sel);
|
||||||
|
if (spec > max_nested) max_nested = spec;
|
||||||
|
}
|
||||||
|
// Unpack and add to our counts
|
||||||
|
ids.* += (max_nested >> 20) & 0x3FF;
|
||||||
|
classes.* += (max_nested >> 10) & 0x3FF;
|
||||||
|
elements.* += max_nested & 0x3FF;
|
||||||
|
},
|
||||||
|
// All other pseudo-classes count as class-level specificity
|
||||||
|
else => classes.* += 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matchesSelector(el: *Element, selector: Selector.Selector, page: *Page) bool {
|
||||||
|
const node = el.asNode();
|
||||||
|
return SelectorList.matches(node, selector, node, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
const VisibilityProperties = struct {
|
||||||
|
display_none: ?bool = null,
|
||||||
|
visibility_hidden: ?bool = null,
|
||||||
|
opacity_zero: ?bool = null,
|
||||||
|
pointer_events_none: ?bool = null,
|
||||||
|
|
||||||
|
// returne true if any field in VisibilityProperties is not null
|
||||||
|
fn isRelevant(self: VisibilityProperties) bool {
|
||||||
|
return self.display_none != null or
|
||||||
|
self.visibility_hidden != null or
|
||||||
|
self.opacity_zero != null or
|
||||||
|
self.pointer_events_none != null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const VisibilityRule = struct {
|
||||||
|
selector: Selector.Selector, // Single selector, not a list
|
||||||
|
props: VisibilityProperties,
|
||||||
|
|
||||||
|
// Packed priority: (specificity << 32) | doc_order
|
||||||
|
priority: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CheckVisibilityOptions = struct {
|
||||||
|
check_opacity: bool = false,
|
||||||
|
check_visibility: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inline styles always win over stylesheets - use max u64 as sentinel
|
||||||
|
const INLINE_PRIORITY: u64 = std.math.maxInt(u64);
|
||||||
|
|
||||||
|
fn getInlineStyleProperty(el: *Element, property_name: String, page: *Page) ?*CSSStyleProperty {
|
||||||
|
const style = el.getOrCreateStyle(page) catch |err| {
|
||||||
|
log.err(.browser, "StyleManager getOrCreateStyle", .{ .err = err });
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
return style.asCSSStyleDeclaration().findProperty(property_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
test "StyleManager: computeSpecificity: element selector" {
|
||||||
|
// div -> (0, 0, 1)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .tag = .div }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: class selector" {
|
||||||
|
// .foo -> (0, 1, 0)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .class = "foo" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1 << 10, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: id selector" {
|
||||||
|
// #bar -> (1, 0, 0)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .id = "bar" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1 << 20, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: combined selector" {
|
||||||
|
// div.foo#bar -> (1, 1, 1)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .tag = .div },
|
||||||
|
.{ .class = "foo" },
|
||||||
|
.{ .id = "bar" },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual((1 << 20) | (1 << 10) | 1, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: universal selector" {
|
||||||
|
// * -> (0, 0, 0)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.universal} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(0, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: multiple classes" {
|
||||||
|
// .a.b.c -> (0, 3, 0)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .class = "a" },
|
||||||
|
.{ .class = "b" },
|
||||||
|
.{ .class = "c" },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(3 << 10, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: descendant combinator" {
|
||||||
|
// div span -> (0, 0, 2)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .tag = .div }} },
|
||||||
|
.segments = &.{
|
||||||
|
.{ .combinator = .descendant, .compound = .{ .parts = &.{.{ .tag = .span }} } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(2, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: :where() has zero specificity" {
|
||||||
|
// :where(.foo) -> (0, 0, 0) regardless of what's inside
|
||||||
|
const inner_selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .class = "foo" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .pseudo_class = .{ .where = &.{inner_selector} } },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(0, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: :not() takes inner specificity" {
|
||||||
|
// :not(.foo) -> (0, 1, 0) - takes specificity of .foo
|
||||||
|
const inner_selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .class = "foo" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .pseudo_class = .{ .not = &.{inner_selector} } },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1 << 10, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: :is() takes most specific inner" {
|
||||||
|
// :is(.foo, #bar) -> (1, 0, 0) - takes the most specific (#bar)
|
||||||
|
const class_selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .class = "foo" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
const id_selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .id = "bar" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .pseudo_class = .{ .is = &.{ class_selector, id_selector } } },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1 << 20, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: pseudo-class (general)" {
|
||||||
|
// :hover -> (0, 1, 0) - pseudo-classes count as class-level
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .pseudo_class = .hover },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1 << 10, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: document order tie-breaking" {
|
||||||
|
// When specificity is equal, higher doc_order (later in document) wins
|
||||||
|
const beats = struct {
|
||||||
|
fn f(spec: u32, doc_order: u32, best_spec: u32, best_doc_order: u32) bool {
|
||||||
|
return spec > best_spec or (spec == best_spec and doc_order > best_doc_order);
|
||||||
|
}
|
||||||
|
}.f;
|
||||||
|
|
||||||
|
// Higher specificity always wins regardless of doc_order
|
||||||
|
try testing.expect(beats(2, 0, 1, 10));
|
||||||
|
try testing.expect(!beats(1, 10, 2, 0));
|
||||||
|
|
||||||
|
// Equal specificity: higher doc_order wins
|
||||||
|
try testing.expect(beats(1, 5, 1, 3)); // doc_order 5 > 3
|
||||||
|
try testing.expect(!beats(1, 3, 1, 5)); // doc_order 3 < 5
|
||||||
|
|
||||||
|
// Equal specificity and doc_order: no win
|
||||||
|
try testing.expect(!beats(1, 5, 1, 5));
|
||||||
|
}
|
||||||
@@ -204,7 +204,7 @@ pub fn ensureEncoded(allocator: Allocator, url: [:0]const u8) ![:0]const u8 {
|
|||||||
return buf.items[0 .. buf.items.len - 1 :0];
|
return buf.items[0 .. buf.items.len - 1 :0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const EncodeSet = enum { path, query, userinfo };
|
const EncodeSet = enum { path, query, userinfo, fragment };
|
||||||
|
|
||||||
fn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime encode_set: EncodeSet) ![]const u8 {
|
fn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime encode_set: EncodeSet) ![]const u8 {
|
||||||
// Check if encoding is needed
|
// Check if encoding is needed
|
||||||
@@ -256,8 +256,10 @@ fn shouldPercentEncode(c: u8, comptime encode_set: EncodeSet) bool {
|
|||||||
';', '=' => encode_set == .userinfo,
|
';', '=' => encode_set == .userinfo,
|
||||||
// Separators: userinfo must encode these
|
// Separators: userinfo must encode these
|
||||||
'/', ':', '@' => encode_set == .userinfo,
|
'/', ':', '@' => encode_set == .userinfo,
|
||||||
// '?' is allowed in queries but not in paths or userinfo
|
// '?' is allowed in queries only
|
||||||
'?' => encode_set != .query,
|
'?' => encode_set != .query,
|
||||||
|
// '#' is allowed in fragments only
|
||||||
|
'#' => encode_set != .fragment,
|
||||||
// Everything else needs encoding (including space)
|
// Everything else needs encoding (including space)
|
||||||
else => true,
|
else => true,
|
||||||
};
|
};
|
||||||
@@ -323,14 +325,22 @@ pub fn getPassword(raw: [:0]const u8) []const u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getPathname(raw: [:0]const u8) []const u8 {
|
pub fn getPathname(raw: [:0]const u8) []const u8 {
|
||||||
const protocol_end = std.mem.indexOf(u8, raw, "://") orelse 0;
|
const protocol_end = std.mem.indexOf(u8, raw, "://");
|
||||||
const path_start = std.mem.indexOfScalarPos(u8, raw, if (protocol_end > 0) protocol_end + 3 else 0, '/') orelse raw.len;
|
|
||||||
|
// Handle scheme:path URLs like about:blank (no "://")
|
||||||
|
if (protocol_end == null) {
|
||||||
|
const colon_pos = std.mem.indexOfScalar(u8, raw, ':') orelse return "";
|
||||||
|
const path = raw[colon_pos + 1 ..];
|
||||||
|
const query_or_hash = std.mem.indexOfAny(u8, path, "?#") orelse path.len;
|
||||||
|
return path[0..query_or_hash];
|
||||||
|
}
|
||||||
|
|
||||||
|
const path_start = std.mem.indexOfScalarPos(u8, raw, protocol_end.? + 3, '/') orelse raw.len;
|
||||||
|
|
||||||
const query_or_hash_start = std.mem.indexOfAnyPos(u8, raw, path_start, "?#") orelse raw.len;
|
const query_or_hash_start = std.mem.indexOfAnyPos(u8, raw, path_start, "?#") orelse raw.len;
|
||||||
|
|
||||||
if (path_start >= query_or_hash_start) {
|
if (path_start >= query_or_hash_start) {
|
||||||
if (std.mem.indexOf(u8, raw, "://") != null) return "/";
|
return "/";
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw[path_start..query_or_hash_start];
|
return raw[path_start..query_or_hash_start];
|
||||||
@@ -347,25 +357,38 @@ pub fn isHTTPS(raw: [:0]const u8) bool {
|
|||||||
|
|
||||||
pub fn getHostname(raw: [:0]const u8) []const u8 {
|
pub fn getHostname(raw: [:0]const u8) []const u8 {
|
||||||
const host = getHost(raw);
|
const host = getHost(raw);
|
||||||
const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return host;
|
const port_sep = findPortSeparator(host) orelse return host;
|
||||||
return host[0..pos];
|
return host[0..port_sep];
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getPort(raw: [:0]const u8) []const u8 {
|
pub fn getPort(raw: [:0]const u8) []const u8 {
|
||||||
const host = getHost(raw);
|
const host = getHost(raw);
|
||||||
const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return "";
|
const port_sep = findPortSeparator(host) orelse return "";
|
||||||
|
return host[port_sep + 1 ..];
|
||||||
|
}
|
||||||
|
|
||||||
if (pos + 1 >= host.len) {
|
// Finds the colon separating host from port, handling IPv6 bracket notation.
|
||||||
return "";
|
// For IPv6 like "[::1]:8080", returns position of ":" after "]".
|
||||||
|
// For IPv6 like "[::1]" (no port), returns null.
|
||||||
|
// For regular hosts, returns position of last ":" if followed by digits.
|
||||||
|
fn findPortSeparator(host: []const u8) ?usize {
|
||||||
|
if (host.len > 0 and host[0] == '[') {
|
||||||
|
// IPv6: find closing bracket, port separator must be after it
|
||||||
|
const bracket_end = std.mem.indexOfScalar(u8, host, ']') orelse return null;
|
||||||
|
if (bracket_end + 1 < host.len and host[bracket_end + 1] == ':') {
|
||||||
|
return bracket_end + 1;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regular host: find last colon and verify it's followed by digits
|
||||||
|
const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return null;
|
||||||
|
if (pos + 1 >= host.len) return null;
|
||||||
|
|
||||||
for (host[pos + 1 ..]) |c| {
|
for (host[pos + 1 ..]) |c| {
|
||||||
if (c < '0' or c > '9') {
|
if (c < '0' or c > '9') return null;
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return pos;
|
||||||
return host[pos + 1 ..];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getSearch(raw: [:0]const u8) []const u8 {
|
pub fn getSearch(raw: [:0]const u8) []const u8 {
|
||||||
@@ -393,21 +416,12 @@ pub fn getOrigin(allocator: Allocator, raw: [:0]const u8) !?[]const u8 {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var authority_start = scheme_end + 3;
|
const auth = parseAuthority(raw) orelse return null;
|
||||||
const has_user_info = if (std.mem.indexOf(u8, raw[authority_start..], "@")) |pos| blk: {
|
const has_user_info = auth.has_user_info;
|
||||||
authority_start += pos + 1;
|
const authority_end = auth.host_end;
|
||||||
break :blk true;
|
|
||||||
} else false;
|
|
||||||
|
|
||||||
// Find end of authority (start of path/query/fragment or end of string)
|
|
||||||
const authority_end_relative = std.mem.indexOfAny(u8, raw[authority_start..], "/?#");
|
|
||||||
const authority_end = if (authority_end_relative) |end|
|
|
||||||
authority_start + end
|
|
||||||
else
|
|
||||||
raw.len;
|
|
||||||
|
|
||||||
// Check for port in the host:port section
|
// Check for port in the host:port section
|
||||||
const host_part = raw[authority_start..authority_end];
|
const host_part = auth.getHost(raw);
|
||||||
if (std.mem.lastIndexOfScalar(u8, host_part, ':')) |colon_pos_in_host| {
|
if (std.mem.lastIndexOfScalar(u8, host_part, ':')) |colon_pos_in_host| {
|
||||||
const port = host_part[colon_pos_in_host + 1 ..];
|
const port = host_part[colon_pos_in_host + 1 ..];
|
||||||
|
|
||||||
@@ -448,31 +462,18 @@ pub fn getOrigin(allocator: Allocator, raw: [:0]const u8) !?[]const u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn getUserInfo(raw: [:0]const u8) ?[]const u8 {
|
fn getUserInfo(raw: [:0]const u8) ?[]const u8 {
|
||||||
const scheme_end = std.mem.indexOf(u8, raw, "://") orelse return null;
|
const auth = parseAuthority(raw) orelse return null;
|
||||||
|
if (!auth.has_user_info) return null;
|
||||||
|
|
||||||
|
// User info is from authority_start to host_start - 1 (excluding the @)
|
||||||
|
const scheme_end = std.mem.indexOf(u8, raw, "://").?;
|
||||||
const authority_start = scheme_end + 3;
|
const authority_start = scheme_end + 3;
|
||||||
|
return raw[authority_start .. auth.host_start - 1];
|
||||||
const pos = std.mem.indexOfScalar(u8, raw[authority_start..], '@') orelse return null;
|
|
||||||
const path_start = std.mem.indexOfScalarPos(u8, raw, authority_start, '/') orelse raw.len;
|
|
||||||
|
|
||||||
const full_pos = authority_start + pos;
|
|
||||||
if (full_pos < path_start) {
|
|
||||||
return raw[authority_start..full_pos];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getHost(raw: [:0]const u8) []const u8 {
|
pub fn getHost(raw: [:0]const u8) []const u8 {
|
||||||
const scheme_end = std.mem.indexOf(u8, raw, "://") orelse return "";
|
const auth = parseAuthority(raw) orelse return "";
|
||||||
|
return auth.getHost(raw);
|
||||||
var authority_start = scheme_end + 3;
|
|
||||||
if (std.mem.indexOf(u8, raw[authority_start..], "@")) |pos| {
|
|
||||||
authority_start += pos + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const authority = raw[authority_start..];
|
|
||||||
const path_start = std.mem.indexOfAny(u8, authority, "/?#") orelse return authority;
|
|
||||||
return authority[0..path_start];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns true if these two URLs point to the same document.
|
// Returns true if these two URLs point to the same document.
|
||||||
@@ -587,11 +588,13 @@ pub fn setPathname(current: [:0]const u8, value: []const u8, allocator: Allocato
|
|||||||
const search = getSearch(current);
|
const search = getSearch(current);
|
||||||
const hash = getHash(current);
|
const hash = getHash(current);
|
||||||
|
|
||||||
|
const encoded = try percentEncodeSegment(allocator, value, .path);
|
||||||
|
|
||||||
// Add / prefix if not present and value is not empty
|
// Add / prefix if not present and value is not empty
|
||||||
const pathname = if (value.len > 0 and value[0] != '/')
|
const pathname = if (encoded.len > 0 and encoded[0] != '/')
|
||||||
try std.fmt.allocPrint(allocator, "/{s}", .{value})
|
try std.fmt.allocPrint(allocator, "/{s}", .{encoded})
|
||||||
else
|
else
|
||||||
value;
|
encoded;
|
||||||
|
|
||||||
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
||||||
}
|
}
|
||||||
@@ -602,11 +605,13 @@ pub fn setSearch(current: [:0]const u8, value: []const u8, allocator: Allocator)
|
|||||||
const pathname = getPathname(current);
|
const pathname = getPathname(current);
|
||||||
const hash = getHash(current);
|
const hash = getHash(current);
|
||||||
|
|
||||||
|
const encoded = try percentEncodeSegment(allocator, value, .query);
|
||||||
|
|
||||||
// Add ? prefix if not present and value is not empty
|
// Add ? prefix if not present and value is not empty
|
||||||
const search = if (value.len > 0 and value[0] != '?')
|
const search = if (encoded.len > 0 and value[0] != '?')
|
||||||
try std.fmt.allocPrint(allocator, "?{s}", .{value})
|
try std.fmt.allocPrint(allocator, "?{s}", .{encoded})
|
||||||
else
|
else
|
||||||
value;
|
encoded;
|
||||||
|
|
||||||
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
||||||
}
|
}
|
||||||
@@ -617,11 +622,13 @@ pub fn setHash(current: [:0]const u8, value: []const u8, allocator: Allocator) !
|
|||||||
const pathname = getPathname(current);
|
const pathname = getPathname(current);
|
||||||
const search = getSearch(current);
|
const search = getSearch(current);
|
||||||
|
|
||||||
|
const encoded = try percentEncodeSegment(allocator, value, .fragment);
|
||||||
|
|
||||||
// Add # prefix if not present and value is not empty
|
// Add # prefix if not present and value is not empty
|
||||||
const hash = if (value.len > 0 and value[0] != '#')
|
const hash = if (encoded.len > 0 and encoded[0] != '#')
|
||||||
try std.fmt.allocPrint(allocator, "#{s}", .{value})
|
try std.fmt.allocPrint(allocator, "#{s}", .{encoded})
|
||||||
else
|
else
|
||||||
value;
|
encoded;
|
||||||
|
|
||||||
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
||||||
}
|
}
|
||||||
@@ -745,6 +752,47 @@ pub fn unescape(arena: Allocator, input: []const u8) ![]const u8 {
|
|||||||
return result.items;
|
return result.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AuthorityInfo = struct {
|
||||||
|
host_start: usize,
|
||||||
|
host_end: usize,
|
||||||
|
has_user_info: bool,
|
||||||
|
|
||||||
|
fn getHost(self: AuthorityInfo, raw: []const u8) []const u8 {
|
||||||
|
return raw[self.host_start..self.host_end];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parses the authority component of a URL, correctly handling userinfo.
|
||||||
|
// Returns null if the URL doesn't have a valid scheme (no "://").
|
||||||
|
// SECURITY: Only looks for @ within the authority portion (before /?#)
|
||||||
|
// to prevent path-based @ injection attacks.
|
||||||
|
fn parseAuthority(raw: []const u8) ?AuthorityInfo {
|
||||||
|
const scheme_end = std.mem.indexOf(u8, raw, "://") orelse return null;
|
||||||
|
const authority_start = scheme_end + 3;
|
||||||
|
|
||||||
|
// Find end of authority FIRST (start of path/query/fragment or end of string)
|
||||||
|
const authority_end = if (std.mem.indexOfAny(u8, raw[authority_start..], "/?#")) |end|
|
||||||
|
authority_start + end
|
||||||
|
else
|
||||||
|
raw.len;
|
||||||
|
|
||||||
|
// Only look for @ within the authority portion, not in path/query/fragment
|
||||||
|
const authority_portion = raw[authority_start..authority_end];
|
||||||
|
if (std.mem.indexOf(u8, authority_portion, "@")) |pos| {
|
||||||
|
return .{
|
||||||
|
.host_start = authority_start + pos + 1,
|
||||||
|
.host_end = authority_end,
|
||||||
|
.has_user_info = true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.host_start = authority_start,
|
||||||
|
.host_end = authority_end,
|
||||||
|
.has_user_info = false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
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"));
|
||||||
@@ -1413,4 +1461,112 @@ test "URL: getHost" {
|
|||||||
try testing.expectEqualSlices(u8, "example.com", getHost("https://user:pass@example.com/page"));
|
try testing.expectEqualSlices(u8, "example.com", getHost("https://user:pass@example.com/page"));
|
||||||
try testing.expectEqualSlices(u8, "example.com:8080", getHost("https://user:pass@example.com:8080/page"));
|
try testing.expectEqualSlices(u8, "example.com:8080", getHost("https://user:pass@example.com:8080/page"));
|
||||||
try testing.expectEqualSlices(u8, "", getHost("not-a-url"));
|
try testing.expectEqualSlices(u8, "", getHost("not-a-url"));
|
||||||
|
|
||||||
|
// SECURITY: @ in path must NOT be treated as userinfo separator
|
||||||
|
try testing.expectEqualSlices(u8, "evil.example.com", getHost("http://evil.example.com/@victim.example.com/"));
|
||||||
|
try testing.expectEqualSlices(u8, "evil.example.com", getHost("https://evil.example.com/path/@victim.example.com"));
|
||||||
|
|
||||||
|
// IPv6 addresses
|
||||||
|
try testing.expectEqualSlices(u8, "[::1]:8080", getHost("http://[::1]:8080/path"));
|
||||||
|
try testing.expectEqualSlices(u8, "[::1]", getHost("http://[::1]/path"));
|
||||||
|
try testing.expectEqualSlices(u8, "[2001:db8::1]", getHost("https://[2001:db8::1]/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "URL: getHostname" {
|
||||||
|
// Regular hosts
|
||||||
|
try testing.expectEqualSlices(u8, "example.com", getHostname("https://example.com:8080/path"));
|
||||||
|
try testing.expectEqualSlices(u8, "example.com", getHostname("https://example.com/path"));
|
||||||
|
|
||||||
|
// IPv6 with port
|
||||||
|
try testing.expectEqualSlices(u8, "[::1]", getHostname("http://[::1]:8080/path"));
|
||||||
|
|
||||||
|
// IPv6 without port - must return full bracket notation
|
||||||
|
try testing.expectEqualSlices(u8, "[::1]", getHostname("http://[::1]/path"));
|
||||||
|
try testing.expectEqualSlices(u8, "[2001:db8::1]", getHostname("https://[2001:db8::1]/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "URL: getPort" {
|
||||||
|
// Regular hosts
|
||||||
|
try testing.expectEqualSlices(u8, "8080", getPort("https://example.com:8080/path"));
|
||||||
|
try testing.expectEqualSlices(u8, "", getPort("https://example.com/path"));
|
||||||
|
|
||||||
|
// IPv6 with port
|
||||||
|
try testing.expectEqualSlices(u8, "8080", getPort("http://[::1]:8080/path"));
|
||||||
|
try testing.expectEqualSlices(u8, "3000", getPort("http://[2001:db8::1]:3000/"));
|
||||||
|
|
||||||
|
// IPv6 without port - colons inside brackets must not be treated as port separator
|
||||||
|
try testing.expectEqualSlices(u8, "", getPort("http://[::1]/path"));
|
||||||
|
try testing.expectEqualSlices(u8, "", getPort("https://[2001:db8::1]/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "URL: setPathname percent-encodes" {
|
||||||
|
// Use arena allocator to match production usage (setPathname makes intermediate allocations)
|
||||||
|
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const allocator = arena.allocator();
|
||||||
|
|
||||||
|
// Spaces must be encoded as %20
|
||||||
|
const result1 = try setPathname("http://a/", "c d", allocator);
|
||||||
|
try testing.expectEqualSlices(u8, "http://a/c%20d", result1);
|
||||||
|
|
||||||
|
// Already-encoded sequences must not be double-encoded
|
||||||
|
const result2 = try setPathname("https://example.com/path", "/already%20encoded", allocator);
|
||||||
|
try testing.expectEqualSlices(u8, "https://example.com/already%20encoded", result2);
|
||||||
|
|
||||||
|
// Query and hash must be preserved
|
||||||
|
const result3 = try setPathname("https://example.com/path?a=b#hash", "/new path", allocator);
|
||||||
|
try testing.expectEqualSlices(u8, "https://example.com/new%20path?a=b#hash", result3);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "URL: getOrigin" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
const Case = struct {
|
||||||
|
url: [:0]const u8,
|
||||||
|
expected: ?[]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cases = [_]Case{
|
||||||
|
// Basic HTTP/HTTPS origins
|
||||||
|
.{ .url = "http://example.com/path", .expected = "http://example.com" },
|
||||||
|
.{ .url = "https://example.com/path", .expected = "https://example.com" },
|
||||||
|
.{ .url = "https://example.com:8080/path", .expected = "https://example.com:8080" },
|
||||||
|
|
||||||
|
// Default ports should be stripped
|
||||||
|
.{ .url = "http://example.com:80/path", .expected = "http://example.com" },
|
||||||
|
.{ .url = "https://example.com:443/path", .expected = "https://example.com" },
|
||||||
|
|
||||||
|
// User info should be stripped from origin
|
||||||
|
.{ .url = "http://user:pass@example.com/path", .expected = "http://example.com" },
|
||||||
|
.{ .url = "https://user@example.com:8080/path", .expected = "https://example.com:8080" },
|
||||||
|
|
||||||
|
// Non-HTTP schemes return null
|
||||||
|
.{ .url = "ftp://example.com/path", .expected = null },
|
||||||
|
.{ .url = "file:///path/to/file", .expected = null },
|
||||||
|
.{ .url = "about:blank", .expected = null },
|
||||||
|
|
||||||
|
// Query and fragment should not affect origin
|
||||||
|
.{ .url = "https://example.com?query=1", .expected = "https://example.com" },
|
||||||
|
.{ .url = "https://example.com#fragment", .expected = "https://example.com" },
|
||||||
|
.{ .url = "https://example.com/path?q=1#frag", .expected = "https://example.com" },
|
||||||
|
|
||||||
|
// SECURITY: @ in path must NOT be treated as userinfo separator
|
||||||
|
// This would be a Same-Origin Policy bypass if mishandled
|
||||||
|
.{ .url = "http://evil.example.com/@victim.example.com/", .expected = "http://evil.example.com" },
|
||||||
|
.{ .url = "https://evil.example.com/path/@victim.example.com/steal", .expected = "https://evil.example.com" },
|
||||||
|
.{ .url = "http://evil.example.com/@victim.example.com:443/", .expected = "http://evil.example.com" },
|
||||||
|
|
||||||
|
// @ in query/fragment must also not affect origin
|
||||||
|
.{ .url = "https://example.com/path?user=foo@bar.com", .expected = "https://example.com" },
|
||||||
|
.{ .url = "https://example.com/path#user@host", .expected = "https://example.com" },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (cases) |case| {
|
||||||
|
const result = try getOrigin(testing.arena_allocator, case.url);
|
||||||
|
if (case.expected) |expected| {
|
||||||
|
try testing.expectString(expected, result.?);
|
||||||
|
} else {
|
||||||
|
try testing.expectEqual(null, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ const Element = @import("webapi/Element.zig");
|
|||||||
const Event = @import("webapi/Event.zig");
|
const Event = @import("webapi/Event.zig");
|
||||||
const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
|
const Session = @import("Session.zig");
|
||||||
|
const Selector = @import("webapi/selector/Selector.zig");
|
||||||
|
|
||||||
pub fn click(node: *DOMNode, page: *Page) !void {
|
pub fn click(node: *DOMNode, page: *Page) !void {
|
||||||
const el = node.is(Element) orelse return error.InvalidNodeType;
|
const el = node.is(Element) orelse return error.InvalidNodeType;
|
||||||
@@ -102,3 +104,34 @@ pub fn scroll(node: ?*DOMNode, x: ?i32, y: ?i32, page: *Page) !void {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn waitForSelector(selector: [:0]const u8, timeout_ms: u32, session: *Session) !*DOMNode {
|
||||||
|
var timer = try std.time.Timer.start();
|
||||||
|
var runner = try session.runner(.{});
|
||||||
|
try runner.wait(.{ .ms = timeout_ms, .until = .load });
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const page = runner.page;
|
||||||
|
const element = Selector.querySelector(page.document.asNode(), selector, page) catch {
|
||||||
|
return error.InvalidSelector;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (element) |el| {
|
||||||
|
return el.asNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms);
|
||||||
|
if (elapsed >= timeout_ms) {
|
||||||
|
return error.Timeout;
|
||||||
|
}
|
||||||
|
switch (try runner.tick(.{ .ms = timeout_ms - elapsed })) {
|
||||||
|
.done => return error.Timeout,
|
||||||
|
.ok => |recommended_sleep_ms| {
|
||||||
|
if (recommended_sleep_ms > 0) {
|
||||||
|
// guanrateed to be <= 20ms
|
||||||
|
std.Thread.sleep(std.time.ns_per_ms * recommended_sleep_ms);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -293,3 +293,191 @@ fn isBang(token: Tokenizer.Token) bool {
|
|||||||
else => false,
|
else => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const Rule = struct {
|
||||||
|
selector: []const u8,
|
||||||
|
block: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn parseStylesheet(input: []const u8) RulesIterator {
|
||||||
|
return RulesIterator.init(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const RulesIterator = struct {
|
||||||
|
input: []const u8,
|
||||||
|
stream: TokenStream,
|
||||||
|
has_skipped_at_rule: bool = false,
|
||||||
|
|
||||||
|
pub fn init(input: []const u8) RulesIterator {
|
||||||
|
return .{
|
||||||
|
.input = input,
|
||||||
|
.stream = TokenStream.init(input),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(self: *RulesIterator) ?Rule {
|
||||||
|
var selector_start: ?usize = null;
|
||||||
|
var selector_end: ?usize = null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const peeked = self.stream.peek() orelse return null;
|
||||||
|
|
||||||
|
if (peeked.token == .curly_bracket_block) {
|
||||||
|
if (selector_start == null) {
|
||||||
|
self.skipBlock();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const open_brace = self.stream.next() orelse return null;
|
||||||
|
const block_start = open_brace.end;
|
||||||
|
var block_end = block_start;
|
||||||
|
|
||||||
|
var depth: usize = 1;
|
||||||
|
while (true) {
|
||||||
|
const span = self.stream.next() orelse {
|
||||||
|
block_end = self.input.len;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if (span.token == .curly_bracket_block) {
|
||||||
|
depth += 1;
|
||||||
|
} else if (span.token == .close_curly_bracket) {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth == 0) {
|
||||||
|
block_end = span.start;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var selector = self.input[selector_start.?..selector_end.?];
|
||||||
|
selector = std.mem.trim(u8, selector, &std.ascii.whitespace);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.selector = selector,
|
||||||
|
.block = self.input[block_start..block_end],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (peeked.token == .at_keyword) {
|
||||||
|
self.has_skipped_at_rule = true;
|
||||||
|
self.skipAtRule();
|
||||||
|
selector_start = null;
|
||||||
|
selector_end = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selector_start == null and (isWhitespaceOrComment(peeked.token) or isSemicolon(peeked.token))) {
|
||||||
|
_ = self.stream.next();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = self.stream.next() orelse return null;
|
||||||
|
if (!isWhitespaceOrComment(span.token)) {
|
||||||
|
if (selector_start == null) selector_start = span.start;
|
||||||
|
selector_end = span.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skipBlock(self: *RulesIterator) void {
|
||||||
|
const span = self.stream.next() orelse return;
|
||||||
|
if (span.token != .curly_bracket_block) return;
|
||||||
|
|
||||||
|
var depth: usize = 1;
|
||||||
|
while (true) {
|
||||||
|
const next_span = self.stream.next() orelse return;
|
||||||
|
if (next_span.token == .curly_bracket_block) {
|
||||||
|
depth += 1;
|
||||||
|
} else if (next_span.token == .close_curly_bracket) {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth == 0) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skipAtRule(self: *RulesIterator) void {
|
||||||
|
_ = self.stream.next(); // consume @keyword
|
||||||
|
var depth: usize = 0;
|
||||||
|
var saw_block = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const peeked = self.stream.peek() orelse return;
|
||||||
|
if (!saw_block and isSemicolon(peeked.token) and depth == 0) {
|
||||||
|
_ = self.stream.next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = self.stream.next() orelse return;
|
||||||
|
if (isWhitespaceOrComment(span.token)) continue;
|
||||||
|
|
||||||
|
if (span.token == .curly_bracket_block) {
|
||||||
|
depth += 1;
|
||||||
|
saw_block = true;
|
||||||
|
} else if (span.token == .close_curly_bracket) {
|
||||||
|
if (depth > 0) depth -= 1;
|
||||||
|
if (saw_block and depth == 0) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
test "RulesIterator: single rule" {
|
||||||
|
var it = RulesIterator.init(".test { color: red; }");
|
||||||
|
const rule = it.next() orelse return error.MissingRule;
|
||||||
|
try testing.expectEqualStrings(".test", rule.selector);
|
||||||
|
try testing.expectEqualStrings(" color: red; ", rule.block);
|
||||||
|
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "RulesIterator: multiple rules" {
|
||||||
|
var it = RulesIterator.init("h1 { margin: 0; } p { padding: 10px; }");
|
||||||
|
|
||||||
|
var rule = it.next() orelse return error.MissingRule;
|
||||||
|
try testing.expectEqualStrings("h1", rule.selector);
|
||||||
|
try testing.expectEqualStrings(" margin: 0; ", rule.block);
|
||||||
|
|
||||||
|
rule = it.next() orelse return error.MissingRule;
|
||||||
|
try testing.expectEqualStrings("p", rule.selector);
|
||||||
|
try testing.expectEqualStrings(" padding: 10px; ", rule.block);
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "RulesIterator: skips at-rules without block" {
|
||||||
|
var it = RulesIterator.init("@import url('style.css'); .test { color: red; }");
|
||||||
|
|
||||||
|
const rule = it.next() orelse return error.MissingRule;
|
||||||
|
try testing.expectEqualStrings(".test", rule.selector);
|
||||||
|
try testing.expectEqualStrings(" color: red; ", rule.block);
|
||||||
|
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "RulesIterator: skips at-rules with block" {
|
||||||
|
var it = RulesIterator.init("@media screen { .test { color: blue; } } .test2 { color: green; }");
|
||||||
|
|
||||||
|
const rule = it.next() orelse return error.MissingRule;
|
||||||
|
try testing.expectEqualStrings(".test2", rule.selector);
|
||||||
|
try testing.expectEqualStrings(" color: green; ", rule.block);
|
||||||
|
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "RulesIterator: comments and whitespace" {
|
||||||
|
var it = RulesIterator.init(" /* comment */ .test /* comment */ { /* comment */ color: red; } \n\t");
|
||||||
|
|
||||||
|
const rule = it.next() orelse return error.MissingRule;
|
||||||
|
try testing.expectEqualStrings(".test", rule.selector);
|
||||||
|
try testing.expectEqualStrings(" /* comment */ color: red; ", rule.block);
|
||||||
|
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "RulesIterator: top-level semicolons" {
|
||||||
|
var it = RulesIterator.init("*{}; ; p{}");
|
||||||
|
var rule = it.next() orelse return error.MissingRule;
|
||||||
|
try testing.expectEqualStrings("*", rule.selector);
|
||||||
|
|
||||||
|
rule = it.next() orelse return error.MissingRule;
|
||||||
|
try testing.expectEqualStrings("p", rule.selector);
|
||||||
|
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||||
|
}
|
||||||
|
|||||||
460
src/browser/forms.zig
Normal file
460
src/browser/forms.zig
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
// 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 Page = @import("Page.zig");
|
||||||
|
const TreeWalker = @import("webapi/TreeWalker.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
pub const SelectOption = struct {
|
||||||
|
value: []const u8,
|
||||||
|
text: []const u8,
|
||||||
|
|
||||||
|
pub fn jsonStringify(self: *const SelectOption, jw: anytype) !void {
|
||||||
|
try jw.beginObject();
|
||||||
|
try jw.objectField("value");
|
||||||
|
try jw.write(self.value);
|
||||||
|
try jw.objectField("text");
|
||||||
|
try jw.write(self.text);
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const FormField = struct {
|
||||||
|
backendNodeId: ?u32 = null,
|
||||||
|
node: *Node,
|
||||||
|
tag_name: []const u8,
|
||||||
|
name: ?[]const u8,
|
||||||
|
input_type: ?[]const u8,
|
||||||
|
required: bool,
|
||||||
|
disabled: bool,
|
||||||
|
value: ?[]const u8,
|
||||||
|
placeholder: ?[]const u8,
|
||||||
|
options: []SelectOption,
|
||||||
|
|
||||||
|
pub fn jsonStringify(self: *const FormField, jw: anytype) !void {
|
||||||
|
try jw.beginObject();
|
||||||
|
|
||||||
|
if (self.backendNodeId) |id| {
|
||||||
|
try jw.objectField("backendNodeId");
|
||||||
|
try jw.write(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.objectField("tagName");
|
||||||
|
try jw.write(self.tag_name);
|
||||||
|
|
||||||
|
if (self.name) |v| {
|
||||||
|
try jw.objectField("name");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.input_type) |v| {
|
||||||
|
try jw.objectField("inputType");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.objectField("required");
|
||||||
|
try jw.write(self.required);
|
||||||
|
|
||||||
|
try jw.objectField("disabled");
|
||||||
|
try jw.write(self.disabled);
|
||||||
|
|
||||||
|
if (self.value) |v| {
|
||||||
|
try jw.objectField("value");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.placeholder) |v| {
|
||||||
|
try jw.objectField("placeholder");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.options.len > 0) {
|
||||||
|
try jw.objectField("options");
|
||||||
|
try jw.beginArray();
|
||||||
|
for (self.options) |opt| {
|
||||||
|
try opt.jsonStringify(jw);
|
||||||
|
}
|
||||||
|
try jw.endArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const FormInfo = struct {
|
||||||
|
backendNodeId: ?u32 = null,
|
||||||
|
node: *Node,
|
||||||
|
action: ?[]const u8,
|
||||||
|
method: ?[]const u8,
|
||||||
|
fields: []FormField,
|
||||||
|
|
||||||
|
pub fn jsonStringify(self: *const FormInfo, jw: anytype) !void {
|
||||||
|
try jw.beginObject();
|
||||||
|
|
||||||
|
if (self.backendNodeId) |id| {
|
||||||
|
try jw.objectField("backendNodeId");
|
||||||
|
try jw.write(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.action) |v| {
|
||||||
|
try jw.objectField("action");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.method) |v| {
|
||||||
|
try jw.objectField("method");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.objectField("fields");
|
||||||
|
try jw.beginArray();
|
||||||
|
for (self.fields) |field| {
|
||||||
|
try field.jsonStringify(jw);
|
||||||
|
}
|
||||||
|
try jw.endArray();
|
||||||
|
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Populate backendNodeId on each form and its fields by registering
|
||||||
|
/// their nodes in the given registry. Works with both CDP and MCP registries.
|
||||||
|
pub fn registerNodes(forms_data: []FormInfo, registry: anytype) !void {
|
||||||
|
for (forms_data) |*form| {
|
||||||
|
const form_registered = try registry.register(form.node);
|
||||||
|
form.backendNodeId = form_registered.id;
|
||||||
|
for (form.fields) |*field| {
|
||||||
|
const field_registered = try registry.register(field.node);
|
||||||
|
field.backendNodeId = field_registered.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect all forms and their fields under `root`.
|
||||||
|
/// Uses Form.getElements() to include fields outside the <form> that
|
||||||
|
/// reference it via the form="id" attribute, matching browser behavior.
|
||||||
|
/// `arena` must be an arena allocator — returned slices borrow its memory.
|
||||||
|
pub fn collectForms(
|
||||||
|
arena: Allocator,
|
||||||
|
root: *Node,
|
||||||
|
page: *Page,
|
||||||
|
) ![]FormInfo {
|
||||||
|
var forms: std.ArrayList(FormInfo) = .empty;
|
||||||
|
|
||||||
|
var tw = TreeWalker.Full.init(root, .{});
|
||||||
|
while (tw.next()) |node| {
|
||||||
|
const form = node.is(Element.Html.Form) orelse continue;
|
||||||
|
const el = form.asElement();
|
||||||
|
|
||||||
|
const fields = try collectFormFields(arena, form, page);
|
||||||
|
if (fields.len == 0) continue;
|
||||||
|
|
||||||
|
const action_attr = el.getAttributeSafe(comptime .wrap("action"));
|
||||||
|
const method_str = form.getMethod();
|
||||||
|
|
||||||
|
try forms.append(arena, .{
|
||||||
|
.node = node,
|
||||||
|
.action = if (action_attr) |a| if (a.len > 0) a else null else null,
|
||||||
|
.method = method_str,
|
||||||
|
.fields = fields,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return forms.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectFormFields(
|
||||||
|
arena: Allocator,
|
||||||
|
form: *Element.Html.Form,
|
||||||
|
page: *Page,
|
||||||
|
) ![]FormField {
|
||||||
|
var fields: std.ArrayList(FormField) = .empty;
|
||||||
|
|
||||||
|
var elements = try form.getElements(page);
|
||||||
|
var it = try elements.iterator();
|
||||||
|
while (it.next()) |el| {
|
||||||
|
const node = el.asNode();
|
||||||
|
|
||||||
|
const is_disabled = el.isDisabled();
|
||||||
|
|
||||||
|
if (el.is(Element.Html.Input)) |input| {
|
||||||
|
if (input._input_type == .hidden) continue;
|
||||||
|
if (input._input_type == .submit or input._input_type == .button or input._input_type == .image) continue;
|
||||||
|
|
||||||
|
try fields.append(arena, .{
|
||||||
|
.node = node,
|
||||||
|
.tag_name = "input",
|
||||||
|
.name = el.getAttributeSafe(comptime .wrap("name")),
|
||||||
|
.input_type = input._input_type.toString(),
|
||||||
|
.required = el.getAttributeSafe(comptime .wrap("required")) != null,
|
||||||
|
.disabled = is_disabled,
|
||||||
|
.value = input.getValue(),
|
||||||
|
.placeholder = el.getAttributeSafe(comptime .wrap("placeholder")),
|
||||||
|
.options = &.{},
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.is(Element.Html.TextArea)) |textarea| {
|
||||||
|
try fields.append(arena, .{
|
||||||
|
.node = node,
|
||||||
|
.tag_name = "textarea",
|
||||||
|
.name = el.getAttributeSafe(comptime .wrap("name")),
|
||||||
|
.input_type = null,
|
||||||
|
.required = el.getAttributeSafe(comptime .wrap("required")) != null,
|
||||||
|
.disabled = is_disabled,
|
||||||
|
.value = textarea.getValue(),
|
||||||
|
.placeholder = el.getAttributeSafe(comptime .wrap("placeholder")),
|
||||||
|
.options = &.{},
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.is(Element.Html.Select)) |select| {
|
||||||
|
const options = try collectSelectOptions(arena, node, page);
|
||||||
|
|
||||||
|
try fields.append(arena, .{
|
||||||
|
.node = node,
|
||||||
|
.tag_name = "select",
|
||||||
|
.name = el.getAttributeSafe(comptime .wrap("name")),
|
||||||
|
.input_type = null,
|
||||||
|
.required = el.getAttributeSafe(comptime .wrap("required")) != null,
|
||||||
|
.disabled = is_disabled,
|
||||||
|
.value = select.getValue(page),
|
||||||
|
.placeholder = null,
|
||||||
|
.options = options,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button elements from getElements() - skip (not fillable)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectSelectOptions(
|
||||||
|
arena: Allocator,
|
||||||
|
select_node: *Node,
|
||||||
|
page: *Page,
|
||||||
|
) ![]SelectOption {
|
||||||
|
var options: std.ArrayList(SelectOption) = .empty;
|
||||||
|
const Option = Element.Html.Option;
|
||||||
|
|
||||||
|
var tw = TreeWalker.Full.init(select_node, .{});
|
||||||
|
while (tw.next()) |node| {
|
||||||
|
const el = node.is(Element) orelse continue;
|
||||||
|
const option = el.is(Option) orelse continue;
|
||||||
|
|
||||||
|
try options.append(arena, .{
|
||||||
|
.value = option.getValue(page),
|
||||||
|
.text = option.getText(page),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return options.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
|
||||||
|
fn testForms(html: []const u8) ![]FormInfo {
|
||||||
|
const page = try testing.test_session.createPage();
|
||||||
|
|
||||||
|
const doc = page.window._document;
|
||||||
|
const div = try doc.createElement("div", null, page);
|
||||||
|
try page.parseHtmlAsChildren(div.asNode(), html);
|
||||||
|
|
||||||
|
return collectForms(page.call_arena, div.asNode(), page);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: login form" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form action="/login" method="POST">
|
||||||
|
\\ <input type="email" name="email" required placeholder="Email">
|
||||||
|
\\ <input type="password" name="password" required>
|
||||||
|
\\ <input type="submit" value="Log In">
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual("/login", forms[0].action.?);
|
||||||
|
try testing.expectEqual("post", forms[0].method.?);
|
||||||
|
try testing.expectEqual(2, forms[0].fields.len);
|
||||||
|
try testing.expectEqual("email", forms[0].fields[0].name.?);
|
||||||
|
try testing.expectEqual("email", forms[0].fields[0].input_type.?);
|
||||||
|
try testing.expect(forms[0].fields[0].required);
|
||||||
|
try testing.expect(!forms[0].fields[0].disabled);
|
||||||
|
try testing.expectEqual("password", forms[0].fields[1].name.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: form with select" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form>
|
||||||
|
\\ <select name="color">
|
||||||
|
\\ <option value="red">Red</option>
|
||||||
|
\\ <option value="blue">Blue</option>
|
||||||
|
\\ </select>
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual(1, forms[0].fields.len);
|
||||||
|
try testing.expectEqual("select", forms[0].fields[0].tag_name);
|
||||||
|
try testing.expectEqual(2, forms[0].fields[0].options.len);
|
||||||
|
try testing.expectEqual("red", forms[0].fields[0].options[0].value);
|
||||||
|
try testing.expectEqual("Red", forms[0].fields[0].options[0].text);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: form with textarea" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form method="POST">
|
||||||
|
\\ <textarea name="message" placeholder="Your message"></textarea>
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual(1, forms[0].fields.len);
|
||||||
|
try testing.expectEqual("textarea", forms[0].fields[0].tag_name);
|
||||||
|
try testing.expectEqual("Your message", forms[0].fields[0].placeholder.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: empty form skipped" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form action="/empty">
|
||||||
|
\\ <p>No fields here</p>
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(0, forms.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: hidden inputs excluded" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form>
|
||||||
|
\\ <input type="hidden" name="csrf" value="token123">
|
||||||
|
\\ <input type="text" name="username">
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual(1, forms[0].fields.len);
|
||||||
|
try testing.expectEqual("username", forms[0].fields[0].name.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: multiple forms" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form action="/search" method="GET">
|
||||||
|
\\ <input type="text" name="q" placeholder="Search">
|
||||||
|
\\</form>
|
||||||
|
\\<form action="/login" method="POST">
|
||||||
|
\\ <input type="email" name="email">
|
||||||
|
\\ <input type="password" name="pass">
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(2, forms.len);
|
||||||
|
try testing.expectEqual(1, forms[0].fields.len);
|
||||||
|
try testing.expectEqual(2, forms[1].fields.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: disabled fields flagged" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form>
|
||||||
|
\\ <input type="text" name="enabled_field">
|
||||||
|
\\ <input type="text" name="disabled_field" disabled>
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual(2, forms[0].fields.len);
|
||||||
|
try testing.expect(!forms[0].fields[0].disabled);
|
||||||
|
try testing.expect(forms[0].fields[1].disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: disabled fieldset" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form>
|
||||||
|
\\ <fieldset disabled>
|
||||||
|
\\ <input type="text" name="in_disabled_fieldset">
|
||||||
|
\\ </fieldset>
|
||||||
|
\\ <input type="text" name="outside_fieldset">
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual(2, forms[0].fields.len);
|
||||||
|
try testing.expect(forms[0].fields[0].disabled);
|
||||||
|
try testing.expect(!forms[0].fields[1].disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: external field via form attribute" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<input type="text" name="external" form="myform">
|
||||||
|
\\<form id="myform" action="/submit">
|
||||||
|
\\ <input type="text" name="internal">
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual(2, forms[0].fields.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: checkbox and radio return value attribute" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form>
|
||||||
|
\\ <input type="checkbox" name="agree" value="yes" checked>
|
||||||
|
\\ <input type="radio" name="color" value="red">
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual(2, forms[0].fields.len);
|
||||||
|
try testing.expectEqual("checkbox", forms[0].fields[0].input_type.?);
|
||||||
|
try testing.expectEqual("yes", forms[0].fields[0].value.?);
|
||||||
|
try testing.expectEqual("radio", forms[0].fields[1].input_type.?);
|
||||||
|
try testing.expectEqual("red", forms[0].fields[1].value.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: form without action or method" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form>
|
||||||
|
\\ <input type="text" name="q">
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual(null, forms[0].action);
|
||||||
|
try testing.expectEqual("get", forms[0].method.?);
|
||||||
|
try testing.expectEqual(1, forms[0].fields.len);
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ pub const InteractivityType = enum {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const InteractiveElement = struct {
|
pub const InteractiveElement = struct {
|
||||||
|
backendNodeId: ?u32 = null,
|
||||||
node: *Node,
|
node: *Node,
|
||||||
tag_name: []const u8,
|
tag_name: []const u8,
|
||||||
role: ?[]const u8,
|
role: ?[]const u8,
|
||||||
@@ -55,6 +56,11 @@ pub const InteractiveElement = struct {
|
|||||||
pub fn jsonStringify(self: *const InteractiveElement, jw: anytype) !void {
|
pub fn jsonStringify(self: *const InteractiveElement, jw: anytype) !void {
|
||||||
try jw.beginObject();
|
try jw.beginObject();
|
||||||
|
|
||||||
|
if (self.backendNodeId) |id| {
|
||||||
|
try jw.objectField("backendNodeId");
|
||||||
|
try jw.write(id);
|
||||||
|
}
|
||||||
|
|
||||||
try jw.objectField("tagName");
|
try jw.objectField("tagName");
|
||||||
try jw.write(self.tag_name);
|
try jw.write(self.tag_name);
|
||||||
|
|
||||||
@@ -123,6 +129,15 @@ pub const InteractiveElement = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Populate backendNodeId on each interactive element by registering
|
||||||
|
/// their nodes in the given registry. Works with both CDP and MCP registries.
|
||||||
|
pub fn registerNodes(elements: []InteractiveElement, registry: anytype) !void {
|
||||||
|
for (elements) |*el| {
|
||||||
|
const registered = try registry.register(el.node);
|
||||||
|
el.backendNodeId = registered.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Collect all interactive elements under `root`.
|
/// Collect all interactive elements under `root`.
|
||||||
pub fn collectInteractiveElements(
|
pub fn collectInteractiveElements(
|
||||||
root: *Node,
|
root: *Node,
|
||||||
@@ -133,6 +148,8 @@ pub fn collectInteractiveElements(
|
|||||||
// so classify and getListenerTypes are both O(1) per element.
|
// so classify and getListenerTypes are both O(1) per element.
|
||||||
const listener_targets = try buildListenerTargetMap(page, arena);
|
const listener_targets = try buildListenerTargetMap(page, arena);
|
||||||
|
|
||||||
|
var css_cache: Element.PointerEventsCache = .empty;
|
||||||
|
|
||||||
var results: std.ArrayList(InteractiveElement) = .empty;
|
var results: std.ArrayList(InteractiveElement) = .empty;
|
||||||
|
|
||||||
var tw = TreeWalker.Full.init(root, .{});
|
var tw = TreeWalker.Full.init(root, .{});
|
||||||
@@ -146,7 +163,7 @@ pub fn collectInteractiveElements(
|
|||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
const itype = classifyInteractivity(el, html_el, listener_targets) orelse continue;
|
const itype = classifyInteractivity(page, el, html_el, listener_targets, &css_cache) orelse continue;
|
||||||
|
|
||||||
const listener_types = getListenerTypes(
|
const listener_types = getListenerTypes(
|
||||||
el.asEventTarget(),
|
el.asEventTarget(),
|
||||||
@@ -160,7 +177,7 @@ pub fn collectInteractiveElements(
|
|||||||
.name = try getAccessibleName(el, arena),
|
.name = try getAccessibleName(el, arena),
|
||||||
.interactivity_type = itype,
|
.interactivity_type = itype,
|
||||||
.listener_types = listener_types,
|
.listener_types = listener_types,
|
||||||
.disabled = isDisabled(el),
|
.disabled = el.isDisabled(),
|
||||||
.tab_index = html_el.getTabIndex(),
|
.tab_index = html_el.getTabIndex(),
|
||||||
.id = el.getAttributeSafe(comptime .wrap("id")),
|
.id = el.getAttributeSafe(comptime .wrap("id")),
|
||||||
.class = el.getAttributeSafe(comptime .wrap("class")),
|
.class = el.getAttributeSafe(comptime .wrap("class")),
|
||||||
@@ -210,10 +227,14 @@ pub fn buildListenerTargetMap(page: *Page, arena: Allocator) !ListenerTargetMap
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn classifyInteractivity(
|
pub fn classifyInteractivity(
|
||||||
|
page: *Page,
|
||||||
el: *Element,
|
el: *Element,
|
||||||
html_el: *Element.Html,
|
html_el: *Element.Html,
|
||||||
listener_targets: ListenerTargetMap,
|
listener_targets: ListenerTargetMap,
|
||||||
|
cache: ?*Element.PointerEventsCache,
|
||||||
) ?InteractivityType {
|
) ?InteractivityType {
|
||||||
|
if (el.hasPointerEventsNone(cache, page)) return null;
|
||||||
|
|
||||||
// 1. Native interactive by tag
|
// 1. Native interactive by tag
|
||||||
switch (el.getTag()) {
|
switch (el.getTag()) {
|
||||||
.button, .summary, .details, .select, .textarea => return .native,
|
.button, .summary, .details, .select, .textarea => return .native,
|
||||||
@@ -406,36 +427,6 @@ fn getTextContent(node: *Node, arena: Allocator) !?[]const u8 {
|
|||||||
// strip out trailing space
|
// strip out trailing space
|
||||||
return arr.items[0 .. arr.items.len - 1];
|
return arr.items[0 .. arr.items.len - 1];
|
||||||
}
|
}
|
||||||
fn isDisabled(el: *Element) bool {
|
|
||||||
if (el.getAttributeSafe(comptime .wrap("disabled")) != null) return true;
|
|
||||||
return isDisabledByFieldset(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if an element is disabled by an ancestor <fieldset disabled>.
|
|
||||||
/// Per spec, elements inside the first <legend> child of a disabled fieldset
|
|
||||||
/// are NOT disabled by that fieldset.
|
|
||||||
fn isDisabledByFieldset(el: *Element) bool {
|
|
||||||
const element_node = el.asNode();
|
|
||||||
var current: ?*Node = element_node._parent;
|
|
||||||
while (current) |node| {
|
|
||||||
current = node._parent;
|
|
||||||
const ancestor = node.is(Element) orelse continue;
|
|
||||||
|
|
||||||
if (ancestor.getTag() == .fieldset and ancestor.getAttributeSafe(comptime .wrap("disabled")) != null) {
|
|
||||||
// Check if element is inside the first <legend> child of this fieldset
|
|
||||||
var child = ancestor.firstElementChild();
|
|
||||||
while (child) |c| {
|
|
||||||
if (c.getTag() == .legend) {
|
|
||||||
if (c.asNode().contains(element_node)) return false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
child = c.nextElementSibling();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getInputType(el: *Element) ?[]const u8 {
|
fn getInputType(el: *Element) ?[]const u8 {
|
||||||
if (el.is(Element.Html.Input)) |input| {
|
if (el.is(Element.Html.Input)) |input| {
|
||||||
@@ -554,6 +545,11 @@ test "browser.interactive: disabled by fieldset" {
|
|||||||
try testing.expect(!elements[1].disabled);
|
try testing.expect(!elements[1].disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: pointer-events none" {
|
||||||
|
const elements = try testInteractive("<button style=\"pointer-events: none;\">Click me</button>");
|
||||||
|
try testing.expectEqual(0, elements.len);
|
||||||
|
}
|
||||||
|
|
||||||
test "browser.interactive: non-interactive div" {
|
test "browser.interactive: non-interactive div" {
|
||||||
const elements = try testInteractive("<div>Just text</div>");
|
const elements = try testInteractive("<div>Just text</div>");
|
||||||
try testing.expectEqual(0, elements.len);
|
try testing.expectEqual(0, elements.len);
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void
|
|||||||
const new_this_handle = info.getThis();
|
const new_this_handle = info.getThis();
|
||||||
var this = js.Object{ .local = local, .handle = new_this_handle };
|
var this = js.Object{ .local = local, .handle = new_this_handle };
|
||||||
if (@typeInfo(ReturnType) == .error_union) {
|
if (@typeInfo(ReturnType) == .error_union) {
|
||||||
const non_error_res = res catch |err| return err;
|
const non_error_res = try res;
|
||||||
this = try local.mapZigInstanceToJs(new_this_handle, non_error_res);
|
this = try local.mapZigInstanceToJs(new_this_handle, non_error_res);
|
||||||
} else {
|
} else {
|
||||||
this = try local.mapZigInstanceToJs(new_this_handle, res);
|
this = try local.mapZigInstanceToJs(new_this_handle, res);
|
||||||
@@ -505,6 +505,7 @@ pub const Function = struct {
|
|||||||
pub const Opts = struct {
|
pub const Opts = struct {
|
||||||
noop: bool = false,
|
noop: bool = false,
|
||||||
static: bool = false,
|
static: bool = false,
|
||||||
|
deletable: bool = true,
|
||||||
dom_exception: bool = false,
|
dom_exception: bool = false,
|
||||||
as_typed_array: bool = false,
|
as_typed_array: bool = false,
|
||||||
null_as_undefined: bool = false,
|
null_as_undefined: bool = false,
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ const lp = @import("lightpanda");
|
|||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const Env = @import("Env.zig");
|
|
||||||
const bridge = @import("bridge.zig");
|
const bridge = @import("bridge.zig");
|
||||||
|
const Env = @import("Env.zig");
|
||||||
const Origin = @import("Origin.zig");
|
const Origin = @import("Origin.zig");
|
||||||
const Scheduler = @import("Scheduler.zig");
|
const Scheduler = @import("Scheduler.zig");
|
||||||
|
|
||||||
@@ -63,7 +63,9 @@ templates: []*const v8.FunctionTemplate,
|
|||||||
// Arena for the lifetime of the context
|
// Arena for the lifetime of the context
|
||||||
arena: Allocator,
|
arena: Allocator,
|
||||||
|
|
||||||
// The page.call_arena
|
// The call_arena for this context. For main world contexts this is
|
||||||
|
// page.call_arena. For isolated world contexts this is a separate arena
|
||||||
|
// owned by the IsolatedWorld.
|
||||||
call_arena: Allocator,
|
call_arena: Allocator,
|
||||||
|
|
||||||
// Because calls can be nested (i.e.a function calling a callback),
|
// Because calls can be nested (i.e.a function calling a callback),
|
||||||
@@ -79,6 +81,16 @@ local: ?*const js.Local = null,
|
|||||||
|
|
||||||
origin: *Origin,
|
origin: *Origin,
|
||||||
|
|
||||||
|
// Identity tracking for this context. For main world contexts, this points to
|
||||||
|
// Session's Identity. For isolated world contexts (CDP inspector), this points
|
||||||
|
// to IsolatedWorld's Identity. This ensures same-origin frames share object
|
||||||
|
// identity while isolated worlds have separate identity tracking.
|
||||||
|
identity: *js.Identity,
|
||||||
|
|
||||||
|
// Allocator to use for identity map operations. For main world contexts this is
|
||||||
|
// session.page_arena, for isolated worlds it's the isolated world's arena.
|
||||||
|
identity_arena: Allocator,
|
||||||
|
|
||||||
// Unlike other v8 types, like functions or objects, modules are not shared
|
// Unlike other v8 types, like functions or objects, modules are not shared
|
||||||
// across origins.
|
// across origins.
|
||||||
global_modules: std.ArrayList(v8.Global) = .empty,
|
global_modules: std.ArrayList(v8.Global) = .empty,
|
||||||
@@ -185,9 +197,8 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
|
|||||||
lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc });
|
lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc });
|
||||||
|
|
||||||
const origin = try self.session.getOrCreateOrigin(key);
|
const origin = try self.session.getOrCreateOrigin(key);
|
||||||
errdefer self.session.releaseOrigin(origin);
|
|
||||||
try origin.takeover(self.origin);
|
|
||||||
|
|
||||||
|
self.session.releaseOrigin(self.origin);
|
||||||
self.origin = origin;
|
self.origin = origin;
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -203,48 +214,24 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn trackGlobal(self: *Context, global: v8.Global) !void {
|
pub fn trackGlobal(self: *Context, global: v8.Global) !void {
|
||||||
return self.origin.trackGlobal(global);
|
return self.session.globals.append(self.session.page_arena, global);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn trackTemp(self: *Context, global: v8.Global) !void {
|
pub fn trackTemp(self: *Context, global: v8.Global) !void {
|
||||||
return self.origin.trackTemp(global);
|
return self.session.temps.put(self.session.page_arena, global.data_ptr, global);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn weakRef(self: *Context, obj: anytype) void {
|
pub const IdentityResult = struct {
|
||||||
const resolved = js.Local.resolveValue(obj);
|
value_ptr: *v8.Global,
|
||||||
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
|
found_existing: bool,
|
||||||
if (comptime IS_DEBUG) {
|
};
|
||||||
// should not be possible
|
|
||||||
std.debug.assert(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn safeWeakRef(self: *Context, obj: anytype) void {
|
pub fn addIdentity(self: *Context, ptr: usize) !IdentityResult {
|
||||||
const resolved = js.Local.resolveValue(obj);
|
const gop = try self.identity.identity_map.getOrPut(self.identity_arena, ptr);
|
||||||
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
|
return .{
|
||||||
if (comptime IS_DEBUG) {
|
.value_ptr = gop.value_ptr,
|
||||||
// should not be possible
|
.found_existing = gop.found_existing,
|
||||||
std.debug.assert(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
v8.v8__Global__ClearWeak(&fc.global);
|
|
||||||
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn strongRef(self: *Context, obj: anytype) void {
|
|
||||||
const resolved = js.Local.resolveValue(obj);
|
|
||||||
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
|
|
||||||
if (comptime IS_DEBUG) {
|
|
||||||
// should not be possible
|
|
||||||
std.debug.assert(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
v8.v8__Global__ClearWeak(&fc.global);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any operation on the context have to be made from a local.
|
// Any operation on the context have to be made from a local.
|
||||||
@@ -545,13 +532,13 @@ pub fn dynamicModuleCallback(
|
|||||||
|
|
||||||
break :blk js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| {
|
break :blk js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| {
|
||||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" });
|
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" });
|
||||||
return @constCast((local.rejectPromise("Out of memory") catch return null).handle);
|
return @constCast(local.rejectPromise(.{ .generic_error = "Out of memory" }).handle);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const specifier = js.String.toSliceZ(.{ .local = &local, .handle = v8_specifier.? }) catch |err| {
|
const specifier = js.String.toSliceZ(.{ .local = &local, .handle = v8_specifier.? }) catch |err| {
|
||||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" });
|
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" });
|
||||||
return @constCast((local.rejectPromise("Out of memory") catch return null).handle);
|
return @constCast(local.rejectPromise(.{ .generic_error = "Out of memory" }).handle);
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalized_specifier = self.script_manager.?.resolveSpecifier(
|
const normalized_specifier = self.script_manager.?.resolveSpecifier(
|
||||||
@@ -560,14 +547,14 @@ pub fn dynamicModuleCallback(
|
|||||||
specifier,
|
specifier,
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback3" });
|
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback3" });
|
||||||
return @constCast((local.rejectPromise("Out of memory") catch return null).handle);
|
return @constCast(local.rejectPromise(.{ .generic_error = "Out of memory" }).handle);
|
||||||
};
|
};
|
||||||
|
|
||||||
const promise = self._dynamicModuleCallback(normalized_specifier, resource, &local) catch |err| blk: {
|
const promise = self._dynamicModuleCallback(normalized_specifier, resource, &local) catch |err| blk: {
|
||||||
log.err(.js, "dynamic module callback", .{
|
log.err(.js, "dynamic module callback", .{
|
||||||
.err = err,
|
.err = err,
|
||||||
});
|
});
|
||||||
break :blk local.rejectPromise("Failed to load module") catch return null;
|
break :blk local.rejectPromise(.{ .generic_error = "Out of memory" });
|
||||||
};
|
};
|
||||||
return @constCast(promise.handle);
|
return @constCast(promise.handle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ 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 Origin = @import("Origin.zig");
|
|
||||||
const Context = @import("Context.zig");
|
const Context = @import("Context.zig");
|
||||||
const Isolate = @import("Isolate.zig");
|
const Isolate = @import("Isolate.zig");
|
||||||
const Platform = @import("Platform.zig");
|
const Platform = @import("Platform.zig");
|
||||||
@@ -254,8 +253,15 @@ pub fn deinit(self: *Env) void {
|
|||||||
allocator.destroy(self.isolate_params);
|
allocator.destroy(self.isolate_params);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn createContext(self: *Env, page: *Page) !*Context {
|
pub const ContextParams = struct {
|
||||||
const context_arena = try self.app.arena_pool.acquire();
|
identity: *js.Identity,
|
||||||
|
identity_arena: Allocator,
|
||||||
|
call_arena: Allocator,
|
||||||
|
debug_name: []const u8 = "Context",
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context {
|
||||||
|
const context_arena = try self.app.arena_pool.acquire(.{ .debug = params.debug_name });
|
||||||
errdefer self.app.arena_pool.release(context_arena);
|
errdefer self.app.arena_pool.release(context_arena);
|
||||||
|
|
||||||
const isolate = self.isolate;
|
const isolate = self.isolate;
|
||||||
@@ -300,33 +306,43 @@ pub fn createContext(self: *Env, page: *Page) !*Context {
|
|||||||
v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao);
|
v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao);
|
||||||
}
|
}
|
||||||
|
|
||||||
// our window wrapped in a v8::Global
|
|
||||||
var global_global: v8.Global = undefined;
|
|
||||||
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
|
|
||||||
|
|
||||||
const context_id = self.context_id;
|
const context_id = self.context_id;
|
||||||
self.context_id = context_id + 1;
|
self.context_id = context_id + 1;
|
||||||
|
|
||||||
const origin = try page._session.getOrCreateOrigin(null);
|
const session = page._session;
|
||||||
errdefer page._session.releaseOrigin(origin);
|
const origin = try session.getOrCreateOrigin(null);
|
||||||
|
errdefer session.releaseOrigin(origin);
|
||||||
|
|
||||||
const context = try context_arena.create(Context);
|
const context = try context_arena.create(Context);
|
||||||
context.* = .{
|
context.* = .{
|
||||||
.env = self,
|
.env = self,
|
||||||
.page = page,
|
.page = page,
|
||||||
.session = page._session,
|
|
||||||
.origin = origin,
|
.origin = origin,
|
||||||
.id = context_id,
|
.id = context_id,
|
||||||
|
.session = session,
|
||||||
.isolate = isolate,
|
.isolate = isolate,
|
||||||
.arena = context_arena,
|
.arena = context_arena,
|
||||||
.handle = context_global,
|
.handle = context_global,
|
||||||
.templates = self.templates,
|
.templates = self.templates,
|
||||||
.call_arena = page.call_arena,
|
.call_arena = params.call_arena,
|
||||||
.microtask_queue = microtask_queue,
|
.microtask_queue = microtask_queue,
|
||||||
.script_manager = &page._script_manager,
|
.script_manager = &page._script_manager,
|
||||||
.scheduler = .init(context_arena),
|
.scheduler = .init(context_arena),
|
||||||
|
.identity = params.identity,
|
||||||
|
.identity_arena = params.identity_arena,
|
||||||
};
|
};
|
||||||
try context.origin.identity_map.putNoClobber(origin.arena, @intFromPtr(page.window), global_global);
|
|
||||||
|
{
|
||||||
|
// Multiple contexts can be created for the same Window (via CDP). We only
|
||||||
|
// need to register the first one.
|
||||||
|
const gop = try params.identity.identity_map.getOrPut(params.identity_arena, @intFromPtr(page.window));
|
||||||
|
if (gop.found_existing == false) {
|
||||||
|
// our window wrapped in a v8::Global
|
||||||
|
var global_global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
|
||||||
|
gop.value_ptr.* = global_global;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Store a pointer to our context inside the v8 context so that, given
|
// Store a pointer to our context inside the v8 context so that, given
|
||||||
// a v8 context, we can get our context out
|
// a v8 context, we can get our context out
|
||||||
|
|||||||
@@ -210,10 +210,10 @@ fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Gl
|
|||||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
if (comptime is_global) {
|
if (comptime is_global) {
|
||||||
try ctx.trackGlobal(global);
|
try ctx.trackGlobal(global);
|
||||||
return .{ .handle = global, .origin = {} };
|
return .{ .handle = global, .temps = {} };
|
||||||
}
|
}
|
||||||
try ctx.trackTemp(global);
|
try ctx.trackTemp(global);
|
||||||
return .{ .handle = global, .origin = ctx.origin };
|
return .{ .handle = global, .temps = &ctx.session.temps };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tempWithThis(self: *const Function, value: anytype) !Temp {
|
pub fn tempWithThis(self: *const Function, value: anytype) !Temp {
|
||||||
@@ -237,7 +237,7 @@ const GlobalType = enum(u8) {
|
|||||||
fn G(comptime global_type: GlobalType) type {
|
fn G(comptime global_type: GlobalType) type {
|
||||||
return struct {
|
return struct {
|
||||||
handle: v8.Global,
|
handle: v8.Global,
|
||||||
origin: if (global_type == .temp) *js.Origin else void,
|
temps: if (global_type == .temp) *std.AutoHashMapUnmanaged(usize, v8.Global) else void,
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
@@ -257,7 +257,10 @@ fn G(comptime global_type: GlobalType) type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn release(self: *const Self) void {
|
pub fn release(self: *const Self) void {
|
||||||
self.origin.releaseTemp(self.handle);
|
if (self.temps.fetchRemove(self.handle.data_ptr)) |kv| {
|
||||||
|
var g = kv.value;
|
||||||
|
v8.v8__Global__Reset(&g);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
46
src/browser/js/Identity.zig
Normal file
46
src/browser/js/Identity.zig
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// 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/>.
|
||||||
|
|
||||||
|
// Identity manages the mapping between Zig instances and their v8::Object wrappers.
|
||||||
|
// This provides object identity semantics - the same Zig instance always maps to
|
||||||
|
// the same JS object within a given Identity scope.
|
||||||
|
//
|
||||||
|
// Main world contexts share a single Identity (on Session), ensuring that
|
||||||
|
// `window.top.document === top's document` works across same-origin frames.
|
||||||
|
//
|
||||||
|
// Isolated worlds (CDP inspector) have their own Identity, ensuring their
|
||||||
|
// v8::Global wrappers don't leak into the main world.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
|
||||||
|
const Session = @import("../Session.zig");
|
||||||
|
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Identity = @This();
|
||||||
|
|
||||||
|
// Maps Zig instance pointers to their v8::Global(Object) wrappers.
|
||||||
|
identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
||||||
|
|
||||||
|
pub fn deinit(self: *Identity) void {
|
||||||
|
var it = self.identity_map.valueIterator();
|
||||||
|
while (it.next()) |global| {
|
||||||
|
v8.v8__Global__Reset(global);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,21 @@ pub fn createError(self: Isolate, msg: []const u8) *const v8.Value {
|
|||||||
return v8.v8__Exception__Error(message).?;
|
return v8.v8__Exception__Error(message).?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn createRangeError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
|
const message = self.initStringHandle(msg);
|
||||||
|
return v8.v8__Exception__RangeError(message).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createReferenceError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
|
const message = self.initStringHandle(msg);
|
||||||
|
return v8.v8__Exception__ReferenceError(message).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createSyntaxError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
|
const message = self.initStringHandle(msg);
|
||||||
|
return v8.v8__Exception__SyntaxError(message).?;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn createTypeError(self: Isolate, msg: []const u8) *const v8.Value {
|
pub fn createTypeError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
const message = self.initStringHandle(msg);
|
const message = self.initStringHandle(msg);
|
||||||
return v8.v8__Exception__TypeError(message).?;
|
return v8.v8__Exception__TypeError(message).?;
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
// 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 Page = @import("../Page.zig");
|
|
||||||
const Session = @import("../Session.zig");
|
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
const string = @import("../../string.zig");
|
const string = @import("../../string.zig");
|
||||||
|
|
||||||
|
const Session = @import("../Session.zig");
|
||||||
|
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const bridge = @import("bridge.zig");
|
const bridge = @import("bridge.zig");
|
||||||
const Caller = @import("Caller.zig");
|
const Caller = @import("Caller.zig");
|
||||||
@@ -33,7 +33,6 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
|
|||||||
|
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
const CallOpts = Caller.CallOpts;
|
const CallOpts = Caller.CallOpts;
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
// Where js.Context has a lifetime tied to the page, and holds the
|
// Where js.Context has a lifetime tied to the page, and holds the
|
||||||
// v8::Global<v8::Context>, this has a much shorter lifetime and holds a
|
// v8::Global<v8::Context>, this has a much shorter lifetime and holds a
|
||||||
@@ -202,20 +201,21 @@ pub fn compileAndRun(self: *const Local, src: []const u8, name: ?[]const u8) !js
|
|||||||
// we can just grab it from the identity_map)
|
// we can just grab it from the identity_map)
|
||||||
pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, value: anytype) !js.Object {
|
pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, value: anytype) !js.Object {
|
||||||
const ctx = self.ctx;
|
const ctx = self.ctx;
|
||||||
const origin_arena = ctx.origin.arena;
|
const context_arena = ctx.arena;
|
||||||
|
|
||||||
const T = @TypeOf(value);
|
const T = @TypeOf(value);
|
||||||
switch (@typeInfo(T)) {
|
switch (@typeInfo(T)) {
|
||||||
.@"struct" => {
|
.@"struct" => {
|
||||||
// Struct, has to be placed on the heap
|
// Struct, has to be placed on the heap
|
||||||
const heap = try origin_arena.create(T);
|
const heap = try context_arena.create(T);
|
||||||
heap.* = value;
|
heap.* = value;
|
||||||
return self.mapZigInstanceToJs(js_obj_handle, heap);
|
return self.mapZigInstanceToJs(js_obj_handle, heap);
|
||||||
},
|
},
|
||||||
.pointer => |ptr| {
|
.pointer => |ptr| {
|
||||||
const resolved = resolveValue(value);
|
const resolved = resolveValue(value);
|
||||||
|
|
||||||
const gop = try ctx.origin.addIdentity(@intFromPtr(resolved.ptr));
|
const resolved_ptr_id = @intFromPtr(resolved.ptr);
|
||||||
|
const gop = try ctx.addIdentity(resolved_ptr_id);
|
||||||
if (gop.found_existing) {
|
if (gop.found_existing) {
|
||||||
// we've seen this instance before, return the same object
|
// we've seen this instance before, return the same object
|
||||||
return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self);
|
return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self);
|
||||||
@@ -244,7 +244,7 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
|
|||||||
// The TAO contains the pointer to our Zig instance as
|
// The TAO contains the pointer to our Zig instance as
|
||||||
// well as any meta data we'll need to use it later.
|
// well as any meta data we'll need to use it later.
|
||||||
// See the TaggedOpaque struct for more details.
|
// See the TaggedOpaque struct for more details.
|
||||||
const tao = try origin_arena.create(TaggedOpaque);
|
const tao = try context_arena.create(TaggedOpaque);
|
||||||
tao.* = .{
|
tao.* = .{
|
||||||
.value = resolved.ptr,
|
.value = resolved.ptr,
|
||||||
.prototype_chain = resolved.prototype_chain.ptr,
|
.prototype_chain = resolved.prototype_chain.ptr,
|
||||||
@@ -264,31 +264,27 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
|
|||||||
// dont' use js_obj.persist(), because we don't want to track this in
|
// dont' use js_obj.persist(), because we don't want to track this in
|
||||||
// context.global_objects, we want to track it in context.identity_map.
|
// context.global_objects, we want to track it in context.identity_map.
|
||||||
v8.v8__Global__New(isolate.handle, js_obj.handle, gop.value_ptr);
|
v8.v8__Global__New(isolate.handle, js_obj.handle, gop.value_ptr);
|
||||||
if (@hasDecl(JsApi.Meta, "finalizer")) {
|
if (resolved.finalizer) |finalizer| {
|
||||||
// It would be great if resolved knew the resolved type, but I
|
const finalizer_ptr_id = finalizer.ptr_id;
|
||||||
// can't figure out how to make that work, since it depends on
|
finalizer.acquireRef(finalizer_ptr_id);
|
||||||
// the [runtime] `value`.
|
|
||||||
// We need the resolved finalizer, which we have in resolved.
|
|
||||||
//
|
|
||||||
// The above if statement would be more clear as:
|
|
||||||
// if (resolved.finalizer_from_v8) |finalizer| {
|
|
||||||
// But that's a runtime check.
|
|
||||||
// Instead, we check if the base has finalizer. The assumption
|
|
||||||
// here is that if a resolve type has a finalizer, then the base
|
|
||||||
// should have a finalizer too.
|
|
||||||
const fc = try ctx.origin.createFinalizerCallback(ctx.session, gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?);
|
|
||||||
{
|
|
||||||
errdefer fc.deinit();
|
|
||||||
try ctx.origin.finalizer_callbacks.put(ctx.origin.arena, @intFromPtr(resolved.ptr), fc);
|
|
||||||
}
|
|
||||||
|
|
||||||
conditionallyReference(value);
|
const session = ctx.session;
|
||||||
if (@hasDecl(JsApi.Meta, "weak")) {
|
const finalizer_gop = try session.finalizer_callbacks.getOrPut(session.page_arena, finalizer_ptr_id);
|
||||||
if (comptime IS_DEBUG) {
|
if (finalizer_gop.found_existing == false) {
|
||||||
std.debug.assert(JsApi.Meta.weak == true);
|
// This is the first context (and very likely only one) to
|
||||||
}
|
// see this Zig instance. We need to create the FinalizerCallback
|
||||||
v8.v8__Global__SetWeakFinalizer(gop.value_ptr, fc, resolved.finalizer_from_v8, v8.kParameter);
|
// so that we can cleanup on page reset if v8 doesn't finalize.
|
||||||
|
errdefer _ = session.finalizer_callbacks.remove(finalizer_ptr_id);
|
||||||
|
finalizer_gop.value_ptr.* = try self.createFinalizerCallback(resolved_ptr_id, finalizer_ptr_id, finalizer.deinit);
|
||||||
}
|
}
|
||||||
|
const fc = finalizer_gop.value_ptr.*;
|
||||||
|
const identity_finalizer = try fc.arena.create(Session.FinalizerCallback.Identity);
|
||||||
|
identity_finalizer.* = .{
|
||||||
|
.fc = fc,
|
||||||
|
.identity = ctx.identity,
|
||||||
|
};
|
||||||
|
|
||||||
|
v8.v8__Global__SetWeakFinalizer(gop.value_ptr, identity_finalizer, finalizer.release, v8.kParameter);
|
||||||
}
|
}
|
||||||
return js_obj;
|
return js_obj;
|
||||||
},
|
},
|
||||||
@@ -1123,12 +1119,19 @@ fn jsUnsignedIntToZig(comptime T: type, max: comptime_int, maybe: u32) !T {
|
|||||||
// This function recursively walks the _type union field (if there is one) to
|
// This function recursively walks the _type union field (if there is one) to
|
||||||
// get the most specific class_id possible.
|
// get the most specific class_id possible.
|
||||||
const Resolved = struct {
|
const Resolved = struct {
|
||||||
weak: bool,
|
|
||||||
ptr: *anyopaque,
|
ptr: *anyopaque,
|
||||||
class_id: u16,
|
class_id: u16,
|
||||||
prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry,
|
prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry,
|
||||||
finalizer_from_v8: ?*const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void = null,
|
finalizer: ?Finalizer,
|
||||||
finalizer_from_zig: ?*const fn (ptr: *anyopaque, session: *Session) void = null,
|
|
||||||
|
const Finalizer = struct {
|
||||||
|
// Resolved.ptr is the most specific value in a chain (e.g. IFrame, not EventTarget, Node, ...)
|
||||||
|
// Finalizer.ptr_id is the most specific value in a chain that defines an acquireRef
|
||||||
|
ptr_id: usize,
|
||||||
|
deinit: *const fn (ptr_id: usize, session: *Session) void,
|
||||||
|
acquireRef: *const fn (ptr_id: usize) void,
|
||||||
|
release: *const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
pub fn resolveValue(value: anytype) Resolved {
|
pub fn resolveValue(value: anytype) Resolved {
|
||||||
const T = bridge.Struct(@TypeOf(value));
|
const T = bridge.Struct(@TypeOf(value));
|
||||||
@@ -1155,27 +1158,85 @@ pub fn resolveValue(value: anytype) Resolved {
|
|||||||
unreachable;
|
unreachable;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolveT(comptime T: type, value: *anyopaque) Resolved {
|
fn resolveT(comptime T: type, value: *T) Resolved {
|
||||||
const Meta = T.JsApi.Meta;
|
const Meta = T.JsApi.Meta;
|
||||||
return .{
|
return .{
|
||||||
.ptr = value,
|
.ptr = value,
|
||||||
.class_id = Meta.class_id,
|
.class_id = Meta.class_id,
|
||||||
.prototype_chain = &Meta.prototype_chain,
|
.prototype_chain = &Meta.prototype_chain,
|
||||||
.weak = if (@hasDecl(Meta, "weak")) Meta.weak else false,
|
.finalizer = blk: {
|
||||||
.finalizer_from_v8 = if (@hasDecl(Meta, "finalizer")) Meta.finalizer.from_v8 else null,
|
const FT = (comptime findFinalizerType(T)) orelse break :blk null;
|
||||||
.finalizer_from_zig = if (@hasDecl(Meta, "finalizer")) Meta.finalizer.from_zig else null,
|
const getFinalizerPtr = comptime finalizerPtrGetter(T, FT);
|
||||||
|
const finalizer_ptr = getFinalizerPtr(value);
|
||||||
|
|
||||||
|
const Wrap = struct {
|
||||||
|
fn deinit(ptr_id: usize, session: *Session) void {
|
||||||
|
FT.deinit(@ptrFromInt(ptr_id), session);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn acquireRef(ptr_id: usize) void {
|
||||||
|
FT.acquireRef(@ptrFromInt(ptr_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
|
||||||
|
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
|
||||||
|
const identity_finalizer: *Session.FinalizerCallback.Identity = @ptrCast(@alignCast(ptr));
|
||||||
|
|
||||||
|
const fc = identity_finalizer.fc;
|
||||||
|
if (identity_finalizer.identity.identity_map.fetchRemove(fc.resolved_ptr_id)) |kv| {
|
||||||
|
var global = kv.value;
|
||||||
|
v8.v8__Global__Reset(&global);
|
||||||
|
}
|
||||||
|
|
||||||
|
FT.releaseRef(@ptrFromInt(fc.finalizer_ptr_id), fc.session);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break :blk .{
|
||||||
|
.ptr_id = @intFromPtr(finalizer_ptr),
|
||||||
|
.deinit = Wrap.deinit,
|
||||||
|
.acquireRef = Wrap.acquireRef,
|
||||||
|
.release = Wrap.release,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn conditionallyReference(value: anytype) void {
|
// Start at the "resolved" type (the most specific) and work our way up the
|
||||||
const T = bridge.Struct(@TypeOf(value));
|
// prototype chain looking for the type that defines acquireRef
|
||||||
if (@hasDecl(T, "acquireRef")) {
|
fn findFinalizerType(comptime T: type) ?type {
|
||||||
value.acquireRef();
|
const S = bridge.Struct(T);
|
||||||
return;
|
if (@hasDecl(S, "acquireRef")) {
|
||||||
|
return S;
|
||||||
}
|
}
|
||||||
if (@hasField(T, "_proto")) {
|
if (@hasField(S, "_proto")) {
|
||||||
conditionallyReference(value._proto);
|
const ProtoPtr = std.meta.fieldInfo(S, ._proto).type;
|
||||||
|
const ProtoChild = @typeInfo(ProtoPtr).pointer.child;
|
||||||
|
return findFinalizerType(ProtoChild);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a function that follows the _proto pointer chain to get to the finalizer type
|
||||||
|
fn finalizerPtrGetter(comptime T: type, comptime FT: type) *const fn (*T) *FT {
|
||||||
|
const S = bridge.Struct(T);
|
||||||
|
if (S == FT) {
|
||||||
|
return struct {
|
||||||
|
fn get(v: *T) *FT {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}.get;
|
||||||
|
}
|
||||||
|
if (@hasField(S, "_proto")) {
|
||||||
|
const ProtoPtr = std.meta.fieldInfo(S, ._proto).type;
|
||||||
|
const ProtoChild = @typeInfo(ProtoPtr).pointer.child;
|
||||||
|
const childGetter = comptime finalizerPtrGetter(ProtoChild, FT);
|
||||||
|
return struct {
|
||||||
|
fn get(v: *T) *FT {
|
||||||
|
return childGetter(v._proto);
|
||||||
|
}
|
||||||
|
}.get;
|
||||||
|
}
|
||||||
|
@compileError("Cannot find path from " ++ @typeName(T) ++ " to " ++ @typeName(FT));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stackTrace(self: *const Local) !?[]const u8 {
|
pub fn stackTrace(self: *const Local) !?[]const u8 {
|
||||||
@@ -1206,9 +1267,9 @@ pub fn stackTrace(self: *const Local) !?[]const u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// == Promise Helpers ==
|
// == Promise Helpers ==
|
||||||
pub fn rejectPromise(self: *const Local, value: anytype) !js.Promise {
|
pub fn rejectPromise(self: *const Local, err: js.PromiseResolver.RejectError) js.Promise {
|
||||||
var resolver = js.PromiseResolver.init(self);
|
var resolver = js.PromiseResolver.init(self);
|
||||||
resolver.reject("Local.rejectPromise", value);
|
resolver.rejectError("Local.rejectPromise", err);
|
||||||
return resolver.promise();
|
return resolver.promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1383,6 +1444,34 @@ pub fn debugContextId(self: *const Local) i32 {
|
|||||||
return v8.v8__Context__DebugContextId(self.handle);
|
return v8.v8__Context__DebugContextId(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn createFinalizerCallback(
|
||||||
|
self: *const Local,
|
||||||
|
|
||||||
|
// Key in identity map
|
||||||
|
// The most specific value (KeyboardEvent, not Event)
|
||||||
|
resolved_ptr_id: usize,
|
||||||
|
|
||||||
|
// The most specific value where finalizers are defined
|
||||||
|
// What actually gets acquired / released / deinit
|
||||||
|
finalizer_ptr_id: usize,
|
||||||
|
deinit: *const fn (ptr_id: usize, session: *Session) void,
|
||||||
|
) !*Session.FinalizerCallback {
|
||||||
|
const session = self.ctx.session;
|
||||||
|
|
||||||
|
const arena = try session.getArena(.{ .debug = "FinalizerCallback" });
|
||||||
|
errdefer session.releaseArena(arena);
|
||||||
|
|
||||||
|
const fc = try arena.create(Session.FinalizerCallback);
|
||||||
|
fc.* = .{
|
||||||
|
.arena = arena,
|
||||||
|
.session = session,
|
||||||
|
._deinit = deinit,
|
||||||
|
.resolved_ptr_id = resolved_ptr_id,
|
||||||
|
.finalizer_ptr_id = finalizer_ptr_id,
|
||||||
|
};
|
||||||
|
return fc;
|
||||||
|
}
|
||||||
|
|
||||||
// Encapsulates a Local and a HandleScope. When we're going from V8->Zig
|
// Encapsulates a Local and a HandleScope. When we're going from V8->Zig
|
||||||
// we easily get both a Local and a HandleScope via Caller.init.
|
// we easily get both a Local and a HandleScope via Caller.init.
|
||||||
// But when we're going from Zig -> V8, things are more complicated.
|
// But when we're going from Zig -> V8, things are more complicated.
|
||||||
|
|||||||
@@ -16,19 +16,21 @@
|
|||||||
// 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/>.
|
||||||
|
|
||||||
// Origin represents the shared Zig<->JS bridge state for all contexts within
|
// Origin represents the security token for contexts within the same origin.
|
||||||
// the same origin. Multiple contexts (frames) from the same origin share a
|
// Multiple contexts (frames) from the same origin share a single Origin,
|
||||||
// single Origin, ensuring that JS objects maintain their identity across frames.
|
// which provides the V8 SecurityToken that allows cross-context access.
|
||||||
|
//
|
||||||
|
// Note: Identity tracking (mapping Zig instances to v8::Objects) is managed
|
||||||
|
// separately via js.Identity - Session has the main world Identity, and
|
||||||
|
// IsolatedWorlds have their own Identity instances.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
|
|
||||||
const App = @import("../../App.zig");
|
const App = @import("../../App.zig");
|
||||||
const Session = @import("../Session.zig");
|
|
||||||
|
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
|
||||||
|
|
||||||
const Origin = @This();
|
const Origin = @This();
|
||||||
|
|
||||||
@@ -38,40 +40,12 @@ arena: Allocator,
|
|||||||
// The key, e.g. lightpanda.io:443
|
// The key, e.g. lightpanda.io:443
|
||||||
key: []const u8,
|
key: []const u8,
|
||||||
|
|
||||||
// Security token - all contexts in this realm must use the same v8::Value instance
|
// Security token - all contexts in this origin must use the same v8::Value instance
|
||||||
// as their security token for V8 to allow cross-context access
|
// as their security token for V8 to allow cross-context access
|
||||||
security_token: v8.Global,
|
security_token: v8.Global,
|
||||||
|
|
||||||
// Serves two purposes. Like `global_objects`, this is used to free
|
|
||||||
// every Global(Object) we've created during the lifetime of the realm.
|
|
||||||
// More importantly, it serves as an identity map - for a given Zig
|
|
||||||
// instance, we map it to the same Global(Object).
|
|
||||||
// The key is the @intFromPtr of the Zig value
|
|
||||||
identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
|
||||||
|
|
||||||
// Some web APIs have to manage opaque values. Ideally, they use an
|
|
||||||
// js.Object, but the js.Object has no lifetime guarantee beyond the
|
|
||||||
// current call. They can call .persist() on their js.Object to get
|
|
||||||
// a `Global(Object)`. We need to track these to free them.
|
|
||||||
// This used to be a map and acted like identity_map; the key was
|
|
||||||
// the @intFromPtr(js_obj.handle). But v8 can re-use address. Without
|
|
||||||
// a reliable way to know if an object has already been persisted,
|
|
||||||
// we now simply persist every time persist() is called.
|
|
||||||
globals: std.ArrayList(v8.Global) = .empty,
|
|
||||||
|
|
||||||
// Temp variants stored in HashMaps for O(1) early cleanup.
|
|
||||||
// Key is global.data_ptr.
|
|
||||||
temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
|
||||||
|
|
||||||
// Any type that is stored in the identity_map which has a finalizer declared
|
|
||||||
// will have its finalizer stored here. This is only used when shutting down
|
|
||||||
// if v8 hasn't called the finalizer directly itself.
|
|
||||||
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
|
|
||||||
|
|
||||||
taken_over: std.ArrayList(*Origin),
|
|
||||||
|
|
||||||
pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {
|
pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {
|
||||||
const arena = try app.arena_pool.acquire();
|
const arena = try app.arena_pool.acquire(.{ .debug = "Origin" });
|
||||||
errdefer app.arena_pool.release(arena);
|
errdefer app.arena_pool.release(arena);
|
||||||
|
|
||||||
var hs: js.HandleScope = undefined;
|
var hs: js.HandleScope = undefined;
|
||||||
@@ -88,175 +62,12 @@ pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {
|
|||||||
.rc = 1,
|
.rc = 1,
|
||||||
.arena = arena,
|
.arena = arena,
|
||||||
.key = owned_key,
|
.key = owned_key,
|
||||||
.temps = .empty,
|
|
||||||
.globals = .empty,
|
|
||||||
.taken_over = .empty,
|
|
||||||
.security_token = token_global,
|
.security_token = token_global,
|
||||||
};
|
};
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Origin, app: *App) void {
|
pub fn deinit(self: *Origin, app: *App) void {
|
||||||
for (self.taken_over.items) |o| {
|
|
||||||
o.deinit(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call finalizers before releasing anything
|
|
||||||
{
|
|
||||||
var it = self.finalizer_callbacks.valueIterator();
|
|
||||||
while (it.next()) |finalizer| {
|
|
||||||
finalizer.*.deinit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
v8.v8__Global__Reset(&self.security_token);
|
v8.v8__Global__Reset(&self.security_token);
|
||||||
|
|
||||||
{
|
|
||||||
var it = self.identity_map.valueIterator();
|
|
||||||
while (it.next()) |global| {
|
|
||||||
v8.v8__Global__Reset(global);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (self.globals.items) |*global| {
|
|
||||||
v8.v8__Global__Reset(global);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
var it = self.temps.valueIterator();
|
|
||||||
while (it.next()) |global| {
|
|
||||||
v8.v8__Global__Reset(global);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.arena_pool.release(self.arena);
|
app.arena_pool.release(self.arena);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn trackGlobal(self: *Origin, global: v8.Global) !void {
|
|
||||||
return self.globals.append(self.arena, global);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const IdentityResult = struct {
|
|
||||||
value_ptr: *v8.Global,
|
|
||||||
found_existing: bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn addIdentity(self: *Origin, ptr: usize) !IdentityResult {
|
|
||||||
const gop = try self.identity_map.getOrPut(self.arena, ptr);
|
|
||||||
return .{
|
|
||||||
.value_ptr = gop.value_ptr,
|
|
||||||
.found_existing = gop.found_existing,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trackTemp(self: *Origin, global: v8.Global) !void {
|
|
||||||
return self.temps.put(self.arena, global.data_ptr, global);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn releaseTemp(self: *Origin, global: v8.Global) void {
|
|
||||||
if (self.temps.fetchRemove(global.data_ptr)) |kv| {
|
|
||||||
var g = kv.value;
|
|
||||||
v8.v8__Global__Reset(&g);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Release an item from the identity_map (called after finalizer runs from V8)
|
|
||||||
pub fn release(self: *Origin, item: *anyopaque) void {
|
|
||||||
var global = self.identity_map.fetchRemove(@intFromPtr(item)) orelse {
|
|
||||||
if (comptime IS_DEBUG) {
|
|
||||||
std.debug.assert(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
v8.v8__Global__Reset(&global.value);
|
|
||||||
|
|
||||||
// The item has been finalized, remove it from the finalizer callback so that
|
|
||||||
// we don't try to call it again on shutdown.
|
|
||||||
const kv = self.finalizer_callbacks.fetchRemove(@intFromPtr(item)) orelse {
|
|
||||||
if (comptime IS_DEBUG) {
|
|
||||||
std.debug.assert(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
const fc = kv.value;
|
|
||||||
fc.session.releaseArena(fc.arena);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn createFinalizerCallback(
|
|
||||||
self: *Origin,
|
|
||||||
session: *Session,
|
|
||||||
global: v8.Global,
|
|
||||||
ptr: *anyopaque,
|
|
||||||
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
|
|
||||||
) !*FinalizerCallback {
|
|
||||||
const arena = try session.getArena(.{ .debug = "FinalizerCallback" });
|
|
||||||
errdefer session.releaseArena(arena);
|
|
||||||
const fc = try arena.create(FinalizerCallback);
|
|
||||||
fc.* = .{
|
|
||||||
.arena = arena,
|
|
||||||
.origin = self,
|
|
||||||
.session = session,
|
|
||||||
.ptr = ptr,
|
|
||||||
.global = global,
|
|
||||||
.zig_finalizer = zig_finalizer,
|
|
||||||
};
|
|
||||||
return fc;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn takeover(self: *Origin, original: *Origin) !void {
|
|
||||||
const arena = self.arena;
|
|
||||||
|
|
||||||
try self.globals.ensureUnusedCapacity(arena, original.globals.items.len);
|
|
||||||
for (original.globals.items) |obj| {
|
|
||||||
self.globals.appendAssumeCapacity(obj);
|
|
||||||
}
|
|
||||||
original.globals.clearRetainingCapacity();
|
|
||||||
|
|
||||||
{
|
|
||||||
try self.temps.ensureUnusedCapacity(arena, original.temps.count());
|
|
||||||
var it = original.temps.iterator();
|
|
||||||
while (it.next()) |kv| {
|
|
||||||
try self.temps.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
|
||||||
}
|
|
||||||
original.temps.clearRetainingCapacity();
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
try self.finalizer_callbacks.ensureUnusedCapacity(arena, original.finalizer_callbacks.count());
|
|
||||||
var it = original.finalizer_callbacks.iterator();
|
|
||||||
while (it.next()) |kv| {
|
|
||||||
kv.value_ptr.*.origin = self;
|
|
||||||
try self.finalizer_callbacks.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
|
||||||
}
|
|
||||||
original.finalizer_callbacks.clearRetainingCapacity();
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
try self.identity_map.ensureUnusedCapacity(arena, original.identity_map.count());
|
|
||||||
var it = original.identity_map.iterator();
|
|
||||||
while (it.next()) |kv| {
|
|
||||||
try self.identity_map.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
|
||||||
}
|
|
||||||
original.identity_map.clearRetainingCapacity();
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.taken_over.append(self.arena, original);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A type that has a finalizer can have its finalizer called one of two ways.
|
|
||||||
// The first is from V8 via the WeakCallback we give to weakRef. But that isn't
|
|
||||||
// guaranteed to fire, so we track this in finalizer_callbacks and call them on
|
|
||||||
// origin shutdown.
|
|
||||||
pub const FinalizerCallback = struct {
|
|
||||||
arena: Allocator,
|
|
||||||
origin: *Origin,
|
|
||||||
session: *Session,
|
|
||||||
ptr: *anyopaque,
|
|
||||||
global: v8.Global,
|
|
||||||
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
|
|
||||||
|
|
||||||
pub fn deinit(self: *FinalizerCallback) void {
|
|
||||||
self.zig_finalizer(self.ptr, self.session);
|
|
||||||
self.session.releaseArena(self.arena);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
// 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");
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
@@ -63,10 +64,10 @@ fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Glo
|
|||||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
if (comptime is_global) {
|
if (comptime is_global) {
|
||||||
try ctx.trackGlobal(global);
|
try ctx.trackGlobal(global);
|
||||||
return .{ .handle = global, .origin = {} };
|
return .{ .handle = global, .temps = {} };
|
||||||
}
|
}
|
||||||
try ctx.trackTemp(global);
|
try ctx.trackTemp(global);
|
||||||
return .{ .handle = global, .origin = ctx.origin };
|
return .{ .handle = global, .temps = &ctx.session.temps };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Temp = G(.temp);
|
pub const Temp = G(.temp);
|
||||||
@@ -80,7 +81,7 @@ const GlobalType = enum(u8) {
|
|||||||
fn G(comptime global_type: GlobalType) type {
|
fn G(comptime global_type: GlobalType) type {
|
||||||
return struct {
|
return struct {
|
||||||
handle: v8.Global,
|
handle: v8.Global,
|
||||||
origin: if (global_type == .temp) *js.Origin else void,
|
temps: if (global_type == .temp) *std.AutoHashMapUnmanaged(usize, v8.Global) else void,
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
@@ -96,7 +97,10 @@ fn G(comptime global_type: GlobalType) type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn release(self: *const Self) void {
|
pub fn release(self: *const Self) void {
|
||||||
self.origin.releaseTemp(self.handle);
|
if (self.temps.fetchRemove(self.handle.data_ptr)) |kv| {
|
||||||
|
var g = kv.value;
|
||||||
|
v8.v8__Global__Reset(&g);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const js = @import("js.zig");
|
|||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
const DOMException = @import("../webapi/DOMException.zig");
|
const DOMException = @import("../webapi/DOMException.zig");
|
||||||
|
|
||||||
const PromiseResolver = @This();
|
const PromiseResolver = @This();
|
||||||
@@ -66,19 +67,37 @@ pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const RejectError = union(enum) {
|
pub const RejectError = union(enum) {
|
||||||
generic: []const u8,
|
/// Not to be confused with `DOMException`; this is bare `Error`.
|
||||||
|
generic_error: []const u8,
|
||||||
|
range_error: []const u8,
|
||||||
|
reference_error: []const u8,
|
||||||
|
syntax_error: []const u8,
|
||||||
type_error: []const u8,
|
type_error: []const u8,
|
||||||
dom_exception: anyerror,
|
/// DOM exceptions are unknown to V8, belongs to web standards.
|
||||||
|
dom_exception: struct { err: anyerror },
|
||||||
};
|
};
|
||||||
pub fn rejectError(self: PromiseResolver, comptime source: []const u8, err: RejectError) void {
|
|
||||||
|
/// Rejects the promise w/ an error object.
|
||||||
|
pub fn rejectError(
|
||||||
|
self: PromiseResolver,
|
||||||
|
comptime source: []const u8,
|
||||||
|
err: RejectError,
|
||||||
|
) void {
|
||||||
const handle = switch (err) {
|
const handle = switch (err) {
|
||||||
.type_error => |str| self.local.isolate.createTypeError(str),
|
.generic_error => |msg| self.local.isolate.createError(msg),
|
||||||
.generic => |str| self.local.isolate.createError(str),
|
.range_error => |msg| self.local.isolate.createRangeError(msg),
|
||||||
|
.reference_error => |msg| self.local.isolate.createReferenceError(msg),
|
||||||
|
.syntax_error => |msg| self.local.isolate.createSyntaxError(msg),
|
||||||
|
.type_error => |msg| self.local.isolate.createTypeError(msg),
|
||||||
|
// "Exceptional".
|
||||||
.dom_exception => |exception| {
|
.dom_exception => |exception| {
|
||||||
self.reject(source, DOMException.fromError(exception));
|
self._reject(DOMException.fromError(exception.err) orelse unreachable) catch |reject_err| {
|
||||||
|
log.err(.bug, "rejectDomException", .{ .source = source, .err = reject_err, .persistent = false });
|
||||||
|
};
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
self._reject(js.Value{ .handle = handle, .local = self.local }) catch |reject_err| {
|
self._reject(js.Value{ .handle = handle, .local = self.local }) catch |reject_err| {
|
||||||
log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false });
|
log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
|
|||||||
|
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
const JsApis = bridge.JsApis;
|
const JsApis = bridge.JsApis;
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const Snapshot = @This();
|
const Snapshot = @This();
|
||||||
|
|
||||||
@@ -137,7 +136,7 @@ pub fn create() !Snapshot {
|
|||||||
defer v8.v8__HandleScope__DESTRUCT(&handle_scope);
|
defer v8.v8__HandleScope__DESTRUCT(&handle_scope);
|
||||||
|
|
||||||
// Create templates (constructors only) FIRST
|
// Create templates (constructors only) FIRST
|
||||||
var templates: [JsApis.len]*v8.FunctionTemplate = undefined;
|
var templates: [JsApis.len]*const v8.FunctionTemplate = undefined;
|
||||||
inline for (JsApis, 0..) |JsApi, i| {
|
inline for (JsApis, 0..) |JsApi, i| {
|
||||||
@setEvalBranchQuota(10_000);
|
@setEvalBranchQuota(10_000);
|
||||||
templates[i] = generateConstructor(JsApi, isolate);
|
templates[i] = generateConstructor(JsApi, isolate);
|
||||||
@@ -419,7 +418,7 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
// via `new ClassName()` - but they could, for example, be created in
|
// via `new ClassName()` - but they could, for example, be created in
|
||||||
// Zig and returned from a function call, which is why we need the
|
// Zig and returned from a function call, which is why we need the
|
||||||
// FunctionTemplate.
|
// FunctionTemplate.
|
||||||
fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionTemplate {
|
fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.FunctionTemplate {
|
||||||
const callback = blk: {
|
const callback = blk: {
|
||||||
if (@hasDecl(JsApi, "constructor")) {
|
if (@hasDecl(JsApi, "constructor")) {
|
||||||
break :blk JsApi.constructor.func;
|
break :blk JsApi.constructor.func;
|
||||||
@@ -429,7 +428,7 @@ fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionT
|
|||||||
break :blk illegalConstructorCallback;
|
break :blk illegalConstructorCallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
const template = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?);
|
const template = v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?;
|
||||||
{
|
{
|
||||||
const internal_field_count = comptime countInternalFields(JsApi);
|
const internal_field_count = comptime countInternalFields(JsApi);
|
||||||
if (internal_field_count > 0) {
|
if (internal_field_count > 0) {
|
||||||
@@ -482,10 +481,15 @@ pub fn countInternalFields(comptime JsApi: type) u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attaches JsApi members to the prototype template (normal case)
|
// Attaches JsApi members to the prototype template (normal case)
|
||||||
fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.FunctionTemplate) void {
|
fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.FunctionTemplate) void {
|
||||||
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
||||||
const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(template);
|
const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(template);
|
||||||
|
|
||||||
|
// Create a signature that validates the receiver is an instance of this template.
|
||||||
|
// This prevents crashes when JavaScript extracts a getter/method and calls it
|
||||||
|
// with the wrong `this` (e.g., documentGetter.call(null)).
|
||||||
|
const signature = v8.v8__Signature__New(isolate, template);
|
||||||
|
|
||||||
const declarations = @typeInfo(JsApi).@"struct".decls;
|
const declarations = @typeInfo(JsApi).@"struct".decls;
|
||||||
var has_named_index_getter = false;
|
var has_named_index_getter = false;
|
||||||
|
|
||||||
@@ -497,23 +501,47 @@ 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__Config(isolate, &.{ .callback = value.getter }).?);
|
const getter_signature = if (value.static) null else signature;
|
||||||
|
const getter_callback = v8.v8__FunctionTemplate__New__Config(isolate, &.{
|
||||||
|
.callback = value.getter,
|
||||||
|
.signature = getter_signature,
|
||||||
|
}).?;
|
||||||
|
const setter_callback = if (value.setter) |setter|
|
||||||
|
v8.v8__FunctionTemplate__New__Config(isolate, &.{
|
||||||
|
.callback = setter,
|
||||||
|
.signature = getter_signature,
|
||||||
|
}).?
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
|
||||||
|
var attribute: v8.PropertyAttribute = 0;
|
||||||
if (value.setter == null) {
|
if (value.setter == null) {
|
||||||
if (value.static) {
|
attribute |= v8.ReadOnly;
|
||||||
v8.v8__Template__SetAccessorProperty__DEFAULT(@ptrCast(template), js_name, getter_callback);
|
}
|
||||||
} else {
|
if (value.deletable == false) {
|
||||||
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(prototype, js_name, getter_callback);
|
attribute |= v8.DontDelete;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value.static) {
|
||||||
|
// Static accessors: use Template's SetAccessorProperty
|
||||||
|
v8.v8__Template__SetAccessorProperty(@ptrCast(template), js_name, getter_callback, setter_callback, attribute);
|
||||||
} else {
|
} else {
|
||||||
if (comptime IS_DEBUG) {
|
v8.v8__ObjectTemplate__SetAccessorProperty__Config(prototype, &.{
|
||||||
std.debug.assert(value.static == false);
|
.key = js_name,
|
||||||
}
|
.getter = getter_callback,
|
||||||
const setter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.setter.? }).?);
|
.setter = setter_callback,
|
||||||
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT2(prototype, js_name, getter_callback, setter_callback);
|
.attribute = attribute,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bridge.Function => {
|
bridge.Function => {
|
||||||
const function_template = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func, .length = value.arity }).?);
|
// For non-static functions, use the signature to validate the receiver
|
||||||
|
const func_signature = if (value.static) null else signature;
|
||||||
|
const function_template = v8.v8__FunctionTemplate__New__Config(isolate, &.{
|
||||||
|
.callback = value.func,
|
||||||
|
.length = value.arity,
|
||||||
|
.signature = func_signature,
|
||||||
|
}).?;
|
||||||
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);
|
||||||
@@ -551,7 +579,7 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
|||||||
has_named_index_getter = true;
|
has_named_index_getter = true;
|
||||||
},
|
},
|
||||||
bridge.Iterator => {
|
bridge.Iterator => {
|
||||||
const function_template = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func }).?);
|
const function_template = 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
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ fn _toSlice(self: String, comptime null_terminate: bool, allocator: Allocator) !
|
|||||||
|
|
||||||
pub fn toSSO(self: String, comptime global: bool) !(if (global) SSO.Global else SSO) {
|
pub fn toSSO(self: String, comptime global: bool) !(if (global) SSO.Global else SSO) {
|
||||||
if (comptime global) {
|
if (comptime global) {
|
||||||
return .{ .str = try self.toSSOWithAlloc(self.local.ctx.origin.arena) };
|
return .{ .str = try self.toSSOWithAlloc(self.local.ctx.session.page_arena) };
|
||||||
}
|
}
|
||||||
return self.toSSOWithAlloc(self.local.call_arena);
|
return self.toSSOWithAlloc(self.local.call_arena);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -300,10 +300,10 @@ fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Globa
|
|||||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
if (comptime is_global) {
|
if (comptime is_global) {
|
||||||
try ctx.trackGlobal(global);
|
try ctx.trackGlobal(global);
|
||||||
return .{ .handle = global, .origin = {} };
|
return .{ .handle = global, .temps = {} };
|
||||||
}
|
}
|
||||||
try ctx.trackTemp(global);
|
try ctx.trackTemp(global);
|
||||||
return .{ .handle = global, .origin = ctx.origin };
|
return .{ .handle = global, .temps = &ctx.session.temps };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toZig(self: Value, comptime T: type) !T {
|
pub fn toZig(self: Value, comptime T: type) !T {
|
||||||
@@ -361,7 +361,7 @@ const GlobalType = enum(u8) {
|
|||||||
fn G(comptime global_type: GlobalType) type {
|
fn G(comptime global_type: GlobalType) type {
|
||||||
return struct {
|
return struct {
|
||||||
handle: v8.Global,
|
handle: v8.Global,
|
||||||
origin: if (global_type == .temp) *js.Origin else void,
|
temps: if (global_type == .temp) *std.AutoHashMapUnmanaged(usize, v8.Global) else void,
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
@@ -381,7 +381,10 @@ fn G(comptime global_type: GlobalType) type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn release(self: *const Self) void {
|
pub fn release(self: *const Self) void {
|
||||||
self.origin.releaseTemp(self.handle);
|
if (self.temps.fetchRemove(self.handle.data_ptr)) |kv| {
|
||||||
|
var g = kv.value;
|
||||||
|
v8.v8__Global__Reset(&g);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,16 +18,12 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const lp = @import("lightpanda");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
const Session = @import("../Session.zig");
|
const Session = @import("../Session.zig");
|
||||||
|
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const Caller = @import("Caller.zig");
|
const Caller = @import("Caller.zig");
|
||||||
const Context = @import("Context.zig");
|
|
||||||
const Origin = @import("Origin.zig");
|
|
||||||
|
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
@@ -105,37 +101,21 @@ pub fn Builder(comptime T: type) type {
|
|||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finalizer(comptime func: *const fn (self: *T, shutdown: bool, session: *Session) void) Finalizer {
|
|
||||||
return .{
|
|
||||||
.from_zig = struct {
|
|
||||||
fn wrap(ptr: *anyopaque, session: *Session) void {
|
|
||||||
func(@ptrCast(@alignCast(ptr)), true, session);
|
|
||||||
}
|
|
||||||
}.wrap,
|
|
||||||
|
|
||||||
.from_v8 = struct {
|
|
||||||
fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
|
|
||||||
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
|
|
||||||
const fc: *Origin.FinalizerCallback = @ptrCast(@alignCast(ptr));
|
|
||||||
|
|
||||||
const origin = fc.origin;
|
|
||||||
const value_ptr = fc.ptr;
|
|
||||||
if (origin.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
|
|
||||||
func(@ptrCast(@alignCast(value_ptr)), false, fc.session);
|
|
||||||
origin.release(value_ptr);
|
|
||||||
} else {
|
|
||||||
// A bit weird, but v8 _requires_ that we release it
|
|
||||||
// If we don't. We'll 100% crash.
|
|
||||||
v8.v8__Global__Reset(&fc.global);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.wrap,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn releaseRef(comptime T: type, ptr_id: usize, session: *Session) void {
|
||||||
|
if (@hasDecl(T, "releaseRef")) {
|
||||||
|
T.releaseRef(@ptrFromInt(ptr_id), session);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (@hasField(T, "_proto")) {
|
||||||
|
releaseRef(Struct(std.meta.fieldInfo(T, ._proto).type), ptr_id, session);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
@compileError(@typeName(T) ++ " marked with finalizer without an acquireRef in its prototype chain");
|
||||||
|
}
|
||||||
|
|
||||||
pub const Constructor = struct {
|
pub const Constructor = struct {
|
||||||
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
||||||
|
|
||||||
@@ -200,6 +180,7 @@ pub const Function = struct {
|
|||||||
|
|
||||||
pub const Accessor = struct {
|
pub const Accessor = struct {
|
||||||
static: bool = false,
|
static: bool = false,
|
||||||
|
deletable: bool = true,
|
||||||
cache: ?Caller.Function.Opts.Caching = null,
|
cache: ?Caller.Function.Opts.Caching = null,
|
||||||
getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
|
getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
|
||||||
setter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
|
setter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
|
||||||
@@ -208,6 +189,7 @@ pub const Accessor = struct {
|
|||||||
var accessor = Accessor{
|
var accessor = Accessor{
|
||||||
.cache = opts.cache,
|
.cache = opts.cache,
|
||||||
.static = opts.static,
|
.static = opts.static,
|
||||||
|
.deletable = opts.deletable,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (@typeInfo(@TypeOf(getter)) != .null) {
|
if (@typeInfo(@TypeOf(getter)) != .null) {
|
||||||
@@ -414,17 +396,6 @@ pub const Property = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Finalizer = struct {
|
|
||||||
// The finalizer wrapper when called from Zig. This is only called on
|
|
||||||
// Origin.deinit
|
|
||||||
from_zig: *const fn (ctx: *anyopaque, session: *Session) void,
|
|
||||||
|
|
||||||
// The finalizer wrapper when called from V8. This may never be called
|
|
||||||
// (hence why we fallback to calling in Origin.deinit). If it is called,
|
|
||||||
// it is only ever called after we SetWeak on the Global.
|
|
||||||
from_v8: *const fn (?*const v8.WeakCallbackInfo) callconv(.c) void,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||||
var caller: Caller = undefined;
|
var caller: Caller = undefined;
|
||||||
@@ -852,6 +823,8 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/event/TextEvent.zig"),
|
@import("../webapi/event/TextEvent.zig"),
|
||||||
@import("../webapi/event/InputEvent.zig"),
|
@import("../webapi/event/InputEvent.zig"),
|
||||||
@import("../webapi/event/PromiseRejectionEvent.zig"),
|
@import("../webapi/event/PromiseRejectionEvent.zig"),
|
||||||
|
@import("../webapi/event/SubmitEvent.zig"),
|
||||||
|
@import("../webapi/event/FormDataEvent.zig"),
|
||||||
@import("../webapi/MessageChannel.zig"),
|
@import("../webapi/MessageChannel.zig"),
|
||||||
@import("../webapi/MessagePort.zig"),
|
@import("../webapi/MessagePort.zig"),
|
||||||
@import("../webapi/media/MediaError.zig"),
|
@import("../webapi/media/MediaError.zig"),
|
||||||
@@ -901,6 +874,7 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/canvas/OffscreenCanvas.zig"),
|
@import("../webapi/canvas/OffscreenCanvas.zig"),
|
||||||
@import("../webapi/canvas/OffscreenCanvasRenderingContext2D.zig"),
|
@import("../webapi/canvas/OffscreenCanvasRenderingContext2D.zig"),
|
||||||
@import("../webapi/SubtleCrypto.zig"),
|
@import("../webapi/SubtleCrypto.zig"),
|
||||||
|
@import("../webapi/CryptoKey.zig"),
|
||||||
@import("../webapi/Selection.zig"),
|
@import("../webapi/Selection.zig"),
|
||||||
@import("../webapi/ImageData.zig"),
|
@import("../webapi/ImageData.zig"),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ pub const Env = @import("Env.zig");
|
|||||||
pub const bridge = @import("bridge.zig");
|
pub const bridge = @import("bridge.zig");
|
||||||
pub const Caller = @import("Caller.zig");
|
pub const Caller = @import("Caller.zig");
|
||||||
pub const Origin = @import("Origin.zig");
|
pub const Origin = @import("Origin.zig");
|
||||||
|
pub const Identity = @import("Identity.zig");
|
||||||
pub const Context = @import("Context.zig");
|
pub const Context = @import("Context.zig");
|
||||||
pub const Local = @import("Local.zig");
|
pub const Local = @import("Local.zig");
|
||||||
pub const Inspector = @import("Inspector.zig");
|
pub const Inspector = @import("Inspector.zig");
|
||||||
|
|||||||
54
src/browser/links.zig
Normal file
54
src/browser/links.zig
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// 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 Element = @import("webapi/Element.zig");
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
const Selector = @import("webapi/selector/Selector.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
/// Collect all links (href attributes from anchor tags) under `root`.
|
||||||
|
/// Returns a slice of strings allocated with `arena`.
|
||||||
|
pub fn collectLinks(arena: Allocator, root: *Node, page: *Page) ![]const []const u8 {
|
||||||
|
var links: std.ArrayList([]const u8) = .empty;
|
||||||
|
|
||||||
|
if (Selector.querySelectorAll(root, "a[href]", page)) |list| {
|
||||||
|
defer list.deinit(page._session);
|
||||||
|
|
||||||
|
for (list._nodes) |node| {
|
||||||
|
if (node.is(Element.Html.Anchor)) |anchor| {
|
||||||
|
const href = anchor.getHref(page) catch |err| {
|
||||||
|
@import("../lightpanda.zig").log.err(.app, "resolve href failed", .{ .err = err });
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (href.len > 0) {
|
||||||
|
try links.append(arena, href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else |err| {
|
||||||
|
@import("../lightpanda.zig").log.err(.app, "query links failed", .{ .err = err });
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return links.items;
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@ const std = @import("std");
|
|||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
const URL = @import("URL.zig");
|
const URL = @import("URL.zig");
|
||||||
const TreeWalker = @import("webapi/TreeWalker.zig");
|
const TreeWalker = @import("webapi/TreeWalker.zig");
|
||||||
const CData = @import("webapi/CData.zig");
|
|
||||||
const Element = @import("webapi/Element.zig");
|
const Element = @import("webapi/Element.zig");
|
||||||
const Node = @import("webapi/Node.zig");
|
const Node = @import("webapi/Node.zig");
|
||||||
const isAllWhitespace = @import("../string.zig").isAllWhitespace;
|
const isAllWhitespace = @import("../string.zig").isAllWhitespace;
|
||||||
|
|||||||
@@ -15,10 +15,10 @@
|
|||||||
a1.play();
|
a1.play();
|
||||||
cb.push(a1.playState);
|
cb.push(a1.playState);
|
||||||
});
|
});
|
||||||
testing.eventually(() => testing.expectEqual(['idle', 'running', 'finished', true], cb));
|
testing.onload(() => testing.expectEqual(['idle', 'running', 'finished', true], cb));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=startTime>
|
<!-- <script id=startTime>
|
||||||
let a2 = document.createElement('div').animate(null, null);
|
let a2 = document.createElement('div').animate(null, null);
|
||||||
// startTime defaults to null
|
// startTime defaults to null
|
||||||
testing.expectEqual(null, a2.startTime);
|
testing.expectEqual(null, a2.startTime);
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
// onfinish callback should be scheduled and called asynchronously
|
// onfinish callback should be scheduled and called asynchronously
|
||||||
a3.onfinish = function() { calls.push('finish'); };
|
a3.onfinish = function() { calls.push('finish'); };
|
||||||
a3.play();
|
a3.play();
|
||||||
testing.eventually(() => testing.expectEqual(['finish'], calls));
|
testing.onload(() => testing.expectEqual(['finish'], calls));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=pause>
|
<script id=pause>
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
a4.pause();
|
a4.pause();
|
||||||
cb4.push(a4.playState)
|
cb4.push(a4.playState)
|
||||||
});
|
});
|
||||||
testing.eventually(() => testing.expectEqual(['running', 'paused'], cb4));
|
testing.onload(() => testing.expectEqual(['running', 'paused'], cb4));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=finish>
|
<script id=finish>
|
||||||
@@ -65,5 +65,6 @@
|
|||||||
cb5.push(a5.playState);
|
cb5.push(a5.playState);
|
||||||
a5.play();
|
a5.play();
|
||||||
});
|
});
|
||||||
testing.eventually(() => testing.expectEqual(['idle', 'finished'], cb5));
|
testing.onload(() => testing.expectEqual(['idle', 'finished'], cb5));
|
||||||
</script>
|
</script>
|
||||||
|
-->
|
||||||
|
|||||||
@@ -125,6 +125,19 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script id="CanvasRenderingContext2D#canvas">
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
testing.expectEqual(ctx.canvas, element);
|
||||||
|
// Setting dimensions via ctx.canvas should update the element.
|
||||||
|
ctx.canvas.width = 40;
|
||||||
|
ctx.canvas.height = 25;
|
||||||
|
testing.expectEqual(element.width, 40);
|
||||||
|
testing.expectEqual(element.height, 25);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id="getter">
|
<script id="getter">
|
||||||
{
|
{
|
||||||
const element = document.createElement("canvas");
|
const element = document.createElement("canvas");
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
|
|
||||||
document.fonts.load("italic bold 16px Roboto");
|
document.fonts.load("italic bold 16px Roboto");
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(true, loading);
|
testing.expectEqual(true, loading);
|
||||||
testing.expectEqual(true, loadingdone);
|
testing.expectEqual(true, loadingdone);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -419,3 +419,117 @@
|
|||||||
testing.expectEqual('anchor-size(--foo width, anchor-size(--bar height))', div.style.width);
|
testing.expectEqual('anchor-size(--foo width, anchor-size(--bar height))', div.style.width);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleSheet_insertRule_deleteRule">
|
||||||
|
{
|
||||||
|
const style = document.createElement('style');
|
||||||
|
document.head.appendChild(style);
|
||||||
|
const sheet = style.sheet;
|
||||||
|
|
||||||
|
testing.expectEqual(0, sheet.cssRules.length);
|
||||||
|
|
||||||
|
sheet.insertRule('.test { color: green; }', 0);
|
||||||
|
testing.expectEqual(1, sheet.cssRules.length);
|
||||||
|
testing.expectEqual('.test', sheet.cssRules[0].selectorText);
|
||||||
|
testing.expectEqual('green', sheet.cssRules[0].style.color);
|
||||||
|
|
||||||
|
sheet.deleteRule(0);
|
||||||
|
testing.expectEqual(0, sheet.cssRules.length);
|
||||||
|
|
||||||
|
let caught = false;
|
||||||
|
try {
|
||||||
|
sheet.deleteRule(5);
|
||||||
|
} catch (e) {
|
||||||
|
caught = true;
|
||||||
|
testing.expectEqual('IndexSizeError', e.name);
|
||||||
|
}
|
||||||
|
testing.expectTrue(caught);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleSheet_insertRule_default_index">
|
||||||
|
{
|
||||||
|
const style = document.createElement('style');
|
||||||
|
document.head.appendChild(style);
|
||||||
|
const sheet = style.sheet;
|
||||||
|
|
||||||
|
testing.expectEqual(0, sheet.cssRules.length);
|
||||||
|
|
||||||
|
// Call without index, should default to 0
|
||||||
|
sheet.insertRule('.test-default { color: blue; }');
|
||||||
|
testing.expectEqual(1, sheet.cssRules.length);
|
||||||
|
testing.expectEqual('.test-default', sheet.cssRules[0].selectorText);
|
||||||
|
|
||||||
|
// Insert another rule without index, should default to 0 and push the first one to index 1
|
||||||
|
sheet.insertRule('.test-at-0 { color: red; }');
|
||||||
|
testing.expectEqual(2, sheet.cssRules.length);
|
||||||
|
testing.expectEqual('.test-at-0', sheet.cssRules[0].selectorText);
|
||||||
|
testing.expectEqual('.test-default', sheet.cssRules[1].selectorText);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleSheet_insertRule_semicolon">
|
||||||
|
{
|
||||||
|
const style = document.createElement('style');
|
||||||
|
document.head.appendChild(style);
|
||||||
|
const sheet = style.sheet;
|
||||||
|
|
||||||
|
// Should not throw even with trailing semicolon
|
||||||
|
sheet.insertRule('*{};');
|
||||||
|
testing.expectEqual(1, sheet.cssRules.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleSheet_insertRule_multiple_rules">
|
||||||
|
{
|
||||||
|
const style = document.createElement('style');
|
||||||
|
document.head.appendChild(style);
|
||||||
|
const sheet = style.sheet;
|
||||||
|
|
||||||
|
let caught = false;
|
||||||
|
try {
|
||||||
|
sheet.insertRule('a { color: red; } b { color: blue; }');
|
||||||
|
} catch (e) {
|
||||||
|
caught = true;
|
||||||
|
testing.expectEqual('SyntaxError', e.name);
|
||||||
|
}
|
||||||
|
testing.expectTrue(caught);
|
||||||
|
testing.expectEqual(0, sheet.cssRules.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleSheet_replaceSync">
|
||||||
|
{
|
||||||
|
const sheet = new CSSStyleSheet();
|
||||||
|
testing.expectEqual(0, sheet.cssRules.length);
|
||||||
|
|
||||||
|
sheet.replaceSync('.test { color: blue; }');
|
||||||
|
testing.expectEqual(1, sheet.cssRules.length);
|
||||||
|
testing.expectEqual('.test', sheet.cssRules[0].selectorText);
|
||||||
|
testing.expectEqual('blue', sheet.cssRules[0].style.color);
|
||||||
|
|
||||||
|
let replacedAsync = false;
|
||||||
|
testing.async(async () => {
|
||||||
|
const result = await sheet.replace('.async-test { margin: 10px; }');
|
||||||
|
testing.expectTrue(result === sheet);
|
||||||
|
testing.expectEqual(1, sheet.cssRules.length);
|
||||||
|
testing.expectEqual('.async-test', sheet.cssRules[0].selectorText);
|
||||||
|
replacedAsync = true;
|
||||||
|
});
|
||||||
|
testing.onload(() => testing.expectTrue(replacedAsync));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleRule_cssText">
|
||||||
|
{
|
||||||
|
const sheet = new CSSStyleSheet();
|
||||||
|
sheet.replaceSync('.test { color: red; margin: 10px; }');
|
||||||
|
|
||||||
|
// Check serialization format
|
||||||
|
const cssText = sheet.cssRules[0].cssText;
|
||||||
|
testing.expectTrue(cssText.includes('.test { '));
|
||||||
|
testing.expectTrue(cssText.includes('color: red;'));
|
||||||
|
testing.expectTrue(cssText.includes('margin: 10px;'));
|
||||||
|
testing.expectTrue(cssText.includes('}'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -342,3 +342,4 @@
|
|||||||
testing.expectEqual('html', doc.lastChild.nodeName);
|
testing.expectEqual('html', doc.lastChild.nodeName);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@
|
|||||||
document.open();
|
document.open();
|
||||||
}, 5);
|
}, 5);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
// The element should be gone now
|
// The element should be gone now
|
||||||
const afterOpen = document.getElementById('will_be_removed');
|
const afterOpen = document.getElementById('will_be_removed');
|
||||||
testing.expectEqual(null, afterOpen);
|
testing.expectEqual(null, afterOpen);
|
||||||
|
|||||||
226
src/browser/tests/element/check_visibility.html
Normal file
226
src/browser/tests/element/check_visibility.html
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<body></body>
|
||||||
|
|
||||||
|
<!-
|
||||||
|
<script id="inline_display_none">
|
||||||
|
{
|
||||||
|
const el = document.createElement("div");
|
||||||
|
document.body.appendChild(el);
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
el.style.display = "none";
|
||||||
|
testing.expectEqual(false, el.checkVisibility());
|
||||||
|
|
||||||
|
el.style.display = "block";
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="inline_visibility_hidden">
|
||||||
|
{
|
||||||
|
const el = document.createElement("div");
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
el.style.visibility = "hidden";
|
||||||
|
// Without visibilityProperty option, visibility:hidden is not checked
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
// With visibilityProperty: true, visibility:hidden is detected
|
||||||
|
testing.expectEqual(false, el.checkVisibility({ visibilityProperty: true }));
|
||||||
|
|
||||||
|
el.style.visibility = "collapse";
|
||||||
|
testing.expectEqual(false, el.checkVisibility({ visibilityProperty: true }));
|
||||||
|
|
||||||
|
el.style.visibility = "visible";
|
||||||
|
testing.expectEqual(true, el.checkVisibility({ visibilityProperty: true }));
|
||||||
|
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="inline_opacity_zero">
|
||||||
|
{
|
||||||
|
const el = document.createElement("div");
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
el.style.opacity = "0";
|
||||||
|
// Without checkOpacity option, opacity:0 is not checked
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
// With checkOpacity: true, opacity:0 is detected
|
||||||
|
testing.expectEqual(false, el.checkVisibility({ checkOpacity: true }));
|
||||||
|
|
||||||
|
el.style.opacity = "0.5";
|
||||||
|
testing.expectEqual(true, el.checkVisibility({ checkOpacity: true }));
|
||||||
|
|
||||||
|
el.style.opacity = "1";
|
||||||
|
testing.expectEqual(true, el.checkVisibility({ checkOpacity: true }));
|
||||||
|
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="parent_hidden_hides_child">
|
||||||
|
{
|
||||||
|
const parent = document.createElement("div");
|
||||||
|
const child = document.createElement("span");
|
||||||
|
parent.appendChild(child);
|
||||||
|
document.body.appendChild(parent);
|
||||||
|
|
||||||
|
testing.expectEqual(true, child.checkVisibility());
|
||||||
|
|
||||||
|
// display:none on parent hides children (no option needed)
|
||||||
|
parent.style.display = "none";
|
||||||
|
testing.expectEqual(false, child.checkVisibility());
|
||||||
|
|
||||||
|
// visibility:hidden on parent - needs visibilityProperty option
|
||||||
|
parent.style.display = "block";
|
||||||
|
parent.style.visibility = "hidden";
|
||||||
|
testing.expectEqual(true, child.checkVisibility()); // without option
|
||||||
|
testing.expectEqual(false, child.checkVisibility({ visibilityProperty: true }));
|
||||||
|
|
||||||
|
// opacity:0 on parent - needs checkOpacity option
|
||||||
|
parent.style.visibility = "visible";
|
||||||
|
parent.style.opacity = "0";
|
||||||
|
testing.expectEqual(true, child.checkVisibility()); // without option
|
||||||
|
testing.expectEqual(false, child.checkVisibility({ checkOpacity: true }));
|
||||||
|
|
||||||
|
parent.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style id="style-basic">
|
||||||
|
.hidden-by-class { display: none; }
|
||||||
|
.visible-by-class { display: block; }
|
||||||
|
</style>
|
||||||
|
<script id="style_tag_basic">
|
||||||
|
{
|
||||||
|
const el = document.createElement("div");
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
el.className = "hidden-by-class";
|
||||||
|
testing.expectEqual(false, el.checkVisibility());
|
||||||
|
|
||||||
|
el.className = "visible-by-class";
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
el.className = "";
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style id="style-specificity">
|
||||||
|
.spec-hidden { display: none; }
|
||||||
|
#spec-visible { display: block; }
|
||||||
|
</style>
|
||||||
|
<script id="specificity_id_beats_class">
|
||||||
|
{
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.id = "spec-visible";
|
||||||
|
el.className = "spec-hidden";
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
// ID selector (#spec-visible: display:block) should beat class selector (.spec-hidden: display:none)
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style id="style-order-1">
|
||||||
|
.order-test { display: none; }
|
||||||
|
</style>
|
||||||
|
<style id="style-order-2">
|
||||||
|
.order-test { display: block; }
|
||||||
|
</style>
|
||||||
|
<script id="rule_order_later_wins">
|
||||||
|
{
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = "order-test";
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
// Second style block should win (display: block)
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style id="style-override">
|
||||||
|
.should-be-hidden { display: none; }
|
||||||
|
</style>
|
||||||
|
<script id="inline_overrides_stylesheet">
|
||||||
|
{
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = "should-be-hidden";
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
testing.expectEqual(false, el.checkVisibility());
|
||||||
|
|
||||||
|
// Inline style should override
|
||||||
|
el.style.display = "block";
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="dynamic_style_element">
|
||||||
|
{
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = "dynamic-style-test";
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
// Add a style element
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.textContent = ".dynamic-style-test { display: none; }";
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
testing.expectEqual(false, el.checkVisibility());
|
||||||
|
|
||||||
|
// Remove the style element
|
||||||
|
style.remove();
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="deep_nesting">
|
||||||
|
{
|
||||||
|
const levels = 5;
|
||||||
|
let current = document.body;
|
||||||
|
const elements = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < levels; i++) {
|
||||||
|
const el = document.createElement("div");
|
||||||
|
current.appendChild(el);
|
||||||
|
elements.push(el);
|
||||||
|
current = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All should be visible
|
||||||
|
for (let i = 0; i < levels; i++) {
|
||||||
|
testing.expectEqual(true, elements[i].checkVisibility());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide middle element
|
||||||
|
elements[2].style.display = "none";
|
||||||
|
|
||||||
|
// Elements 0, 1 should still be visible
|
||||||
|
testing.expectEqual(true, elements[0].checkVisibility());
|
||||||
|
testing.expectEqual(true, elements[1].checkVisibility());
|
||||||
|
|
||||||
|
// Elements 2, 3, 4 should be hidden
|
||||||
|
testing.expectEqual(false, elements[2].checkVisibility());
|
||||||
|
testing.expectEqual(false, elements[3].checkVisibility());
|
||||||
|
testing.expectEqual(false, elements[4].checkVisibility());
|
||||||
|
|
||||||
|
elements[0].remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -532,6 +532,6 @@
|
|||||||
testing.expectEqual(true, result);
|
testing.expectEqual(true, result);
|
||||||
});
|
});
|
||||||
|
|
||||||
testing.eventually(() => testing.expectEqual(true, asyncBlockDispatched));
|
testing.onload(() => testing.expectEqual(true, asyncBlockDispatched));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -463,3 +463,44 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Test: requestSubmit(submitter) sets SubmitEvent.submitter -->
|
||||||
|
<form id="test_form_submitter" action="/should-not-navigate6" method="get">
|
||||||
|
<button id="submitter_btn" type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script id="requestSubmit_sets_submitter">
|
||||||
|
{
|
||||||
|
const form = $('#test_form_submitter');
|
||||||
|
const btn = $('#submitter_btn');
|
||||||
|
let capturedSubmitter = undefined;
|
||||||
|
|
||||||
|
form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
capturedSubmitter = e.submitter;
|
||||||
|
});
|
||||||
|
|
||||||
|
form.requestSubmit(btn);
|
||||||
|
testing.expectEqual(btn, capturedSubmitter);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Test: requestSubmit() without submitter sets submitter to the form element -->
|
||||||
|
<form id="test_form_submitter2" action="/should-not-navigate7" method="get">
|
||||||
|
<input type="text" name="q" value="test">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script id="requestSubmit_default_submitter_is_form">
|
||||||
|
{
|
||||||
|
const form = $('#test_form_submitter2');
|
||||||
|
let capturedSubmitter = undefined;
|
||||||
|
|
||||||
|
form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
capturedSubmitter = e.submitter;
|
||||||
|
});
|
||||||
|
|
||||||
|
form.requestSubmit();
|
||||||
|
testing.expectEqual(form, capturedSubmitter);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -29,10 +29,12 @@
|
|||||||
|
|
||||||
testing.expectEqual('', img.src);
|
testing.expectEqual('', img.src);
|
||||||
testing.expectEqual('', img.alt);
|
testing.expectEqual('', img.alt);
|
||||||
|
testing.expectEqual('', img.currentSrc);
|
||||||
|
|
||||||
img.src = 'test.png';
|
img.src = 'test.png';
|
||||||
// src property returns resolved absolute URL
|
// src property returns resolved absolute URL
|
||||||
testing.expectEqual(testing.BASE_URL + 'element/html/test.png', img.src);
|
testing.expectEqual(testing.BASE_URL + 'element/html/test.png', img.src);
|
||||||
|
testing.expectEqual(testing.BASE_URL + 'element/html/test.png', img.currentSrc);
|
||||||
// getAttribute returns the raw attribute value
|
// getAttribute returns the raw attribute value
|
||||||
testing.expectEqual('test.png', img.getAttribute('src'));
|
testing.expectEqual('test.png', img.getAttribute('src'));
|
||||||
|
|
||||||
@@ -137,7 +139,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
testing.eventually(() => testing.expectEqual(true, result));
|
testing.onload(() => testing.expectEqual(true, result));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -148,7 +150,7 @@
|
|||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
img.addEventListener("load", () => { fired = true; });
|
img.addEventListener("load", () => { fired = true; });
|
||||||
document.body.appendChild(img);
|
document.body.appendChild(img);
|
||||||
testing.eventually(() => testing.expectEqual(false, fired));
|
testing.onload(() => testing.expectEqual(false, fired));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -161,7 +163,7 @@
|
|||||||
document.body.appendChild(img);
|
document.body.appendChild(img);
|
||||||
img.src = "https://cdn.lightpanda.io/website/assets/images/docs/hn.png";
|
img.src = "https://cdn.lightpanda.io/website/assets/images/docs/hn.png";
|
||||||
|
|
||||||
testing.eventually(() => testing.expectEqual(true, result));
|
testing.onload(() => testing.expectEqual(true, result));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -210,7 +210,7 @@
|
|||||||
});
|
});
|
||||||
input.setSelectionRange(1, 4);
|
input.setSelectionRange(1, 4);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(5, eventCount);
|
testing.expectEqual(5, eventCount);
|
||||||
testing.expectEqual('selectionchange', lastEvent.type);
|
testing.expectEqual('selectionchange', lastEvent.type);
|
||||||
testing.expectEqual(input, lastEvent.target);
|
testing.expectEqual(input, lastEvent.target);
|
||||||
@@ -247,7 +247,7 @@
|
|||||||
|
|
||||||
input.select();
|
input.select();
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, eventCount);
|
testing.expectEqual(1, eventCount);
|
||||||
testing.expectEqual('select', lastEvent.type);
|
testing.expectEqual('select', lastEvent.type);
|
||||||
testing.expectEqual(input, lastEvent.target);
|
testing.expectEqual(input, lastEvent.target);
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
link.rel = 'stylesheet';
|
link.rel = 'stylesheet';
|
||||||
link.addEventListener('load', () => { fired = true; });
|
link.addEventListener('load', () => { fired = true; });
|
||||||
document.head.appendChild(link);
|
document.head.appendChild(link);
|
||||||
testing.eventually(() => testing.expectEqual(false, fired));
|
testing.onload(() => testing.expectEqual(false, fired));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
link.href = 'https://lightpanda.io/opensource-browser/15';
|
link.href = 'https://lightpanda.io/opensource-browser/15';
|
||||||
link.addEventListener('load', () => { fired = true; });
|
link.addEventListener('load', () => { fired = true; });
|
||||||
document.head.appendChild(link);
|
document.head.appendChild(link);
|
||||||
testing.eventually(() => testing.expectEqual(false, fired));
|
testing.onload(() => testing.expectEqual(false, fired));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
// then set href.
|
// then set href.
|
||||||
link.href = 'https://lightpanda.io/opensource-browser/15';
|
link.href = 'https://lightpanda.io/opensource-browser/15';
|
||||||
|
|
||||||
testing.eventually(() => testing.expectEqual(true, result));
|
testing.onload(() => testing.expectEqual(true, result));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
results.forEach((r) => {
|
results.forEach((r) => {
|
||||||
testing.expectEqual(true, r);
|
testing.expectEqual(true, r);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -236,9 +236,11 @@
|
|||||||
{
|
{
|
||||||
const audio = document.createElement('audio');
|
const audio = document.createElement('audio');
|
||||||
testing.expectEqual('', audio.src);
|
testing.expectEqual('', audio.src);
|
||||||
|
testing.expectEqual('', audio.currentSrc);
|
||||||
|
|
||||||
audio.src = 'test.mp3';
|
audio.src = 'test.mp3';
|
||||||
testing.expectEqual(testing.BASE_URL + 'element/html/test.mp3', audio.src);
|
testing.expectEqual(testing.BASE_URL + 'element/html/test.mp3', audio.src);
|
||||||
|
testing.expectEqual(testing.BASE_URL + 'element/html/test.mp3', audio.currentSrc);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,14 @@
|
|||||||
script1.async = false;
|
script1.async = false;
|
||||||
script1.src = "dynamic1.js";
|
script1.src = "dynamic1.js";
|
||||||
document.getElementsByTagName('head')[0].appendChild(script1);
|
document.getElementsByTagName('head')[0].appendChild(script1);
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, loaded1);
|
testing.expectEqual(1, loaded1);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=no_double_execute>
|
<script id=no_double_execute>
|
||||||
document.getElementsByTagName('head')[0].appendChild(script1);
|
document.getElementsByTagName('head')[0].appendChild(script1);
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, loaded1);
|
testing.expectEqual(1, loaded1);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
const script2a = document.createElement('script');
|
const script2a = document.createElement('script');
|
||||||
script2a.src = "dynamic2.js";
|
script2a.src = "dynamic2.js";
|
||||||
document.getElementsByTagName('head')[0].appendChild(script2a);
|
document.getElementsByTagName('head')[0].appendChild(script2a);
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(2, loaded2);
|
testing.expectEqual(2, loaded2);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=src_after_append>
|
<script id=src_after_append>
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(2, loaded2);
|
testing.expectEqual(2, loaded2);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
s6.type = 'module';
|
s6.type = 'module';
|
||||||
s6.textContent = 'window.module_executed = true;';
|
s6.textContent = 'window.module_executed = true;';
|
||||||
document.head.appendChild(s6);
|
document.head.appendChild(s6);
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectTrue(window.module_executed);
|
testing.expectTrue(window.module_executed);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
testing.expectEqual(testing.BASE_URL + 'element/html/script/empty.js', s.src);
|
testing.expectEqual(testing.BASE_URL + 'element/html/script/empty.js', s.src);
|
||||||
document.head.appendChild(s);
|
document.head.appendChild(s);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(true, dom_load);
|
testing.expectEqual(true, dom_load);
|
||||||
testing.expectEqual(true, attribute_load);
|
testing.expectEqual(true, attribute_load);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -427,7 +427,7 @@
|
|||||||
div.setAttribute('slot', 'content');
|
div.setAttribute('slot', 'content');
|
||||||
host.appendChild(div);
|
host.appendChild(div);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, calls);
|
testing.expectEqual(1, calls);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -455,7 +455,7 @@
|
|||||||
|
|
||||||
div.setAttribute('slot', 'other');
|
div.setAttribute('slot', 'other');
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, calls);
|
testing.expectEqual(1, calls);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -483,7 +483,7 @@
|
|||||||
|
|
||||||
div.remove();
|
div.remove();
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, calls);
|
testing.expectEqual(1, calls);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -511,7 +511,7 @@
|
|||||||
|
|
||||||
div.slot = 'other';
|
div.slot = 'other';
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, calls);
|
testing.expectEqual(1, calls);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,6 +128,20 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
testing.eventually(() => testing.expectEqual(true, result));
|
testing.onload(() => testing.expectEqual(true, result));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="style-tag-content-parsing">
|
||||||
|
{
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.textContent = '.content-test { padding: 5px; }';
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
const sheet = style.sheet;
|
||||||
|
testing.expectTrue(sheet instanceof CSSStyleSheet);
|
||||||
|
testing.expectEqual(1, sheet.cssRules.length);
|
||||||
|
testing.expectEqual('.content-test', sheet.cssRules[0].selectorText);
|
||||||
|
testing.expectEqual('5px', sheet.cssRules[0].style.padding);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -256,7 +256,7 @@
|
|||||||
|
|
||||||
textarea.select();
|
textarea.select();
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, eventCount);
|
testing.expectEqual(1, eventCount);
|
||||||
testing.expectEqual('select', lastEvent.type);
|
testing.expectEqual('select', lastEvent.type);
|
||||||
testing.expectEqual(textarea, lastEvent.target);
|
testing.expectEqual(textarea, lastEvent.target);
|
||||||
@@ -295,7 +295,7 @@
|
|||||||
});
|
});
|
||||||
textarea.setSelectionRange(1, 4);
|
textarea.setSelectionRange(1, 4);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(5, eventCount);
|
testing.expectEqual(5, eventCount);
|
||||||
testing.expectEqual('selectionchange', lastEvent.type);
|
testing.expectEqual('selectionchange', lastEvent.type);
|
||||||
testing.expectEqual(textarea, lastEvent.target);
|
testing.expectEqual(textarea, lastEvent.target);
|
||||||
|
|||||||
139
src/browser/tests/element/replace_children.html
Normal file
139
src/browser/tests/element/replace_children.html
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>element.replaceChildren Tests</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="test">Original content</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script id=error_replace_with_self>
|
||||||
|
{
|
||||||
|
// Test that element.replaceChildren(element) throws HierarchyRequestError
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
testing.expectError('HierarchyRequest', () => {
|
||||||
|
doc.body.replaceChildren(doc.body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=error_replace_with_ancestor>
|
||||||
|
{
|
||||||
|
// Test that replacing with an ancestor throws HierarchyRequestError
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const child = doc.createElement('div');
|
||||||
|
doc.body.appendChild(child);
|
||||||
|
|
||||||
|
testing.expectError('HierarchyRequest', () => {
|
||||||
|
child.replaceChildren(doc.body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_children_basic>
|
||||||
|
{
|
||||||
|
// Test basic element.replaceChildren
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const child1 = doc.createElement('div');
|
||||||
|
const child2 = doc.createElement('span');
|
||||||
|
doc.body.appendChild(child1);
|
||||||
|
|
||||||
|
doc.body.replaceChildren(child2);
|
||||||
|
|
||||||
|
testing.expectEqual(1, doc.body.childNodes.length);
|
||||||
|
testing.expectEqual(child2, doc.body.firstChild);
|
||||||
|
testing.expectEqual(null, child1.parentNode);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_children_empty>
|
||||||
|
{
|
||||||
|
// Test element.replaceChildren with no arguments removes all children
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
doc.body.appendChild(doc.createElement('div'));
|
||||||
|
doc.body.appendChild(doc.createElement('span'));
|
||||||
|
|
||||||
|
doc.body.replaceChildren();
|
||||||
|
|
||||||
|
testing.expectEqual(0, doc.body.childNodes.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_children_fragment>
|
||||||
|
{
|
||||||
|
// Test element.replaceChildren with DocumentFragment
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const frag = doc.createDocumentFragment();
|
||||||
|
frag.appendChild(doc.createElement('div'));
|
||||||
|
frag.appendChild(doc.createElement('span'));
|
||||||
|
|
||||||
|
doc.body.replaceChildren(frag);
|
||||||
|
|
||||||
|
testing.expectEqual(2, doc.body.childNodes.length);
|
||||||
|
testing.expectEqual('DIV', doc.body.firstChild.tagName);
|
||||||
|
testing.expectEqual('SPAN', doc.body.lastChild.tagName);
|
||||||
|
testing.expectEqual(0, frag.childNodes.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=error_fragment_replace_with_self>
|
||||||
|
{
|
||||||
|
// Test that replacing with a fragment containing self throws
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const frag = doc.createDocumentFragment();
|
||||||
|
const child = doc.createElement('div');
|
||||||
|
frag.appendChild(child);
|
||||||
|
|
||||||
|
testing.expectError('HierarchyRequest', () => {
|
||||||
|
child.replaceChildren(frag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_children_text>
|
||||||
|
{
|
||||||
|
// Test element.replaceChildren with text
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
doc.body.appendChild(doc.createElement('div'));
|
||||||
|
|
||||||
|
doc.body.replaceChildren('Hello', 'World');
|
||||||
|
|
||||||
|
testing.expectEqual(2, doc.body.childNodes.length);
|
||||||
|
testing.expectEqual('Hello', doc.body.firstChild.textContent);
|
||||||
|
testing.expectEqual('World', doc.body.lastChild.textContent);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_children_mixed>
|
||||||
|
{
|
||||||
|
// Test element.replaceChildren with mixed nodes and text
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const span = doc.createElement('span');
|
||||||
|
span.textContent = 'middle';
|
||||||
|
|
||||||
|
doc.body.replaceChildren('start', span, 'end');
|
||||||
|
|
||||||
|
testing.expectEqual(3, doc.body.childNodes.length);
|
||||||
|
testing.expectEqual('start', doc.body.childNodes[0].textContent);
|
||||||
|
testing.expectEqual('SPAN', doc.body.childNodes[1].tagName);
|
||||||
|
testing.expectEqual('end', doc.body.childNodes[2].textContent);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_children_reparents>
|
||||||
|
{
|
||||||
|
// Test that replaceChildren properly reparents nodes from another parent
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const div1 = doc.createElement('div');
|
||||||
|
const div2 = doc.createElement('div');
|
||||||
|
const child = doc.createElement('span');
|
||||||
|
|
||||||
|
div1.appendChild(child);
|
||||||
|
testing.expectEqual(div1, child.parentNode);
|
||||||
|
|
||||||
|
div2.replaceChildren(child);
|
||||||
|
testing.expectEqual(div2, child.parentNode);
|
||||||
|
testing.expectEqual(0, div1.childNodes.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
|
|
||||||
<script id=abortsignal_timeout>
|
<script id=abortsignal_timeout>
|
||||||
var s3 = AbortSignal.timeout(10);
|
var s3 = AbortSignal.timeout(10);
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(true, s3.aborted);
|
testing.expectEqual(true, s3.aborted);
|
||||||
testing.expectEqual('TimeoutError', s3.reason);
|
testing.expectEqual('TimeoutError', s3.reason);
|
||||||
testing.expectError('Error: TimeoutError', () => {
|
testing.expectError('Error: TimeoutError', () => {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
|
|
||||||
window.postMessage('test data', '*');
|
window.postMessage('test data', '*');
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual('test data', receivedEvent.data);
|
testing.expectEqual('test data', receivedEvent.data);
|
||||||
testing.expectEqual(window, receivedEvent.source);
|
testing.expectEqual(window, receivedEvent.source);
|
||||||
testing.expectEqual('message', receivedEvent.type);
|
testing.expectEqual('message', receivedEvent.type);
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
const testObj = { type: 'test', value: 123, nested: { key: 'value' } };
|
const testObj = { type: 'test', value: 123, nested: { key: 'value' } };
|
||||||
window.postMessage(testObj, '*');
|
window.postMessage(testObj, '*');
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(testObj, receivedData);
|
testing.expectEqual(testObj, receivedData);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
|
|
||||||
window.postMessage(42, '*');
|
window.postMessage(42, '*');
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(42, received);
|
testing.expectEqual(42, received);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
const arr = [1, 2, 3, 'test'];
|
const arr = [1, 2, 3, 'test'];
|
||||||
window.postMessage(arr, '*');
|
window.postMessage(arr, '*');
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(arr, received);
|
testing.expectEqual(arr, received);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
|
|
||||||
window.postMessage(null, '*');
|
window.postMessage(null, '*');
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(null, received);
|
testing.expectEqual(null, received);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@
|
|||||||
|
|
||||||
window.postMessage('test', '*');
|
window.postMessage('test', '*');
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual('http://127.0.0.1:9582', receivedOrigin);
|
testing.expectEqual('http://127.0.0.1:9582', receivedOrigin);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
window.postMessage('trigger', '*');
|
window.postMessage('trigger', '*');
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(2, count);
|
testing.expectEqual(2, count);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
$('#f2').src = 'support/sub2.html';
|
$('#f2').src = 'support/sub2.html';
|
||||||
testing.expectEqual(true, true);
|
testing.expectEqual(true, true);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(undefined, window[20]);
|
testing.expectEqual(undefined, window[20]);
|
||||||
|
|
||||||
testing.expectEqual(window, window[1].top);
|
testing.expectEqual(window, window[1].top);
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
f3.src = 'invalid'; // still fires load!
|
f3.src = 'invalid'; // still fires load!
|
||||||
document.documentElement.appendChild(f3);
|
document.documentElement.appendChild(f3);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual('f1_onload_loaded', window.f1_onload);
|
testing.expectEqual('f1_onload_loaded', window.f1_onload);
|
||||||
testing.expectEqual(true, f3_load_event);
|
testing.expectEqual(true, f3_load_event);
|
||||||
});
|
});
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
f4.src = "about:blank";
|
f4.src = "about:blank";
|
||||||
document.documentElement.appendChild(f4);
|
document.documentElement.appendChild(f4);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual("<html><head></head><body></body></html>", f4.contentDocument.documentElement.outerHTML);
|
testing.expectEqual("<html><head></head><body></body></html>", f4.contentDocument.documentElement.outerHTML);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -112,35 +112,47 @@
|
|||||||
document.documentElement.appendChild(f5);
|
document.documentElement.appendChild(f5);
|
||||||
f5.src = "about:blank";
|
f5.src = "about:blank";
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual("<html><head></head><body></body></html>", f5.contentDocument.documentElement.outerHTML);
|
testing.expectEqual("<html><head></head><body></body></html>", f5.contentDocument.documentElement.outerHTML);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=link_click>
|
<script id=link_click type=module>
|
||||||
testing.async(async (restore) => {
|
const state = await testing.async();
|
||||||
await new Promise((resolve) => {
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
let f6 = document.createElement('iframe');
|
let f6 = document.createElement('iframe');
|
||||||
f6.id = 'f6';
|
f6.id = 'f6';
|
||||||
f6.addEventListener('load', () => {
|
f6.addEventListener('load', () => {
|
||||||
if (++count == 2) {
|
if (++count == 2) {
|
||||||
resolve();
|
state.resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
f6.contentDocument.querySelector('#link').click();
|
f6.contentDocument.querySelector('#link').click();
|
||||||
});
|
});
|
||||||
f6.src = "support/with_link.html";
|
|
||||||
document.documentElement.appendChild(f6);
|
f6.src = 'support/with_link.html';
|
||||||
});
|
document.documentElement.appendChild(f6);
|
||||||
restore();
|
|
||||||
|
await state.done(() => {
|
||||||
testing.expectEqual("<html><head></head><body>It was clicked!\n</body></html>", f6.contentDocument.documentElement.outerHTML);
|
testing.expectEqual("<html><head></head><body>It was clicked!\n</body></html>", f6.contentDocument.documentElement.outerHTML);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=about_blank_nav>
|
||||||
|
{
|
||||||
|
let i = document.createElement('iframe');
|
||||||
|
document.documentElement.appendChild(i);
|
||||||
|
i.contentWindow.location.href = 'support/page.html';
|
||||||
|
testing.onload(() => {
|
||||||
|
testing.expectEqual('<html><head></head><body>a-page\n</body></html>', i.contentDocument.documentElement.outerHTML);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id=count>
|
<script id=count>
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(8, window.length);
|
testing.expectEqual(9, window.length);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
{
|
{
|
||||||
let reply = null;
|
let reply = null;
|
||||||
window.addEventListener('message', (e) => {
|
window.addEventListener('message', (e) => {
|
||||||
console.warn('reply')
|
|
||||||
reply = e.data;
|
reply = e.data;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -17,7 +16,7 @@
|
|||||||
iframe.contentWindow.postMessage('ping', '*');
|
iframe.contentWindow.postMessage('ping', '*');
|
||||||
});
|
});
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual('pong', reply.data);
|
testing.expectEqual('pong', reply.data);
|
||||||
testing.expectEqual(testing.ORIGIN, reply.origin);
|
testing.expectEqual(testing.ORIGIN, reply.origin);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener('message', (e) => {
|
window.addEventListener('message', (e) => {
|
||||||
console.warn('Frame Message', e.data);
|
|
||||||
if (e.data === 'ping') {
|
if (e.data === 'ping') {
|
||||||
window.top.postMessage({data: 'pong', origin: e.origin}, '*');
|
window.top.postMessage({data: 'pong', origin: e.origin}, '*');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<a id=l1 target=f1 href=support/page.html></a>
|
<a id=l1 target=f1 href=support/page.html></a>
|
||||||
<script id=anchor>
|
<script id=anchor>
|
||||||
$('#l1').click();
|
$('#l1').click();
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual('<html><head></head><body>a-page\n</body></html>', $('#frame1').contentDocument.documentElement.outerHTML);
|
testing.expectEqual('<html><head></head><body>a-page\n</body></html>', $('#frame1').contentDocument.documentElement.outerHTML);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
form.action = 'support/page.html';
|
form.action = 'support/page.html';
|
||||||
form.submit();
|
form.submit();
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual('<html><head></head><body>a-page\n</body></html>', frame2.contentDocument.documentElement.outerHTML);
|
testing.expectEqual('<html><head></head><body>a-page\n</body></html>', frame2.contentDocument.documentElement.outerHTML);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<script id=formtarget>
|
<script id=formtarget>
|
||||||
{
|
{
|
||||||
$('#submit1').click();
|
$('#submit1').click();
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual('<html><head></head><body>a-page\n</body></html>', $('#f3').contentDocument.documentElement.outerHTML);
|
testing.expectEqual('<html><head></head><body>a-page\n</body></html>', $('#f3').contentDocument.documentElement.outerHTML);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
// If support/history.html has a failed assertion, it'll log the error and
|
// If support/history.html has a failed assertion, it'll log the error and
|
||||||
// stop the script. If it succeeds, it'll set support_history_completed
|
// stop the script. If it succeeds, it'll set support_history_completed
|
||||||
// which we can use here to assume everything passed.
|
// which we can use here to assume everything passed.
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(true, window.support_history_completed);
|
testing.expectEqual(true, window.support_history_completed);
|
||||||
testing.expectEqual(true, window.support_history_popstateEventFired);
|
testing.expectEqual(true, window.support_history_popstateEventFired);
|
||||||
testing.expectEqual({testInProgress: true }, window.support_history_popstateEventState);
|
testing.expectEqual({testInProgress: true }, window.support_history_popstateEventState);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
observer.observe(target);
|
observer.observe(target);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(true, callbackCalled);
|
testing.expectEqual(true, callbackCalled);
|
||||||
testing.expectEqual(1, entries.length);
|
testing.expectEqual(1, entries.length);
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
count += 1;
|
count += 1;
|
||||||
}).observe(div);
|
}).observe(div);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(0, count);
|
testing.expectEqual(0, count);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
}).observe(div1);
|
}).observe(div1);
|
||||||
|
|
||||||
div2.appendChild(div1);
|
div2.appendChild(div1);
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, count);
|
testing.expectEqual(1, count);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
observer.observe(target);
|
observer.observe(target);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, callCount);
|
testing.expectEqual(1, callCount);
|
||||||
|
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
const observer2 = new IntersectionObserver(() => {});
|
const observer2 = new IntersectionObserver(() => {});
|
||||||
observer2.observe(target);
|
observer2.observe(target);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
observer2.disconnect();
|
observer2.disconnect();
|
||||||
testing.expectEqual(1, callCount);
|
testing.expectEqual(1, callCount);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
observer.observe(target1);
|
observer.observe(target1);
|
||||||
observer.observe(target2);
|
observer.observe(target2);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(2, entryCount);
|
testing.expectEqual(2, entryCount);
|
||||||
testing.expectTrue(seenTargets.has(target1));
|
testing.expectTrue(seenTargets.has(target1));
|
||||||
testing.expectTrue(seenTargets.has(target2));
|
testing.expectTrue(seenTargets.has(target2));
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
observer.unobserve(target1);
|
observer.unobserve(target1);
|
||||||
observer.observe(target2);
|
observer.observe(target2);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
// Should only see target2, not target1
|
// Should only see target2, not target1
|
||||||
testing.expectEqual(1, seenTargets.length);
|
testing.expectEqual(1, seenTargets.length);
|
||||||
testing.expectEqual(target2, seenTargets[0]);
|
testing.expectEqual(target2, seenTargets[0]);
|
||||||
|
|||||||
@@ -12,5 +12,5 @@
|
|||||||
|
|
||||||
let replaced = false;
|
let replaced = false;
|
||||||
css.replace('body{}').then(() => replaced = true);
|
css.replace('body{}').then(() => replaced = true);
|
||||||
testing.eventually(() => testing.expectEqual(true, replaced));
|
testing.onload(() => testing.expectEqual(true, replaced));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,5 +11,5 @@
|
|||||||
cb.push('finished');
|
cb.push('finished');
|
||||||
cb.push(x == a1);
|
cb.push(x == a1);
|
||||||
});
|
});
|
||||||
testing.eventually(() => testing.expectEqual(['finished', true], cb));
|
testing.onload(() => testing.expectEqual(['finished', true], cb));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
count += 1;
|
count += 1;
|
||||||
}).observe(div);
|
}).observe(div);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(0, count);
|
testing.expectEqual(0, count);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
}).observe(div1);
|
}).observe(div1);
|
||||||
|
|
||||||
div2.appendChild(div1);
|
div2.appendChild(div1);
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, count);
|
testing.expectEqual(1, count);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
observer.observe(div1);
|
observer.observe(div1);
|
||||||
testing.expectEqual(0, count);
|
testing.expectEqual(0, count);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, count);
|
testing.expectEqual(1, count);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
testing.expectEqual(0, count);
|
testing.expectEqual(0, count);
|
||||||
|
|
||||||
observer.unobserve(div1);
|
observer.unobserve(div1);
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(0, count);
|
testing.expectEqual(0, count);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
testing.expectEqual(0, count);
|
testing.expectEqual(0, count);
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(0, count);
|
testing.expectEqual(0, count);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
document.body.appendChild(div1);
|
document.body.appendChild(div1);
|
||||||
new IntersectionObserver(entries => { entry = entries[0]; }).observe(div1);
|
new IntersectionObserver(entries => { entry = entries[0]; }).observe(div1);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(125, entry.boundingClientRect.x);
|
testing.expectEqual(125, entry.boundingClientRect.x);
|
||||||
testing.expectEqual(1, entry.intersectionRatio);
|
testing.expectEqual(1, entry.intersectionRatio);
|
||||||
testing.expectEqual(125, entry.intersectionRect.x);
|
testing.expectEqual(125, entry.intersectionRect.x);
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
observer.observe(div);
|
observer.observe(div);
|
||||||
capture.push('post-observe');
|
capture.push('post-observe');
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual([
|
testing.expectEqual([
|
||||||
'pre-append',
|
'pre-append',
|
||||||
'post-append',
|
'post-append',
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<script id=timeout>
|
<script id=timeout>
|
||||||
var s3 = AbortSignal.timeout(10);
|
var s3 = AbortSignal.timeout(10);
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(true, s3.aborted);
|
testing.expectEqual(true, s3.aborted);
|
||||||
testing.expectEqual('TimeoutError', s3.reason);
|
testing.expectEqual('TimeoutError', s3.reason);
|
||||||
testing.expectError('Error: TimeoutError', () => {
|
testing.expectError('Error: TimeoutError', () => {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
popstateEventState = event.state;
|
popstateEventState = event.state;
|
||||||
});
|
});
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(true, popstateEventFired);
|
testing.expectEqual(true, popstateEventFired);
|
||||||
testing.expectEqual(state, popstateEventState);
|
testing.expectEqual(state, popstateEventState);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
popstateEventState = event.state;
|
popstateEventState = event.state;
|
||||||
};
|
};
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(true, popstateEventFired);
|
testing.expectEqual(true, popstateEventFired);
|
||||||
testing.expectEqual(state, popstateEventState);
|
testing.expectEqual(state, popstateEventState);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,5 +24,5 @@
|
|||||||
// inline script should ignore defer and async attributes. If we don't do
|
// inline script should ignore defer and async attributes. If we don't do
|
||||||
// this correctly, we'd end up in an infinite loop
|
// this correctly, we'd end up in an infinite loop
|
||||||
// https://github.com/lightpanda-io/browser/issues/1014
|
// https://github.com/lightpanda-io/browser/issues/1014
|
||||||
testing.eventually(() => testing.expectEqual(2, dyn1_loaded));
|
testing.onload(() => testing.expectEqual(2, dyn1_loaded));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
lp.appendChild(div);
|
lp.appendChild(div);
|
||||||
testing.expectEqual(slot, div.assignedSlot);
|
testing.expectEqual(slot, div.assignedSlot);
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, calls)
|
testing.expectEqual(1, calls)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
|
|
||||||
const div = $('#s2');
|
const div = $('#s2');
|
||||||
div.removeAttribute('slot');
|
div.removeAttribute('slot');
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, calls)
|
testing.expectEqual(1, calls)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
|
|
||||||
const div = $('#s3');
|
const div = $('#s3');
|
||||||
div.slot = 'other';
|
div.slot = 'other';
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, calls)
|
testing.expectEqual(1, calls)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
div.slot = 'other';
|
div.slot = 'other';
|
||||||
lp.appendChild(div);
|
lp.appendChild(div);
|
||||||
div.slot = 'slot-1'
|
div.slot = 'slot-1'
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, calls)
|
testing.expectEqual(1, calls)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#s5').remove();
|
$('#s5').remove();
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, calls)
|
testing.expectEqual(1, calls)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
start = timestamp;
|
start = timestamp;
|
||||||
}
|
}
|
||||||
requestAnimationFrame(step);
|
requestAnimationFrame(step);
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(true, start > 0)
|
testing.expectEqual(true, start > 0)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,23 +24,23 @@
|
|||||||
start = 0;
|
start = 0;
|
||||||
});
|
});
|
||||||
cancelAnimationFrame(request_id);
|
cancelAnimationFrame(request_id);
|
||||||
testing.eventually(() => testing.expectEqual(true, start > 0));
|
testing.onload(() => testing.expectEqual(true, start > 0));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=setTimeout>
|
<script id=setTimeout>
|
||||||
let longCall = false;
|
let longCall = false;
|
||||||
window.setTimeout(() => {longCall = true}, 5001);
|
window.setTimeout(() => {longCall = true}, 5001);
|
||||||
testing.eventually(() => testing.expectEqual(false, longCall));
|
testing.onload(() => testing.expectEqual(false, longCall));
|
||||||
|
|
||||||
let wst1 = 0;
|
let wst1 = 0;
|
||||||
window.setTimeout(() => {wst1 += 1}, 1);
|
window.setTimeout(() => {wst1 += 1}, 1);
|
||||||
testing.eventually(() => testing.expectEqual(1, wst1));
|
testing.onload(() => testing.expectEqual(1, wst1));
|
||||||
|
|
||||||
let wst2 = 1;
|
let wst2 = 1;
|
||||||
window.setTimeout((a, b) => {
|
window.setTimeout((a, b) => {
|
||||||
wst2 = a + b;
|
wst2 = a + b;
|
||||||
}, 1, 2, 3);
|
}, 1, 2, 3);
|
||||||
testing.eventually(() => testing.expectEqual(5, wst2));
|
testing.onload(() => testing.expectEqual(5, wst2));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=eventTarget>
|
<script id=eventTarget>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
<script id=queueMicroTask>
|
<script id=queueMicroTask>
|
||||||
var qm = false;
|
var qm = false;
|
||||||
window.queueMicrotask(() => {qm = true });
|
window.queueMicrotask(() => {qm = true });
|
||||||
testing.eventually(() => testing.expectEqual(true, qm));
|
testing.onload(() => testing.expectEqual(true, qm));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=DOMContentLoaded>
|
<script id=DOMContentLoaded>
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
window.addEventListener('DOMContentLoaded', (e) => {
|
window.addEventListener('DOMContentLoaded', (e) => {
|
||||||
dcl = e.target == document;
|
dcl = e.target == document;
|
||||||
});
|
});
|
||||||
testing.eventually(() => testing.expectEqual(true, dcl));
|
testing.onload(() => testing.expectEqual(true, dcl));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=window.onload>
|
<script id=window.onload>
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
window.onload = callback;
|
window.onload = callback;
|
||||||
testing.expectEqual(callback, window.onload);
|
testing.expectEqual(callback, window.onload);
|
||||||
|
|
||||||
testing.eventually(() => testing.expectEqual(true, isDocumentTarget));
|
testing.onload(() => testing.expectEqual(true, isDocumentTarget));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=reportError>
|
<script id=reportError>
|
||||||
|
|||||||
14
src/browser/tests/mcp_wait_for_selector.html
Normal file
14
src/browser/tests/mcp_wait_for_selector.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<div id="existing">Already here</div>
|
||||||
|
<script>
|
||||||
|
setTimeout(function() {
|
||||||
|
var el = document.createElement("div");
|
||||||
|
el.id = "delayed";
|
||||||
|
el.textContent = "Appeared after delay";
|
||||||
|
document.body.appendChild(el);
|
||||||
|
}, 20);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -734,3 +734,101 @@
|
|||||||
testing.expectEqual([['field', 'data'], ['x', '0'], ['y', '0']], entries);
|
testing.expectEqual([['field', 'data'], ['x', '0'], ['y', '0']], entries);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=formDataEventFires>
|
||||||
|
{
|
||||||
|
// formdata event fires on the form when FormData is constructed with a form
|
||||||
|
const form = document.createElement('form');
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.name = 'field';
|
||||||
|
input.value = 'hello';
|
||||||
|
form.appendChild(input);
|
||||||
|
|
||||||
|
let eventFired = false;
|
||||||
|
let receivedFormData = null;
|
||||||
|
|
||||||
|
form.addEventListener('formdata', (e) => {
|
||||||
|
eventFired = true;
|
||||||
|
receivedFormData = e.formData;
|
||||||
|
});
|
||||||
|
|
||||||
|
const fd = new FormData(form);
|
||||||
|
testing.expectEqual(true, eventFired);
|
||||||
|
testing.expectEqual(fd, receivedFormData);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=formDataEventNotFiredWithoutForm>
|
||||||
|
{
|
||||||
|
// formdata event should NOT fire when FormData is constructed without a form
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('a', '1');
|
||||||
|
testing.expectEqual('1', fd.get('a'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=formDataEventBubbles>
|
||||||
|
{
|
||||||
|
// formdata event should bubble
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const form = document.createElement('form');
|
||||||
|
container.appendChild(form);
|
||||||
|
document.body.appendChild(container);
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.name = 'x';
|
||||||
|
input.value = '1';
|
||||||
|
form.appendChild(input);
|
||||||
|
|
||||||
|
let bubbled = false;
|
||||||
|
container.addEventListener('formdata', () => {
|
||||||
|
bubbled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const fd = new FormData(form);
|
||||||
|
testing.expectEqual(true, bubbled);
|
||||||
|
|
||||||
|
document.body.removeChild(container);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=formDataEventNotCancelable>
|
||||||
|
{
|
||||||
|
// formdata event should not be cancelable
|
||||||
|
const form = document.createElement('form');
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.name = 'key';
|
||||||
|
input.value = 'val';
|
||||||
|
form.appendChild(input);
|
||||||
|
|
||||||
|
let cancelable = null;
|
||||||
|
form.addEventListener('formdata', (e) => {
|
||||||
|
cancelable = e.cancelable;
|
||||||
|
});
|
||||||
|
|
||||||
|
const fd = new FormData(form);
|
||||||
|
testing.expectEqual(false, cancelable);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=formDataEventModifyFormData>
|
||||||
|
{
|
||||||
|
// Listeners can modify formData during the event
|
||||||
|
const form = document.createElement('form');
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.name = 'original';
|
||||||
|
input.value = 'data';
|
||||||
|
form.appendChild(input);
|
||||||
|
|
||||||
|
form.addEventListener('formdata', (e) => {
|
||||||
|
e.formData.append('added', 'by-listener');
|
||||||
|
});
|
||||||
|
|
||||||
|
const fd = new FormData(form);
|
||||||
|
testing.expectEqual('data', fd.get('original'));
|
||||||
|
testing.expectEqual('by-listener', fd.get('added'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -28,3 +28,40 @@
|
|||||||
d1.appendChild(p2);
|
d1.appendChild(p2);
|
||||||
assertChildren(['p1', 'p2'], d1);
|
assertChildren(['p1', 'p2'], d1);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div id=d3></div>
|
||||||
|
<script id=appendChild_fragment_mutation>
|
||||||
|
// Test that appendChild with DocumentFragment handles synchronous callbacks
|
||||||
|
// (like custom element connectedCallback) that modify the fragment during iteration.
|
||||||
|
// This reproduces a bug where the iterator captures "next" node pointers
|
||||||
|
// before processing, but callbacks can remove those nodes from the fragment.
|
||||||
|
const d3 = $('#d3');
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
|
// Create custom element whose connectedCallback modifies the fragment
|
||||||
|
let bElement = null;
|
||||||
|
class ModifyingElement extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
// When this element is connected, remove 'b' from the fragment
|
||||||
|
if (bElement && bElement.parentNode === fragment) {
|
||||||
|
fragment.removeChild(bElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('modifying-element', ModifyingElement);
|
||||||
|
|
||||||
|
const a = document.createElement('modifying-element');
|
||||||
|
a.id = 'a';
|
||||||
|
const b = document.createElement('span');
|
||||||
|
b.id = 'b';
|
||||||
|
bElement = b;
|
||||||
|
fragment.appendChild(a);
|
||||||
|
fragment.appendChild(b);
|
||||||
|
|
||||||
|
// This should not crash - appendChild should handle the modification gracefully
|
||||||
|
d3.appendChild(fragment);
|
||||||
|
|
||||||
|
// 'a' should be in d3, 'b' was removed by connectedCallback and is now detached
|
||||||
|
assertChildren(['a'], d3);
|
||||||
|
testing.expectEqual(null, b.parentNode);
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
iframe.src = blob_url;
|
iframe.src = blob_url;
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual('Hello Blob', iframe.contentDocument.getElementById('test').textContent);
|
testing.expectEqual('Hello Blob', iframe.contentDocument.getElementById('test').textContent);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
document.body.appendChild(iframe2);
|
document.body.appendChild(iframe2);
|
||||||
iframe2.src = url2;
|
iframe2.src = url2;
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual('First', iframe1.contentDocument.body.textContent);
|
testing.expectEqual('First', iframe1.contentDocument.body.textContent);
|
||||||
testing.expectEqual('Second', iframe2.contentDocument.body.textContent);
|
testing.expectEqual('Second', iframe2.contentDocument.body.textContent);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
call1 = true;
|
call1 = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(document, ex1.target);
|
testing.expectEqual(document, ex1.target);
|
||||||
testing.expectEqual('DOMContentLoaded', ex1.type);
|
testing.expectEqual('DOMContentLoaded', ex1.type);
|
||||||
testing.expectEqual(true, call1);
|
testing.expectEqual(true, call1);
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
// With buffered: true, existing marks should be delivered
|
// With buffered: true, existing marks should be delivered
|
||||||
observer.observe({ type: "mark", buffered: true });
|
observer.observe({ type: "mark", buffered: true });
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(true, receivedEntries !== null);
|
testing.expectEqual(true, receivedEntries !== null);
|
||||||
testing.expectEqual(2, receivedEntries.length);
|
testing.expectEqual(2, receivedEntries.length);
|
||||||
testing.expectEqual("early1", receivedEntries[0].name);
|
testing.expectEqual("early1", receivedEntries[0].name);
|
||||||
|
|||||||
@@ -582,7 +582,7 @@
|
|||||||
document.removeEventListener('selectionchange', listener);
|
document.removeEventListener('selectionchange', listener);
|
||||||
textNode.textContent = "The quick brown fox";
|
textNode.textContent = "The quick brown fox";
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(14, eventCount);
|
testing.expectEqual(14, eventCount);
|
||||||
testing.expectEqual('selectionchange', lastEvent.type);
|
testing.expectEqual('selectionchange', lastEvent.type);
|
||||||
testing.expectEqual(document, lastEvent.target);
|
testing.expectEqual(document, lastEvent.target);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
let eventuallies = [];
|
let eventuallies = [];
|
||||||
let async_capture = null;
|
let async_capture = null;
|
||||||
let current_script_id = null;
|
let current_script_id = null;
|
||||||
|
let async_pending = 0;
|
||||||
|
|
||||||
function expectTrue(actual) {
|
function expectTrue(actual) {
|
||||||
expectEqual(true, actual);
|
expectEqual(true, actual);
|
||||||
@@ -52,10 +53,10 @@
|
|||||||
throw new Error('no error');
|
throw new Error('no error');
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventually(cb) {
|
function onload(cb) {
|
||||||
const script_id = _currentScriptId();
|
const script_id = _currentScriptId();
|
||||||
if (!script_id) {
|
if (!script_id) {
|
||||||
throw new Error('testing.eventually called outside of a script');
|
throw new Error('testing.onload called outside of a script');
|
||||||
}
|
}
|
||||||
eventuallies.push({
|
eventuallies.push({
|
||||||
callback: cb,
|
callback: cb,
|
||||||
@@ -64,6 +65,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function async(cb) {
|
async function async(cb) {
|
||||||
|
if (cb == undefined) {
|
||||||
|
let resolve = null
|
||||||
|
const promise = new Promise((r) => { resolve = r});
|
||||||
|
async_pending += 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
promise: promise,
|
||||||
|
resolve: resolve,
|
||||||
|
capture: {script_id: document.currentScript.id, stack: new Error().stack},
|
||||||
|
done: async function(cb) {
|
||||||
|
await this.promise;
|
||||||
|
async_pending -= 1;
|
||||||
|
async_capture = this.capture;
|
||||||
|
cb();
|
||||||
|
async_capture = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let capture = {script_id: document.currentScript.id, stack: new Error().stack};
|
let capture = {script_id: document.currentScript.id, stack: new Error().stack};
|
||||||
await cb(() => { async_capture = capture; });
|
await cb(() => { async_capture = capture; });
|
||||||
async_capture = null;
|
async_capture = null;
|
||||||
@@ -74,6 +94,10 @@
|
|||||||
throw new Error('Failed');
|
throw new Error('Failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (async_pending > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
for (let e of eventuallies) {
|
for (let e of eventuallies) {
|
||||||
current_script_id = e.script_id;
|
current_script_id = e.script_id;
|
||||||
e.callback();
|
e.callback();
|
||||||
@@ -97,6 +121,8 @@
|
|||||||
throw new Error(`script id: '${script_id}' failed: ${status || 'no assertions'}`);
|
throw new Error(`script id: '${script_id}' failed: ${status || 'no assertions'}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IS_TEST_RUNNER = window.navigator.userAgent.startsWith("Lightpanda/");
|
const IS_TEST_RUNNER = window.navigator.userAgent.startsWith("Lightpanda/");
|
||||||
@@ -110,7 +136,7 @@
|
|||||||
expectEqual: expectEqual,
|
expectEqual: expectEqual,
|
||||||
expectError: expectError,
|
expectError: expectError,
|
||||||
withError: withError,
|
withError: withError,
|
||||||
eventually: eventually,
|
onload: onload,
|
||||||
IS_TEST_RUNNER: IS_TEST_RUNNER,
|
IS_TEST_RUNNER: IS_TEST_RUNNER,
|
||||||
HOST: '127.0.0.1',
|
HOST: '127.0.0.1',
|
||||||
ORIGIN: 'http://127.0.0.1:9582',
|
ORIGIN: 'http://127.0.0.1:9582',
|
||||||
|
|||||||
@@ -591,6 +591,35 @@
|
|||||||
testing.expectEqual('/new/path', url.pathname);
|
testing.expectEqual('/new/path', url.pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pathname setter must percent-encode spaces and special characters
|
||||||
|
{
|
||||||
|
const url = new URL('http://a/');
|
||||||
|
url.pathname = 'c d';
|
||||||
|
testing.expectEqual('http://a/c%20d', url.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://example.com/path');
|
||||||
|
url.pathname = '/path with spaces/file name';
|
||||||
|
testing.expectEqual('https://example.com/path%20with%20spaces/file%20name', url.href);
|
||||||
|
testing.expectEqual('/path%20with%20spaces/file%20name', url.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already-encoded sequences should not be double-encoded
|
||||||
|
{
|
||||||
|
const url = new URL('https://example.com/path');
|
||||||
|
url.pathname = '/already%20encoded';
|
||||||
|
testing.expectEqual('https://example.com/already%20encoded', url.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the exact check the URL polyfill uses to decide if native URL is sufficient
|
||||||
|
{
|
||||||
|
const url = new URL('b', 'http://a');
|
||||||
|
url.pathname = 'c d';
|
||||||
|
testing.expectEqual('http://a/c%20d', url.href);
|
||||||
|
testing.expectEqual(true, !!url.searchParams);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const url = new URL('https://example.com/path');
|
const url = new URL('https://example.com/path');
|
||||||
url.search = '?a=b';
|
url.search = '?a=b';
|
||||||
@@ -656,6 +685,20 @@
|
|||||||
testing.expectEqual('', url.hash);
|
testing.expectEqual('', url.hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://example.com/path');
|
||||||
|
url.hash = '#a b';
|
||||||
|
testing.expectEqual('https://example.com/path#a%20b', url.href);
|
||||||
|
testing.expectEqual('#a%20b', url.hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://example.com/path');
|
||||||
|
url.hash = 'a b';
|
||||||
|
testing.expectEqual('https://example.com/path#a%20b', url.href);
|
||||||
|
testing.expectEqual('#a%20b', url.hash);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const url = new URL('https://example.com/path?a=b');
|
const url = new URL('https://example.com/path?a=b');
|
||||||
url.search = '';
|
url.search = '';
|
||||||
@@ -673,6 +716,20 @@
|
|||||||
testing.expectEqual(null, url.searchParams.get('a'));
|
testing.expectEqual(null, url.searchParams.get('a'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://example.com/path?a=b');
|
||||||
|
const sp = url.searchParams;
|
||||||
|
testing.expectEqual('b', sp.get('a'));
|
||||||
|
url.search = 'c=d b';
|
||||||
|
|
||||||
|
testing.expectEqual('d b', url.searchParams.get('c'));
|
||||||
|
testing.expectEqual(null, url.searchParams.get('a'));
|
||||||
|
|
||||||
|
url.search = 'c d=d b';
|
||||||
|
testing.expectEqual('d b', url.searchParams.get('c d'));
|
||||||
|
testing.expectEqual(null, url.searchParams.get('c'));
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const url = new URL('https://example.com/path?a=b');
|
const url = new URL('https://example.com/path?a=b');
|
||||||
const sp = url.searchParams;
|
const sp = url.searchParams;
|
||||||
@@ -798,3 +855,19 @@
|
|||||||
testing.expectEqual(true, url2.startsWith('blob:'));
|
testing.expectEqual(true, url2.startsWith('blob:'));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="about:blank">
|
||||||
|
{
|
||||||
|
const url = new URL('about:blank');
|
||||||
|
testing.expectEqual('about:blank', url.href);
|
||||||
|
testing.expectEqual('null', url.origin);
|
||||||
|
testing.expectEqual('about:', url.protocol);
|
||||||
|
testing.expectEqual('blank', url.pathname);
|
||||||
|
testing.expectEqual('', url.username);
|
||||||
|
testing.expectEqual('', url.password);
|
||||||
|
testing.expectEqual('', url.host);
|
||||||
|
testing.expectEqual('', url.hostname);
|
||||||
|
testing.expectEqual('', url.port);
|
||||||
|
testing.expectEqual('', url.search);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
testing.expectEqual(window, e.currentTarget);
|
testing.expectEqual(window, e.currentTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, called);
|
testing.expectEqual(1, called);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// Verify: handler fires, "event" parameter is a proper Event, and handler is a function.
|
// Verify: handler fires, "event" parameter is a proper Event, and handler is a function.
|
||||||
let loadEvent = null;
|
let loadEvent = null;
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual("function", typeof document.body.onload);
|
testing.expectEqual("function", typeof document.body.onload);
|
||||||
testing.expectTrue(loadEvent instanceof Event);
|
testing.expectTrue(loadEvent instanceof Event);
|
||||||
testing.expectEqual("load", loadEvent.type);
|
testing.expectEqual("load", loadEvent.type);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user