mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-29 16:10:04 +00:00
Compare commits
164 Commits
wpt-faster
...
0.2.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a876275828 | ||
|
|
e83b8aa36d | ||
|
|
179f9c1169 | ||
|
|
9c37961042 | ||
|
|
c9fa76da0c | ||
|
|
7718184e22 | ||
|
|
b81b41cbf0 | ||
|
|
3a0cead03a | ||
|
|
92ce6a916a | ||
|
|
130bf7ba11 | ||
|
|
2e40354a3a | ||
|
|
3074bde2f3 | ||
|
|
ed9f5aae2e | ||
|
|
8e315e551a | ||
|
|
bad690da65 | ||
|
|
c5c1d1f2f8 | ||
|
|
eb18dc89f6 | ||
|
|
afb0c29243 | ||
|
|
267eee9693 | ||
|
|
39352a6bda | ||
|
|
0838b510f8 | ||
|
|
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 |
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: ${{ github.ref_type == 'tag' && github.ref_name || '1.0.0-nightly' }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) ${{ env.GIT_VERSION_FLAG }}
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dversion_string=${{ env.VERSION }}
|
||||||
|
|
||||||
- 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 -Dversion_string=${{ env.VERSION }}
|
||||||
|
|
||||||
- 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 -Dversion_string=${{ env.VERSION }}
|
||||||
|
|
||||||
- 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 -Dversion_string=${{ env.VERSION }}
|
||||||
|
|
||||||
- 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 }}
|
||||||
|
|||||||
6
.github/workflows/wpt.yml
vendored
6
.github/workflows/wpt.yml
vendored
@@ -14,8 +14,6 @@ on:
|
|||||||
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
wpt-build-release:
|
wpt-build-release:
|
||||||
@@ -42,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
|
||||||
@@ -118,7 +116,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
./wpt serve 2> /dev/null & echo $! > WPT.pid
|
./wpt serve 2> /dev/null & echo $! > WPT.pid
|
||||||
sleep 20s
|
sleep 20s
|
||||||
./wptrunner -lpd-path ./lightpanda -json -concurrency 10 -pool 10 --mem-limit 400 > wpt.json
|
./wptrunner -lpd-path ./lightpanda -json -concurrency 5 -pool 5 --mem-limit 400 > wpt.json
|
||||||
kill `cat WPT.pid`
|
kill `cat WPT.pid`
|
||||||
|
|
||||||
- name: write commit
|
- name: write commit
|
||||||
|
|||||||
@@ -53,8 +53,7 @@ RUN zig build -Doptimize=ReleaseFast \
|
|||||||
# build release
|
# build release
|
||||||
RUN zig build -Doptimize=ReleaseFast \
|
RUN zig build -Doptimize=ReleaseFast \
|
||||||
-Dsnapshot_path=../../snapshot.bin \
|
-Dsnapshot_path=../../snapshot.bin \
|
||||||
-Dprebuilt_v8_path=v8/libc_v8.a \
|
-Dprebuilt_v8_path=v8/libc_v8.a
|
||||||
-Dgit_commit=$(git rev-parse --short HEAD)
|
|
||||||
|
|
||||||
FROM debian:stable-slim
|
FROM debian:stable-slim
|
||||||
|
|
||||||
@@ -75,4 +74,4 @@ EXPOSE 9222/tcp
|
|||||||
# Using "tini" as PID1 ensures that signals work as expected, so e.g. "docker stop" will not hang.
|
# Using "tini" as PID1 ensures that signals work as expected, so e.g. "docker stop" will not hang.
|
||||||
# (See https://github.com/krallin/tini#why-tini).
|
# (See https://github.com/krallin/tini#why-tini).
|
||||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
CMD ["/bin/lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222", "--log_level", "info"]
|
CMD ["/bin/lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222", "--log-level", "info"]
|
||||||
|
|||||||
@@ -4,11 +4,3 @@ License names used in this document are as per [SPDX License
|
|||||||
List](https://spdx.org/licenses/).
|
List](https://spdx.org/licenses/).
|
||||||
|
|
||||||
The default license for this project is [AGPL-3.0-only](LICENSE).
|
The default license for this project is [AGPL-3.0-only](LICENSE).
|
||||||
|
|
||||||
The following directories and their subdirectories are licensed under their
|
|
||||||
original upstream licenses:
|
|
||||||
|
|
||||||
```
|
|
||||||
vendor/
|
|
||||||
tests/wpt/
|
|
||||||
```
|
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -58,13 +58,13 @@ build-v8-snapshot:
|
|||||||
## Build in release-fast mode
|
## Build in release-fast mode
|
||||||
build: build-v8-snapshot
|
build: build-v8-snapshot
|
||||||
@printf "\033[36mBuilding (release fast)...\033[0m\n"
|
@printf "\033[36mBuilding (release fast)...\033[0m\n"
|
||||||
@$(ZIG) build -Doptimize=ReleaseFast -Dsnapshot_path=../../snapshot.bin -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
@$(ZIG) build -Doptimize=ReleaseFast -Dsnapshot_path=../../snapshot.bin || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||||
@printf "\033[33mBuild OK\033[0m\n"
|
@printf "\033[33mBuild OK\033[0m\n"
|
||||||
|
|
||||||
## Build in debug mode
|
## Build in debug mode
|
||||||
build-dev:
|
build-dev:
|
||||||
@printf "\033[36mBuilding (debug)...\033[0m\n"
|
@printf "\033[36mBuilding (debug)...\033[0m\n"
|
||||||
@$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
@$(ZIG) build || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||||
@printf "\033[33mBuild OK\033[0m\n"
|
@printf "\033[33mBuild OK\033[0m\n"
|
||||||
|
|
||||||
## Run the server in release mode
|
## Run the server in release mode
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -18,15 +18,15 @@ Not a Chromium fork. Not a WebKit patch. A new browser, written in Zig.
|
|||||||
</div>
|
</div>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/execution-time.svg">
|
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/execution-time-v2.svg">
|
||||||
](https://github.com/lightpanda-io/demo)
|
](https://github.com/lightpanda-io/demo)
|
||||||
 
|
 
|
||||||
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/memory-frame.svg">
|
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/memory-frame-v2.svg">
|
||||||
](https://github.com/lightpanda-io/demo)
|
](https://github.com/lightpanda-io/demo)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
_Puppeteer requesting 100 pages from a local website on a AWS EC2 m5.large instance.
|
_chromedp requesting 933 real web pages over the network on a AWS EC2 m5.large instance.
|
||||||
See [benchmark details](https://github.com/lightpanda-io/demo)._
|
See [benchmark details](https://github.com/lightpanda-io/demo/blob/main/BENCHMARKS.md#crawler-benchmark)._
|
||||||
|
|
||||||
Lightpanda is the open-source browser made for headless usage:
|
Lightpanda is the open-source browser made for headless usage:
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ docker run -d --name lightpanda -p 9222:9222 lightpanda/browser:nightly
|
|||||||
### Dump a URL
|
### Dump a URL
|
||||||
|
|
||||||
```console
|
```console
|
||||||
./lightpanda fetch --obey_robots --log_format pretty --log_level info https://demo-browser.lightpanda.io/campfire-commerce/
|
./lightpanda fetch --obey-robots --log-format pretty --log-level info https://demo-browser.lightpanda.io/campfire-commerce/
|
||||||
```
|
```
|
||||||
```console
|
```console
|
||||||
INFO telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
INFO telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
||||||
@@ -117,7 +117,7 @@ INFO http : request complete . . . . . . . . . . . . . . . . [+141ms]
|
|||||||
### Start a CDP server
|
### Start a CDP server
|
||||||
|
|
||||||
```console
|
```console
|
||||||
./lightpanda serve --obey_robots --log_format pretty --log_level info --host 127.0.0.1 --port 9222
|
./lightpanda serve --obey-robots --log-format pretty --log-level info --host 127.0.0.1 --port 9222
|
||||||
```
|
```
|
||||||
```console
|
```console
|
||||||
INFO telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
INFO telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
||||||
@@ -186,7 +186,7 @@ Here are the key features we have implemented:
|
|||||||
- [x] Custom HTTP headers
|
- [x] Custom HTTP headers
|
||||||
- [x] Proxy support
|
- [x] Proxy support
|
||||||
- [x] Network interception
|
- [x] Network interception
|
||||||
- [x] Respect `robots.txt` with option `--obey_robots`
|
- [x] Respect `robots.txt` with option `--obey-robots`
|
||||||
|
|
||||||
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
||||||
|
|
||||||
@@ -317,7 +317,7 @@ First start the WPT's HTTP server from your `wpt/` clone dir.
|
|||||||
Run a Lightpanda browser
|
Run a Lightpanda browser
|
||||||
|
|
||||||
```
|
```
|
||||||
zig build run -- --insecure_disable_tls_host_verification
|
zig build run -- --insecure-disable-tls-host-verification
|
||||||
```
|
```
|
||||||
|
|
||||||
Then you can start the wptrunner from the Demo's clone dir:
|
Then you can start the wptrunner from the Demo's clone dir:
|
||||||
|
|||||||
92
build.zig
92
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,41 @@ fn buildCurl(
|
|||||||
return lib;
|
return lib;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Manifest = struct {
|
/// Returns `MAJOR.MINOR.PATCH-dev` when `git describe` fails.
|
||||||
version: []const u8,
|
fn resolveVersion(b: *std.Build) std.SemanticVersion {
|
||||||
minimum_zig_version: []const u8,
|
const version_string = b.option([]const u8, "version_string", "Override the version of this build");
|
||||||
|
if (version_string) |semver_string| {
|
||||||
fn init(b: *std.Build) Manifest {
|
return std.SemanticVersion.parse(semver_string) catch |err| {
|
||||||
const input = @embedFile("build.zig.zon");
|
std.debug.panic("Expected -Dversion-string={s} to be a semantic version: {}", .{ semver_string, err });
|
||||||
|
|
||||||
var diagnostics: std.zon.parse.Diagnostics = .{};
|
|
||||||
defer diagnostics.deinit(b.allocator);
|
|
||||||
|
|
||||||
return std.zon.parse.fromSlice(Manifest, b.allocator, input, &diagnostics, .{
|
|
||||||
.free_on_error = true,
|
|
||||||
.ignore_unknown_fields = true,
|
|
||||||
}) catch |err| {
|
|
||||||
switch (err) {
|
|
||||||
error.OutOfMemory => @panic("OOM"),
|
|
||||||
error.ParseZon => {
|
|
||||||
std.debug.print("Parse diagnostics:\n{f}\n", .{diagnostics});
|
|
||||||
std.process.exit(1);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
// If it's a stable release (no pre or build metadata in build.zig.zon), use it as is
|
||||||
|
if (lightpanda_version.pre == null and lightpanda_version.build == null) return lightpanda_version;
|
||||||
|
|
||||||
|
// For dev/nightly versions, calculate the commit count and hash
|
||||||
|
const git_hash_raw = runGit(b, &.{ "rev-parse", "--short", "HEAD" }) catch return lightpanda_version;
|
||||||
|
const commit_hash = std.mem.trim(u8, git_hash_raw, " \n\r");
|
||||||
|
|
||||||
|
const git_count_raw = runGit(b, &.{ "rev-list", "--count", "HEAD" }) catch return lightpanda_version;
|
||||||
|
const commit_count = std.mem.trim(u8, git_count_raw, " \n\r");
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.major = lightpanda_version.major,
|
||||||
|
.minor = lightpanda_version.minor,
|
||||||
|
.patch = lightpanda_version.patch,
|
||||||
|
.pre = b.fmt("{s}.{s}", .{ lightpanda_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,6 +1,6 @@
|
|||||||
.{
|
.{
|
||||||
.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 = .{
|
||||||
|
|||||||
@@ -17,12 +17,15 @@
|
|||||||
// 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 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 +33,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 +52,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) {
|
||||||
|
std.debug.print("ArenaPool leak detected: '{s}' count={d}\n", .{ kv.key_ptr.*, 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 +80,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 +102,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 +125,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) {
|
||||||
|
std.debug.print("ArenaPool double-free detected: '{s}'\n", .{entry.debug});
|
||||||
|
@panic("ArenaPool: double-free detected");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std.debug.print("ArenaPool release of untracked arena: '{s}'\n", .{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 +155,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 +178,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 +196,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 +221,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 +236,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 +260,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 = .load,
|
||||||
};
|
};
|
||||||
|
|
||||||
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 = .load;
|
||||||
|
|
||||||
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| {
|
||||||
|
|||||||
@@ -45,7 +45,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);
|
||||||
@@ -302,15 +302,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 +317,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);
|
||||||
@@ -484,11 +489,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 +513,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 +536,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 +605,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 +733,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 +745,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 +753,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 +762,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 +780,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 +924,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);
|
||||||
|
|||||||
@@ -848,14 +848,28 @@ fn processMessages(self: *Client) !bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the server sends "Connection: close" and closes the TLS
|
||||||
|
// connection without a close_notify alert, BoringSSL reports
|
||||||
|
// RecvError. If we already received valid HTTP headers, this is
|
||||||
|
// a normal end-of-body (the connection closure signals the end
|
||||||
|
// of the response per HTTP/1.1 when there is no Content-Length).
|
||||||
|
// We must check this before endTransfer, which may reset the
|
||||||
|
// easy handle.
|
||||||
|
const is_conn_close_recv = blk: {
|
||||||
|
const err = msg.err orelse break :blk false;
|
||||||
|
if (err != error.RecvError) break :blk false;
|
||||||
|
const hdr = msg.conn.getResponseHeader("connection", 0) orelse break :blk false;
|
||||||
|
break :blk std.ascii.eqlIgnoreCase(hdr.value, "close");
|
||||||
|
};
|
||||||
|
|
||||||
// release it ASAP so that it's available; some done_callbacks
|
// release it ASAP so that it's available; some done_callbacks
|
||||||
// will load more resources.
|
// will load more resources.
|
||||||
self.endTransfer(transfer);
|
self.endTransfer(transfer);
|
||||||
|
|
||||||
defer transfer.deinit();
|
defer transfer.deinit();
|
||||||
|
|
||||||
if (msg.err) |err| {
|
if (msg.err != null and !is_conn_close_recv) {
|
||||||
requestFailed(transfer, err, true);
|
requestFailed(transfer, msg.err.?, true);
|
||||||
} else blk: {
|
} else blk: {
|
||||||
// make sure the transfer can't be immediately aborted from a callback
|
// make sure the transfer can't be immediately aborted from a callback
|
||||||
// since we still need it here.
|
// since we still need it here.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -35,6 +35,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");
|
||||||
|
|
||||||
@@ -60,6 +61,7 @@ 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 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");
|
||||||
@@ -144,6 +146,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 +272,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),
|
||||||
};
|
};
|
||||||
@@ -298,11 +302,18 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void
|
|||||||
._visual_viewport = visual_viewport,
|
._visual_viewport = visual_viewport,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 +367,7 @@ pub fn deinit(self: *Page, abort_http: bool) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self._script_manager.deinit();
|
self._script_manager.deinit();
|
||||||
|
self._style_manager.deinit();
|
||||||
|
|
||||||
session.releaseArena(self.call_arena);
|
session.releaseArena(self.call_arena);
|
||||||
}
|
}
|
||||||
@@ -437,6 +449,12 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
|||||||
if (is_about_blank or is_blob) {
|
if (is_about_blank or is_blob) {
|
||||||
self.url = if (is_about_blank) "about:blank" else try self.arena.dupeZ(u8, request_url);
|
self.url = if (is_about_blank) "about:blank" else try self.arena.dupeZ(u8, request_url);
|
||||||
|
|
||||||
|
// even though this might be the same _data_ as `default_location`, we
|
||||||
|
// have to do this to make sure window.location is at a unique _address_.
|
||||||
|
// If we don't do this, mulitple window._location will have the same
|
||||||
|
// address and thus be mapped to the same v8::Object in the identity map.
|
||||||
|
self.window._location = try Location.init(self.url, self);
|
||||||
|
|
||||||
if (is_blob) {
|
if (is_blob) {
|
||||||
// strip out blob:
|
// strip out blob:
|
||||||
self.origin = try URL.getOrigin(self.arena, request_url[5.. :0]);
|
self.origin = try URL.getOrigin(self.arena, request_url[5.. :0]);
|
||||||
@@ -583,13 +601,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 },
|
||||||
);
|
);
|
||||||
@@ -2557,6 +2596,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 +2618,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 +2633,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 +3488,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();
|
||||||
@@ -3532,10 +3587,12 @@ test "WebApi: Page" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test "WebApi: Frames" {
|
test "WebApi: Frames" {
|
||||||
const filter: testing.LogFilter = .init(&.{.js});
|
// TOO FLAKY, disabled for now
|
||||||
defer filter.deinit();
|
|
||||||
|
|
||||||
try testing.htmlRunner("frames", .{});
|
// const filter: testing.LogFilter = .init(&.{.js});
|
||||||
|
// defer filter.deinit();
|
||||||
|
|
||||||
|
// try testing.htmlRunner("frames", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
test "WebApi: Integration" {
|
test "WebApi: Integration" {
|
||||||
|
|||||||
241
src/browser/Runner.zig
Normal file
241
src/browser/Runner.zig
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
// 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 App = @import("../App.zig");
|
||||||
|
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
const Session = @import("Session.zig");
|
||||||
|
const Browser = @import("Browser.zig");
|
||||||
|
const Factory = @import("Factory.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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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];
|
||||||
@@ -587,11 +597,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 +614,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 +631,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);
|
||||||
}
|
}
|
||||||
@@ -1414,3 +1430,22 @@ test "URL: getHost" {
|
|||||||
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"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -133,6 +133,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 +148,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 +162,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 +212,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 +412,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 +530,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);
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ templates: []*const v8.FunctionTemplate,
|
|||||||
// Arena for the lifetime of the context
|
// Arena for the lifetime of the context
|
||||||
arena: Allocator,
|
arena: Allocator,
|
||||||
|
|
||||||
// The page.call_arena
|
// The call_arena for this context. For main world contexts this is
|
||||||
|
// page.call_arena. For isolated world contexts this is a separate arena
|
||||||
|
// owned by the IsolatedWorld.
|
||||||
call_arena: Allocator,
|
call_arena: Allocator,
|
||||||
|
|
||||||
// Because calls can be nested (i.e.a function calling a callback),
|
// Because calls can be nested (i.e.a function calling a callback),
|
||||||
@@ -79,6 +81,16 @@ local: ?*const js.Local = null,
|
|||||||
|
|
||||||
origin: *Origin,
|
origin: *Origin,
|
||||||
|
|
||||||
|
// Identity tracking for this context. For main world contexts, this points to
|
||||||
|
// Session's Identity. For isolated world contexts (CDP inspector), this points
|
||||||
|
// to IsolatedWorld's Identity. This ensures same-origin frames share object
|
||||||
|
// identity while isolated worlds have separate identity tracking.
|
||||||
|
identity: *js.Identity,
|
||||||
|
|
||||||
|
// Allocator to use for identity map operations. For main world contexts this is
|
||||||
|
// session.page_arena, for isolated worlds it's the isolated world's arena.
|
||||||
|
identity_arena: Allocator,
|
||||||
|
|
||||||
// Unlike other v8 types, like functions or objects, modules are not shared
|
// Unlike other v8 types, like functions or objects, modules are not shared
|
||||||
// across origins.
|
// across origins.
|
||||||
global_modules: std.ArrayList(v8.Global) = .empty,
|
global_modules: std.ArrayList(v8.Global) = .empty,
|
||||||
@@ -185,9 +197,8 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
|
|||||||
lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc });
|
lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc });
|
||||||
|
|
||||||
const origin = try self.session.getOrCreateOrigin(key);
|
const origin = try self.session.getOrCreateOrigin(key);
|
||||||
errdefer self.session.releaseOrigin(origin);
|
|
||||||
try origin.takeover(self.origin);
|
|
||||||
|
|
||||||
|
self.session.releaseOrigin(self.origin);
|
||||||
self.origin = origin;
|
self.origin = origin;
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -203,16 +214,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 +235,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 +248,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 +258,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;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const Snapshot = @import("Snapshot.zig");
|
|||||||
const Inspector = @import("Inspector.zig");
|
const Inspector = @import("Inspector.zig");
|
||||||
|
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
|
const Session = @import("../Session.zig");
|
||||||
const Window = @import("../webapi/Window.zig");
|
const Window = @import("../webapi/Window.zig");
|
||||||
|
|
||||||
const JsApis = bridge.JsApis;
|
const JsApis = bridge.JsApis;
|
||||||
@@ -254,8 +255,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 +308,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
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const js = @import("js.zig");
|
|||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
const Session = @import("../Session.zig");
|
||||||
|
|
||||||
const Function = @This();
|
const Function = @This();
|
||||||
|
|
||||||
@@ -210,10 +211,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 +238,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 +258,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
76
src/browser/js/Identity.zig
Normal file
76
src/browser/js/Identity.zig
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// 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 Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -202,20 +202,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 +244,7 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
|
|||||||
// The TAO contains the pointer to our Zig instance as
|
// The TAO contains the pointer to our Zig instance as
|
||||||
// well as any meta data we'll need to use it later.
|
// well as any meta data we'll need to use it later.
|
||||||
// See the TaggedOpaque struct for more details.
|
// See the TaggedOpaque struct for more details.
|
||||||
const tao = try origin_arena.create(TaggedOpaque);
|
const tao = try context_arena.create(TaggedOpaque);
|
||||||
tao.* = .{
|
tao.* = .{
|
||||||
.value = resolved.ptr,
|
.value = resolved.ptr,
|
||||||
.prototype_chain = resolved.prototype_chain.ptr,
|
.prototype_chain = resolved.prototype_chain.ptr,
|
||||||
@@ -276,10 +276,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,9 +16,12 @@
|
|||||||
// 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;
|
||||||
|
|
||||||
|
const Session = @import("../Session.zig");
|
||||||
|
|
||||||
const Promise = @This();
|
const Promise = @This();
|
||||||
|
|
||||||
local: *const js.Local,
|
local: *const js.Local,
|
||||||
@@ -63,10 +66,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 +83,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 +99,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const v8 = js.v8;
|
|||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
const Session = @import("../Session.zig");
|
||||||
|
|
||||||
const Value = @This();
|
const Value = @This();
|
||||||
|
|
||||||
@@ -300,10 +301,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 +362,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 +382,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ const v8 = js.v8;
|
|||||||
|
|
||||||
const Caller = @import("Caller.zig");
|
const Caller = @import("Caller.zig");
|
||||||
const Context = @import("Context.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 +116,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.
|
||||||
@@ -852,6 +850,7 @@ 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/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");
|
||||||
|
|||||||
@@ -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>
|
||||||
|
-->
|
||||||
|
|||||||
@@ -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,7 +112,7 @@
|
|||||||
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -120,9 +120,10 @@
|
|||||||
|
|
||||||
<script id=link_click>
|
<script id=link_click>
|
||||||
testing.async(async (restore) => {
|
testing.async(async (restore) => {
|
||||||
|
let f6;
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
let f6 = document.createElement('iframe');
|
f6 = document.createElement('iframe');
|
||||||
f6.id = 'f6';
|
f6.id = 'f6';
|
||||||
f6.addEventListener('load', () => {
|
f6.addEventListener('load', () => {
|
||||||
if (++count == 2) {
|
if (++count == 2) {
|
||||||
@@ -139,8 +140,19 @@
|
|||||||
});
|
});
|
||||||
</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>
|
||||||
|
|||||||
@@ -17,7 +17,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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -52,10 +52,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,
|
||||||
@@ -110,7 +110,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);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
testing.expectEqual(true, timer1 != timer2);
|
testing.expectEqual(true, timer1 != timer2);
|
||||||
|
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(true, set_interval1);
|
testing.expectEqual(true, set_interval1);
|
||||||
testing.expectEqual(false, set_interval2);
|
testing.expectEqual(false, set_interval2);
|
||||||
});
|
});
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
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=invalid-timer-clear>
|
<script id=invalid-timer-clear>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
// noop
|
// noop
|
||||||
window.removeEventListener('load', fn);
|
window.removeEventListener('load', fn);
|
||||||
testing.eventually(() => {
|
testing.onload(() => {
|
||||||
testing.expectEqual(1, call1);
|
testing.expectEqual(1, call1);
|
||||||
testing.expectEqual(2, call2);
|
testing.expectEqual(2, call2);
|
||||||
});
|
});
|
||||||
@@ -285,6 +285,6 @@
|
|||||||
unhandledCalled += 1;
|
unhandledCalled += 1;
|
||||||
});
|
});
|
||||||
Promise.reject({x: 'Fail'});
|
Promise.reject({x: 'Fail'});
|
||||||
testing.eventually(() => testing.expectEqual(2, unhandledCalled));
|
testing.onload(() => testing.expectEqual(2, unhandledCalled));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
107
src/browser/webapi/CryptoKey.zig
Normal file
107
src/browser/webapi/CryptoKey.zig
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
// 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 crypto = @import("../../sys/libcrypto.zig");
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
|
|
||||||
|
/// Represents a cryptographic key obtained from one of the SubtleCrypto methods
|
||||||
|
/// generateKey(), deriveKey(), importKey(), or unwrapKey().
|
||||||
|
const CryptoKey = @This();
|
||||||
|
|
||||||
|
/// Algorithm being used.
|
||||||
|
_type: Type,
|
||||||
|
/// Whether the key is extractable.
|
||||||
|
_extractable: bool,
|
||||||
|
/// Bit flags of `usages`; see `Usages` type.
|
||||||
|
_usages: u8,
|
||||||
|
/// Raw bytes of key.
|
||||||
|
_key: []const u8,
|
||||||
|
/// Different algorithms may use different data structures;
|
||||||
|
/// this union can be used for such situations. Active field is understood
|
||||||
|
/// from `_type`.
|
||||||
|
_vary: extern union {
|
||||||
|
/// Used by HMAC.
|
||||||
|
digest: *const crypto.EVP_MD,
|
||||||
|
/// Used by asymmetric algorithms (X25519, Ed25519).
|
||||||
|
pkey: *crypto.EVP_PKEY,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair
|
||||||
|
pub const Pair = struct {
|
||||||
|
privateKey: *CryptoKey,
|
||||||
|
publicKey: *CryptoKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Key-creating functions expect this format.
|
||||||
|
pub const KeyOrPair = union(enum) { key: *CryptoKey, pair: Pair };
|
||||||
|
|
||||||
|
pub const Type = enum(u8) { hmac, rsa, x25519 };
|
||||||
|
|
||||||
|
/// Changing the names of fields would affect bitmask creation.
|
||||||
|
pub const Usages = struct {
|
||||||
|
// zig fmt: off
|
||||||
|
pub const encrypt = 0x001;
|
||||||
|
pub const decrypt = 0x002;
|
||||||
|
pub const sign = 0x004;
|
||||||
|
pub const verify = 0x008;
|
||||||
|
pub const deriveKey = 0x010;
|
||||||
|
pub const deriveBits = 0x020;
|
||||||
|
pub const wrapKey = 0x040;
|
||||||
|
pub const unwrapKey = 0x080;
|
||||||
|
// zig fmt: on
|
||||||
|
};
|
||||||
|
|
||||||
|
pub inline fn canSign(self: *const CryptoKey) bool {
|
||||||
|
return self._usages & Usages.sign != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn canVerify(self: *const CryptoKey) bool {
|
||||||
|
return self._usages & Usages.verify != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn canDeriveBits(self: *const CryptoKey) bool {
|
||||||
|
return self._usages & Usages.deriveBits != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn canExportKey(self: *const CryptoKey) bool {
|
||||||
|
return self._extractable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only valid for HMAC.
|
||||||
|
pub inline fn getDigest(self: *const CryptoKey) *const crypto.EVP_MD {
|
||||||
|
return self._vary.digest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only valid for asymmetric algorithms (X25519, Ed25519).
|
||||||
|
pub inline fn getKeyObject(self: *const CryptoKey) *crypto.EVP_PKEY {
|
||||||
|
return self._vary.pkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(CryptoKey);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "CryptoKey";
|
||||||
|
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -548,35 +548,8 @@ pub fn prepend(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !vo
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn replaceChildren(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {
|
pub fn replaceChildren(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {
|
||||||
try validateDocumentNodes(self, nodes, true);
|
try validateDocumentNodes(self, nodes, false);
|
||||||
|
return self.asNode().replaceChildren(nodes, page);
|
||||||
page.domChanged();
|
|
||||||
const parent = self.asNode();
|
|
||||||
|
|
||||||
// Remove all existing children
|
|
||||||
var it = parent.childrenIterator();
|
|
||||||
while (it.next()) |child| {
|
|
||||||
page.removeNode(parent, child, .{ .will_be_reconnected = false });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append new children
|
|
||||||
const parent_is_connected = parent.isConnected();
|
|
||||||
for (nodes) |node_or_text| {
|
|
||||||
const child = try node_or_text.toNode(page);
|
|
||||||
|
|
||||||
// DocumentFragments are special - append all their children
|
|
||||||
if (child.is(Node.DocumentFragment)) |_| {
|
|
||||||
try page.appendAllChildren(child, parent);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var child_connected = false;
|
|
||||||
if (child._parent) |previous_parent| {
|
|
||||||
child_connected = child.isConnected();
|
|
||||||
page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
|
|
||||||
}
|
|
||||||
try page.appendNode(parent, child, .{ .child_already_connected = child_connected });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn elementFromPoint(self: *Document, x: f64, y: f64, page: *Page) !?*Element {
|
pub fn elementFromPoint(self: *Document, x: f64, y: f64, page: *Page) !?*Element {
|
||||||
@@ -591,7 +564,7 @@ pub fn elementFromPoint(self: *Document, x: f64, y: f64, page: *Page) !?*Element
|
|||||||
while (stack.items.len > 0) {
|
while (stack.items.len > 0) {
|
||||||
const node = stack.pop() orelse break;
|
const node = stack.pop() orelse break;
|
||||||
if (node.is(Element)) |element| {
|
if (node.is(Element)) |element| {
|
||||||
if (element.checkVisibility(page)) {
|
if (element.checkVisibilityCached(null, page)) {
|
||||||
const rect = element.getBoundingClientRectForVisible(page);
|
const rect = element.getBoundingClientRectForVisible(page);
|
||||||
if (x >= rect.getLeft() and x <= rect.getRight() and y >= rect.getTop() and y <= rect.getBottom()) {
|
if (x >= rect.getLeft() and x <= rect.getRight() and y >= rect.getTop() and y <= rect.getBottom()) {
|
||||||
topmost = element;
|
topmost = element;
|
||||||
@@ -717,9 +690,16 @@ pub fn write(self: *Document, text: []const []const u8, page: *Page) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine insertion point:
|
// Determine insertion point:
|
||||||
// - If _write_insertion_point is set, continue from there (subsequent write)
|
// - If _write_insertion_point is set and still parented correctly, continue from there
|
||||||
// - Otherwise, start after the script (first write)
|
// - Otherwise, start after the script (first write, or previous insertion point was removed)
|
||||||
var insert_after: ?*Node = self._write_insertion_point orelse script.asNode();
|
var insert_after: ?*Node = blk: {
|
||||||
|
if (self._write_insertion_point) |wip| {
|
||||||
|
if (wip._parent == parent) {
|
||||||
|
break :blk wip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break :blk script.asNode();
|
||||||
|
};
|
||||||
|
|
||||||
for (children_to_insert.items) |child| {
|
for (children_to_insert.items) |child| {
|
||||||
// Clear parent pointer (child is currently parented to fragment/HTML wrapper)
|
// Clear parent pointer (child is currently parented to fragment/HTML wrapper)
|
||||||
@@ -896,6 +876,10 @@ fn validateDocumentNodes(self: *Document, nodes: []const Node.NodeOrText, compti
|
|||||||
if (has_doctype) {
|
if (has_doctype) {
|
||||||
return error.HierarchyError;
|
return error.HierarchyError;
|
||||||
}
|
}
|
||||||
|
if (has_element) {
|
||||||
|
// Doctype cannot be inserted if document already has an element
|
||||||
|
return error.HierarchyError;
|
||||||
|
}
|
||||||
has_doctype = true;
|
has_doctype = true;
|
||||||
},
|
},
|
||||||
.cdata => |cd| switch (cd._type) {
|
.cdata => |cd| switch (cd._type) {
|
||||||
@@ -918,6 +902,10 @@ fn validateDocumentNodes(self: *Document, nodes: []const Node.NodeOrText, compti
|
|||||||
if (has_doctype) {
|
if (has_doctype) {
|
||||||
return error.HierarchyError;
|
return error.HierarchyError;
|
||||||
}
|
}
|
||||||
|
if (has_element) {
|
||||||
|
// Doctype cannot be inserted if document already has an element
|
||||||
|
return error.HierarchyError;
|
||||||
|
}
|
||||||
has_doctype = true;
|
has_doctype = true;
|
||||||
},
|
},
|
||||||
.cdata => |cd| switch (cd._type) {
|
.cdata => |cd| switch (cd._type) {
|
||||||
|
|||||||
@@ -143,25 +143,7 @@ pub fn prepend(self: *DocumentFragment, nodes: []const Node.NodeOrText, page: *P
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn replaceChildren(self: *DocumentFragment, nodes: []const Node.NodeOrText, page: *Page) !void {
|
pub fn replaceChildren(self: *DocumentFragment, nodes: []const Node.NodeOrText, page: *Page) !void {
|
||||||
page.domChanged();
|
return self.asNode().replaceChildren(nodes, page);
|
||||||
var parent = self.asNode();
|
|
||||||
|
|
||||||
var it = parent.childrenIterator();
|
|
||||||
while (it.next()) |child| {
|
|
||||||
page.removeNode(parent, child, .{ .will_be_reconnected = false });
|
|
||||||
}
|
|
||||||
|
|
||||||
const parent_is_connected = parent.isConnected();
|
|
||||||
for (nodes) |node_or_text| {
|
|
||||||
const child = try node_or_text.toNode(page);
|
|
||||||
|
|
||||||
// If the new children has already a parent, remove from it.
|
|
||||||
if (child._parent) |p| {
|
|
||||||
page.removeNode(p, child, .{ .will_be_reconnected = true });
|
|
||||||
}
|
|
||||||
|
|
||||||
try page.appendNode(parent, child, .{ .child_already_connected = parent_is_connected });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer, page: *Page) !void {
|
pub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const String = @import("../../string.zig").String;
|
|||||||
|
|
||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
|
const StyleManager = @import("../StyleManager.zig");
|
||||||
const reflect = @import("../reflect.zig");
|
const reflect = @import("../reflect.zig");
|
||||||
|
|
||||||
const Node = @import("Node.zig");
|
const Node = @import("Node.zig");
|
||||||
@@ -572,6 +573,32 @@ pub fn hasAttributeSafe(self: *const Element, name: String) bool {
|
|||||||
return attributes.hasSafe(name);
|
return attributes.hasSafe(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn isDisabled(self: *Element) bool {
|
||||||
|
if (self.getAttributeSafe(comptime .wrap("disabled")) != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const element_node = self.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) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn hasAttributes(self: *const Element) bool {
|
pub fn hasAttributes(self: *const Element) bool {
|
||||||
const attributes = self._attributes orelse return false;
|
const attributes = self._attributes orelse return false;
|
||||||
return attributes.isEmpty() == false;
|
return attributes.isEmpty() == false;
|
||||||
@@ -784,24 +811,7 @@ pub fn getDataset(self: *Element, page: *Page) !*DOMStringMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
|
pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
|
||||||
page.domChanged();
|
return self.asNode().replaceChildren(nodes, page);
|
||||||
var parent = self.asNode();
|
|
||||||
|
|
||||||
var it = parent.childrenIterator();
|
|
||||||
while (it.next()) |child| {
|
|
||||||
page.removeNode(parent, child, .{ .will_be_reconnected = false });
|
|
||||||
}
|
|
||||||
|
|
||||||
const parent_is_connected = parent.isConnected();
|
|
||||||
for (nodes) |node_or_text| {
|
|
||||||
var child_connected = false;
|
|
||||||
const child = try node_or_text.toNode(page);
|
|
||||||
if (child._parent) |previous_parent| {
|
|
||||||
child_connected = child.isConnected();
|
|
||||||
page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
|
|
||||||
}
|
|
||||||
try page.appendNode(parent, child, .{ .child_already_connected = child_connected });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replaceWith(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
|
pub fn replaceWith(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
|
||||||
@@ -1041,20 +1051,32 @@ pub fn parentElement(self: *Element) ?*Element {
|
|||||||
return self._proto.parentElement();
|
return self._proto.parentElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn checkVisibility(self: *Element, page: *Page) bool {
|
/// Cache for visibility checks - re-exported from StyleManager for convenience.
|
||||||
var current: ?*Element = self;
|
pub const VisibilityCache = StyleManager.VisibilityCache;
|
||||||
|
|
||||||
while (current) |el| {
|
/// Cache for pointer-events checks - re-exported from StyleManager for convenience.
|
||||||
if (el.getStyle(page)) |style| {
|
pub const PointerEventsCache = StyleManager.PointerEventsCache;
|
||||||
const display = style.asCSSStyleDeclaration().getPropertyValue("display", page);
|
|
||||||
if (std.mem.eql(u8, display, "none")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
current = el.parentElement();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
pub fn hasPointerEventsNone(self: *Element, cache: ?*PointerEventsCache, page: *Page) bool {
|
||||||
|
return page._style_manager.hasPointerEventsNone(self, cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn checkVisibilityCached(self: *Element, cache: ?*VisibilityCache, page: *Page) bool {
|
||||||
|
return !page._style_manager.isHidden(self, cache, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
const CheckVisibilityOpts = struct {
|
||||||
|
checkOpacity: bool = false,
|
||||||
|
opacityProperty: bool = false,
|
||||||
|
checkVisibilityCSS: bool = false,
|
||||||
|
visibilityProperty: bool = false,
|
||||||
|
};
|
||||||
|
pub fn checkVisibility(self: *Element, opts_: ?CheckVisibilityOpts, page: *Page) bool {
|
||||||
|
const opts = opts_ orelse CheckVisibilityOpts{};
|
||||||
|
return !page._style_manager.isHidden(self, null, .{
|
||||||
|
.check_opacity = opts.checkOpacity or opts.opacityProperty,
|
||||||
|
.check_visibility = opts.visibilityProperty or opts.checkVisibilityCSS,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getElementDimensions(self: *Element, page: *Page) struct { width: f64, height: f64 } {
|
fn getElementDimensions(self: *Element, page: *Page) struct { width: f64, height: f64 } {
|
||||||
@@ -1091,7 +1113,7 @@ fn getElementDimensions(self: *Element, page: *Page) struct { width: f64, height
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getClientWidth(self: *Element, page: *Page) f64 {
|
pub fn getClientWidth(self: *Element, page: *Page) f64 {
|
||||||
if (!self.checkVisibility(page)) {
|
if (!self.checkVisibilityCached(null, page)) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
const dims = self.getElementDimensions(page);
|
const dims = self.getElementDimensions(page);
|
||||||
@@ -1099,7 +1121,7 @@ pub fn getClientWidth(self: *Element, page: *Page) f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getClientHeight(self: *Element, page: *Page) f64 {
|
pub fn getClientHeight(self: *Element, page: *Page) f64 {
|
||||||
if (!self.checkVisibility(page)) {
|
if (!self.checkVisibilityCached(null, page)) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
const dims = self.getElementDimensions(page);
|
const dims = self.getElementDimensions(page);
|
||||||
@@ -1107,7 +1129,7 @@ pub fn getClientHeight(self: *Element, page: *Page) f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getBoundingClientRect(self: *Element, page: *Page) DOMRect {
|
pub fn getBoundingClientRect(self: *Element, page: *Page) DOMRect {
|
||||||
if (!self.checkVisibility(page)) {
|
if (!self.checkVisibilityCached(null, page)) {
|
||||||
return .{
|
return .{
|
||||||
._x = 0.0,
|
._x = 0.0,
|
||||||
._y = 0.0,
|
._y = 0.0,
|
||||||
@@ -1137,7 +1159,7 @@ pub fn getBoundingClientRectForVisible(self: *Element, page: *Page) DOMRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect {
|
pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect {
|
||||||
if (!self.checkVisibility(page)) {
|
if (!self.checkVisibilityCached(null, page)) {
|
||||||
return &.{};
|
return &.{};
|
||||||
}
|
}
|
||||||
const rects = try page.call_arena.alloc(DOMRect, 1);
|
const rects = try page.call_arena.alloc(DOMRect, 1);
|
||||||
@@ -1182,7 +1204,7 @@ pub fn getScrollWidth(self: *Element, page: *Page) f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getOffsetHeight(self: *Element, page: *Page) f64 {
|
pub fn getOffsetHeight(self: *Element, page: *Page) f64 {
|
||||||
if (!self.checkVisibility(page)) {
|
if (!self.checkVisibilityCached(null, page)) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
const dims = self.getElementDimensions(page);
|
const dims = self.getElementDimensions(page);
|
||||||
@@ -1190,7 +1212,7 @@ pub fn getOffsetHeight(self: *Element, page: *Page) f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getOffsetWidth(self: *Element, page: *Page) f64 {
|
pub fn getOffsetWidth(self: *Element, page: *Page) f64 {
|
||||||
if (!self.checkVisibility(page)) {
|
if (!self.checkVisibilityCached(null, page)) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
const dims = self.getElementDimensions(page);
|
const dims = self.getElementDimensions(page);
|
||||||
@@ -1198,14 +1220,14 @@ pub fn getOffsetWidth(self: *Element, page: *Page) f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getOffsetTop(self: *Element, page: *Page) f64 {
|
pub fn getOffsetTop(self: *Element, page: *Page) f64 {
|
||||||
if (!self.checkVisibility(page)) {
|
if (!self.checkVisibilityCached(null, page)) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
return calculateDocumentPosition(self.asNode());
|
return calculateDocumentPosition(self.asNode());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getOffsetLeft(self: *Element, page: *Page) f64 {
|
pub fn getOffsetLeft(self: *Element, page: *Page) f64 {
|
||||||
if (!self.checkVisibility(page)) {
|
if (!self.checkVisibilityCached(null, page)) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
return calculateSiblingPosition(self.asNode());
|
return calculateSiblingPosition(self.asNode());
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ pub const Type = union(enum) {
|
|||||||
pop_state_event: *@import("event/PopStateEvent.zig"),
|
pop_state_event: *@import("event/PopStateEvent.zig"),
|
||||||
ui_event: *@import("event/UIEvent.zig"),
|
ui_event: *@import("event/UIEvent.zig"),
|
||||||
promise_rejection_event: *@import("event/PromiseRejectionEvent.zig"),
|
promise_rejection_event: *@import("event/PromiseRejectionEvent.zig"),
|
||||||
|
submit_event: *@import("event/SubmitEvent.zig"),
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Options = struct {
|
pub const Options = struct {
|
||||||
@@ -174,6 +175,7 @@ pub fn is(self: *Event, comptime T: type) ?*T {
|
|||||||
.page_transition_event => |e| return if (T == @import("event/PageTransitionEvent.zig")) e else null,
|
.page_transition_event => |e| return if (T == @import("event/PageTransitionEvent.zig")) e else null,
|
||||||
.pop_state_event => |e| return if (T == @import("event/PopStateEvent.zig")) e else null,
|
.pop_state_event => |e| return if (T == @import("event/PopStateEvent.zig")) e else null,
|
||||||
.promise_rejection_event => |e| return if (T == @import("event/PromiseRejectionEvent.zig")) e else null,
|
.promise_rejection_event => |e| return if (T == @import("event/PromiseRejectionEvent.zig")) e else null,
|
||||||
|
.submit_event => |e| return if (T == @import("event/SubmitEvent.zig")) e else null,
|
||||||
.ui_event => |e| {
|
.ui_event => |e| {
|
||||||
if (T == @import("event/UIEvent.zig")) {
|
if (T == @import("event/UIEvent.zig")) {
|
||||||
return e;
|
return e;
|
||||||
|
|||||||
@@ -93,12 +93,12 @@ pub fn init(callback: js.Function.Temp, options: ?ObserverInit, page: *Page) !*I
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *IntersectionObserver, shutdown: bool, session: *Session) void {
|
pub fn deinit(self: *IntersectionObserver, shutdown: bool, session: *Session) void {
|
||||||
if (shutdown) {
|
self._callback.release();
|
||||||
self._callback.release();
|
if ((comptime IS_DEBUG) and !shutdown) {
|
||||||
session.releaseArena(self._arena);
|
std.debug.assert(self._observing.items.len == 0);
|
||||||
} else if (comptime IS_DEBUG) {
|
|
||||||
std.debug.assert(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session.releaseArena(self._arena);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void {
|
pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void {
|
||||||
@@ -111,6 +111,7 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void
|
|||||||
|
|
||||||
// Register with page if this is our first observation
|
// Register with page if this is our first observation
|
||||||
if (self._observing.items.len == 0) {
|
if (self._observing.items.len == 0) {
|
||||||
|
page.js.strongRef(self);
|
||||||
try page.registerIntersectionObserver(self);
|
try page.registerIntersectionObserver(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,18 +146,22 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) voi
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self._observing.items.len == 0) {
|
||||||
|
page.js.safeWeakRef(self);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
|
pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
|
||||||
|
page.unregisterIntersectionObserver(self);
|
||||||
|
self._observing.clearRetainingCapacity();
|
||||||
self._previous_states.clearRetainingCapacity();
|
self._previous_states.clearRetainingCapacity();
|
||||||
|
|
||||||
for (self._pending_entries.items) |entry| {
|
for (self._pending_entries.items) |entry| {
|
||||||
entry.deinit(false, page._session);
|
entry.deinit(false, page._session);
|
||||||
}
|
}
|
||||||
self._pending_entries.clearRetainingCapacity();
|
self._pending_entries.clearRetainingCapacity();
|
||||||
|
page.js.safeWeakRef(self);
|
||||||
self._observing.clearRetainingCapacity();
|
|
||||||
page.unregisterIntersectionObserver(self);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {
|
pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {
|
||||||
@@ -358,6 +363,7 @@ pub const JsApi = struct {
|
|||||||
pub const name = "IntersectionObserver";
|
pub const name = "IntersectionObserver";
|
||||||
pub const prototype_chain = bridge.prototypeChain();
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
pub var class_id: bridge.ClassId = undefined;
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
pub const weak = true;
|
||||||
pub const finalizer = bridge.finalizer(IntersectionObserver.deinit);
|
pub const finalizer = bridge.finalizer(IntersectionObserver.deinit);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -86,12 +86,12 @@ pub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void {
|
pub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void {
|
||||||
if (shutdown) {
|
self._callback.release();
|
||||||
self._callback.release();
|
if ((comptime IS_DEBUG) and !shutdown) {
|
||||||
session.releaseArena(self._arena);
|
std.debug.assert(self._observing.items.len == 0);
|
||||||
} else if (comptime IS_DEBUG) {
|
|
||||||
std.debug.assert(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session.releaseArena(self._arena);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {
|
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {
|
||||||
@@ -158,6 +158,7 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
|
|||||||
|
|
||||||
// Register with page if this is our first observation
|
// Register with page if this is our first observation
|
||||||
if (self._observing.items.len == 0) {
|
if (self._observing.items.len == 0) {
|
||||||
|
page.js.strongRef(self);
|
||||||
try page.registerMutationObserver(self);
|
try page.registerMutationObserver(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,13 +169,13 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn disconnect(self: *MutationObserver, page: *Page) void {
|
pub fn disconnect(self: *MutationObserver, page: *Page) void {
|
||||||
|
page.unregisterMutationObserver(self);
|
||||||
|
self._observing.clearRetainingCapacity();
|
||||||
for (self._pending_records.items) |record| {
|
for (self._pending_records.items) |record| {
|
||||||
record.deinit(false, page._session);
|
record.deinit(false, page._session);
|
||||||
}
|
}
|
||||||
self._pending_records.clearRetainingCapacity();
|
self._pending_records.clearRetainingCapacity();
|
||||||
|
page.js.safeWeakRef(self);
|
||||||
self._observing.clearRetainingCapacity();
|
|
||||||
page.unregisterMutationObserver(self);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord {
|
pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord {
|
||||||
@@ -440,6 +441,7 @@ pub const JsApi = struct {
|
|||||||
pub const name = "MutationObserver";
|
pub const name = "MutationObserver";
|
||||||
pub const prototype_chain = bridge.prototypeChain();
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
pub var class_id: bridge.ClassId = undefined;
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
pub const weak = true;
|
||||||
pub const finalizer = bridge.finalizer(MutationObserver.deinit);
|
pub const finalizer = bridge.finalizer(MutationObserver.deinit);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1005,6 +1005,49 @@ pub fn getElementsByClassName(self: *Node, class_name: []const u8, page: *Page)
|
|||||||
return collections.NodeLive(.class_name).init(self, class_names.items, page);
|
return collections.NodeLive(.class_name).init(self, class_names.items, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shared implementation of replaceChildren for Element, Document, and DocumentFragment.
|
||||||
|
/// Validates all nodes, removes existing children, then appends new children.
|
||||||
|
pub fn replaceChildren(self: *Node, nodes: []const NodeOrText, page: *Page) !void {
|
||||||
|
// First pass: validate all nodes and collect them
|
||||||
|
// We need to collect because DocumentFragments contribute their children, not themselves
|
||||||
|
var children_to_add: std.ArrayList(*Node) = .empty;
|
||||||
|
|
||||||
|
for (nodes) |node_or_text| {
|
||||||
|
const child = try node_or_text.toNode(page);
|
||||||
|
|
||||||
|
// DocumentFragments contribute their children, not themselves
|
||||||
|
if (child.is(DocumentFragment)) |frag| {
|
||||||
|
var frag_it = frag.asNode().childrenIterator();
|
||||||
|
while (frag_it.next()) |frag_child| {
|
||||||
|
try validateNodeInsertion(self, frag_child);
|
||||||
|
try children_to_add.append(page.call_arena, frag_child);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try validateNodeInsertion(self, child);
|
||||||
|
try children_to_add.append(page.call_arena, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
page.domChanged();
|
||||||
|
|
||||||
|
// Remove all existing children
|
||||||
|
var it = self.childrenIterator();
|
||||||
|
while (it.next()) |child| {
|
||||||
|
page.removeNode(self, child, .{ .will_be_reconnected = false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append new children
|
||||||
|
const parent_is_connected = self.isConnected();
|
||||||
|
for (children_to_add.items) |child| {
|
||||||
|
var child_connected = false;
|
||||||
|
if (child._parent) |previous_parent| {
|
||||||
|
child_connected = child.isConnected();
|
||||||
|
page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
|
||||||
|
}
|
||||||
|
try page.appendNode(self, child, .{ .child_already_connected = child_connected });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Writes a JSON representation of the node and its children
|
// Writes a JSON representation of the node and its children
|
||||||
pub fn jsonStringify(self: *const Node, writer: *std.json.Stringify) !void {
|
pub fn jsonStringify(self: *const Node, writer: *std.json.Stringify) !void {
|
||||||
// stupid json api requires this to be const,
|
// stupid json api requires this to be const,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -19,16 +19,16 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const lp = @import("lightpanda");
|
const lp = @import("lightpanda");
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
const crypto = @import("../../sys/libcrypto.zig");
|
||||||
const crypto = @import("../../crypto.zig");
|
|
||||||
const DOMException = @import("DOMException.zig");
|
|
||||||
|
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
|
|
||||||
pub fn registerTypes() []const type {
|
const CryptoKey = @import("CryptoKey.zig");
|
||||||
return &.{ SubtleCrypto, CryptoKey };
|
|
||||||
}
|
const algorithm = @import("crypto/algorithm.zig");
|
||||||
|
const HMAC = @import("crypto/HMAC.zig");
|
||||||
|
const X25519 = @import("crypto/X25519.zig");
|
||||||
|
|
||||||
/// The SubtleCrypto interface of the Web Crypto API provides a number of low-level
|
/// The SubtleCrypto interface of the Web Crypto API provides a number of low-level
|
||||||
/// cryptographic functions.
|
/// cryptographic functions.
|
||||||
@@ -38,69 +38,36 @@ const SubtleCrypto = @This();
|
|||||||
/// Don't optimize away the type.
|
/// Don't optimize away the type.
|
||||||
_pad: bool = false,
|
_pad: bool = false,
|
||||||
|
|
||||||
const Algorithm = union(enum) {
|
|
||||||
/// For RSASSA-PKCS1-v1_5, RSA-PSS, or RSA-OAEP: pass an RsaHashedKeyGenParams object.
|
|
||||||
rsa_hashed_key_gen: RsaHashedKeyGen,
|
|
||||||
/// For HMAC: pass an HmacKeyGenParams object.
|
|
||||||
hmac_key_gen: HmacKeyGen,
|
|
||||||
/// Can be Ed25519 or X25519.
|
|
||||||
name: []const u8,
|
|
||||||
/// Can be Ed25519 or X25519.
|
|
||||||
object: struct { name: []const u8 },
|
|
||||||
|
|
||||||
/// https://developer.mozilla.org/en-US/docs/Web/API/RsaHashedKeyGenParams
|
|
||||||
const RsaHashedKeyGen = struct {
|
|
||||||
name: []const u8,
|
|
||||||
/// This should be at least 2048.
|
|
||||||
/// Some organizations are now recommending that it should be 4096.
|
|
||||||
modulusLength: u32,
|
|
||||||
publicExponent: js.TypedArray(u8),
|
|
||||||
hash: union(enum) {
|
|
||||||
string: []const u8,
|
|
||||||
object: struct { name: []const u8 },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// https://developer.mozilla.org/en-US/docs/Web/API/HmacKeyGenParams
|
|
||||||
const HmacKeyGen = struct {
|
|
||||||
/// Always HMAC.
|
|
||||||
name: []const u8,
|
|
||||||
/// Its also possible to pass this in an object.
|
|
||||||
hash: union(enum) {
|
|
||||||
string: []const u8,
|
|
||||||
object: struct { name: []const u8 },
|
|
||||||
},
|
|
||||||
/// If omitted, default is the block size of the chosen hash function.
|
|
||||||
length: ?usize,
|
|
||||||
};
|
|
||||||
/// Alias.
|
|
||||||
const HmacImport = HmacKeyGen;
|
|
||||||
|
|
||||||
const EcdhKeyDeriveParams = struct {
|
|
||||||
/// Can be Ed25519 or X25519.
|
|
||||||
name: []const u8,
|
|
||||||
public: *const CryptoKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Algorithm for deriveBits() and deriveKey().
|
|
||||||
const DeriveBits = union(enum) {
|
|
||||||
ecdh_or_x25519: EcdhKeyDeriveParams,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Generate a new key (for symmetric algorithms) or key pair (for public-key algorithms).
|
/// Generate a new key (for symmetric algorithms) or key pair (for public-key algorithms).
|
||||||
pub fn generateKey(
|
pub fn generateKey(
|
||||||
_: *const SubtleCrypto,
|
_: *const SubtleCrypto,
|
||||||
algorithm: Algorithm,
|
algo: algorithm.Init,
|
||||||
extractable: bool,
|
extractable: bool,
|
||||||
key_usages: []const []const u8,
|
key_usages: []const []const u8,
|
||||||
page: *Page,
|
page: *Page,
|
||||||
) !js.Promise {
|
) !js.Promise {
|
||||||
const key_or_pair = CryptoKey.init(algorithm, extractable, key_usages, page) catch {
|
switch (algo) {
|
||||||
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.SyntaxError } });
|
.hmac_key_gen => |params| return HMAC.init(params, extractable, key_usages, page),
|
||||||
};
|
.name => |name| {
|
||||||
|
if (std.mem.eql(u8, "X25519", name)) {
|
||||||
|
return X25519.init(extractable, key_usages, page);
|
||||||
|
}
|
||||||
|
|
||||||
return page.js.local.?.resolvePromise(key_or_pair);
|
log.warn(.not_implemented, "generateKey", .{ .name = name });
|
||||||
|
},
|
||||||
|
.object => |object| {
|
||||||
|
// Ditto.
|
||||||
|
const name = object.name;
|
||||||
|
if (std.mem.eql(u8, "X25519", name)) {
|
||||||
|
return X25519.init(extractable, key_usages, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn(.not_implemented, "generateKey", .{ .name = name });
|
||||||
|
},
|
||||||
|
else => log.warn(.not_implemented, "generateKey", .{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.SyntaxError } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Exports a key: that is, it takes as input a CryptoKey object and gives you
|
/// Exports a key: that is, it takes as input a CryptoKey object and gives you
|
||||||
@@ -133,16 +100,23 @@ pub fn exportKey(
|
|||||||
/// Derive a secret key from a master key.
|
/// Derive a secret key from a master key.
|
||||||
pub fn deriveBits(
|
pub fn deriveBits(
|
||||||
_: *const SubtleCrypto,
|
_: *const SubtleCrypto,
|
||||||
algorithm: Algorithm.DeriveBits,
|
algo: algorithm.Derive,
|
||||||
base_key: *const CryptoKey, // Private key.
|
base_key: *const CryptoKey, // Private key.
|
||||||
length: usize,
|
length: usize,
|
||||||
page: *Page,
|
page: *Page,
|
||||||
) !js.Promise {
|
) !js.Promise {
|
||||||
return switch (algorithm) {
|
return switch (algo) {
|
||||||
.ecdh_or_x25519 => |p| {
|
.ecdh_or_x25519 => |params| {
|
||||||
const name = p.name;
|
const name = params.name;
|
||||||
if (std.mem.eql(u8, name, "X25519")) {
|
if (std.mem.eql(u8, name, "X25519")) {
|
||||||
return page.js.local.?.resolvePromise(base_key.deriveBitsX25519(p.public, length, page));
|
const result = X25519.deriveBits(base_key, params.public, length, page) catch |err| switch (err) {
|
||||||
|
error.InvalidAccessError => return page.js.local.?.rejectPromise(.{
|
||||||
|
.dom_exception = .{ .err = error.InvalidAccessError },
|
||||||
|
}),
|
||||||
|
else => return err,
|
||||||
|
};
|
||||||
|
|
||||||
|
return page.js.local.?.resolvePromise(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, name, "ECDH")) {
|
if (std.mem.eql(u8, name, "ECDH")) {
|
||||||
@@ -154,48 +128,18 @@ pub fn deriveBits(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const SignatureAlgorithm = union(enum) {
|
|
||||||
string: []const u8,
|
|
||||||
object: struct { name: []const u8 },
|
|
||||||
|
|
||||||
pub fn isHMAC(self: SignatureAlgorithm) bool {
|
|
||||||
const name = switch (self) {
|
|
||||||
.string => |string| string,
|
|
||||||
.object => |object| object.name,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (name.len < 4) return false;
|
|
||||||
const hmac: u32 = @bitCast([4]u8{ 'H', 'M', 'A', 'C' });
|
|
||||||
return @as(u32, @bitCast(name[0..4].*)) == hmac;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Generate a digital signature.
|
/// Generate a digital signature.
|
||||||
pub fn sign(
|
pub fn sign(
|
||||||
_: *const SubtleCrypto,
|
_: *const SubtleCrypto,
|
||||||
/// This can either be provided as string or object.
|
|
||||||
/// We can't use the `Algorithm` type defined before though since there
|
|
||||||
/// are couple of changes between the two.
|
|
||||||
/// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign#algorithm
|
/// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign#algorithm
|
||||||
algorithm: SignatureAlgorithm,
|
algo: algorithm.Sign,
|
||||||
key: *CryptoKey,
|
key: *CryptoKey,
|
||||||
data: []const u8, // ArrayBuffer.
|
data: []const u8, // ArrayBuffer.
|
||||||
page: *Page,
|
page: *Page,
|
||||||
) !js.Promise {
|
) !js.Promise {
|
||||||
return switch (key._type) {
|
return switch (key._type) {
|
||||||
.hmac => {
|
// Call sign for HMAC.
|
||||||
// Verify algorithm.
|
.hmac => return HMAC.sign(algo, key, data, page),
|
||||||
if (!algorithm.isHMAC()) {
|
|
||||||
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call sign for HMAC.
|
|
||||||
const result = key.signHMAC(data, page) catch {
|
|
||||||
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
|
||||||
};
|
|
||||||
|
|
||||||
return page.js.local.?.resolvePromise(result);
|
|
||||||
},
|
|
||||||
else => {
|
else => {
|
||||||
log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type });
|
log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type });
|
||||||
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
||||||
@@ -206,452 +150,43 @@ pub fn sign(
|
|||||||
/// Verify a digital signature.
|
/// Verify a digital signature.
|
||||||
pub fn verify(
|
pub fn verify(
|
||||||
_: *const SubtleCrypto,
|
_: *const SubtleCrypto,
|
||||||
algorithm: SignatureAlgorithm,
|
algo: algorithm.Sign,
|
||||||
key: *const CryptoKey,
|
key: *const CryptoKey,
|
||||||
signature: []const u8, // ArrayBuffer.
|
signature: []const u8, // ArrayBuffer.
|
||||||
data: []const u8, // ArrayBuffer.
|
data: []const u8, // ArrayBuffer.
|
||||||
page: *Page,
|
page: *Page,
|
||||||
) !js.Promise {
|
) !js.Promise {
|
||||||
if (!algorithm.isHMAC()) {
|
if (!algo.isHMAC()) {
|
||||||
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
||||||
}
|
}
|
||||||
|
|
||||||
return switch (key._type) {
|
return switch (key._type) {
|
||||||
.hmac => key.verifyHMAC(signature, data, page),
|
.hmac => HMAC.verify(key, signature, data, page),
|
||||||
else => page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }),
|
else => page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn digest(_: *const SubtleCrypto, algorithm: []const u8, data: js.TypedArray(u8), page: *Page) !js.Promise {
|
/// Generates a digest of the given data, using the specified hash function.
|
||||||
|
pub fn digest(_: *const SubtleCrypto, algo: []const u8, data: js.TypedArray(u8), page: *Page) !js.Promise {
|
||||||
const local = page.js.local.?;
|
const local = page.js.local.?;
|
||||||
if (algorithm.len > 10) {
|
|
||||||
|
if (algo.len > 10) {
|
||||||
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||||
}
|
}
|
||||||
const normalized = std.ascii.lowerString(&page.buf, algorithm);
|
|
||||||
if (std.mem.eql(u8, normalized, "sha-1")) {
|
|
||||||
const Sha1 = std.crypto.hash.Sha1;
|
|
||||||
Sha1.hash(data.values, page.buf[0..Sha1.digest_length], .{});
|
|
||||||
return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha1.digest_length] });
|
|
||||||
}
|
|
||||||
if (std.mem.eql(u8, normalized, "sha-256")) {
|
|
||||||
const Sha256 = std.crypto.hash.sha2.Sha256;
|
|
||||||
Sha256.hash(data.values, page.buf[0..Sha256.digest_length], .{});
|
|
||||||
return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha256.digest_length] });
|
|
||||||
}
|
|
||||||
if (std.mem.eql(u8, normalized, "sha-384")) {
|
|
||||||
const Sha384 = std.crypto.hash.sha2.Sha384;
|
|
||||||
Sha384.hash(data.values, page.buf[0..Sha384.digest_length], .{});
|
|
||||||
return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha384.digest_length] });
|
|
||||||
}
|
|
||||||
if (std.mem.eql(u8, normalized, "sha-512")) {
|
|
||||||
const Sha512 = std.crypto.hash.sha2.Sha512;
|
|
||||||
Sha512.hash(data.values, page.buf[0..Sha512.digest_length], .{});
|
|
||||||
return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha512.digest_length] });
|
|
||||||
}
|
|
||||||
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the desired digest by its name.
|
const normalized = std.ascii.upperString(&page.buf, algo);
|
||||||
fn findDigest(name: []const u8) error{Invalid}!*const crypto.EVP_MD {
|
const digest_type = crypto.findDigest(normalized) catch {
|
||||||
if (std.mem.eql(u8, "SHA-256", name)) {
|
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||||
return crypto.EVP_sha256();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, "SHA-384", name)) {
|
|
||||||
return crypto.EVP_sha384();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, "SHA-512", name)) {
|
|
||||||
return crypto.EVP_sha512();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, "SHA-1", name)) {
|
|
||||||
return crypto.EVP_sha1();
|
|
||||||
}
|
|
||||||
|
|
||||||
return error.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
const KeyOrPair = union(enum) { key: *CryptoKey, pair: CryptoKeyPair };
|
|
||||||
|
|
||||||
/// https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair
|
|
||||||
const CryptoKeyPair = struct {
|
|
||||||
privateKey: *CryptoKey,
|
|
||||||
publicKey: *CryptoKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Represents a cryptographic key obtained from one of the SubtleCrypto methods
|
|
||||||
/// generateKey(), deriveKey(), importKey(), or unwrapKey().
|
|
||||||
pub const CryptoKey = struct {
|
|
||||||
/// Algorithm being used.
|
|
||||||
_type: Type,
|
|
||||||
/// Whether the key is extractable.
|
|
||||||
_extractable: bool,
|
|
||||||
/// Bit flags of `usages`; see `Usages` type.
|
|
||||||
_usages: u8,
|
|
||||||
/// Raw bytes of key.
|
|
||||||
_key: []const u8,
|
|
||||||
/// Different algorithms may use different data structures;
|
|
||||||
/// this union can be used for such situations. Active field is understood
|
|
||||||
/// from `_type`.
|
|
||||||
_vary: extern union {
|
|
||||||
/// Used by HMAC.
|
|
||||||
digest: *const crypto.EVP_MD,
|
|
||||||
/// Used by asymmetric algorithms (X25519, Ed25519).
|
|
||||||
pkey: *crypto.EVP_PKEY,
|
|
||||||
},
|
|
||||||
|
|
||||||
pub const Type = enum(u8) { hmac, rsa, x25519 };
|
|
||||||
|
|
||||||
/// Changing the names of fields would affect bitmask creation.
|
|
||||||
pub const Usages = struct {
|
|
||||||
// zig fmt: off
|
|
||||||
pub const encrypt = 0x001;
|
|
||||||
pub const decrypt = 0x002;
|
|
||||||
pub const sign = 0x004;
|
|
||||||
pub const verify = 0x008;
|
|
||||||
pub const deriveKey = 0x010;
|
|
||||||
pub const deriveBits = 0x020;
|
|
||||||
pub const wrapKey = 0x040;
|
|
||||||
pub const unwrapKey = 0x080;
|
|
||||||
// zig fmt: on
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(
|
const bytes = data.values;
|
||||||
algorithm: Algorithm,
|
const out = page.buf[0..crypto.EVP_MAX_MD_SIZE];
|
||||||
extractable: bool,
|
var out_size: c_uint = 0;
|
||||||
key_usages: []const []const u8,
|
const result = crypto.EVP_Digest(bytes.ptr, bytes.len, out, &out_size, digest_type, null);
|
||||||
page: *Page,
|
lp.assert(result == 1, "SubtleCrypto.digest", .{ .algo = algo });
|
||||||
) !KeyOrPair {
|
|
||||||
return switch (algorithm) {
|
|
||||||
.hmac_key_gen => |hmac| initHMAC(hmac, extractable, key_usages, page),
|
|
||||||
.name => |name| {
|
|
||||||
if (std.mem.eql(u8, "X25519", name)) {
|
|
||||||
return initX25519(extractable, key_usages, page);
|
|
||||||
}
|
|
||||||
log.warn(.not_implemented, "CryptoKey.init", .{ .name = name });
|
|
||||||
return error.NotSupported;
|
|
||||||
},
|
|
||||||
.object => |object| {
|
|
||||||
// Ditto.
|
|
||||||
const name = object.name;
|
|
||||||
if (std.mem.eql(u8, "X25519", name)) {
|
|
||||||
return initX25519(extractable, key_usages, page);
|
|
||||||
}
|
|
||||||
log.warn(.not_implemented, "CryptoKey.init", .{ .name = name });
|
|
||||||
return error.NotSupported;
|
|
||||||
},
|
|
||||||
else => {
|
|
||||||
log.warn(.not_implemented, "CryptoKey.init", .{ .algorithm = algorithm });
|
|
||||||
return error.NotSupported;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fn canSign(self: *const CryptoKey) bool {
|
return local.resolvePromise(js.ArrayBuffer{ .values = out[0..out_size] });
|
||||||
return self._usages & Usages.sign != 0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
inline fn canVerify(self: *const CryptoKey) bool {
|
|
||||||
return self._usages & Usages.verify != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fn canDeriveBits(self: *const CryptoKey) bool {
|
|
||||||
return self._usages & Usages.deriveBits != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fn canExportKey(self: *const CryptoKey) bool {
|
|
||||||
return self._extractable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Only valid for HMAC.
|
|
||||||
inline fn getDigest(self: *const CryptoKey) *const crypto.EVP_MD {
|
|
||||||
return self._vary.digest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Only valid for asymmetric algorithms (X25519, Ed25519).
|
|
||||||
inline fn getKeyObject(self: *const CryptoKey) *crypto.EVP_PKEY {
|
|
||||||
return self._vary.pkey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// HMAC.
|
|
||||||
|
|
||||||
fn initHMAC(
|
|
||||||
algorithm: Algorithm.HmacKeyGen,
|
|
||||||
extractable: bool,
|
|
||||||
key_usages: []const []const u8,
|
|
||||||
page: *Page,
|
|
||||||
) !KeyOrPair {
|
|
||||||
const hash = switch (algorithm.hash) {
|
|
||||||
.string => |str| str,
|
|
||||||
.object => |obj| obj.name,
|
|
||||||
};
|
|
||||||
// Find digest.
|
|
||||||
const d = try findDigest(hash);
|
|
||||||
|
|
||||||
// We need at least a single usage.
|
|
||||||
if (key_usages.len == 0) {
|
|
||||||
return error.SyntaxError;
|
|
||||||
}
|
|
||||||
// Calculate usages mask.
|
|
||||||
const decls = @typeInfo(Usages).@"struct".decls;
|
|
||||||
var usages_mask: u8 = 0;
|
|
||||||
iter_usages: for (key_usages) |usage| {
|
|
||||||
inline for (decls) |decl| {
|
|
||||||
if (std.mem.eql(u8, decl.name, usage)) {
|
|
||||||
usages_mask |= @field(Usages, decl.name);
|
|
||||||
continue :iter_usages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Unknown usage if got here.
|
|
||||||
return error.SyntaxError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const block_size: usize = blk: {
|
|
||||||
// Caller provides this in bits, not bytes.
|
|
||||||
if (algorithm.length) |length| {
|
|
||||||
break :blk length / 8;
|
|
||||||
}
|
|
||||||
// Prefer block size of the hash function instead.
|
|
||||||
break :blk crypto.EVP_MD_block_size(d);
|
|
||||||
};
|
|
||||||
|
|
||||||
const key = try page.arena.alloc(u8, block_size);
|
|
||||||
errdefer page.arena.free(key);
|
|
||||||
|
|
||||||
// HMAC is simply CSPRNG.
|
|
||||||
const res = crypto.RAND_bytes(key.ptr, key.len);
|
|
||||||
lp.assert(res == 1, "SubtleCrypto.initHMAC", .{ .res = res });
|
|
||||||
|
|
||||||
const crypto_key = try page._factory.create(CryptoKey{
|
|
||||||
._type = .hmac,
|
|
||||||
._extractable = extractable,
|
|
||||||
._usages = usages_mask,
|
|
||||||
._key = key,
|
|
||||||
._vary = .{ .digest = d },
|
|
||||||
});
|
|
||||||
|
|
||||||
return .{ .key = crypto_key };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn signHMAC(self: *const CryptoKey, data: []const u8, page: *Page) !js.ArrayBuffer {
|
|
||||||
if (!self.canSign()) {
|
|
||||||
return error.InvalidAccessError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = try page.call_arena.alloc(u8, crypto.EVP_MD_size(self.getDigest()));
|
|
||||||
errdefer page.call_arena.free(buffer);
|
|
||||||
var out_len: u32 = 0;
|
|
||||||
// Try to sign.
|
|
||||||
const signed = crypto.HMAC(
|
|
||||||
self.getDigest(),
|
|
||||||
@ptrCast(self._key.ptr),
|
|
||||||
self._key.len,
|
|
||||||
data.ptr,
|
|
||||||
data.len,
|
|
||||||
buffer.ptr,
|
|
||||||
&out_len,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (signed != null) {
|
|
||||||
return js.ArrayBuffer{ .values = buffer[0..out_len] };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not DOM exception, failed on our side.
|
|
||||||
return error.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verifyHMAC(
|
|
||||||
self: *const CryptoKey,
|
|
||||||
signature: []const u8,
|
|
||||||
data: []const u8,
|
|
||||||
page: *Page,
|
|
||||||
) !js.Promise {
|
|
||||||
if (!self.canVerify()) {
|
|
||||||
return error.InvalidAccessError;
|
|
||||||
}
|
|
||||||
|
|
||||||
var buffer: [crypto.EVP_MAX_MD_BLOCK_SIZE]u8 = undefined;
|
|
||||||
var out_len: u32 = 0;
|
|
||||||
// Try to sign.
|
|
||||||
const signed = crypto.HMAC(
|
|
||||||
self.getDigest(),
|
|
||||||
@ptrCast(self._key.ptr),
|
|
||||||
self._key.len,
|
|
||||||
data.ptr,
|
|
||||||
data.len,
|
|
||||||
&buffer,
|
|
||||||
&out_len,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (signed != null) {
|
|
||||||
// CRYPTO_memcmp compare in constant time so prohibits time-based attacks.
|
|
||||||
const res = crypto.CRYPTO_memcmp(signed, @ptrCast(signature.ptr), signature.len);
|
|
||||||
return page.js.local.?.resolvePromise(res == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return page.js.local.?.resolvePromise(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// X25519.
|
|
||||||
|
|
||||||
/// Create a pair of X25519.
|
|
||||||
fn initX25519(
|
|
||||||
extractable: bool,
|
|
||||||
key_usages: []const []const u8,
|
|
||||||
page: *Page,
|
|
||||||
) !KeyOrPair {
|
|
||||||
// This code has too many allocations here and there, might be nice to
|
|
||||||
// gather them together with a single alloc call. Not sure if factory
|
|
||||||
// pattern is suitable for it though.
|
|
||||||
|
|
||||||
// Calculate usages; only matters for private key.
|
|
||||||
// Only deriveKey() and deriveBits() be used for X25519.
|
|
||||||
if (key_usages.len == 0) {
|
|
||||||
return error.SyntaxError;
|
|
||||||
}
|
|
||||||
var mask: u8 = 0;
|
|
||||||
iter_usages: for (key_usages) |usage| {
|
|
||||||
inline for ([_][]const u8{ "deriveKey", "deriveBits" }) |name| {
|
|
||||||
if (std.mem.eql(u8, name, usage)) {
|
|
||||||
mask |= @field(Usages, name);
|
|
||||||
continue :iter_usages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Unknown usage if got here.
|
|
||||||
return error.SyntaxError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const public_value = try page.arena.alloc(u8, crypto.X25519_PUBLIC_VALUE_LEN);
|
|
||||||
errdefer page.arena.free(public_value);
|
|
||||||
|
|
||||||
const private_key = try page.arena.alloc(u8, crypto.X25519_PRIVATE_KEY_LEN);
|
|
||||||
errdefer page.arena.free(private_key);
|
|
||||||
|
|
||||||
// There's no info about whether this can fail; so I assume it cannot.
|
|
||||||
crypto.X25519_keypair(@ptrCast(public_value), @ptrCast(private_key));
|
|
||||||
|
|
||||||
// Create EVP_PKEY for public key.
|
|
||||||
// Seems we can use `EVP_PKEY_from_raw_private_key` for this, Chrome
|
|
||||||
// prefer not to, yet BoringSSL added it and recommends instead of what
|
|
||||||
// we're doing currently.
|
|
||||||
const public_pkey = crypto.EVP_PKEY_new_raw_public_key(
|
|
||||||
crypto.EVP_PKEY_X25519,
|
|
||||||
null,
|
|
||||||
public_value.ptr,
|
|
||||||
public_value.len,
|
|
||||||
);
|
|
||||||
if (public_pkey == null) {
|
|
||||||
return error.OutOfMemory;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create EVP_PKEY for private key.
|
|
||||||
// Seems we can use `EVP_PKEY_from_raw_private_key` for this, Chrome
|
|
||||||
// prefer not to, yet BoringSSL added it and recommends instead of what
|
|
||||||
// we're doing currently.
|
|
||||||
const private_pkey = crypto.EVP_PKEY_new_raw_private_key(
|
|
||||||
crypto.EVP_PKEY_X25519,
|
|
||||||
null,
|
|
||||||
private_key.ptr,
|
|
||||||
private_key.len,
|
|
||||||
);
|
|
||||||
if (private_pkey == null) {
|
|
||||||
return error.OutOfMemory;
|
|
||||||
}
|
|
||||||
|
|
||||||
const private = try page._factory.create(CryptoKey{
|
|
||||||
._type = .x25519,
|
|
||||||
._extractable = extractable,
|
|
||||||
._usages = mask,
|
|
||||||
._key = private_key,
|
|
||||||
._vary = .{ .pkey = private_pkey.? },
|
|
||||||
});
|
|
||||||
errdefer page._factory.destroy(private);
|
|
||||||
|
|
||||||
const public = try page._factory.create(CryptoKey{
|
|
||||||
._type = .x25519,
|
|
||||||
// Public keys are always extractable.
|
|
||||||
._extractable = true,
|
|
||||||
// Always empty for public key.
|
|
||||||
._usages = 0,
|
|
||||||
._key = public_value,
|
|
||||||
._vary = .{ .pkey = public_pkey.? },
|
|
||||||
});
|
|
||||||
errdefer page._factory.destroy(public);
|
|
||||||
|
|
||||||
return .{ .pair = .{ .privateKey = private, .publicKey = public } };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deriveBitsX25519(
|
|
||||||
private: *const CryptoKey,
|
|
||||||
public: *const CryptoKey,
|
|
||||||
length_in_bits: usize,
|
|
||||||
page: *Page,
|
|
||||||
) !js.ArrayBuffer {
|
|
||||||
if (!private.canDeriveBits()) {
|
|
||||||
return error.InvalidAccessError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maybe_ctx = crypto.EVP_PKEY_CTX_new(private.getKeyObject(), null);
|
|
||||||
if (maybe_ctx) |ctx| {
|
|
||||||
// Context is valid, free it on failure.
|
|
||||||
errdefer crypto.EVP_PKEY_CTX_free(ctx);
|
|
||||||
|
|
||||||
// Init derive operation and set public key as peer.
|
|
||||||
if (crypto.EVP_PKEY_derive_init(ctx) != 1 or
|
|
||||||
crypto.EVP_PKEY_derive_set_peer(ctx, public.getKeyObject()) != 1)
|
|
||||||
{
|
|
||||||
// Failed on our end.
|
|
||||||
return error.Internal;
|
|
||||||
}
|
|
||||||
|
|
||||||
const derived_key = try page.call_arena.alloc(u8, 32);
|
|
||||||
errdefer page.call_arena.free(derived_key);
|
|
||||||
|
|
||||||
var out_key_len: usize = derived_key.len;
|
|
||||||
const result = crypto.EVP_PKEY_derive(ctx, derived_key.ptr, &out_key_len);
|
|
||||||
if (result != 1) {
|
|
||||||
// Failed on our end.
|
|
||||||
return error.Internal;
|
|
||||||
}
|
|
||||||
// Sanity check.
|
|
||||||
lp.assert(derived_key.len == out_key_len, "SubtleCrypto.deriveBitsX25519", .{});
|
|
||||||
|
|
||||||
// Length is in bits, convert to byte length.
|
|
||||||
const length = (length_in_bits / 8) + (7 + (length_in_bits % 8)) / 8;
|
|
||||||
// Truncate the slice to specified length.
|
|
||||||
// Same as `derived_key`.
|
|
||||||
const tailored = blk: {
|
|
||||||
if (length > derived_key.len) {
|
|
||||||
return error.LengthTooLong;
|
|
||||||
}
|
|
||||||
break :blk derived_key[0..length];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Zero any "unused bits" in the final byte.
|
|
||||||
const remainder_bits: u3 = @intCast(length_in_bits % 8);
|
|
||||||
if (remainder_bits != 0) {
|
|
||||||
tailored[tailored.len - 1] &= ~(@as(u8, 0xFF) >> remainder_bits);
|
|
||||||
}
|
|
||||||
|
|
||||||
return js.ArrayBuffer{ .values = tailored };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Failed on our end.
|
|
||||||
return error.Internal;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const JsApi = struct {
|
|
||||||
pub const bridge = js.Bridge(CryptoKey);
|
|
||||||
|
|
||||||
pub const Meta = struct {
|
|
||||||
pub const name = "CryptoKey";
|
|
||||||
|
|
||||||
pub var class_id: bridge.ClassId = undefined;
|
|
||||||
pub const prototype_chain = bridge.prototypeChain();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
pub const bridge = js.Bridge(SubtleCrypto);
|
pub const bridge = js.Bridge(SubtleCrypto);
|
||||||
|
|||||||
@@ -37,6 +37,14 @@ pub const resolve = @import("../URL.zig").resolve;
|
|||||||
pub const eqlDocument = @import("../URL.zig").eqlDocument;
|
pub const eqlDocument = @import("../URL.zig").eqlDocument;
|
||||||
|
|
||||||
pub fn init(url: [:0]const u8, base_: ?[:0]const u8, page: *Page) !*URL {
|
pub fn init(url: [:0]const u8, base_: ?[:0]const u8, page: *Page) !*URL {
|
||||||
|
const arena = page.arena;
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, url, "about:blank")) {
|
||||||
|
return page._factory.create(URL{
|
||||||
|
._raw = "about:blank",
|
||||||
|
._arena = arena,
|
||||||
|
});
|
||||||
|
}
|
||||||
const url_is_absolute = @import("../URL.zig").isCompleteHTTPUrl(url);
|
const url_is_absolute = @import("../URL.zig").isCompleteHTTPUrl(url);
|
||||||
|
|
||||||
const base = if (base_) |b| blk: {
|
const base = if (base_) |b| blk: {
|
||||||
@@ -53,7 +61,6 @@ pub fn init(url: [:0]const u8, base_: ?[:0]const u8, page: *Page) !*URL {
|
|||||||
return error.TypeError;
|
return error.TypeError;
|
||||||
} else page.url;
|
} else page.url;
|
||||||
|
|
||||||
const arena = page.arena;
|
|
||||||
const raw = try resolve(arena, base, url, .{ .always_dupe = true });
|
const raw = try resolve(arena, base, url, .{ .always_dupe = true });
|
||||||
|
|
||||||
return page._factory.create(URL{
|
return page._factory.create(URL{
|
||||||
|
|||||||
@@ -285,23 +285,23 @@ pub fn queueMicrotask(_: *Window, cb: js.Function, page: *Page) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn clearTimeout(self: *Window, id: u32) void {
|
pub fn clearTimeout(self: *Window, id: u32) void {
|
||||||
var sc = self._timers.get(id) orelse return;
|
var sc = self._timers.fetchRemove(id) orelse return;
|
||||||
sc.removed = true;
|
sc.value.removed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clearInterval(self: *Window, id: u32) void {
|
pub fn clearInterval(self: *Window, id: u32) void {
|
||||||
var sc = self._timers.get(id) orelse return;
|
var sc = self._timers.fetchRemove(id) orelse return;
|
||||||
sc.removed = true;
|
sc.value.removed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clearImmediate(self: *Window, id: u32) void {
|
pub fn clearImmediate(self: *Window, id: u32) void {
|
||||||
var sc = self._timers.get(id) orelse return;
|
var sc = self._timers.fetchRemove(id) orelse return;
|
||||||
sc.removed = true;
|
sc.value.removed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cancelAnimationFrame(self: *Window, id: u32) void {
|
pub fn cancelAnimationFrame(self: *Window, id: u32) void {
|
||||||
var sc = self._timers.get(id) orelse return;
|
var sc = self._timers.fetchRemove(id) orelse return;
|
||||||
sc.removed = true;
|
sc.value.removed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RequestIdleCallbackOpts = struct {
|
const RequestIdleCallbackOpts = struct {
|
||||||
@@ -319,8 +319,8 @@ pub fn requestIdleCallback(self: *Window, cb: js.Function.Temp, opts_: ?RequestI
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn cancelIdleCallback(self: *Window, id: u32) void {
|
pub fn cancelIdleCallback(self: *Window, id: u32) void {
|
||||||
var sc = self._timers.get(id) orelse return;
|
var sc = self._timers.fetchRemove(id) orelse return;
|
||||||
sc.removed = true;
|
sc.value.removed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
|
pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
|
||||||
@@ -704,7 +704,6 @@ const ScheduleCallback = struct {
|
|||||||
const window = page.window;
|
const window = page.window;
|
||||||
|
|
||||||
if (self.removed) {
|
if (self.removed) {
|
||||||
_ = window._timers.remove(self.timer_id);
|
|
||||||
self.deinit();
|
self.deinit();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const Filters = union(Mode) {
|
|||||||
selected_options,
|
selected_options,
|
||||||
links,
|
links,
|
||||||
anchors,
|
anchors,
|
||||||
form: *Form,
|
form: struct { form: *Form, form_id: ?[]const u8 },
|
||||||
|
|
||||||
fn TypeOf(comptime mode: Mode) type {
|
fn TypeOf(comptime mode: Mode) type {
|
||||||
@setEvalBranchQuota(2000);
|
@setEvalBranchQuota(2000);
|
||||||
@@ -304,9 +304,13 @@ pub fn NodeLive(comptime mode: Mode) type {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.getAttributeSafe(comptime .wrap("form"))) |form_attr| {
|
if (self._filter.form_id) |form_id| {
|
||||||
const form_id = self._filter.asElement().getAttributeSafe(comptime .wrap("id")) orelse return false;
|
if (el.getAttributeSafe(comptime .wrap("form"))) |element_form_attr| {
|
||||||
return std.mem.eql(u8, form_attr, form_id);
|
return std.mem.eql(u8, element_form_attr, form_id);
|
||||||
|
}
|
||||||
|
} else if (el.hasAttributeSafe(comptime .wrap("form"))) {
|
||||||
|
// Form has no id, element explicitly references another form
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No form attribute - match if descendant of our form
|
// No form attribute - match if descendant of our form
|
||||||
@@ -324,7 +328,7 @@ pub fn NodeLive(comptime mode: Mode) type {
|
|||||||
// This trades one O(form_size) reverse walk for N O(depth) ancestor
|
// This trades one O(form_size) reverse walk for N O(depth) ancestor
|
||||||
// checks, where N = number of controls. For forms with many nested
|
// checks, where N = number of controls. For forms with many nested
|
||||||
// controls, this could be significantly faster.
|
// controls, this could be significantly faster.
|
||||||
return self._filter.asNode().contains(node);
|
return self._filter.form.asNode().contains(node);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user