mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-28 07:33:16 +00:00
Compare commits
229 Commits
docs/updat
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03ed45637a | ||
|
|
9068fe718e | ||
|
|
5369d25213 | ||
|
|
649d8d1024 | ||
|
|
15d60d845a | ||
|
|
c4b837b598 | ||
|
|
54391238c9 | ||
|
|
d33edc5697 | ||
|
|
16ca8d4b14 | ||
|
|
707ffb4893 | ||
|
|
4782b37216 | ||
|
|
ce197256dd | ||
|
|
e6d644998a | ||
|
|
67bd555e75 | ||
|
|
a10e533701 | ||
|
|
0065677273 | ||
|
|
226d9bfc6f | ||
|
|
2e65ae632e | ||
|
|
ea422075c7 | ||
|
|
1d54e6944b | ||
|
|
de32e5cf34 | ||
|
|
c8d8ca5e94 | ||
|
|
7f2139f612 | ||
|
|
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 | ||
|
|
2d91acbd14 | ||
|
|
88681b1fdb | ||
|
|
1feb121ba7 | ||
|
|
35cdc3c348 | ||
|
|
1353f76bf1 | ||
|
|
3e2be5b317 | ||
|
|
448eca0c32 | ||
|
|
5404ca723c | ||
|
|
e56ffe4b60 | ||
|
|
02d05ae464 | ||
|
|
a74e97854d | ||
|
|
6925fc3f70 | ||
|
|
84557cb4e6 | ||
|
|
f1293b7346 | ||
|
|
a4cb5031d1 | ||
|
|
f70865e174 | ||
|
|
38e9f86088 | ||
|
|
d9c5f56500 | ||
|
|
6c5733bba3 | ||
|
|
b8f1622b52 | ||
|
|
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={0}', github.ref_name) || '-Dversion=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
|
||||||
|
|||||||
17
README.md
17
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]
|
||||||
@@ -170,6 +170,7 @@ You may still encounter errors or crashes. Please open an issue with specifics i
|
|||||||
|
|
||||||
Here are the key features we have implemented:
|
Here are the key features we have implemented:
|
||||||
|
|
||||||
|
- [ ] CORS [#2015](https://github.com/lightpanda-io/browser/issues/2015)
|
||||||
- [x] HTTP loader ([Libcurl](https://curl.se/libcurl/))
|
- [x] HTTP loader ([Libcurl](https://curl.se/libcurl/))
|
||||||
- [x] HTML parser ([html5ever](https://github.com/servo/html5ever))
|
- [x] HTML parser ([html5ever](https://github.com/servo/html5ever))
|
||||||
- [x] DOM tree
|
- [x] DOM tree
|
||||||
@@ -186,7 +187,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 +318,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:
|
||||||
|
|||||||
105
build.zig
105
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,56 @@ fn buildCurl(
|
|||||||
return lib;
|
return lib;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Manifest = struct {
|
/// Resolves the semantic version of the build.
|
||||||
version: []const u8,
|
///
|
||||||
minimum_zig_version: []const u8,
|
/// The base version is read from `build.zig.zon`. This can be overridden
|
||||||
|
/// using the `-Dversion` command-line flag:
|
||||||
|
/// - If the flag contains a full semantic version (e.g., `1.2.3`), it replaces
|
||||||
|
/// the base version entirely.
|
||||||
|
/// - If the flag contains a simple string (e.g., `nightly`), it replaces only
|
||||||
|
/// the pre-release tag of the base version (e.g., `1.0.0-dev` -> `1.0.0-nightly`).
|
||||||
|
///
|
||||||
|
/// For versions that have a pre-release tag and no explicit build metadata,
|
||||||
|
/// this function automatically enriches the version with the git commit count
|
||||||
|
/// and short hash (e.g., `1.0.0-dev.5243+dbe45229`).
|
||||||
|
fn resolveVersion(b: *std.Build) std.SemanticVersion {
|
||||||
|
const opt_version = b.option([]const u8, "version", "Override the version of this build");
|
||||||
|
|
||||||
fn init(b: *std.Build) Manifest {
|
const version = if (opt_version) |v|
|
||||||
const input = @embedFile("build.zig.zon");
|
std.SemanticVersion.parse(v) catch blk: {
|
||||||
|
var fallback = lightpanda_version;
|
||||||
|
fallback.pre = v;
|
||||||
|
break :blk fallback;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
lightpanda_version;
|
||||||
|
|
||||||
var diagnostics: std.zon.parse.Diagnostics = .{};
|
// Only enrich versions that have a pre-release field and no explicit build metadata.
|
||||||
defer diagnostics.deinit(b.allocator);
|
if (version.pre == null or version.build != null) return version;
|
||||||
|
|
||||||
return std.zon.parse.fromSlice(Manifest, b.allocator, input, &diagnostics, .{
|
// For dev/nightly versions, calculate the commit count and hash
|
||||||
.free_on_error = true,
|
const git_hash_raw = runGit(b, &.{ "rev-parse", "--short", "HEAD" }) catch return version;
|
||||||
.ignore_unknown_fields = true,
|
const commit_hash = std.mem.trim(u8, git_hash_raw, " \n\r");
|
||||||
}) catch |err| {
|
|
||||||
switch (err) {
|
const git_count_raw = runGit(b, &.{ "rev-list", "--count", "HEAD" }) catch return version;
|
||||||
error.OutOfMemory => @panic("OOM"),
|
const commit_count = std.mem.trim(u8, git_count_raw, " \n\r");
|
||||||
error.ParseZon => {
|
|
||||||
std.debug.print("Parse diagnostics:\n{f}\n", .{diagnostics});
|
return .{
|
||||||
std.process.exit(1);
|
.major = version.major,
|
||||||
},
|
.minor = version.minor,
|
||||||
}
|
.patch = version.patch,
|
||||||
};
|
.pre = b.fmt("{s}.{s}", .{ version.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 = .{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
223
src/Config.zig
223
src/Config.zig
@@ -163,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{
|
||||||
@@ -199,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,
|
||||||
@@ -217,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,
|
||||||
@@ -224,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 {
|
||||||
@@ -293,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
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -376,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 ++
|
||||||
\\
|
\\
|
||||||
@@ -400,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.
|
||||||
\\
|
\\
|
||||||
@@ -485,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,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" });
|
||||||
@@ -554,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;
|
||||||
@@ -619,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| {
|
||||||
@@ -639,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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -675,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;
|
||||||
@@ -709,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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -718,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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -821,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;
|
||||||
@@ -857,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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -865,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -880,27 +953,27 @@ 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);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -386,6 +386,14 @@ 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) !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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -371,12 +381,9 @@ pub fn getTitle(self: *Page) !?[]const u8 {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add comon headers for a request:
|
// Add common headers for a request:
|
||||||
// * cookies
|
|
||||||
// * referer
|
// * referer
|
||||||
pub fn headersForRequest(self: *Page, temp: Allocator, url: [:0]const u8, headers: *HttpClient.Headers) !void {
|
pub fn headersForRequest(self: *Page, headers: *HttpClient.Headers) !void {
|
||||||
try self.requestCookie(.{}).headersForRequest(temp, url, headers);
|
|
||||||
|
|
||||||
// Build the referer
|
// Build the referer
|
||||||
const referer = blk: {
|
const referer = blk: {
|
||||||
if (self.referer_header == null) {
|
if (self.referer_header == null) {
|
||||||
@@ -437,6 +444,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]);
|
||||||
@@ -525,8 +538,6 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
|||||||
if (opts.header) |hdr| {
|
if (opts.header) |hdr| {
|
||||||
try headers.add(hdr);
|
try headers.add(hdr);
|
||||||
}
|
}
|
||||||
try self.requestCookie(.{ .is_navigation = true }).headersForRequest(self.arena, self.url, &headers);
|
|
||||||
|
|
||||||
// We dispatch page_navigate event before sending the request.
|
// We dispatch page_navigate event before sending the request.
|
||||||
// It ensures the event page_navigated is not dispatched before this one.
|
// It ensures the event page_navigated is not dispatched before this one.
|
||||||
session.notification.dispatch(.page_navigate, &.{
|
session.notification.dispatch(.page_navigate, &.{
|
||||||
@@ -553,6 +564,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
|||||||
.headers = headers,
|
.headers = headers,
|
||||||
.body = opts.body,
|
.body = opts.body,
|
||||||
.cookie_jar = &session.cookie_jar,
|
.cookie_jar = &session.cookie_jar,
|
||||||
|
.cookie_origin = self.url,
|
||||||
.resource_type = .document,
|
.resource_type = .document,
|
||||||
.notification = self._session.notification,
|
.notification = self._session.notification,
|
||||||
.header_callback = pageHeaderDoneCallback,
|
.header_callback = pageHeaderDoneCallback,
|
||||||
@@ -583,13 +595,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 },
|
||||||
);
|
);
|
||||||
@@ -995,6 +1028,7 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
parser.parse(html);
|
parser.parse(html);
|
||||||
|
self._parse_state = .complete;
|
||||||
self.documentIsComplete();
|
self.documentIsComplete();
|
||||||
},
|
},
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
@@ -2557,6 +2591,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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2568,8 +2613,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 });
|
||||||
@@ -2581,8 +2628,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 });
|
||||||
@@ -3434,7 +3483,8 @@ 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();
|
||||||
@@ -3497,19 +3547,6 @@ pub fn insertText(self: *Page, v: []const u8) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const RequestCookieOpts = struct {
|
|
||||||
is_http: bool = true,
|
|
||||||
is_navigation: bool = false,
|
|
||||||
};
|
|
||||||
pub fn requestCookie(self: *const Page, opts: RequestCookieOpts) HttpClient.RequestCookie {
|
|
||||||
return .{
|
|
||||||
.jar = &self._session.cookie_jar,
|
|
||||||
.origin = self.url,
|
|
||||||
.is_http = opts.is_http,
|
|
||||||
.is_navigation = opts.is_navigation,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn asUint(comptime string: anytype) std.meta.Int(
|
fn asUint(comptime string: anytype) std.meta.Int(
|
||||||
.unsigned,
|
.unsigned,
|
||||||
@bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0
|
@bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0
|
||||||
@@ -3532,9 +3569,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;
|
||||||
|
|
||||||
@@ -138,9 +136,9 @@ fn clearList(list: *std.DoublyLinkedList) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getHeaders(self: *ScriptManager, arena: Allocator, url: [:0]const u8) !net_http.Headers {
|
fn getHeaders(self: *ScriptManager) !net_http.Headers {
|
||||||
var headers = try self.client.newHeaders();
|
var headers = try self.client.newHeaders();
|
||||||
try self.page.headersForRequest(arena, url, &headers);
|
try self.page.headersForRequest(&headers);
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,9 +278,10 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
.ctx = script,
|
.ctx = script,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.frame_id = page._frame_id,
|
.frame_id = page._frame_id,
|
||||||
.headers = try self.getHeaders(arena, url),
|
.headers = try self.getHeaders(),
|
||||||
.blocking = is_blocking,
|
.blocking = is_blocking,
|
||||||
.cookie_jar = &page._session.cookie_jar,
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
|
.cookie_origin = page.url,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
.notification = page._session.notification,
|
.notification = page._session.notification,
|
||||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||||
@@ -405,8 +404,9 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
.ctx = script,
|
.ctx = script,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.frame_id = page._frame_id,
|
.frame_id = page._frame_id,
|
||||||
.headers = try self.getHeaders(arena, url),
|
.headers = try self.getHeaders(),
|
||||||
.cookie_jar = &page._session.cookie_jar,
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
|
.cookie_origin = page.url,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
.notification = page._session.notification,
|
.notification = page._session.notification,
|
||||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||||
@@ -508,10 +508,11 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
|||||||
.url = url,
|
.url = url,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.frame_id = page._frame_id,
|
.frame_id = page._frame_id,
|
||||||
.headers = try self.getHeaders(arena, url),
|
.headers = try self.getHeaders(),
|
||||||
.ctx = script,
|
.ctx = script,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
.cookie_jar = &page._session.cookie_jar,
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
|
.cookie_origin = page.url,
|
||||||
.notification = page._session.notification,
|
.notification = page._session.notification,
|
||||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||||
.header_callback = Script.headerCallback,
|
.header_callback = Script.headerCallback,
|
||||||
@@ -654,7 +655,6 @@ pub const Script = struct {
|
|||||||
debug_transfer_aborted: bool = false,
|
debug_transfer_aborted: bool = false,
|
||||||
debug_transfer_bytes_received: usize = 0,
|
debug_transfer_bytes_received: usize = 0,
|
||||||
debug_transfer_notified_fail: bool = false,
|
debug_transfer_notified_fail: bool = false,
|
||||||
debug_transfer_redirecting: bool = false,
|
|
||||||
debug_transfer_intercept_state: u8 = 0,
|
debug_transfer_intercept_state: u8 = 0,
|
||||||
debug_transfer_auth_challenge: bool = false,
|
debug_transfer_auth_challenge: bool = false,
|
||||||
debug_transfer_easy_id: usize = 0,
|
debug_transfer_easy_id: usize = 0,
|
||||||
@@ -730,7 +730,6 @@ pub const Script = struct {
|
|||||||
.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,
|
|
||||||
.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,
|
||||||
@@ -739,10 +738,9 @@ pub const Script = struct {
|
|||||||
.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,
|
|
||||||
.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;
|
||||||
@@ -750,10 +748,9 @@ pub const Script = struct {
|
|||||||
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_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 });
|
||||||
|
|||||||
@@ -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,41 @@ 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 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 +111,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 +179,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 +224,9 @@ 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
|
self.identity.deinit();
|
||||||
if (comptime IS_DEBUG) {
|
self.identity = .{};
|
||||||
var it = self._arena_pool_leak_track.valueIterator();
|
|
||||||
while (it.next()) |value_ptr| {
|
|
||||||
if (value_ptr.count > 0) {
|
|
||||||
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self._arena_pool_leak_track.clearRetainingCapacity();
|
|
||||||
}
|
|
||||||
|
|
||||||
// All origins should have been released when contexts were destroyed
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
std.debug.assert(self.origins.count() == 0);
|
std.debug.assert(self.origins.count() == 0);
|
||||||
}
|
}
|
||||||
@@ -259,10 +237,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 +270,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 +290,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 +308,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 +330,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 +344,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 +353,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 +426,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 +456,36 @@ pub fn nextPageId(self: *Session) u32 {
|
|||||||
self.page_id_gen = id;
|
self.page_id_gen = id;
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// page reset.
|
||||||
|
pub const FinalizerCallback = struct {
|
||||||
|
arena: Allocator,
|
||||||
|
session: *Session,
|
||||||
|
ptr: *anyopaque,
|
||||||
|
global: v8.Global,
|
||||||
|
identity: *js.Identity,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release this item from the identity tracking maps (called after finalizer runs from V8)
|
||||||
|
pub fn releaseIdentity(self: *FinalizerCallback) void {
|
||||||
|
const session = self.session;
|
||||||
|
const id = @intFromPtr(self.ptr);
|
||||||
|
|
||||||
|
if (self.identity.identity_map.fetchRemove(id)) |kv| {
|
||||||
|
var global = kv.value;
|
||||||
|
v8.v8__Global__Reset(&global);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = self.identity.finalizer_callbacks.remove(id);
|
||||||
|
|
||||||
|
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,
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const log = @import("../../log.zig");
|
|||||||
|
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const Env = @import("Env.zig");
|
const Env = @import("Env.zig");
|
||||||
const bridge = @import("bridge.zig");
|
|
||||||
const Origin = @import("Origin.zig");
|
const Origin = @import("Origin.zig");
|
||||||
const Scheduler = @import("Scheduler.zig");
|
const Scheduler = @import("Scheduler.zig");
|
||||||
|
|
||||||
@@ -63,7 +62,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 +80,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 +196,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,16 +213,16 @@ 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.identity.globals.append(self.identity_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.identity.temps.put(self.identity_arena, global.data_ptr, global);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn weakRef(self: *Context, obj: anytype) void {
|
pub fn weakRef(self: *Context, obj: anytype) void {
|
||||||
const resolved = js.Local.resolveValue(obj);
|
const resolved = js.Local.resolveValue(obj);
|
||||||
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
|
const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
// should not be possible
|
// should not be possible
|
||||||
std.debug.assert(false);
|
std.debug.assert(false);
|
||||||
@@ -224,7 +234,7 @@ pub fn weakRef(self: *Context, obj: anytype) void {
|
|||||||
|
|
||||||
pub fn safeWeakRef(self: *Context, obj: anytype) void {
|
pub fn safeWeakRef(self: *Context, obj: anytype) void {
|
||||||
const resolved = js.Local.resolveValue(obj);
|
const resolved = js.Local.resolveValue(obj);
|
||||||
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
|
const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
// should not be possible
|
// should not be possible
|
||||||
std.debug.assert(false);
|
std.debug.assert(false);
|
||||||
@@ -237,7 +247,7 @@ pub fn safeWeakRef(self: *Context, obj: anytype) void {
|
|||||||
|
|
||||||
pub fn strongRef(self: *Context, obj: anytype) void {
|
pub fn strongRef(self: *Context, obj: anytype) void {
|
||||||
const resolved = js.Local.resolveValue(obj);
|
const resolved = js.Local.resolveValue(obj);
|
||||||
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
|
const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
// should not be possible
|
// should not be possible
|
||||||
std.debug.assert(false);
|
std.debug.assert(false);
|
||||||
@@ -247,6 +257,48 @@ pub fn strongRef(self: *Context, obj: anytype) void {
|
|||||||
v8.v8__Global__ClearWeak(&fc.global);
|
v8.v8__Global__ClearWeak(&fc.global);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const IdentityResult = struct {
|
||||||
|
value_ptr: *v8.Global,
|
||||||
|
found_existing: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn addIdentity(self: *Context, ptr: usize) !IdentityResult {
|
||||||
|
const gop = try self.identity.identity_map.getOrPut(self.identity_arena, ptr);
|
||||||
|
return .{
|
||||||
|
.value_ptr = gop.value_ptr,
|
||||||
|
.found_existing = gop.found_existing,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn releaseTemp(self: *Context, global: v8.Global) void {
|
||||||
|
if (self.identity.temps.fetchRemove(global.data_ptr)) |kv| {
|
||||||
|
var g = kv.value;
|
||||||
|
v8.v8__Global__Reset(&g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createFinalizerCallback(
|
||||||
|
self: *Context,
|
||||||
|
global: v8.Global,
|
||||||
|
ptr: *anyopaque,
|
||||||
|
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
|
||||||
|
) !*Session.FinalizerCallback {
|
||||||
|
const session = self.session;
|
||||||
|
const arena = try session.getArena(.{ .debug = "FinalizerCallback" });
|
||||||
|
errdefer session.releaseArena(arena);
|
||||||
|
const fc = try arena.create(Session.FinalizerCallback);
|
||||||
|
fc.* = .{
|
||||||
|
.arena = arena,
|
||||||
|
.session = session,
|
||||||
|
.ptr = ptr,
|
||||||
|
.global = global,
|
||||||
|
.zig_finalizer = zig_finalizer,
|
||||||
|
// Store identity pointer for cleanup when V8 GCs the object
|
||||||
|
.identity = self.identity,
|
||||||
|
};
|
||||||
|
return fc;
|
||||||
|
}
|
||||||
|
|
||||||
// Any operation on the context have to be made from a local.
|
// Any operation on the context have to be made from a local.
|
||||||
pub fn localScope(self: *Context, ls: *js.Local.Scope) void {
|
pub fn localScope(self: *Context, ls: *js.Local.Scope) void {
|
||||||
const isolate = self.isolate;
|
const isolate = self.isolate;
|
||||||
|
|||||||
@@ -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.identity.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
75
src/browser/js/Identity.zig
Normal file
75
src/browser/js/Identity.zig
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// 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,
|
||||||
|
|
||||||
|
// Tracked global v8 objects that need to be released on cleanup.
|
||||||
|
globals: std.ArrayList(v8.Global) = .empty,
|
||||||
|
|
||||||
|
// Temporary v8 globals that can be released early. Key is global.data_ptr.
|
||||||
|
temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
||||||
|
|
||||||
|
// Finalizer callbacks for weak references. Key is @intFromPtr of the Zig instance.
|
||||||
|
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *Session.FinalizerCallback) = .empty,
|
||||||
|
|
||||||
|
pub fn deinit(self: *Identity) void {
|
||||||
|
{
|
||||||
|
var it = self.finalizer_callbacks.valueIterator();
|
||||||
|
while (it.next()) |finalizer| {
|
||||||
|
finalizer.*.deinit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Page = @import("../Page.zig");
|
|
||||||
const Session = @import("../Session.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");
|
||||||
@@ -33,7 +32,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 +200,20 @@ 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 gop = try ctx.addIdentity(@intFromPtr(resolved.ptr));
|
||||||
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 +242,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,
|
||||||
@@ -276,10 +274,10 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
|
|||||||
// Instead, we check if the base has finalizer. The assumption
|
// Instead, we check if the base has finalizer. The assumption
|
||||||
// here is that if a resolve type has a finalizer, then the base
|
// here is that if a resolve type has a finalizer, then the base
|
||||||
// should have a finalizer too.
|
// should have a finalizer too.
|
||||||
const fc = try ctx.origin.createFinalizerCallback(ctx.session, gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?);
|
const fc = try ctx.createFinalizerCallback(gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?);
|
||||||
{
|
{
|
||||||
errdefer fc.deinit();
|
errdefer fc.deinit();
|
||||||
try ctx.origin.finalizer_callbacks.put(ctx.origin.arena, @intFromPtr(resolved.ptr), fc);
|
try ctx.identity.finalizer_callbacks.put(ctx.identity_arena, @intFromPtr(resolved.ptr), fc);
|
||||||
}
|
}
|
||||||
|
|
||||||
conditionallyReference(value);
|
conditionallyReference(value);
|
||||||
|
|||||||
@@ -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.identity.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.identity.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;
|
||||||
|
|
||||||
@@ -117,13 +113,12 @@ pub fn Builder(comptime T: type) type {
|
|||||||
.from_v8 = struct {
|
.from_v8 = struct {
|
||||||
fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
|
||||||
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
|
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
|
||||||
const fc: *Origin.FinalizerCallback = @ptrCast(@alignCast(ptr));
|
const fc: *Session.FinalizerCallback = @ptrCast(@alignCast(ptr));
|
||||||
|
|
||||||
const origin = fc.origin;
|
|
||||||
const value_ptr = fc.ptr;
|
const value_ptr = fc.ptr;
|
||||||
if (origin.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
|
if (fc.identity.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
|
||||||
func(@ptrCast(@alignCast(value_ptr)), false, fc.session);
|
func(@ptrCast(@alignCast(value_ptr)), false, fc.session);
|
||||||
origin.release(value_ptr);
|
fc.releaseIdentity();
|
||||||
} else {
|
} else {
|
||||||
// A bit weird, but v8 _requires_ that we release it
|
// A bit weird, but v8 _requires_ that we release it
|
||||||
// If we don't. We'll 100% crash.
|
// If we don't. We'll 100% crash.
|
||||||
@@ -200,6 +195,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 +204,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) {
|
||||||
@@ -852,6 +849,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 +900,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);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// Verify: handler fires exactly once, and body.onload reflects to window.onload.
|
// Verify: handler fires exactly once, and body.onload reflects to window.onload.
|
||||||
let called = 0;
|
let called = 0;
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
// The attribute handler should have fired exactly once.
|
// The attribute handler should have fired exactly once.
|
||||||
testing.expectEqual(1, called);
|
testing.expectEqual(1, called);
|
||||||
|
|
||||||
|
|||||||
51
src/browser/tests/window/cross_origin.html
Normal file
51
src/browser/tests/window/cross_origin.html
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<iframe src=support/frame1.html></iframe>
|
||||||
|
|
||||||
|
<script id=post_message type=module>
|
||||||
|
const state = await testing.async();
|
||||||
|
|
||||||
|
{
|
||||||
|
const ALT_BASE = testing.BASE_URL.replace('127.0.0.1', 'localhost');
|
||||||
|
|
||||||
|
{
|
||||||
|
let iframe2 = document.createElement('iframe');
|
||||||
|
iframe2.src = ALT_BASE + 'window/support/frame1.html';
|
||||||
|
document.documentElement.appendChild(iframe2);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let iframe3 = document.createElement('iframe');
|
||||||
|
iframe3.src = ALT_BASE + 'window/support/frame2.html';
|
||||||
|
document.documentElement.appendChild(iframe3);
|
||||||
|
}
|
||||||
|
|
||||||
|
let captures = [];
|
||||||
|
window.addEventListener('message', (e) => {
|
||||||
|
captures.push(e.data);
|
||||||
|
if (captures.length == 3) {
|
||||||
|
state.resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await state.done(() => {
|
||||||
|
const expected_urls = [
|
||||||
|
testing.BASE_URL + 'window/support/frame1.html',
|
||||||
|
ALT_BASE + 'window/support/frame1.html',
|
||||||
|
ALT_BASE + 'window/support/frame2.html',
|
||||||
|
];
|
||||||
|
|
||||||
|
// No strong order guarantee for messaages, and we don't care about the order
|
||||||
|
// so long as it's the correct data.
|
||||||
|
testing.expectEqual(expected_urls.sort(), captures.map((c) => {return c.url}).sort());
|
||||||
|
captures.forEach((c) => {
|
||||||
|
if (c.url.includes(testing.BASE_URL)) {
|
||||||
|
testing.expectEqual(false, c.document_is_undefined);
|
||||||
|
} else {
|
||||||
|
testing.expectEqual(true, c.document_is_undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
7
src/browser/tests/window/support/frame1.html
Normal file
7
src/browser/tests/window/support/frame1.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script>
|
||||||
|
window.parent.postMessage({
|
||||||
|
url: location.toString(),
|
||||||
|
document_is_undefined: window.parent.document === undefined,
|
||||||
|
}, '*')
|
||||||
|
</script>
|
||||||
7
src/browser/tests/window/support/frame2.html
Normal file
7
src/browser/tests/window/support/frame2.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script>
|
||||||
|
window.top.postMessage({
|
||||||
|
url: location.toString(),
|
||||||
|
document_is_undefined: window.top.document === undefined,
|
||||||
|
}, '*')
|
||||||
|
</script>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user