mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 07:03:29 +00:00
Compare commits
407 Commits
nikneym/mo
...
20314fccec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20314fccec | ||
|
|
018e95bea7 | ||
|
|
c9dc4ef57a | ||
|
|
6c9d013e20 | ||
|
|
d2d10d5db4 | ||
|
|
37a8a24528 | ||
|
|
d0b83c674c | ||
|
|
b58ff2c869 | ||
|
|
b2e41837d9 | ||
|
|
2e6ec1e23b | ||
|
|
7808d12de2 | ||
|
|
1015fc09ee | ||
|
|
1c37b1c70e | ||
|
|
28ec8d4b94 | ||
|
|
6e42df2e71 | ||
|
|
6b924e8a4c | ||
|
|
80ae3c9fc6 | ||
|
|
2422c8718c | ||
|
|
b5ef8418a6 | ||
|
|
8d4cf400ce | ||
|
|
c6a0368c61 | ||
|
|
033eb82ae5 | ||
|
|
2d14452dda | ||
|
|
a69164b482 | ||
|
|
d4d35670a0 | ||
|
|
b40e7ece91 | ||
|
|
9c4367b26e | ||
|
|
0eb639ac76 | ||
|
|
9778eed1ed | ||
|
|
8b4ffeb911 | ||
|
|
b55b9bba0a | ||
|
|
82a45253de | ||
|
|
4c957041e2 | ||
|
|
b8f9598de3 | ||
|
|
907bd33d87 | ||
|
|
e9b08f19cf | ||
|
|
f97697535f | ||
|
|
e80c8d5bff | ||
|
|
70a009a52b | ||
|
|
8ab9364f19 | ||
|
|
186655e614 | ||
|
|
43958b81f8 | ||
|
|
2d8a95946a | ||
|
|
a7c3bad9ad | ||
|
|
7d39bc979f | ||
|
|
d60d3ebaac | ||
|
|
ba66b7c5db | ||
|
|
8342f0c394 | ||
|
|
69884b9d8d | ||
|
|
c568a75599 | ||
|
|
9deb5249a9 | ||
|
|
fb6fbffe3f | ||
|
|
510c61cc20 | ||
|
|
6915738e02 | ||
|
|
4f62cc833b | ||
|
|
46ffb801db | ||
|
|
d2065f713f | ||
|
|
6f8c3abb55 | ||
|
|
163a0e8b70 | ||
|
|
ca3efb3ad9 | ||
|
|
4468932346 | ||
|
|
9a03ba61c5 | ||
|
|
fe3777041d | ||
|
|
1c579a98b4 | ||
|
|
3e10cf0a64 | ||
|
|
ef9784a7d4 | ||
|
|
6f1c3c8fd2 | ||
|
|
e12c650ea5 | ||
|
|
9373cbb440 | ||
|
|
fd6d038956 | ||
|
|
9845392b71 | ||
|
|
0795b7a583 | ||
|
|
29f0e71f10 | ||
|
|
1a47f7b5a8 | ||
|
|
6a30ab7a57 | ||
|
|
758f7deb93 | ||
|
|
9f4e3bf792 | ||
|
|
a5dfe8ab28 | ||
|
|
c52dce1c48 | ||
|
|
288379aa7d | ||
|
|
a9739bf361 | ||
|
|
c69adcb163 | ||
|
|
0b4a1b4a1b | ||
|
|
cc0c1bcf3a | ||
|
|
55746f1a1d | ||
|
|
7bb8581a95 | ||
|
|
521c0f8460 | ||
|
|
14a23123c0 | ||
|
|
09be5e23f1 | ||
|
|
0aaed08c1e | ||
|
|
4bfe3b6fe1 | ||
|
|
b610aa1c0c | ||
|
|
73da04bea2 | ||
|
|
18c851e53f | ||
|
|
41f4533bc0 | ||
|
|
4db8a967b6 | ||
|
|
ff70f4e79f | ||
|
|
c9517aff7d | ||
|
|
3657a49a2c | ||
|
|
71e7aa5262 | ||
|
|
2e435f5d4e | ||
|
|
859b03c4a6 | ||
|
|
ee8786444f | ||
|
|
d87d782fd5 | ||
|
|
afac4fc37f | ||
|
|
de83521e08 | ||
|
|
99f8fe1592 | ||
|
|
02c092a122 | ||
|
|
70ca74747f | ||
|
|
594d754022 | ||
|
|
c381e4153d | ||
|
|
e761c7e8f4 | ||
|
|
b8d4e3ac50 | ||
|
|
4c2b95d00b | ||
|
|
cea4f052ba | ||
|
|
9b4ea7a040 | ||
|
|
26c2b258b4 | ||
|
|
27c9e18535 | ||
|
|
b53c2bfa0c | ||
|
|
80605633c4 | ||
|
|
acf06fdd8f | ||
|
|
58cc5b4684 | ||
|
|
c502bd901e | ||
|
|
55027747fd | ||
|
|
f6d77afe2e | ||
|
|
cd9466dafa | ||
|
|
4bf79e4bc9 | ||
|
|
7afecf0f85 | ||
|
|
0b38b7d473 | ||
|
|
1b462da4aa | ||
|
|
07948304b2 | ||
|
|
0634acdac4 | ||
|
|
75e0637d2d | ||
|
|
852c30b2e5 | ||
|
|
dc85c6552a | ||
|
|
76e8506022 | ||
|
|
2d6e2551f6 | ||
|
|
080b1d9a7c | ||
|
|
fe008b0966 | ||
|
|
4ad10d057b | ||
|
|
a65aa9f312 | ||
|
|
5b43c16f35 | ||
|
|
9cb37dc011 | ||
|
|
2ba6737c41 | ||
|
|
33d737f957 | ||
|
|
381a18a40e | ||
|
|
207f0655dd | ||
|
|
88d64da257 | ||
|
|
cf378dfd6d | ||
|
|
a3939d9a66 | ||
|
|
ef363209a4 | ||
|
|
fe9a10c617 | ||
|
|
2e734fae57 | ||
|
|
432e3c3a5e | ||
|
|
a4b13a80ce | ||
|
|
a6997a7e85 | ||
|
|
a60d06af6b | ||
|
|
dab8012b6a | ||
|
|
66f82fd9cc | ||
|
|
0bff8ba632 | ||
|
|
32226297ab | ||
|
|
ab18c90b36 | ||
|
|
27b6fd561a | ||
|
|
15b64d5a25 | ||
|
|
08a50a8ada | ||
|
|
9d172bb29d | ||
|
|
c891322129 | ||
|
|
77434850f7 | ||
|
|
69b65dbd41 | ||
|
|
c335a545a3 | ||
|
|
5bcccec610 | ||
|
|
20ae9c3a53 | ||
|
|
92ca7c5a4b | ||
|
|
37fa41b4a2 | ||
|
|
298f959e13 | ||
|
|
1cb431f204 | ||
|
|
74dc7b278b | ||
|
|
b47d8a794c | ||
|
|
eaf845959c | ||
|
|
651521d346 | ||
|
|
fb37b29671 | ||
|
|
2ecf9016ba | ||
|
|
444b08be32 | ||
|
|
2b84712eee | ||
|
|
20cb6cdd8b | ||
|
|
477a5e5338 | ||
|
|
2a151229cb | ||
|
|
1d50e091c7 | ||
|
|
c587e380a0 | ||
|
|
54f9bfba84 | ||
|
|
489ba131c5 | ||
|
|
5eac1a146f | ||
|
|
d7ce6bdeff | ||
|
|
e88473d090 | ||
|
|
b9024ab032 | ||
|
|
98906be0f6 | ||
|
|
220775715d | ||
|
|
ecbf52157b | ||
|
|
a579977f66 | ||
|
|
418dc6fdc2 | ||
|
|
2aa4b03673 | ||
|
|
f236a65a79 | ||
|
|
f7b08a1160 | ||
|
|
eed10dd1bb | ||
|
|
9992bd0999 | ||
|
|
6912175e7e | ||
|
|
a59c32757e | ||
|
|
2438a0e60b | ||
|
|
a850a902ce | ||
|
|
b7ba993ba6 | ||
|
|
3eb0d57d5b | ||
|
|
6bf2ff9168 | ||
|
|
92226a8d06 | ||
|
|
134424dfdc | ||
|
|
58ceb66452 | ||
|
|
902b8fc789 | ||
|
|
923491a510 | ||
|
|
255b45d07b | ||
|
|
8f68b5b289 | ||
|
|
252fd78473 | ||
|
|
b692c5db60 | ||
|
|
eff7d58f4b | ||
|
|
17e9bdf8e8 | ||
|
|
22d2694b71 | ||
|
|
e74d7fa454 | ||
|
|
464f42a121 | ||
|
|
05e7079178 | ||
|
|
f03fcc9a31 | ||
|
|
c3ad054bb3 | ||
|
|
202e137d77 | ||
|
|
6b35664e37 | ||
|
|
1a7dbd56ac | ||
|
|
1a40853aae | ||
|
|
6bad2b16e4 | ||
|
|
db166b4633 | ||
|
|
71bc624a74 | ||
|
|
907a941795 | ||
|
|
559783eed7 | ||
|
|
68585c8837 | ||
|
|
eccbc9d9b3 | ||
|
|
e7d1d55170 | ||
|
|
f04754c254 | ||
|
|
a8e5a48b87 | ||
|
|
283a9af406 | ||
|
|
e3896455db | ||
|
|
5e6d2700a2 | ||
|
|
dfd0dfe0f6 | ||
|
|
e6b9be5020 | ||
|
|
6f7c87516f | ||
|
|
516a78326d | ||
|
|
853b7f84ef | ||
|
|
b248a2515e | ||
|
|
6826c42c65 | ||
|
|
4f041e48a3 | ||
|
|
ec6800500b | ||
|
|
856d65a8e9 | ||
|
|
8a2efde365 | ||
|
|
2ddcc6d9e6 | ||
|
|
25962326d2 | ||
|
|
bbc2fbf984 | ||
|
|
edc53d6de3 | ||
|
|
47710210bd | ||
|
|
823b7f0670 | ||
|
|
f5130ce48f | ||
|
|
347524a5b3 | ||
|
|
51830f5907 | ||
|
|
346f538c3b | ||
|
|
9d2948ff50 | ||
|
|
36ce227bf6 | ||
|
|
024f7ad9ef | ||
|
|
f8425fe614 | ||
|
|
7802a1b5a4 | ||
|
|
17549d8a43 | ||
|
|
f6ed706855 | ||
|
|
89ef25501b | ||
|
|
4870125e64 | ||
|
|
2d24e3c7f7 | ||
|
|
cdb3f46506 | ||
|
|
e225ed9f19 | ||
|
|
17bebf4f3a | ||
|
|
26550129ea | ||
|
|
66362c2762 | ||
|
|
f6f0e141a1 | ||
|
|
f22ee54bd8 | ||
|
|
2a969f911e | ||
|
|
2a0964f66b | ||
|
|
c553a2cd38 | ||
|
|
24330a7491 | ||
|
|
cd763a7a35 | ||
|
|
ed11eab0a7 | ||
|
|
a875ce4d68 | ||
|
|
969bfb4e53 | ||
|
|
76dae43103 | ||
|
|
af75ce79ac | ||
|
|
fe89c2ff9b | ||
|
|
bb2595eca5 | ||
|
|
618fff0191 | ||
|
|
9bbd06ce76 | ||
|
|
20463a662b | ||
|
|
9251180501 | ||
|
|
2659043afd | ||
|
|
7766892ad2 | ||
|
|
a7848f43cd | ||
|
|
cf8f76b454 | ||
|
|
f68f184c68 | ||
|
|
463440bce4 | ||
|
|
51ee313910 | ||
|
|
744b0bfff7 | ||
|
|
949479aa81 | ||
|
|
8743841145 | ||
|
|
6225cb38ae | ||
|
|
8dcba37672 | ||
|
|
38b922df75 | ||
|
|
6d884382a1 | ||
|
|
752e75e94b | ||
|
|
5ca41b5e13 | ||
|
|
1b3707ad33 | ||
|
|
c6e82d5af6 | ||
|
|
814e41122a | ||
|
|
a133a71eb9 | ||
|
|
dc2addb0ed | ||
|
|
f9014bb90c | ||
|
|
df0b6d5b07 | ||
|
|
56c6e8be06 | ||
|
|
b47b8297d6 | ||
|
|
5d1e17c598 | ||
|
|
94fe34bd10 | ||
|
|
e68ff62723 | ||
|
|
04487b6b91 | ||
|
|
49a27a67bc | ||
|
|
745de2ede2 | ||
|
|
82e5698f1d | ||
|
|
c4090851c5 | ||
|
|
9cb4431e89 | ||
|
|
2221d0cb6f | ||
|
|
5ea97c4910 | ||
|
|
a40590b4bf | ||
|
|
58acb2b821 | ||
|
|
6b9dc90639 | ||
|
|
b7d26cf0d5 | ||
|
|
59b4033ab2 | ||
|
|
13a7219dbd | ||
|
|
eae8a90a89 | ||
|
|
a87f4abd5f | ||
|
|
1b73691c69 | ||
|
|
e00066466b | ||
|
|
b87a8ba97d | ||
|
|
57aa270032 | ||
|
|
90a96fd8a7 | ||
|
|
c05470515f | ||
|
|
81ed4f3699 | ||
|
|
c9ac1eab11 | ||
|
|
1ba542fb3b | ||
|
|
4f127c9de3 | ||
|
|
16656f6c13 | ||
|
|
0f13e062fe | ||
|
|
2e68407fbe | ||
|
|
974f350f27 | ||
|
|
27ffea9052 | ||
|
|
9b2b35e8a2 | ||
|
|
3b51ca3947 | ||
|
|
62a2d08b53 | ||
|
|
e790bde717 | ||
|
|
0ab6b15292 | ||
|
|
2aeeb14c21 | ||
|
|
e5e57ab3bd | ||
|
|
f3ce5dcfbd | ||
|
|
bc341e98fc | ||
|
|
80851f4861 | ||
|
|
22b4456bce | ||
|
|
8d67502997 | ||
|
|
8f31fd778b | ||
|
|
f79f25bcf4 | ||
|
|
68e237eec5 | ||
|
|
8895c70c7f | ||
|
|
3964f8649d | ||
|
|
b7fb0ef1d3 | ||
|
|
66e403c5b4 | ||
|
|
0913abe806 | ||
|
|
6d3065c4c6 | ||
|
|
9092d1f8eb | ||
|
|
1bd1f123a3 | ||
|
|
44c072dcbb | ||
|
|
45c59e2990 | ||
|
|
75f0cd6e62 | ||
|
|
80f758018c | ||
|
|
b5e2c62fdd | ||
|
|
ede35718ae | ||
|
|
31fe2807aa | ||
|
|
f77693d768 | ||
|
|
96e3c16cca | ||
|
|
edd41b37f0 | ||
|
|
139d0038f2 | ||
|
|
d25fc64d7a | ||
|
|
42092ac16a | ||
|
|
e4860d5bae | ||
|
|
a5d9b658fb | ||
|
|
f464e89415 | ||
|
|
cdc439c4ef | ||
|
|
746168f9ed | ||
|
|
5ad4885102 | ||
|
|
7eb53ca2bc | ||
|
|
56b08bddd8 | ||
|
|
0aa1e0200f | ||
|
|
575f827958 | ||
|
|
67935b11c9 | ||
|
|
9971de2ccd |
24
.github/actions/install/action.yml
vendored
24
.github/actions/install/action.yml
vendored
@@ -5,7 +5,7 @@ inputs:
|
|||||||
zig:
|
zig:
|
||||||
description: 'Zig version to install'
|
description: 'Zig version to install'
|
||||||
required: false
|
required: false
|
||||||
default: '0.15.1'
|
default: '0.15.2'
|
||||||
arch:
|
arch:
|
||||||
description: 'CPU arch used to select the v8 lib'
|
description: 'CPU arch used to select the v8 lib'
|
||||||
required: false
|
required: false
|
||||||
@@ -17,11 +17,11 @@ inputs:
|
|||||||
zig-v8:
|
zig-v8:
|
||||||
description: 'zig v8 version to install'
|
description: 'zig v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
default: 'v0.1.28'
|
default: 'v0.1.33'
|
||||||
v8:
|
v8:
|
||||||
description: 'v8 version to install'
|
description: 'v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
default: '13.6.233.8'
|
default: '14.0.365.4'
|
||||||
cache-dir:
|
cache-dir:
|
||||||
description: 'cache dir to use'
|
description: 'cache dir to use'
|
||||||
required: false
|
required: false
|
||||||
@@ -67,9 +67,23 @@ runs:
|
|||||||
mkdir -p v8/out/${{ inputs.os }}/release/obj/zig/
|
mkdir -p v8/out/${{ inputs.os }}/release/obj/zig/
|
||||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/release/obj/zig/libc_v8.a
|
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/release/obj/zig/libc_v8.a
|
||||||
|
|
||||||
- name: libiconv
|
- name: Cache libiconv
|
||||||
|
id: cache-libiconv
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-libiconv
|
||||||
|
with:
|
||||||
|
path: ${{ inputs.cache-dir }}/libiconv
|
||||||
|
key: vendor/libiconv/libiconv-1.17
|
||||||
|
|
||||||
|
- name: download libiconv
|
||||||
|
if: ${{ steps.cache-libiconv.outputs.cache-hit != 'true' }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: make install-libiconv
|
run: make download-libiconv
|
||||||
|
|
||||||
|
- name: build libiconv
|
||||||
|
shell: bash
|
||||||
|
run: make build-libiconv
|
||||||
|
|
||||||
- name: build mimalloc
|
- name: build mimalloc
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
9
.github/workflows/e2e-test.yml
vendored
9
.github/workflows/e2e-test.yml
vendored
@@ -108,6 +108,15 @@ jobs:
|
|||||||
go run runner/main.go
|
go run runner/main.go
|
||||||
kill `cat LPD.pid` `cat PROXY.id`
|
kill `cat LPD.pid` `cat PROXY.id`
|
||||||
|
|
||||||
|
- name: run request interception through proxy
|
||||||
|
run: |
|
||||||
|
export PROXY_USERNAME=username PROXY_PASSWORD=password
|
||||||
|
./proxy/proxy & echo $! > PROXY.id
|
||||||
|
./lightpanda serve & echo $! > LPD.pid
|
||||||
|
URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js
|
||||||
|
BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js
|
||||||
|
kill `cat LPD.pid` `cat PROXY.id`
|
||||||
|
|
||||||
cdp-and-hyperfine-bench:
|
cdp-and-hyperfine-bench:
|
||||||
name: cdp-and-hyperfine-bench
|
name: cdp-and-hyperfine-bench
|
||||||
needs: zig-build-release
|
needs: zig-build-release
|
||||||
|
|||||||
1
.github/workflows/wpt.yml
vendored
1
.github/workflows/wpt.yml
vendored
@@ -5,6 +5,7 @@ env:
|
|||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
||||||
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
||||||
|
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
|
|||||||
2
.github/workflows/zig-fmt.yml
vendored
2
.github/workflows/zig-fmt.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
name: zig-fmt
|
name: zig-fmt
|
||||||
|
|
||||||
env:
|
env:
|
||||||
ZIG_VERSION: 0.15.1
|
ZIG_VERSION: 0.15.2
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|||||||
5
.gitmodules
vendored
5
.gitmodules
vendored
@@ -9,7 +9,7 @@
|
|||||||
url = https://github.com/lightpanda-io/libdom.git/
|
url = https://github.com/lightpanda-io/libdom.git/
|
||||||
[submodule "vendor/netsurf/share/netsurf-buildsystem"]
|
[submodule "vendor/netsurf/share/netsurf-buildsystem"]
|
||||||
path = vendor/netsurf/share/netsurf-buildsystem
|
path = vendor/netsurf/share/netsurf-buildsystem
|
||||||
url = https://source.netsurf-browser.org/buildsystem.git
|
url = https://github.com/lightpanda-io/netsurf-buildsystem.git
|
||||||
[submodule "vendor/netsurf/libhubbub"]
|
[submodule "vendor/netsurf/libhubbub"]
|
||||||
path = vendor/netsurf/libhubbub
|
path = vendor/netsurf/libhubbub
|
||||||
url = https://github.com/lightpanda-io/libhubbub.git/
|
url = https://github.com/lightpanda-io/libhubbub.git/
|
||||||
@@ -31,3 +31,6 @@
|
|||||||
[submodule "vendor/curl"]
|
[submodule "vendor/curl"]
|
||||||
path = vendor/curl
|
path = vendor/curl
|
||||||
url = https://github.com/curl/curl.git
|
url = https://github.com/curl/curl.git
|
||||||
|
[submodule "vendor/brotli"]
|
||||||
|
path = vendor/brotli
|
||||||
|
url = https://github.com/google/brotli
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
FROM debian:stable
|
FROM debian:stable
|
||||||
|
|
||||||
ARG MINISIG=0.12
|
ARG MINISIG=0.12
|
||||||
ARG ZIG=0.15.1
|
ARG ZIG=0.15.2
|
||||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||||
ARG V8=13.6.233.8
|
ARG V8=14.0.365.4
|
||||||
ARG ZIG_V8=v0.1.28
|
ARG ZIG_V8=v0.1.33
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
RUN apt-get update -yq && \
|
RUN apt-get update -yq && \
|
||||||
|
|||||||
17
Makefile
17
Makefile
@@ -96,9 +96,16 @@ wpt-summary:
|
|||||||
@printf "\e[36mBuilding wpt...\e[0m\n"
|
@printf "\e[36mBuilding wpt...\e[0m\n"
|
||||||
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||||
|
|
||||||
## Test
|
## Test - `grep` is used to filter out the huge compile command on build
|
||||||
|
ifeq ($(OS), macos)
|
||||||
test:
|
test:
|
||||||
@TEST_FILTER='${F}' $(ZIG) build test -freference-trace --summary all
|
@script -q /dev/null sh -c 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' 2>&1 \
|
||||||
|
| grep --line-buffered -v "^/.*zig test -freference-trace"
|
||||||
|
else
|
||||||
|
test:
|
||||||
|
@script -qec 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' /dev/null 2>&1 \
|
||||||
|
| grep --line-buffered -v "^/.*zig test -freference-trace"
|
||||||
|
endif
|
||||||
|
|
||||||
## Run demo/runner end to end tests
|
## Run demo/runner end to end tests
|
||||||
end2end:
|
end2end:
|
||||||
@@ -199,14 +206,16 @@ download-libiconv:
|
|||||||
ifeq ("$(wildcard vendor/libiconv/libiconv-1.17)","")
|
ifeq ("$(wildcard vendor/libiconv/libiconv-1.17)","")
|
||||||
@mkdir -p vendor/libiconv
|
@mkdir -p vendor/libiconv
|
||||||
@cd vendor/libiconv && \
|
@cd vendor/libiconv && \
|
||||||
curl https://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.17.tar.gz | tar -xvzf -
|
curl -L https://github.com/lightpanda-io/libiconv/releases/download/1.17/libiconv-1.17.tar.gz | tar -xvzf -
|
||||||
endif
|
endif
|
||||||
|
|
||||||
install-libiconv: download-libiconv clean-libiconv
|
build-libiconv: clean-libiconv
|
||||||
@cd vendor/libiconv/libiconv-1.17 && \
|
@cd vendor/libiconv/libiconv-1.17 && \
|
||||||
./configure --prefix=$(ICONV) --enable-static && \
|
./configure --prefix=$(ICONV) --enable-static && \
|
||||||
make && make install
|
make && make install
|
||||||
|
|
||||||
|
install-libiconv: download-libiconv build-libiconv
|
||||||
|
|
||||||
clean-libiconv:
|
clean-libiconv:
|
||||||
ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","")
|
ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","")
|
||||||
@cd vendor/libiconv/libiconv-1.17 && \
|
@cd vendor/libiconv/libiconv-1.17 && \
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Lightpanda is the open-source browser made for headless usage:
|
|||||||
|
|
||||||
- Javascript execution
|
- Javascript execution
|
||||||
- Support of Web APIs (partial, WIP)
|
- Support of Web APIs (partial, WIP)
|
||||||
- Compatible with Playwright[^1], Puppeteer, chromedp through CDP
|
- Compatible with Playwright[^1], Puppeteer, chromedp through [CDP](https://chromedevtools.github.io/devtools-protocol/)
|
||||||
|
|
||||||
Fast web automation for AI agents, LLM training, scraping and testing:
|
Fast web automation for AI agents, LLM training, scraping and testing:
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ You can also follow the progress of our Javascript support in our dedicated [zig
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.1`. You have to
|
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.2`. You have to
|
||||||
install it with the right version in order to build the project.
|
install it with the right version in order to build the project.
|
||||||
|
|
||||||
Lightpanda also depends on
|
Lightpanda also depends on
|
||||||
@@ -190,10 +190,10 @@ For systems with [Nix](https://nixos.org/download/), you can use the devShell:
|
|||||||
nix develop
|
nix develop
|
||||||
```
|
```
|
||||||
|
|
||||||
For MacOS, you only need cmake:
|
For MacOS, you need [Xcode](https://developer.apple.com/xcode/) and the following pacakges from homebrew:
|
||||||
|
|
||||||
```
|
```
|
||||||
brew install cmake
|
brew install cmake pkgconf
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install and build dependencies
|
### Install and build dependencies
|
||||||
|
|||||||
60
build.zig
60
build.zig
@@ -23,7 +23,7 @@ const Build = std.Build;
|
|||||||
|
|
||||||
/// Do not rename this constant. It is scanned by some scripts to determine
|
/// Do not rename this constant. It is scanned by some scripts to determine
|
||||||
/// which zig version to install.
|
/// which zig version to install.
|
||||||
const recommended_zig_version = "0.15.1";
|
const recommended_zig_version = "0.15.2";
|
||||||
|
|
||||||
pub fn build(b: *Build) !void {
|
pub fn build(b: *Build) !void {
|
||||||
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
|
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
|
||||||
@@ -245,6 +245,7 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
|
|||||||
mod.addCMacro("HAVE_ASSERT_H", "1");
|
mod.addCMacro("HAVE_ASSERT_H", "1");
|
||||||
mod.addCMacro("HAVE_BASENAME", "1");
|
mod.addCMacro("HAVE_BASENAME", "1");
|
||||||
mod.addCMacro("HAVE_BOOL_T", "1");
|
mod.addCMacro("HAVE_BOOL_T", "1");
|
||||||
|
mod.addCMacro("HAVE_BROTLI", "1");
|
||||||
mod.addCMacro("HAVE_BUILTIN_AVAILABLE", "1");
|
mod.addCMacro("HAVE_BUILTIN_AVAILABLE", "1");
|
||||||
mod.addCMacro("HAVE_CLOCK_GETTIME_MONOTONIC", "1");
|
mod.addCMacro("HAVE_CLOCK_GETTIME_MONOTONIC", "1");
|
||||||
mod.addCMacro("HAVE_DLFCN_H", "1");
|
mod.addCMacro("HAVE_DLFCN_H", "1");
|
||||||
@@ -379,9 +380,11 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
|
|||||||
}
|
}
|
||||||
|
|
||||||
try buildZlib(b, mod);
|
try buildZlib(b, mod);
|
||||||
|
try buildBrotli(b, mod);
|
||||||
try buildMbedtls(b, mod);
|
try buildMbedtls(b, mod);
|
||||||
try buildNghttp2(b, mod);
|
try buildNghttp2(b, mod);
|
||||||
try buildCurl(b, mod);
|
try buildCurl(b, mod);
|
||||||
|
try buildAda(b, mod);
|
||||||
|
|
||||||
switch (target.result.os.tag) {
|
switch (target.result.os.tag) {
|
||||||
.macos => {
|
.macos => {
|
||||||
@@ -484,6 +487,30 @@ fn buildZlib(b: *Build, m: *Build.Module) !void {
|
|||||||
} });
|
} });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn buildBrotli(b: *Build, m: *Build.Module) !void {
|
||||||
|
const brotli = b.addLibrary(.{
|
||||||
|
.name = "brotli",
|
||||||
|
.root_module = m,
|
||||||
|
});
|
||||||
|
|
||||||
|
const root = "vendor/brotli/c/";
|
||||||
|
brotli.addIncludePath(b.path(root ++ "include"));
|
||||||
|
brotli.addCSourceFiles(.{ .flags = &.{}, .files = &.{
|
||||||
|
root ++ "common/constants.c",
|
||||||
|
root ++ "common/context.c",
|
||||||
|
root ++ "common/dictionary.c",
|
||||||
|
root ++ "common/platform.c",
|
||||||
|
root ++ "common/shared_dictionary.c",
|
||||||
|
root ++ "common/transform.c",
|
||||||
|
root ++ "dec/bit_reader.c",
|
||||||
|
root ++ "dec/decode.c",
|
||||||
|
root ++ "dec/huffman.c",
|
||||||
|
root ++ "dec/prefix.c",
|
||||||
|
root ++ "dec/state.c",
|
||||||
|
root ++ "dec/static_init.c",
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
|
||||||
fn buildMbedtls(b: *Build, m: *Build.Module) !void {
|
fn buildMbedtls(b: *Build, m: *Build.Module) !void {
|
||||||
const mbedtls = b.addLibrary(.{
|
const mbedtls = b.addLibrary(.{
|
||||||
.name = "mbedtls",
|
.name = "mbedtls",
|
||||||
@@ -823,3 +850,34 @@ fn buildCurl(b: *Build, m: *Build.Module) !void {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn buildAda(b: *Build, m: *Build.Module) !void {
|
||||||
|
const ada_dep = b.dependency("ada-singleheader", .{});
|
||||||
|
|
||||||
|
const ada_mod = b.createModule(.{
|
||||||
|
.root_source_file = b.path("vendor/ada/root.zig"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ada_lib = b.addLibrary(.{
|
||||||
|
.name = "ada",
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.link_libcpp = true,
|
||||||
|
.target = m.resolved_target,
|
||||||
|
.optimize = m.optimize,
|
||||||
|
}),
|
||||||
|
.linkage = .static,
|
||||||
|
});
|
||||||
|
|
||||||
|
ada_lib.addCSourceFile(.{
|
||||||
|
.file = ada_dep.path("ada.cpp"),
|
||||||
|
.flags = &.{ "-std=c++20", "-O3" },
|
||||||
|
.language = .cpp,
|
||||||
|
});
|
||||||
|
|
||||||
|
ada_lib.installHeader(ada_dep.path("ada_c.h"), "ada_c.h");
|
||||||
|
|
||||||
|
// Link the library to ada module.
|
||||||
|
ada_mod.linkLibrary(ada_lib);
|
||||||
|
// Expose ada module to main module.
|
||||||
|
m.addImport("ada", ada_mod);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,9 +5,13 @@
|
|||||||
.fingerprint = 0xda130f3af836cea0,
|
.fingerprint = 0xda130f3af836cea0,
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
.v8 = .{
|
.v8 = .{
|
||||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/d3040a953e5a37290dae20e7ddf138b7aeb5e67d.tar.gz",
|
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/305bb3706716d32d59b2ffa674731556caa1002b.tar.gz",
|
||||||
.hash = "v8-0.0.0-xddH6w_EAwA8vK0NAxfxfI7IcbnpkUAcXKNujn7qwnmY",
|
.hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5",
|
||||||
},
|
},
|
||||||
//.v8 = .{ .path = "../zig-v8-fork" }
|
//.v8 = .{ .path = "../zig-v8-fork" }
|
||||||
|
.@"ada-singleheader" = .{
|
||||||
|
.url = "https://github.com/ada-url/ada/releases/download/v3.3.0/singleheader.zip",
|
||||||
|
.hash = "N-V-__8AAPmhFAAw64ALjlzd5YMtzpSrmZ6KymsT84BKfB4s",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
12
flake.lock
generated
12
flake.lock
generated
@@ -75,11 +75,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1756822655,
|
"lastModified": 1760968520,
|
||||||
"narHash": "sha256-xQAk8xLy7srAkR5NMZFsQFioL02iTHuuEIs3ohGpgdk=",
|
"narHash": "sha256-EjGslHDzCBKOVr+dnDB1CAD7wiQSHfUt3suOpFj9O1Q=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "4bdac60bfe32c41103ae500ddf894c258291dd61",
|
"rev": "e755547441a0413942a37692f7bf7fc6315bb7f6",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -136,11 +136,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1756555914,
|
"lastModified": 1760747435,
|
||||||
"narHash": "sha256-7yoSPIVEuL+3Wzf6e7NHuW3zmruHizRrYhGerjRHTLI=",
|
"narHash": "sha256-wNB/W3x+or4mdNxFPNOH5/WFckNpKgFRZk7OnOsLtm0=",
|
||||||
"owner": "mitchellh",
|
"owner": "mitchellh",
|
||||||
"repo": "zig-overlay",
|
"repo": "zig-overlay",
|
||||||
"rev": "d0df3a2fd0f11134409d6d5ea0e510e5e477f7d6",
|
"rev": "d0f239b887b1ac736c0f3dde91bf5bf2ecf3a420",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
targetPkgs =
|
targetPkgs =
|
||||||
pkgs: with pkgs; [
|
pkgs: with pkgs; [
|
||||||
# Build Tools
|
# Build Tools
|
||||||
zigpkgs."0.15.1"
|
zigpkgs."0.15.2"
|
||||||
zls
|
zls
|
||||||
python3
|
python3
|
||||||
pkg-config
|
pkg-config
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {
|
|||||||
error.FileNotFound => return req.respond("server error", .{ .status = .not_found }),
|
error.FileNotFound => return req.respond("server error", .{ .status = .not_found }),
|
||||||
else => return err,
|
else => return err,
|
||||||
};
|
};
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
const stat = try file.stat();
|
const stat = try file.stat();
|
||||||
var send_buffer: [4096]u8 = undefined;
|
var send_buffer: [4096]u8 = undefined;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const Allocator = std.mem.Allocator;
|
|||||||
|
|
||||||
const log = @import("log.zig");
|
const log = @import("log.zig");
|
||||||
const Http = @import("http/Http.zig");
|
const Http = @import("http/Http.zig");
|
||||||
const Platform = @import("runtime/js.zig").Platform;
|
const Platform = @import("browser/js/Platform.zig");
|
||||||
|
|
||||||
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
||||||
const Notification = @import("notification.zig").Notification;
|
const Notification = @import("notification.zig").Notification;
|
||||||
@@ -36,6 +36,7 @@ pub const App = struct {
|
|||||||
http_connect_timeout_ms: ?u31 = null,
|
http_connect_timeout_ms: ?u31 = null,
|
||||||
http_max_host_open: ?u8 = null,
|
http_max_host_open: ?u8 = null,
|
||||||
http_max_concurrent: ?u8 = null,
|
http_max_concurrent: ?u8 = null,
|
||||||
|
user_agent: [:0]const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, config: Config) !*App {
|
pub fn init(allocator: Allocator, config: Config) !*App {
|
||||||
@@ -53,6 +54,7 @@ pub const App = struct {
|
|||||||
.http_proxy = config.http_proxy,
|
.http_proxy = config.http_proxy,
|
||||||
.tls_verify_host = config.tls_verify_host,
|
.tls_verify_host = config.tls_verify_host,
|
||||||
.proxy_bearer_token = config.proxy_bearer_token,
|
.proxy_bearer_token = config.proxy_bearer_token,
|
||||||
|
.user_agent = config.user_agent,
|
||||||
});
|
});
|
||||||
errdefer http.deinit();
|
errdefer http.deinit();
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const log = @import("../log.zig");
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const Scheduler = @This();
|
const Scheduler = @This();
|
||||||
@@ -38,8 +37,10 @@ pub fn init(allocator: Allocator) Scheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(self: *Scheduler) void {
|
pub fn reset(self: *Scheduler) void {
|
||||||
self.high_priority.clearRetainingCapacity();
|
// Our allocator is the page arena, it's been reset. We cannot use
|
||||||
self.low_priority.clearRetainingCapacity();
|
// clearAndRetainCapacity, since that space is no longer ours
|
||||||
|
self.high_priority.clearAndFree();
|
||||||
|
self.low_priority.clearAndFree();
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddOpts = struct {
|
const AddOpts = struct {
|
||||||
@@ -65,14 +66,11 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: Ad
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runHighPriority(self: *Scheduler) !?i32 {
|
pub fn run(self: *Scheduler) !?i32 {
|
||||||
|
_ = try self.runQueue(&self.low_priority);
|
||||||
return self.runQueue(&self.high_priority);
|
return self.runQueue(&self.high_priority);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runLowPriority(self: *Scheduler) !?i32 {
|
|
||||||
return self.runQueue(&self.low_priority);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn runQueue(self: *Scheduler, queue: *Queue) !?i32 {
|
fn runQueue(self: *Scheduler, queue: *Queue) !?i32 {
|
||||||
// this is O(1)
|
// this is O(1)
|
||||||
if (queue.count() == 0) {
|
if (queue.count() == 0) {
|
||||||
@@ -127,33 +125,24 @@ test "Scheduler" {
|
|||||||
var task = TestTask{ .allocator = testing.arena_allocator };
|
var task = TestTask{ .allocator = testing.arena_allocator };
|
||||||
|
|
||||||
var s = Scheduler.init(testing.arena_allocator);
|
var s = Scheduler.init(testing.arena_allocator);
|
||||||
try testing.expectEqual(null, s.runHighPriority());
|
try testing.expectEqual(null, s.run());
|
||||||
try testing.expectEqual(0, task.calls.items.len);
|
try testing.expectEqual(0, task.calls.items.len);
|
||||||
|
|
||||||
try s.add(&task, TestTask.run1, 3, .{});
|
try s.add(&task, TestTask.run1, 3, .{});
|
||||||
|
|
||||||
try testing.expectDelta(3, try s.runHighPriority(), 1);
|
try testing.expectDelta(3, try s.run(), 1);
|
||||||
try testing.expectEqual(0, task.calls.items.len);
|
try testing.expectEqual(0, task.calls.items.len);
|
||||||
|
|
||||||
std.Thread.sleep(std.time.ns_per_ms * 5);
|
std.Thread.sleep(std.time.ns_per_ms * 5);
|
||||||
try testing.expectEqual(null, s.runHighPriority());
|
try testing.expectEqual(null, s.run());
|
||||||
try testing.expectEqualSlices(u32, &.{1}, task.calls.items);
|
try testing.expectEqualSlices(u32, &.{1}, task.calls.items);
|
||||||
|
|
||||||
try s.add(&task, TestTask.run2, 3, .{});
|
try s.add(&task, TestTask.run2, 3, .{});
|
||||||
try s.add(&task, TestTask.run1, 2, .{});
|
try s.add(&task, TestTask.run1, 2, .{});
|
||||||
|
|
||||||
std.Thread.sleep(std.time.ns_per_ms * 5);
|
std.Thread.sleep(std.time.ns_per_ms * 5);
|
||||||
try testing.expectDelta(null, try s.runHighPriority(), 1);
|
try testing.expectDelta(null, try s.run(), 1);
|
||||||
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
|
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
|
||||||
|
|
||||||
std.Thread.sleep(std.time.ns_per_ms * 5);
|
|
||||||
// won't run low_priority
|
|
||||||
try testing.expectEqual(null, try s.runHighPriority());
|
|
||||||
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
|
|
||||||
|
|
||||||
//runs low_priority
|
|
||||||
try testing.expectDelta(2, try s.runLowPriority(), 1);
|
|
||||||
try testing.expectEqualSlices(u32, &.{ 1, 1, 2, 2 }, task.calls.items);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TestTask = struct {
|
const TestTask = struct {
|
||||||
|
|||||||
@@ -18,10 +18,10 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const js = @import("js/js.zig");
|
||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
const parser = @import("netsurf.zig");
|
const parser = @import("netsurf.zig");
|
||||||
|
|
||||||
const Env = @import("env.zig").Env;
|
|
||||||
const Page = @import("page.zig").Page;
|
const Page = @import("page.zig").Page;
|
||||||
const DataURI = @import("DataURI.zig");
|
const DataURI = @import("DataURI.zig");
|
||||||
const Http = @import("../http/Http.zig");
|
const Http = @import("../http/Http.zig");
|
||||||
@@ -38,9 +38,6 @@ page: *Page,
|
|||||||
// used to prevent recursive evalutaion
|
// used to prevent recursive evalutaion
|
||||||
is_evaluating: bool,
|
is_evaluating: bool,
|
||||||
|
|
||||||
// used to prevent executing scripts while we're doing a blocking load
|
|
||||||
is_blocking: bool = false,
|
|
||||||
|
|
||||||
// Only once this is true can deferred scripts be run
|
// Only once this is true can deferred scripts be run
|
||||||
static_scripts_done: bool,
|
static_scripts_done: bool,
|
||||||
|
|
||||||
@@ -48,12 +45,6 @@ static_scripts_done: bool,
|
|||||||
// on shutdown/abort, we need to cleanup any pending ones.
|
// on shutdown/abort, we need to cleanup any pending ones.
|
||||||
asyncs: OrderList,
|
asyncs: OrderList,
|
||||||
|
|
||||||
// When an async script is ready to be evaluated, it's moved from asyncs to
|
|
||||||
// this list. You might think we can evaluate an async script as soon as it's
|
|
||||||
// done, but we can only evaluate scripts when `is_blocking == false`. So this
|
|
||||||
// becomes a list of scripts to execute on the next evaluate().
|
|
||||||
asyncs_ready: OrderList,
|
|
||||||
|
|
||||||
// Normal scripts (non-deferred & non-async). These must be executed in order
|
// Normal scripts (non-deferred & non-async). These must be executed in order
|
||||||
scripts: OrderList,
|
scripts: OrderList,
|
||||||
|
|
||||||
@@ -67,6 +58,22 @@ client: *Http.Client,
|
|||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
buffer_pool: BufferPool,
|
buffer_pool: BufferPool,
|
||||||
script_pool: std.heap.MemoryPool(PendingScript),
|
script_pool: std.heap.MemoryPool(PendingScript),
|
||||||
|
sync_module_pool: std.heap.MemoryPool(SyncModule),
|
||||||
|
async_module_pool: std.heap.MemoryPool(AsyncModule),
|
||||||
|
|
||||||
|
// We can download multiple sync modules in parallel, but we want to process
|
||||||
|
// then in order. We can't use an OrderList, like the other script types,
|
||||||
|
// because the order we load them might not be the order we want to process
|
||||||
|
// them in (I'm not sure this is true, but as far as I can tell, v8 doesn't
|
||||||
|
// make any guarantees about the list of sub-module dependencies it gives us
|
||||||
|
// So this is more like a cache. When a SyncModule is complete, it's put here
|
||||||
|
// and can be requested as needed.
|
||||||
|
sync_modules: std.StringHashMapUnmanaged(*SyncModule),
|
||||||
|
|
||||||
|
// Mapping between module specifier and resolution.
|
||||||
|
// see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap
|
||||||
|
// importmap contains resolved urls.
|
||||||
|
importmap: std.StringHashMapUnmanaged([:0]const u8),
|
||||||
|
|
||||||
const OrderList = std.DoublyLinkedList;
|
const OrderList = std.DoublyLinkedList;
|
||||||
|
|
||||||
@@ -78,27 +85,51 @@ pub fn init(browser: *Browser, page: *Page) ScriptManager {
|
|||||||
.asyncs = .{},
|
.asyncs = .{},
|
||||||
.scripts = .{},
|
.scripts = .{},
|
||||||
.deferreds = .{},
|
.deferreds = .{},
|
||||||
.asyncs_ready = .{},
|
.importmap = .empty,
|
||||||
|
.sync_modules = .empty,
|
||||||
.is_evaluating = false,
|
.is_evaluating = false,
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.client = browser.http_client,
|
.client = browser.http_client,
|
||||||
.static_scripts_done = false,
|
.static_scripts_done = false,
|
||||||
.buffer_pool = BufferPool.init(allocator, 5),
|
.buffer_pool = BufferPool.init(allocator, 5),
|
||||||
.script_pool = std.heap.MemoryPool(PendingScript).init(allocator),
|
.script_pool = std.heap.MemoryPool(PendingScript).init(allocator),
|
||||||
|
.sync_module_pool = std.heap.MemoryPool(SyncModule).init(allocator),
|
||||||
|
.async_module_pool = std.heap.MemoryPool(AsyncModule).init(allocator),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *ScriptManager) void {
|
pub fn deinit(self: *ScriptManager) void {
|
||||||
self.reset();
|
self.reset();
|
||||||
|
var it = self.sync_modules.valueIterator();
|
||||||
|
while (it.next()) |value_ptr| {
|
||||||
|
value_ptr.*.buffer.deinit(self.allocator);
|
||||||
|
self.sync_module_pool.destroy(value_ptr.*);
|
||||||
|
}
|
||||||
|
|
||||||
self.buffer_pool.deinit();
|
self.buffer_pool.deinit();
|
||||||
self.script_pool.deinit();
|
self.script_pool.deinit();
|
||||||
|
self.sync_module_pool.deinit();
|
||||||
|
self.async_module_pool.deinit();
|
||||||
|
|
||||||
|
self.sync_modules.deinit(self.allocator);
|
||||||
|
// we don't deinit self.importmap b/c we use the page's arena for its
|
||||||
|
// allocations.
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(self: *ScriptManager) void {
|
pub fn reset(self: *ScriptManager) void {
|
||||||
|
var it = self.sync_modules.valueIterator();
|
||||||
|
while (it.next()) |value_ptr| {
|
||||||
|
value_ptr.*.buffer.deinit(self.allocator);
|
||||||
|
self.sync_module_pool.destroy(value_ptr.*);
|
||||||
|
}
|
||||||
|
self.sync_modules.clearRetainingCapacity();
|
||||||
|
// Our allocator is the page arena, it's been reset. We cannot use
|
||||||
|
// clearAndRetainCapacity, since that space is no longer ours
|
||||||
|
self.importmap = .empty;
|
||||||
|
|
||||||
self.clearList(&self.asyncs);
|
self.clearList(&self.asyncs);
|
||||||
self.clearList(&self.scripts);
|
self.clearList(&self.scripts);
|
||||||
self.clearList(&self.deferreds);
|
self.clearList(&self.deferreds);
|
||||||
self.clearList(&self.asyncs_ready);
|
|
||||||
self.static_scripts_done = false;
|
self.static_scripts_done = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +142,7 @@ fn clearList(_: *const ScriptManager, list: *OrderList) void {
|
|||||||
std.debug.assert(list.first == null);
|
std.debug.assert(list.first == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
|
pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime ctx: []const u8) !void {
|
||||||
if (try parser.elementGetAttribute(element, "nomodule") != null) {
|
if (try parser.elementGetAttribute(element, "nomodule") != null) {
|
||||||
// these scripts should only be loaded if we don't support modules
|
// these scripts should only be loaded if we don't support modules
|
||||||
// but since we do support modules, we can just skip them.
|
// but since we do support modules, we can just skip them.
|
||||||
@@ -144,6 +175,9 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
|
|||||||
if (std.ascii.eqlIgnoreCase(script_type, "module")) {
|
if (std.ascii.eqlIgnoreCase(script_type, "module")) {
|
||||||
break :blk .module;
|
break :blk .module;
|
||||||
}
|
}
|
||||||
|
if (std.ascii.eqlIgnoreCase(script_type, "importmap")) {
|
||||||
|
break :blk .importmap;
|
||||||
|
}
|
||||||
|
|
||||||
// "type" could be anything, but only the above are ones we need to process.
|
// "type" could be anything, but only the above are ones we need to process.
|
||||||
// Common other ones are application/json, application/ld+json, text/template
|
// Common other ones are application/json, application/ld+json, text/template
|
||||||
@@ -157,11 +191,12 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
|
|||||||
if (try parser.elementGetAttribute(element, "src")) |src| {
|
if (try parser.elementGetAttribute(element, "src")) |src| {
|
||||||
if (try DataURI.parse(page.arena, src)) |data_uri| {
|
if (try DataURI.parse(page.arena, src)) |data_uri| {
|
||||||
source = .{ .@"inline" = data_uri };
|
source = .{ .@"inline" = data_uri };
|
||||||
|
} else {
|
||||||
|
remote_url = try URL.stitch(page.arena, src, page.url.raw, .{ .null_terminated = true });
|
||||||
|
source = .{ .remote = .{} };
|
||||||
}
|
}
|
||||||
remote_url = try URL.stitch(page.arena, src, page.url.raw, .{ .null_terminated = true });
|
|
||||||
source = .{ .remote = .{} };
|
|
||||||
} else {
|
} else {
|
||||||
const inline_source = try parser.nodeTextContent(@ptrCast(element)) orelse return;
|
const inline_source = parser.nodeTextContent(@ptrCast(element)) orelse return;
|
||||||
source = .{ .@"inline" = inline_source };
|
source = .{ .@"inline" = inline_source };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +212,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
|
|||||||
if (source == .@"inline" and self.scripts.first == null) {
|
if (source == .@"inline" and self.scripts.first == null) {
|
||||||
// inline script with no pending scripts, execute it immediately.
|
// inline script with no pending scripts, execute it immediately.
|
||||||
// (if there is a pending script, then we cannot execute this immediately
|
// (if there is a pending script, then we cannot execute this immediately
|
||||||
// as it needs to best executed in order)
|
// as it needs to be executed in order)
|
||||||
return script.eval(page);
|
return script.eval(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,14 +233,18 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
|
|||||||
self.scripts.append(&pending_script.node);
|
self.scripts.append(&pending_script.node);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
log.debug(.http, "script queue", .{ .url = remote_url.? });
|
log.debug(.http, "script queue", .{
|
||||||
|
.ctx = ctx,
|
||||||
|
.url = remote_url.?,
|
||||||
|
.stack = page.js.stackTrace() catch "???",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pending_script.getList().append(&pending_script.node);
|
pending_script.getList().append(&pending_script.node);
|
||||||
|
|
||||||
errdefer pending_script.deinit();
|
errdefer pending_script.deinit();
|
||||||
|
|
||||||
var headers = try Http.Headers.init();
|
var headers = try self.client.newHeaders();
|
||||||
try page.requestCookie(.{}).headersForRequest(page.arena, remote_url.?, &headers);
|
try page.requestCookie(.{}).headersForRequest(page.arena, remote_url.?, &headers);
|
||||||
|
|
||||||
try self.client.request(.{
|
try self.client.request(.{
|
||||||
@@ -223,88 +262,144 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// @TODO: Improving this would have the simplest biggest performance improvement
|
// Resolve a module specifier to an valid URL.
|
||||||
// for most sites.
|
pub fn resolveSpecifier(self: *ScriptManager, arena: Allocator, specifier: []const u8, base: []const u8) ![:0]const u8 {
|
||||||
//
|
// If the specifier is mapped in the importmap, return the pre-resolved value.
|
||||||
// For JS imports (both static and dynamic), we currently block to get the
|
if (self.importmap.get(specifier)) |s| {
|
||||||
// result (the content of the file).
|
return s;
|
||||||
//
|
|
||||||
// For static imports, this is necessary, since v8 is expecting the compiled module
|
|
||||||
// as part of the function return. (we should try to pre-load the JavaScript
|
|
||||||
// source via module.GetModuleRequests(), but that's for a later time).
|
|
||||||
//
|
|
||||||
// For dynamic dynamic imports, this is not strictly necessary since the v8
|
|
||||||
// call returns a Promise; we could make this a normal get call, associated with
|
|
||||||
// the promise, and when done, resolve the promise.
|
|
||||||
//
|
|
||||||
// In both cases, for now at least, we just issue a "blocking" request. We block
|
|
||||||
// by ticking the http client until the script is complete.
|
|
||||||
//
|
|
||||||
// This uses the client.blockingRequest call which has a dedicated handle for
|
|
||||||
// these blocking requests. Because they are blocking, we're guaranteed to have
|
|
||||||
// only 1 at a time, thus the 1 reserved handle.
|
|
||||||
//
|
|
||||||
// You almost don't need the http client's blocking handle. In most cases, you
|
|
||||||
// should always have 1 free handle whenever you get here, because we always
|
|
||||||
// release the handle before executing the doneCallback. So, if a module does:
|
|
||||||
// import * as x from 'blah'
|
|
||||||
// And we need to load 'blah', there should always be 1 free handle - the handle
|
|
||||||
// of the http GET we just completed before executing the module.
|
|
||||||
// The exception to this, and the reason we need a special blocking handle, is
|
|
||||||
// for inline modules within the HTML page itself:
|
|
||||||
// <script type=module>import ....</script>
|
|
||||||
// Unlike external modules which can only ever be executed after releasing an
|
|
||||||
// http handle, these are executed without there necessarily being a free handle.
|
|
||||||
// Thus, Http/Client.zig maintains a dedicated handle for these calls.
|
|
||||||
pub fn blockingGet(self: *ScriptManager, url: [:0]const u8) !BlockingResult {
|
|
||||||
std.debug.assert(self.is_blocking == false);
|
|
||||||
|
|
||||||
self.is_blocking = true;
|
|
||||||
defer {
|
|
||||||
self.is_blocking = false;
|
|
||||||
|
|
||||||
// we blocked evaluation while loading this script, there could be
|
|
||||||
// scripts ready to process.
|
|
||||||
self.evaluate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var blocking = Blocking{
|
return URL.stitch(
|
||||||
.allocator = self.allocator,
|
arena,
|
||||||
.buffer_pool = &self.buffer_pool,
|
specifier,
|
||||||
};
|
base,
|
||||||
|
.{ .alloc = .if_needed, .null_terminated = true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
var headers = try Http.Headers.init();
|
pub fn getModule(self: *ScriptManager, url: [:0]const u8, referrer: []const u8) !void {
|
||||||
|
const gop = try self.sync_modules.getOrPut(self.allocator, url);
|
||||||
|
if (gop.found_existing) {
|
||||||
|
// already requested
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
errdefer _ = self.sync_modules.remove(url);
|
||||||
|
|
||||||
|
const sync = try self.sync_module_pool.create();
|
||||||
|
errdefer self.sync_module_pool.destroy(sync);
|
||||||
|
|
||||||
|
sync.* = .{ .manager = self };
|
||||||
|
gop.value_ptr.* = sync;
|
||||||
|
|
||||||
|
var headers = try self.client.newHeaders();
|
||||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
||||||
|
|
||||||
|
log.debug(.http, "script queue", .{
|
||||||
|
.url = url,
|
||||||
|
.ctx = "module",
|
||||||
|
.referrer = referrer,
|
||||||
|
.stack = self.page.js.stackTrace() catch "???",
|
||||||
|
});
|
||||||
|
|
||||||
|
try self.client.request(.{
|
||||||
|
.url = url,
|
||||||
|
.ctx = sync,
|
||||||
|
.method = .GET,
|
||||||
|
.headers = headers,
|
||||||
|
.cookie_jar = self.page.cookie_jar,
|
||||||
|
.resource_type = .script,
|
||||||
|
.start_callback = if (log.enabled(.http, .debug)) SyncModule.startCallback else null,
|
||||||
|
.header_callback = SyncModule.headerCallback,
|
||||||
|
.data_callback = SyncModule.dataCallback,
|
||||||
|
.done_callback = SyncModule.doneCallback,
|
||||||
|
.error_callback = SyncModule.errorCallback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn waitForModule(self: *ScriptManager, url: [:0]const u8) !GetResult {
|
||||||
|
// Normally it's dangerous to hold on to map pointers. But here, the map
|
||||||
|
// can't change. It's possible that by calling `tick`, other entries within
|
||||||
|
// the map will have their value change, but the map itself is immutable
|
||||||
|
// during this tick.
|
||||||
|
const entry = self.sync_modules.getEntry(url) orelse {
|
||||||
|
return error.UnknownModule;
|
||||||
|
};
|
||||||
|
const sync = entry.value_ptr.*;
|
||||||
|
|
||||||
|
// We can have multiple scripts waiting for the same module in concurrency.
|
||||||
|
// We use the waiters to ensures only the last waiter deinit the resources.
|
||||||
|
sync.waiters += 1;
|
||||||
|
defer sync.waiters -= 1;
|
||||||
|
|
||||||
var client = self.client;
|
var client = self.client;
|
||||||
try client.blockingRequest(.{
|
while (true) {
|
||||||
|
switch (sync.state) {
|
||||||
|
.loading => {},
|
||||||
|
.done => {
|
||||||
|
if (sync.waiters == 1) {
|
||||||
|
// Our caller has its own higher level cache (caching the
|
||||||
|
// actual compiled module). There's no reason for us to keep
|
||||||
|
// this if we are the last waiter.
|
||||||
|
defer self.sync_module_pool.destroy(sync);
|
||||||
|
defer self.sync_modules.removeByPtr(entry.key_ptr);
|
||||||
|
return .{
|
||||||
|
.shared = false,
|
||||||
|
.buffer = sync.buffer,
|
||||||
|
.buffer_pool = &self.buffer_pool,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.shared = true,
|
||||||
|
.buffer = sync.buffer,
|
||||||
|
.buffer_pool = &self.buffer_pool,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
.err => |err| return err,
|
||||||
|
}
|
||||||
|
// rely on http's timeout settings to avoid an endless/long loop.
|
||||||
|
_ = try client.tick(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getAsyncModule(self: *ScriptManager, url: [:0]const u8, cb: AsyncModule.Callback, cb_data: *anyopaque, referrer: []const u8) !void {
|
||||||
|
const async = try self.async_module_pool.create();
|
||||||
|
errdefer self.async_module_pool.destroy(async);
|
||||||
|
|
||||||
|
async.* = .{
|
||||||
|
.cb = cb,
|
||||||
|
.manager = self,
|
||||||
|
.cb_data = cb_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
var headers = try self.client.newHeaders();
|
||||||
|
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
||||||
|
|
||||||
|
log.debug(.http, "script queue", .{
|
||||||
|
.url = url,
|
||||||
|
.ctx = "dynamic module",
|
||||||
|
.referrer = referrer,
|
||||||
|
.stack = self.page.js.stackTrace() catch "???",
|
||||||
|
});
|
||||||
|
|
||||||
|
try self.client.request(.{
|
||||||
.url = url,
|
.url = url,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.headers = headers,
|
.headers = headers,
|
||||||
.cookie_jar = self.page.cookie_jar,
|
.cookie_jar = self.page.cookie_jar,
|
||||||
.ctx = &blocking,
|
.ctx = async,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
.start_callback = if (log.enabled(.http, .debug)) Blocking.startCallback else null,
|
.start_callback = if (log.enabled(.http, .debug)) AsyncModule.startCallback else null,
|
||||||
.header_callback = Blocking.headerCallback,
|
.header_callback = AsyncModule.headerCallback,
|
||||||
.data_callback = Blocking.dataCallback,
|
.data_callback = AsyncModule.dataCallback,
|
||||||
.done_callback = Blocking.doneCallback,
|
.done_callback = AsyncModule.doneCallback,
|
||||||
.error_callback = Blocking.errorCallback,
|
.error_callback = AsyncModule.errorCallback,
|
||||||
});
|
});
|
||||||
|
|
||||||
// rely on http's timeout settings to avoid an endless/long loop.
|
|
||||||
while (true) {
|
|
||||||
_ = try client.tick(200);
|
|
||||||
switch (blocking.state) {
|
|
||||||
.running => {},
|
|
||||||
.done => |result| return result,
|
|
||||||
.err => |err| return err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn staticScriptsDone(self: *ScriptManager) void {
|
pub fn staticScriptsDone(self: *ScriptManager) void {
|
||||||
std.debug.assert(self.static_scripts_done == false);
|
std.debug.assert(self.static_scripts_done == false);
|
||||||
self.static_scripts_done = true;
|
self.static_scripts_done = true;
|
||||||
|
self.evaluate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to evaluate completed scripts (in order). This is called whenever a script
|
// try to evaluate completed scripts (in order). This is called whenever a script
|
||||||
@@ -318,24 +413,10 @@ fn evaluate(self: *ScriptManager) void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.is_blocking) {
|
|
||||||
// Cannot evaluate scripts while a blocking-load is in progress. Not
|
|
||||||
// only could that result in incorrect evaluation order, it could
|
|
||||||
// trigger another blocking request, while we're doing a blocking request.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const page = self.page;
|
const page = self.page;
|
||||||
self.is_evaluating = true;
|
self.is_evaluating = true;
|
||||||
defer self.is_evaluating = false;
|
defer self.is_evaluating = false;
|
||||||
|
|
||||||
// every script in asyncs_ready is ready to be evaluated.
|
|
||||||
while (self.asyncs_ready.first) |n| {
|
|
||||||
var pending_script: *PendingScript = @fieldParentPtr("node", n);
|
|
||||||
defer pending_script.deinit();
|
|
||||||
pending_script.script.eval(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (self.scripts.first) |n| {
|
while (self.scripts.first) |n| {
|
||||||
var pending_script: *PendingScript = @fieldParentPtr("node", n);
|
var pending_script: *PendingScript = @fieldParentPtr("node", n);
|
||||||
if (pending_script.complete == false) {
|
if (pending_script.complete == false) {
|
||||||
@@ -385,6 +466,12 @@ pub fn isDone(self: *const ScriptManager) bool {
|
|||||||
self.deferreds.first == null; // and there are no more <script defer src=> to wait for
|
self.deferreds.first == null; // and there are no more <script defer src=> to wait for
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn asyncScriptIsDone(self: *ScriptManager) void {
|
||||||
|
if (self.isDone()) {
|
||||||
|
self.page.documentIsComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn startCallback(transfer: *Http.Transfer) !void {
|
fn startCallback(transfer: *Http.Transfer) !void {
|
||||||
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
|
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
|
||||||
script.startCallback(transfer) catch |err| {
|
script.startCallback(transfer) catch |err| {
|
||||||
@@ -416,6 +503,38 @@ fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
|||||||
script.errorCallback(err);
|
script.errorCallback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
|
||||||
|
const content = script.source.content();
|
||||||
|
|
||||||
|
const Imports = struct {
|
||||||
|
imports: std.json.ArrayHashMap([]const u8),
|
||||||
|
};
|
||||||
|
|
||||||
|
const imports = try std.json.parseFromSliceLeaky(
|
||||||
|
Imports,
|
||||||
|
self.page.arena,
|
||||||
|
content,
|
||||||
|
.{ .allocate = .alloc_always },
|
||||||
|
);
|
||||||
|
|
||||||
|
var iter = imports.imports.map.iterator();
|
||||||
|
while (iter.next()) |entry| {
|
||||||
|
// > Relative URLs are resolved to absolute URL addresses using the
|
||||||
|
// > base URL of the document containing the import map.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_modules_using_import_maps
|
||||||
|
const resolved_url = try URL.stitch(
|
||||||
|
self.page.arena,
|
||||||
|
entry.value_ptr.*,
|
||||||
|
self.page.url.raw,
|
||||||
|
.{ .alloc = .if_needed, .null_terminated = true },
|
||||||
|
);
|
||||||
|
|
||||||
|
try self.importmap.put(self.page.arena, entry.key_ptr.*, resolved_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// A script which is pending execution.
|
// A script which is pending execution.
|
||||||
// It could be pending because:
|
// It could be pending because:
|
||||||
// (a) we're still downloading its content or
|
// (a) we're still downloading its content or
|
||||||
@@ -493,11 +612,15 @@ pub const PendingScript = struct {
|
|||||||
|
|
||||||
const manager = self.manager;
|
const manager = self.manager;
|
||||||
self.complete = true;
|
self.complete = true;
|
||||||
if (self.script.is_async) {
|
if (!self.script.is_async) {
|
||||||
manager.asyncs.remove(&self.node);
|
manager.evaluate();
|
||||||
manager.asyncs_ready.append(&self.node);
|
return;
|
||||||
}
|
}
|
||||||
manager.evaluate();
|
// async script can be evaluated immediately
|
||||||
|
self.script.eval(manager.page);
|
||||||
|
self.deinit();
|
||||||
|
// asyncScriptIsDone must be run after the pending script is deinit.
|
||||||
|
manager.asyncScriptIsDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn errorCallback(self: *PendingScript, err: anyerror) void {
|
fn errorCallback(self: *PendingScript, err: anyerror) void {
|
||||||
@@ -521,7 +644,7 @@ pub const PendingScript = struct {
|
|||||||
|
|
||||||
const script = &self.script;
|
const script = &self.script;
|
||||||
if (script.is_async) {
|
if (script.is_async) {
|
||||||
return if (self.complete) &self.manager.asyncs_ready else &self.manager.asyncs;
|
return &self.manager.asyncs;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (script.is_defer) {
|
if (script.is_defer) {
|
||||||
@@ -543,11 +666,12 @@ const Script = struct {
|
|||||||
const Kind = enum {
|
const Kind = enum {
|
||||||
module,
|
module,
|
||||||
javascript,
|
javascript,
|
||||||
|
importmap,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Callback = union(enum) {
|
const Callback = union(enum) {
|
||||||
string: []const u8,
|
string: []const u8,
|
||||||
function: Env.Function,
|
function: js.Function,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Source = union(enum) {
|
const Source = union(enum) {
|
||||||
@@ -583,8 +707,25 @@ const Script = struct {
|
|||||||
.cacheable = cacheable,
|
.cacheable = cacheable,
|
||||||
});
|
});
|
||||||
|
|
||||||
const js_context = page.main_context;
|
// Handle importmap special case here: the content is a JSON containing
|
||||||
var try_catch: Env.TryCatch = undefined;
|
// imports.
|
||||||
|
if (self.kind == .importmap) {
|
||||||
|
page.script_manager.parseImportmap(self) catch |err| {
|
||||||
|
log.err(.browser, "parse importmap script", .{
|
||||||
|
.err = err,
|
||||||
|
.src = url,
|
||||||
|
.kind = self.kind,
|
||||||
|
.cacheable = cacheable,
|
||||||
|
});
|
||||||
|
self.executeCallback("onerror", page);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.executeCallback("onload", page);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const js_context = page.js;
|
||||||
|
var try_catch: js.TryCatch = undefined;
|
||||||
try_catch.init(js_context);
|
try_catch.init(js_context);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
@@ -594,8 +735,9 @@ const Script = struct {
|
|||||||
.javascript => _ = js_context.eval(content, url) catch break :blk false,
|
.javascript => _ = js_context.eval(content, url) catch break :blk false,
|
||||||
.module => {
|
.module => {
|
||||||
// We don't care about waiting for the evaluation here.
|
// We don't care about waiting for the evaluation here.
|
||||||
_ = js_context.module(content, url, cacheable) catch break :blk false;
|
js_context.module(false, content, url, cacheable) catch break :blk false;
|
||||||
},
|
},
|
||||||
|
.importmap => unreachable, // handled before the try/catch.
|
||||||
}
|
}
|
||||||
break :blk true;
|
break :blk true;
|
||||||
};
|
};
|
||||||
@@ -626,11 +768,11 @@ const Script = struct {
|
|||||||
|
|
||||||
switch (callback) {
|
switch (callback) {
|
||||||
.string => |str| {
|
.string => |str| {
|
||||||
var try_catch: Env.TryCatch = undefined;
|
var try_catch: js.TryCatch = undefined;
|
||||||
try_catch.init(page.main_context);
|
try_catch.init(page.js);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
_ = page.main_context.exec(str, typ) catch |err| {
|
_ = page.js.exec(str, typ) catch |err| {
|
||||||
const msg = try_catch.err(page.arena) catch @errorName(err) orelse "unknown";
|
const msg = try_catch.err(page.arena) catch @errorName(err) orelse "unknown";
|
||||||
log.warn(.user_script, "script callback", .{
|
log.warn(.user_script, "script callback", .{
|
||||||
.url = self.url,
|
.url = self.url,
|
||||||
@@ -648,11 +790,8 @@ const Script = struct {
|
|||||||
};
|
};
|
||||||
defer parser.eventDestroy(loadevt);
|
defer parser.eventDestroy(loadevt);
|
||||||
|
|
||||||
var result: Env.Function.Result = undefined;
|
var result: js.Function.Result = undefined;
|
||||||
const iface = Event.toInterface(loadevt) catch |err| {
|
const iface = Event.toInterface(loadevt);
|
||||||
log.err(.browser, "SM event interface", .{ .err = err });
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
f.tryCall(void, .{iface}, &result) catch {
|
f.tryCall(void, .{iface}, &result) catch {
|
||||||
log.warn(.user_script, "script callback", .{
|
log.warn(.user_script, "script callback", .{
|
||||||
.url = self.url,
|
.url = self.url,
|
||||||
@@ -754,16 +893,17 @@ const BufferPool = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Blocking = struct {
|
const SyncModule = struct {
|
||||||
allocator: Allocator,
|
manager: *ScriptManager,
|
||||||
buffer_pool: *BufferPool,
|
|
||||||
state: State = .{ .running = {} },
|
|
||||||
buffer: std.ArrayListUnmanaged(u8) = .{},
|
buffer: std.ArrayListUnmanaged(u8) = .{},
|
||||||
|
state: State = .loading,
|
||||||
|
// number of waiters for the module.
|
||||||
|
waiters: u8 = 0,
|
||||||
|
|
||||||
const State = union(enum) {
|
const State = union(enum) {
|
||||||
running: void,
|
done,
|
||||||
|
loading,
|
||||||
err: anyerror,
|
err: anyerror,
|
||||||
done: BlockingResult,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fn startCallback(transfer: *Http.Transfer) !void {
|
fn startCallback(transfer: *Http.Transfer) !void {
|
||||||
@@ -779,12 +919,13 @@ const Blocking = struct {
|
|||||||
.content_type = header.contentType(),
|
.content_type = header.contentType(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var self: *SyncModule = @ptrCast(@alignCast(transfer.ctx));
|
||||||
if (header.status != 200) {
|
if (header.status != 200) {
|
||||||
|
self.finished(.{ .err = error.InvalidStatusCode });
|
||||||
return error.InvalidStatusCode;
|
return error.InvalidStatusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
|
self.buffer = self.manager.buffer_pool.get();
|
||||||
self.buffer = self.buffer_pool.get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||||
@@ -794,8 +935,8 @@ const Blocking = struct {
|
|||||||
// .blocking = true,
|
// .blocking = true,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
|
var self: *SyncModule = @ptrCast(@alignCast(transfer.ctx));
|
||||||
self.buffer.appendSlice(self.allocator, data) catch |err| {
|
self.buffer.appendSlice(self.manager.allocator, data) catch |err| {
|
||||||
log.err(.http, "SM.dataCallback", .{
|
log.err(.http, "SM.dataCallback", .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.len = data.len,
|
.len = data.len,
|
||||||
@@ -807,29 +948,107 @@ const Blocking = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn doneCallback(ctx: *anyopaque) !void {
|
fn doneCallback(ctx: *anyopaque) !void {
|
||||||
var self: *Blocking = @ptrCast(@alignCast(ctx));
|
var self: *SyncModule = @ptrCast(@alignCast(ctx));
|
||||||
self.state = .{ .done = .{
|
self.finished(.done);
|
||||||
.buffer = self.buffer,
|
|
||||||
.buffer_pool = self.buffer_pool,
|
|
||||||
} };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||||
var self: *Blocking = @ptrCast(@alignCast(ctx));
|
var self: *SyncModule = @ptrCast(@alignCast(ctx));
|
||||||
self.state = .{ .err = err };
|
self.finished(.{ .err = err });
|
||||||
self.buffer_pool.release(self.buffer);
|
}
|
||||||
|
|
||||||
|
fn finished(self: *SyncModule, state: State) void {
|
||||||
|
self.state = state;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const BlockingResult = struct {
|
pub const AsyncModule = struct {
|
||||||
|
cb: Callback,
|
||||||
|
cb_data: *anyopaque,
|
||||||
|
manager: *ScriptManager,
|
||||||
|
buffer: std.ArrayListUnmanaged(u8) = .{},
|
||||||
|
|
||||||
|
pub const Callback = *const fn (ptr: *anyopaque, result: anyerror!GetResult) void;
|
||||||
|
|
||||||
|
fn startCallback(transfer: *Http.Transfer) !void {
|
||||||
|
log.debug(.http, "script fetch start", .{ .req = transfer, .async = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn headerCallback(transfer: *Http.Transfer) !void {
|
||||||
|
const header = &transfer.response_header.?;
|
||||||
|
log.debug(.http, "script header", .{
|
||||||
|
.req = transfer,
|
||||||
|
.async = true,
|
||||||
|
.status = header.status,
|
||||||
|
.content_type = header.contentType(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (header.status != 200) {
|
||||||
|
return error.InvalidStatusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
var self: *AsyncModule = @ptrCast(@alignCast(transfer.ctx));
|
||||||
|
self.buffer = self.manager.buffer_pool.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||||
|
// too verbose
|
||||||
|
// log.debug(.http, "script data chunk", .{
|
||||||
|
// .req = transfer,
|
||||||
|
// .blocking = true,
|
||||||
|
// });
|
||||||
|
|
||||||
|
var self: *AsyncModule = @ptrCast(@alignCast(transfer.ctx));
|
||||||
|
self.buffer.appendSlice(self.manager.allocator, data) catch |err| {
|
||||||
|
log.err(.http, "SM.dataCallback", .{
|
||||||
|
.err = err,
|
||||||
|
.len = data.len,
|
||||||
|
.ascyn = true,
|
||||||
|
.transfer = transfer,
|
||||||
|
});
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn doneCallback(ctx: *anyopaque) !void {
|
||||||
|
var self: *AsyncModule = @ptrCast(@alignCast(ctx));
|
||||||
|
defer self.manager.async_module_pool.destroy(self);
|
||||||
|
self.cb(self.cb_data, .{
|
||||||
|
.shared = false,
|
||||||
|
.buffer = self.buffer,
|
||||||
|
.buffer_pool = &self.manager.buffer_pool,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||||
|
var self: *AsyncModule = @ptrCast(@alignCast(ctx));
|
||||||
|
|
||||||
|
if (err != error.Abort) {
|
||||||
|
self.cb(self.cb_data, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.buffer.items.len > 0) {
|
||||||
|
self.manager.buffer_pool.release(self.buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.manager.async_module_pool.destroy(self);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const GetResult = struct {
|
||||||
buffer: std.ArrayListUnmanaged(u8),
|
buffer: std.ArrayListUnmanaged(u8),
|
||||||
buffer_pool: *BufferPool,
|
buffer_pool: *BufferPool,
|
||||||
|
shared: bool,
|
||||||
|
|
||||||
pub fn deinit(self: *BlockingResult) void {
|
pub fn deinit(self: *GetResult) void {
|
||||||
|
// if the result is shared, don't deinit.
|
||||||
|
if (self.shared) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.buffer_pool.release(self.buffer);
|
self.buffer_pool.release(self.buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn src(self: *const BlockingResult) []const u8 {
|
pub fn src(self: *const GetResult) []const u8 {
|
||||||
return self.buffer.items;
|
return self.buffer.items;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
189
src/browser/SlotChangeMonitor.zig
Normal file
189
src/browser/SlotChangeMonitor.zig
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const log = @import("../log.zig");
|
||||||
|
const parser = @import("netsurf.zig");
|
||||||
|
const collection = @import("dom/html_collection.zig");
|
||||||
|
|
||||||
|
const Page = @import("page.zig").Page;
|
||||||
|
|
||||||
|
const SlotChangeMonitor = @This();
|
||||||
|
|
||||||
|
page: *Page,
|
||||||
|
event_node: parser.EventNode,
|
||||||
|
slots_changed: std.ArrayList(*parser.Slot),
|
||||||
|
|
||||||
|
// Monitors the document in order to trigger slotchange events.
|
||||||
|
pub fn init(page: *Page) !*SlotChangeMonitor {
|
||||||
|
// on the heap, we need a stable address for event_node
|
||||||
|
const self = try page.arena.create(SlotChangeMonitor);
|
||||||
|
self.* = .{
|
||||||
|
.page = page,
|
||||||
|
.slots_changed = .empty,
|
||||||
|
.event_node = .{ .func = mutationCallback },
|
||||||
|
};
|
||||||
|
const root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document));
|
||||||
|
|
||||||
|
_ = try parser.eventTargetAddEventListener(
|
||||||
|
parser.toEventTarget(parser.Node, root),
|
||||||
|
"DOMNodeInserted",
|
||||||
|
&self.event_node,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
_ = try parser.eventTargetAddEventListener(
|
||||||
|
parser.toEventTarget(parser.Node, root),
|
||||||
|
"DOMNodeRemoved",
|
||||||
|
&self.event_node,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
_ = try parser.eventTargetAddEventListener(
|
||||||
|
parser.toEventTarget(parser.Node, root),
|
||||||
|
"DOMAttrModified",
|
||||||
|
&self.event_node,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a element, finds its slot, if any.
|
||||||
|
pub fn findSlot(element: *parser.Element, page: *const Page) !?*parser.Slot {
|
||||||
|
const target_name = (try parser.elementGetAttribute(element, "slot")) orelse return null;
|
||||||
|
return findNamedSlot(element, target_name, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given an element and a name, find the slo, if any. This is only useful for
|
||||||
|
// MutationEvents where findSlot is unreliable because parser.elementGetAttribute(element, "slot")
|
||||||
|
// could return the new or old value.
|
||||||
|
fn findNamedSlot(element: *parser.Element, target_name: []const u8, page: *const Page) !?*parser.Slot {
|
||||||
|
// I believe elements need to be added as direct descendents of the host,
|
||||||
|
// so we don't need to go find the host, we just grab the parent.
|
||||||
|
const host = parser.nodeParentNode(@ptrCast(element)) orelse return null;
|
||||||
|
const state = page.getNodeState(host) orelse return null;
|
||||||
|
const shadow_root = state.shadow_root orelse return null;
|
||||||
|
|
||||||
|
// if we're here, we found a host, now find the slot
|
||||||
|
var nodes = collection.HTMLCollectionByTagName(
|
||||||
|
@ptrCast(@alignCast(shadow_root.proto)),
|
||||||
|
"slot",
|
||||||
|
.{ .include_root = false },
|
||||||
|
);
|
||||||
|
for (0..1000) |i| {
|
||||||
|
const n = (try nodes.item(@intCast(i))) orelse return null;
|
||||||
|
const slot_name = (try parser.elementGetAttribute(@ptrCast(n), "name")) orelse "";
|
||||||
|
if (std.mem.eql(u8, target_name, slot_name)) {
|
||||||
|
return @ptrCast(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event callback from the mutation event, signaling either the addition of
|
||||||
|
// a node, removal of a node, or a change in attribute
|
||||||
|
fn mutationCallback(en: *parser.EventNode, event: *parser.Event) void {
|
||||||
|
const mutation_event = parser.eventToMutationEvent(event);
|
||||||
|
const self: *SlotChangeMonitor = @fieldParentPtr("event_node", en);
|
||||||
|
self._mutationCallback(mutation_event) catch |err| {
|
||||||
|
log.err(.web_api, "slot change callback", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _mutationCallback(self: *SlotChangeMonitor, event: *parser.MutationEvent) !void {
|
||||||
|
const event_type = parser.eventType(@ptrCast(event));
|
||||||
|
if (std.mem.eql(u8, event_type, "DOMNodeInserted")) {
|
||||||
|
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
|
||||||
|
return self.nodeAddedOrRemoved(@ptrCast(event_target));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, event_type, "DOMNodeRemoved")) {
|
||||||
|
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
|
||||||
|
return self.nodeAddedOrRemoved(@ptrCast(event_target));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, event_type, "DOMAttrModified")) {
|
||||||
|
const attribute_name = try parser.mutationEventAttributeName(event);
|
||||||
|
if (std.mem.eql(u8, attribute_name, "slot") == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const new_value = parser.mutationEventNewValue(event);
|
||||||
|
const prev_value = parser.mutationEventPrevValue(event);
|
||||||
|
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
|
||||||
|
return self.nodeAttributeChanged(@ptrCast(event_target), new_value, prev_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A node was removed or added. If it's an element, and if it has a slot attribute
|
||||||
|
// then we'll dispatch a slotchange event.
|
||||||
|
fn nodeAddedOrRemoved(self: *SlotChangeMonitor, node: *parser.Node) !void {
|
||||||
|
if (parser.nodeType(node) != .element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const el: *parser.Element = @ptrCast(node);
|
||||||
|
if (try findSlot(el, self.page)) |slot| {
|
||||||
|
return self.scheduleSlotChange(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An attribute was modified. If the attribute is "slot", then we'll trigger 1
|
||||||
|
// slotchange for the old slot (if there was one) and 1 slotchange for the new
|
||||||
|
// one (if there is one)
|
||||||
|
fn nodeAttributeChanged(self: *SlotChangeMonitor, node: *parser.Node, new_value: ?[]const u8, prev_value: ?[]const u8) !void {
|
||||||
|
if (parser.nodeType(node) != .element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const el: *parser.Element = @ptrCast(node);
|
||||||
|
if (try findNamedSlot(el, prev_value orelse "", self.page)) |slot| {
|
||||||
|
try self.scheduleSlotChange(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (try findNamedSlot(el, new_value orelse "", self.page)) |slot| {
|
||||||
|
try self.scheduleSlotChange(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK. Our MutationEvent is not a MutationObserver - it's an older, deprecated
|
||||||
|
// API. It gets dispatched in the middle of the change. While I'm sure it has
|
||||||
|
// some rules, from our point of view, it fires too early. DOMAttrModified fires
|
||||||
|
// before the attribute is actually updated and DOMNodeRemoved before the node
|
||||||
|
// is actually removed. This is a problem if the callback will call
|
||||||
|
// `slot.assignedNodes`, since that won't return the new state.
|
||||||
|
// So, we use the page schedule to schedule the dispatching of the slotchange
|
||||||
|
// event.
|
||||||
|
fn scheduleSlotChange(self: *SlotChangeMonitor, slot: *parser.Slot) !void {
|
||||||
|
for (self.slots_changed.items) |changed| {
|
||||||
|
if (slot == changed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.slots_changed.append(self.page.arena, slot);
|
||||||
|
if (self.slots_changed.items.len == 1) {
|
||||||
|
// first item added, schedule the callback
|
||||||
|
try self.page.scheduler.add(self, scheduleCallback, 0, .{ .name = "slot change" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback from the schedule. Time to dispatch the slotchange event
|
||||||
|
fn scheduleCallback(ctx: *anyopaque) ?u32 {
|
||||||
|
var self: *SlotChangeMonitor = @ptrCast(@alignCast(ctx));
|
||||||
|
self._scheduleCallback() catch |err| {
|
||||||
|
log.err(.app, "slot change schedule", .{ .err = err });
|
||||||
|
};
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _scheduleCallback(self: *SlotChangeMonitor) !void {
|
||||||
|
for (self.slots_changed.items) |slot| {
|
||||||
|
const event = try parser.eventCreate();
|
||||||
|
defer parser.eventDestroy(event);
|
||||||
|
try parser.eventInit(event, "slotchange", .{});
|
||||||
|
_ = try parser.eventTargetDispatchEvent(
|
||||||
|
parser.toEventTarget(parser.Element, @ptrCast(@alignCast(slot))),
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.slots_changed.clearRetainingCapacity();
|
||||||
|
}
|
||||||
@@ -26,17 +26,16 @@
|
|||||||
// this quickly proved necessary, since different fields are needed on the same
|
// this quickly proved necessary, since different fields are needed on the same
|
||||||
// data at different levels of the prototype chain. This isn't memory efficient.
|
// data at different levels of the prototype chain. This isn't memory efficient.
|
||||||
|
|
||||||
const Env = @import("env.zig").Env;
|
const js = @import("js/js.zig");
|
||||||
const parser = @import("netsurf.zig");
|
const parser = @import("netsurf.zig");
|
||||||
const DataSet = @import("html/DataSet.zig");
|
const DataSet = @import("html/DataSet.zig");
|
||||||
const ShadowRoot = @import("dom/shadow_root.zig").ShadowRoot;
|
const ShadowRoot = @import("dom/shadow_root.zig").ShadowRoot;
|
||||||
const StyleSheet = @import("cssom/StyleSheet.zig");
|
const StyleSheet = @import("cssom/StyleSheet.zig");
|
||||||
const CSSStyleSheet = @import("cssom/CSSStyleSheet.zig");
|
|
||||||
const CSSStyleDeclaration = @import("cssom/CSSStyleDeclaration.zig");
|
const CSSStyleDeclaration = @import("cssom/CSSStyleDeclaration.zig");
|
||||||
|
|
||||||
// for HTMLScript (but probably needs to be added to more)
|
// for HTMLScript (but probably needs to be added to more)
|
||||||
onload: ?Env.Function = null,
|
onload: ?js.Function = null,
|
||||||
onerror: ?Env.Function = null,
|
onerror: ?js.Function = null,
|
||||||
|
|
||||||
// for HTMLElement
|
// for HTMLElement
|
||||||
style: CSSStyleDeclaration = .empty,
|
style: CSSStyleDeclaration = .empty,
|
||||||
@@ -54,7 +53,7 @@ style_sheet: ?*StyleSheet = null,
|
|||||||
|
|
||||||
// for dom/document
|
// for dom/document
|
||||||
active_element: ?*parser.Element = null,
|
active_element: ?*parser.Element = null,
|
||||||
adopted_style_sheets: ?Env.JsObject = null,
|
adopted_style_sheets: ?js.Object = null,
|
||||||
|
|
||||||
// for HTMLSelectElement
|
// for HTMLSelectElement
|
||||||
// By default, if no option is explicitly selected, the first option should
|
// By default, if no option is explicitly selected, the first option should
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ const std = @import("std");
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
|
const js = @import("js/js.zig");
|
||||||
const State = @import("State.zig");
|
const State = @import("State.zig");
|
||||||
const Env = @import("env.zig").Env;
|
|
||||||
const App = @import("../app.zig").App;
|
const App = @import("../app.zig").App;
|
||||||
const Session = @import("session.zig").Session;
|
const Session = @import("session.zig").Session;
|
||||||
const Notification = @import("../notification.zig").Notification;
|
const Notification = @import("../notification.zig").Notification;
|
||||||
@@ -34,11 +34,12 @@ const HttpClient = @import("../http/Client.zig");
|
|||||||
// You can create multiple browser instances.
|
// You can create multiple browser instances.
|
||||||
// A browser contains only one session.
|
// A browser contains only one session.
|
||||||
pub const Browser = struct {
|
pub const Browser = struct {
|
||||||
env: *Env,
|
env: *js.Env,
|
||||||
app: *App,
|
app: *App,
|
||||||
session: ?Session,
|
session: ?Session,
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
http_client: *HttpClient,
|
http_client: *HttpClient,
|
||||||
|
call_arena: ArenaAllocator,
|
||||||
page_arena: ArenaAllocator,
|
page_arena: ArenaAllocator,
|
||||||
session_arena: ArenaAllocator,
|
session_arena: ArenaAllocator,
|
||||||
transfer_arena: ArenaAllocator,
|
transfer_arena: ArenaAllocator,
|
||||||
@@ -48,7 +49,7 @@ pub const Browser = struct {
|
|||||||
pub fn init(app: *App) !Browser {
|
pub fn init(app: *App) !Browser {
|
||||||
const allocator = app.allocator;
|
const allocator = app.allocator;
|
||||||
|
|
||||||
const env = try Env.init(allocator, &app.platform, .{});
|
const env = try js.Env.init(allocator, &app.platform, .{});
|
||||||
errdefer env.deinit();
|
errdefer env.deinit();
|
||||||
|
|
||||||
const notification = try Notification.init(allocator, app.notification);
|
const notification = try Notification.init(allocator, app.notification);
|
||||||
@@ -63,6 +64,7 @@ pub const Browser = struct {
|
|||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.notification = notification,
|
.notification = notification,
|
||||||
.http_client = app.http.client,
|
.http_client = app.http.client,
|
||||||
|
.call_arena = ArenaAllocator.init(allocator),
|
||||||
.page_arena = ArenaAllocator.init(allocator),
|
.page_arena = ArenaAllocator.init(allocator),
|
||||||
.session_arena = ArenaAllocator.init(allocator),
|
.session_arena = ArenaAllocator.init(allocator),
|
||||||
.transfer_arena = ArenaAllocator.init(allocator),
|
.transfer_arena = ArenaAllocator.init(allocator),
|
||||||
@@ -73,6 +75,7 @@ pub const Browser = struct {
|
|||||||
pub fn deinit(self: *Browser) void {
|
pub fn deinit(self: *Browser) void {
|
||||||
self.closeSession();
|
self.closeSession();
|
||||||
self.env.deinit();
|
self.env.deinit();
|
||||||
|
self.call_arena.deinit();
|
||||||
self.page_arena.deinit();
|
self.page_arena.deinit();
|
||||||
self.session_arena.deinit();
|
self.session_arena.deinit();
|
||||||
self.transfer_arena.deinit();
|
self.transfer_arena.deinit();
|
||||||
|
|||||||
@@ -20,48 +20,47 @@ const std = @import("std");
|
|||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const js = @import("../js/js.zig");
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
const JsObject = @import("../env.zig").Env.JsObject;
|
|
||||||
|
|
||||||
pub const Console = struct {
|
pub const Console = struct {
|
||||||
// TODO: configurable writer
|
// TODO: configurable writer
|
||||||
timers: std.StringHashMapUnmanaged(u32) = .{},
|
timers: std.StringHashMapUnmanaged(u32) = .{},
|
||||||
counts: std.StringHashMapUnmanaged(u32) = .{},
|
counts: std.StringHashMapUnmanaged(u32) = .{},
|
||||||
|
|
||||||
pub fn _lp(values: []JsObject, page: *Page) !void {
|
pub fn _lp(values: []js.Object, page: *Page) !void {
|
||||||
if (values.len == 0) {
|
if (values.len == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) });
|
log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _log(values: []JsObject, page: *Page) !void {
|
pub fn _log(values: []js.Object, page: *Page) !void {
|
||||||
if (values.len == 0) {
|
if (values.len == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.info(.console, "info", .{ .args = try serializeValues(values, page) });
|
log.info(.console, "info", .{ .args = try serializeValues(values, page) });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _info(values: []JsObject, page: *Page) !void {
|
pub fn _info(values: []js.Object, page: *Page) !void {
|
||||||
return _log(values, page);
|
return _log(values, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _debug(values: []JsObject, page: *Page) !void {
|
pub fn _debug(values: []js.Object, page: *Page) !void {
|
||||||
if (values.len == 0) {
|
if (values.len == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.debug(.console, "debug", .{ .args = try serializeValues(values, page) });
|
log.debug(.console, "debug", .{ .args = try serializeValues(values, page) });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _warn(values: []JsObject, page: *Page) !void {
|
pub fn _warn(values: []js.Object, page: *Page) !void {
|
||||||
if (values.len == 0) {
|
if (values.len == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.warn(.console, "warn", .{ .args = try serializeValues(values, page) });
|
log.warn(.console, "warn", .{ .args = try serializeValues(values, page) });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _error(values: []JsObject, page: *Page) !void {
|
pub fn _error(values: []js.Object, page: *Page) !void {
|
||||||
if (values.len == 0) {
|
if (values.len == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -72,6 +71,16 @@ pub const Console = struct {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn _trace(values: []js.Object, page: *Page) !void {
|
||||||
|
if (values.len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.debug(.console, "debug", .{
|
||||||
|
.stack = page.js.stackTrace() catch "???",
|
||||||
|
.args = try serializeValues(values, page),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn _clear() void {}
|
pub fn _clear() void {}
|
||||||
|
|
||||||
pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void {
|
pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void {
|
||||||
@@ -133,7 +142,7 @@ pub const Console = struct {
|
|||||||
log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value });
|
log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _assert(assertion: JsObject, values: []JsObject, page: *Page) !void {
|
pub fn _assert(assertion: js.Object, values: []js.Object, page: *Page) !void {
|
||||||
if (assertion.isTruthy()) {
|
if (assertion.isTruthy()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -144,7 +153,7 @@ pub const Console = struct {
|
|||||||
log.info(.console, "assertion failed", .{ .values = serialized_values });
|
log.info(.console, "assertion failed", .{ .values = serialized_values });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serializeValues(values: []JsObject, page: *Page) ![]const u8 {
|
fn serializeValues(values: []js.Object, page: *Page) ![]const u8 {
|
||||||
if (values.len == 0) {
|
if (values.len == 0) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -166,74 +175,3 @@ pub const Console = struct {
|
|||||||
fn timestamp() u32 {
|
fn timestamp() u32 {
|
||||||
return @import("../../datetime.zig").timestamp();
|
return @import("../../datetime.zig").timestamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
// const testing = @import("../../testing.zig");
|
|
||||||
// test "Browser.Console" {
|
|
||||||
// defer testing.reset();
|
|
||||||
|
|
||||||
// var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
// defer runner.deinit();
|
|
||||||
|
|
||||||
// {
|
|
||||||
// try runner.testCases(&.{
|
|
||||||
// .{ "console.log('a')", "undefined" },
|
|
||||||
// .{ "console.warn('hello world', 23, true, new Object())", "undefined" },
|
|
||||||
// }, .{});
|
|
||||||
|
|
||||||
// const captured = test_capture.captured.items;
|
|
||||||
// try testing.expectEqual("[info] args= 1: a", captured[0]);
|
|
||||||
// try testing.expectEqual("[warn] args= 1: hello world 2: 23 3: true 4: #<Object>", captured[1]);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// {
|
|
||||||
// test_capture.reset();
|
|
||||||
// try runner.testCases(&.{
|
|
||||||
// .{ "console.countReset()", "undefined" },
|
|
||||||
// .{ "console.count()", "undefined" },
|
|
||||||
// .{ "console.count('teg')", "undefined" },
|
|
||||||
// .{ "console.count('teg')", "undefined" },
|
|
||||||
// .{ "console.count('teg')", "undefined" },
|
|
||||||
// .{ "console.count()", "undefined" },
|
|
||||||
// .{ "console.countReset('teg')", "undefined" },
|
|
||||||
// .{ "console.countReset()", "undefined" },
|
|
||||||
// .{ "console.count()", "undefined" },
|
|
||||||
// }, .{});
|
|
||||||
|
|
||||||
// const captured = test_capture.captured.items;
|
|
||||||
// try testing.expectEqual("[invalid counter] label=default", captured[0]);
|
|
||||||
// try testing.expectEqual("[count] label=default count=1", captured[1]);
|
|
||||||
// try testing.expectEqual("[count] label=teg count=1", captured[2]);
|
|
||||||
// try testing.expectEqual("[count] label=teg count=2", captured[3]);
|
|
||||||
// try testing.expectEqual("[count] label=teg count=3", captured[4]);
|
|
||||||
// try testing.expectEqual("[count] label=default count=2", captured[5]);
|
|
||||||
// try testing.expectEqual("[count reset] label=teg count=3", captured[6]);
|
|
||||||
// try testing.expectEqual("[count reset] label=default count=2", captured[7]);
|
|
||||||
// try testing.expectEqual("[count] label=default count=1", captured[8]);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// {
|
|
||||||
// test_capture.reset();
|
|
||||||
// try runner.testCases(&.{
|
|
||||||
// .{ "console.assert(true)", "undefined" },
|
|
||||||
// .{ "console.assert('a', 2, 3, 4)", "undefined" },
|
|
||||||
// .{ "console.assert('')", "undefined" },
|
|
||||||
// .{ "console.assert('', 'x', true)", "undefined" },
|
|
||||||
// .{ "console.assert(false, 'x')", "undefined" },
|
|
||||||
// }, .{});
|
|
||||||
|
|
||||||
// const captured = test_capture.captured.items;
|
|
||||||
// try testing.expectEqual("[assertion failed] values=", captured[0]);
|
|
||||||
// try testing.expectEqual("[assertion failed] values= 1: x 2: true", captured[1]);
|
|
||||||
// try testing.expectEqual("[assertion failed] values= 1: x", captured[2]);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// {
|
|
||||||
// test_capture.reset();
|
|
||||||
// try runner.testCases(&.{
|
|
||||||
// .{ "[1].forEach(console.log)", null },
|
|
||||||
// }, .{});
|
|
||||||
|
|
||||||
// const captured = test_capture.captured.items;
|
|
||||||
// try testing.expectEqual("[info] args= 1: 1 2: 0 3: [1]", captured[0]);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -17,14 +17,14 @@
|
|||||||
// 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 Env = @import("../env.zig").Env;
|
const js = @import("../js/js.zig");
|
||||||
const uuidv4 = @import("../../id.zig").uuidv4;
|
const uuidv4 = @import("../../id.zig").uuidv4;
|
||||||
|
|
||||||
// https://w3c.github.io/webcrypto/#crypto-interface
|
// https://w3c.github.io/webcrypto/#crypto-interface
|
||||||
pub const Crypto = struct {
|
pub const Crypto = struct {
|
||||||
_not_empty: bool = true,
|
_not_empty: bool = true,
|
||||||
|
|
||||||
pub fn _getRandomValues(_: *const Crypto, js_obj: Env.JsObject) !Env.JsObject {
|
pub fn _getRandomValues(_: *const Crypto, js_obj: js.Object) !js.Object {
|
||||||
var into = try js_obj.toZig(Crypto, "getRandomValues", RandomValues);
|
var into = try js_obj.toZig(Crypto, "getRandomValues", RandomValues);
|
||||||
const buf = into.asBuffer();
|
const buf = into.asBuffer();
|
||||||
if (buf.len > 65_536) {
|
if (buf.len > 65_536) {
|
||||||
|
|||||||
@@ -46,17 +46,15 @@ pub fn parse(alloc: std.mem.Allocator, s: []const u8, opts: parser.ParseOptions)
|
|||||||
// matchFirst call m.match with the first node that matches the selector s, from the
|
// matchFirst call m.match with the first node that matches the selector s, from the
|
||||||
// descendants of n and returns true. If none matches, it returns false.
|
// descendants of n and returns true. If none matches, it returns false.
|
||||||
pub fn matchFirst(s: *const Selector, node: anytype, m: anytype) !bool {
|
pub fn matchFirst(s: *const Selector, node: anytype, m: anytype) !bool {
|
||||||
var c = try node.firstChild();
|
var child = node.firstChild();
|
||||||
while (true) {
|
while (child) |c| {
|
||||||
if (c == null) break;
|
if (try s.match(c)) {
|
||||||
|
try m.match(c);
|
||||||
if (try s.match(c.?)) {
|
|
||||||
try m.match(c.?);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (try matchFirst(s, c.?, m)) return true;
|
if (try matchFirst(s, c, m)) return true;
|
||||||
c = try c.?.nextSibling();
|
child = c.nextSibling();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -64,13 +62,11 @@ pub fn matchFirst(s: *const Selector, node: anytype, m: anytype) !bool {
|
|||||||
// matchAll call m.match with the all the nodes that matches the selector s, from the
|
// matchAll call m.match with the all the nodes that matches the selector s, from the
|
||||||
// descendants of n.
|
// descendants of n.
|
||||||
pub fn matchAll(s: *const Selector, node: anytype, m: anytype) !void {
|
pub fn matchAll(s: *const Selector, node: anytype, m: anytype) !void {
|
||||||
var c = try node.firstChild();
|
var child = node.firstChild();
|
||||||
while (true) {
|
while (child) |c| {
|
||||||
if (c == null) break;
|
if (try s.match(c)) try m.match(c);
|
||||||
|
try matchAll(s, c, m);
|
||||||
if (try s.match(c.?)) try m.match(c.?);
|
child = c.nextSibling();
|
||||||
try matchAll(s, c.?, m);
|
|
||||||
c = try c.?.nextSibling();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,68 +19,74 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
|
const css = @import("css.zig");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
// Node implementation with Netsurf Libdom C lib.
|
// Node implementation with Netsurf Libdom C lib.
|
||||||
pub const Node = struct {
|
pub const Node = struct {
|
||||||
node: *parser.Node,
|
node: *parser.Node,
|
||||||
|
|
||||||
pub fn firstChild(n: Node) !?Node {
|
pub fn firstChild(n: Node) ?Node {
|
||||||
const c = try parser.nodeFirstChild(n.node);
|
const c = parser.nodeFirstChild(n.node);
|
||||||
if (c) |cc| return .{ .node = cc };
|
if (c) |cc| return .{ .node = cc };
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lastChild(n: Node) !?Node {
|
pub fn lastChild(n: Node) ?Node {
|
||||||
const c = try parser.nodeLastChild(n.node);
|
const c = parser.nodeLastChild(n.node);
|
||||||
if (c) |cc| return .{ .node = cc };
|
if (c) |cc| return .{ .node = cc };
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nextSibling(n: Node) !?Node {
|
pub fn nextSibling(n: Node) ?Node {
|
||||||
const c = try parser.nodeNextSibling(n.node);
|
const c = parser.nodeNextSibling(n.node);
|
||||||
if (c) |cc| return .{ .node = cc };
|
if (c) |cc| return .{ .node = cc };
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prevSibling(n: Node) !?Node {
|
pub fn prevSibling(n: Node) ?Node {
|
||||||
const c = try parser.nodePreviousSibling(n.node);
|
const c = parser.nodePreviousSibling(n.node);
|
||||||
if (c) |cc| return .{ .node = cc };
|
if (c) |cc| return .{ .node = cc };
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parent(n: Node) !?Node {
|
pub fn parent(n: Node) ?Node {
|
||||||
const c = try parser.nodeParentNode(n.node);
|
const c = parser.nodeParentNode(n.node);
|
||||||
if (c) |cc| return .{ .node = cc };
|
if (c) |cc| return .{ .node = cc };
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isElement(n: Node) bool {
|
pub fn isElement(n: Node) bool {
|
||||||
const t = parser.nodeType(n.node) catch return false;
|
return parser.nodeType(n.node) == .element;
|
||||||
return t == .element;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isDocument(n: Node) bool {
|
pub fn isDocument(n: Node) bool {
|
||||||
const t = parser.nodeType(n.node) catch return false;
|
return parser.nodeType(n.node) == .document;
|
||||||
return t == .document;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isComment(n: Node) bool {
|
pub fn isComment(n: Node) bool {
|
||||||
const t = parser.nodeType(n.node) catch return false;
|
return parser.nodeType(n.node) == .comment;
|
||||||
return t == .comment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isText(n: Node) bool {
|
pub fn isText(n: Node) bool {
|
||||||
const t = parser.nodeType(n.node) catch return false;
|
return parser.nodeType(n.node) == .text;
|
||||||
return t == .text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isEmptyText(n: Node) !bool {
|
pub fn text(n: Node) ?[]const u8 {
|
||||||
const data = try parser.nodeTextContent(n.node);
|
const data = parser.nodeTextContent(n.node);
|
||||||
|
if (data == null) return null;
|
||||||
|
if (data.?.len == 0) return null;
|
||||||
|
|
||||||
|
return std.mem.trim(u8, data.?, &std.ascii.whitespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isEmptyText(n: Node) bool {
|
||||||
|
const data = parser.nodeTextContent(n.node);
|
||||||
if (data == null) return true;
|
if (data == null) return true;
|
||||||
if (data.?.len == 0) return true;
|
if (data.?.len == 0) return true;
|
||||||
|
|
||||||
@@ -88,7 +94,7 @@ pub const Node = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn tag(n: Node) ![]const u8 {
|
pub fn tag(n: Node) ![]const u8 {
|
||||||
return try parser.nodeName(n.node);
|
return parser.nodeName(n.node);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn attr(n: Node, key: []const u8) !?[]const u8 {
|
pub fn attr(n: Node, key: []const u8) !?[]const u8 {
|
||||||
@@ -100,3 +106,318 @@ pub const Node = struct {
|
|||||||
return a.node == b.node;
|
return a.node == b.node;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MatcherTest = struct {
|
||||||
|
const Nodes = std.ArrayListUnmanaged(Node);
|
||||||
|
|
||||||
|
nodes: Nodes,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
fn init(allocator: Allocator) MatcherTest {
|
||||||
|
return .{
|
||||||
|
.nodes = .empty,
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(m: *MatcherTest) void {
|
||||||
|
m.nodes.deinit(m.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(m: *MatcherTest) void {
|
||||||
|
m.nodes.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn match(m: *MatcherTest, n: Node) !void {
|
||||||
|
try m.nodes.append(m.allocator, n);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "Browser.CSS.Libdom: matchFirst" {
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
|
||||||
|
parser.init();
|
||||||
|
defer parser.deinit();
|
||||||
|
|
||||||
|
var matcher = MatcherTest.init(alloc);
|
||||||
|
defer matcher.deinit();
|
||||||
|
|
||||||
|
const testcases = [_]struct {
|
||||||
|
q: []const u8,
|
||||||
|
html: []const u8,
|
||||||
|
exp: usize,
|
||||||
|
}{
|
||||||
|
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
||||||
|
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 1 },
|
||||||
|
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
||||||
|
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
||||||
|
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
||||||
|
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
||||||
|
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
||||||
|
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||||
|
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
||||||
|
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||||
|
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||||
|
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||||
|
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||||
|
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
||||||
|
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
||||||
|
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
||||||
|
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
||||||
|
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
||||||
|
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 1 },
|
||||||
|
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||||
|
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||||
|
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
||||||
|
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
||||||
|
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||||
|
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||||
|
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
||||||
|
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
||||||
|
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
||||||
|
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||||
|
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
||||||
|
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
||||||
|
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||||
|
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
||||||
|
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||||
|
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
||||||
|
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
||||||
|
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
||||||
|
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||||
|
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||||
|
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||||
|
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
||||||
|
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
||||||
|
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 1 },
|
||||||
|
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
||||||
|
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
||||||
|
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 1 },
|
||||||
|
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||||
|
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||||
|
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 1 },
|
||||||
|
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||||
|
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||||
|
.{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
||||||
|
.{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||||
|
.{ .q = ":containsOwn(\"Inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
||||||
|
.{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||||
|
// .{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
||||||
|
.{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
||||||
|
.{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
||||||
|
.{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
||||||
|
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 1 },
|
||||||
|
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
||||||
|
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
||||||
|
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 1 },
|
||||||
|
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
||||||
|
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
||||||
|
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (testcases) |tc| {
|
||||||
|
matcher.reset();
|
||||||
|
|
||||||
|
const doc = try parser.documentHTMLParseFromStr(tc.html);
|
||||||
|
defer parser.documentHTMLClose(doc) catch {};
|
||||||
|
|
||||||
|
const s = css.parse(alloc, tc.q, .{}) catch |e| {
|
||||||
|
std.debug.print("parse, query: {s}\n", .{tc.q});
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
|
||||||
|
defer s.deinit(alloc);
|
||||||
|
|
||||||
|
const node = Node{ .node = parser.documentHTMLToNode(doc) };
|
||||||
|
|
||||||
|
_ = css.matchFirst(&s, node, &matcher) catch |e| {
|
||||||
|
std.debug.print("match, query: {s}\n", .{tc.q});
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
||||||
|
std.debug.print("expectation, query: {s}\n", .{tc.q});
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Browser.CSS.Libdom: matchAll" {
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
|
||||||
|
parser.init();
|
||||||
|
defer parser.deinit();
|
||||||
|
|
||||||
|
var matcher = MatcherTest.init(alloc);
|
||||||
|
defer matcher.deinit();
|
||||||
|
|
||||||
|
const testcases = [_]struct {
|
||||||
|
q: []const u8,
|
||||||
|
html: []const u8,
|
||||||
|
exp: usize,
|
||||||
|
}{
|
||||||
|
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
||||||
|
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 3 },
|
||||||
|
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 3 },
|
||||||
|
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
||||||
|
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
||||||
|
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
||||||
|
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
||||||
|
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
||||||
|
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||||
|
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
||||||
|
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||||
|
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||||
|
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||||
|
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||||
|
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
||||||
|
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
||||||
|
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
||||||
|
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
||||||
|
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 2 },
|
||||||
|
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 2 },
|
||||||
|
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||||
|
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||||
|
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
||||||
|
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
||||||
|
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||||
|
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||||
|
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
||||||
|
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
||||||
|
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
||||||
|
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||||
|
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
||||||
|
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
||||||
|
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||||
|
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
||||||
|
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 2 },
|
||||||
|
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
||||||
|
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
||||||
|
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||||
|
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||||
|
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||||
|
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||||
|
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
||||||
|
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
||||||
|
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
||||||
|
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||||
|
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||||
|
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||||
|
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
||||||
|
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
||||||
|
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 3 },
|
||||||
|
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 2 },
|
||||||
|
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
||||||
|
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 2 },
|
||||||
|
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 2 },
|
||||||
|
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||||
|
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 3 },
|
||||||
|
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||||
|
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||||
|
.{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
||||||
|
.{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||||
|
.{ .q = ":containsOwn(\"Inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
||||||
|
.{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||||
|
.{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
||||||
|
.{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
||||||
|
.{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 2 },
|
||||||
|
.{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
||||||
|
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||||
|
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||||
|
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||||
|
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 3 },
|
||||||
|
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||||
|
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 2 },
|
||||||
|
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 2 },
|
||||||
|
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
||||||
|
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 5 },
|
||||||
|
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 2 },
|
||||||
|
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
||||||
|
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
||||||
|
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (testcases) |tc| {
|
||||||
|
matcher.reset();
|
||||||
|
|
||||||
|
const doc = try parser.documentHTMLParseFromStr(tc.html);
|
||||||
|
defer parser.documentHTMLClose(doc) catch {};
|
||||||
|
|
||||||
|
const s = css.parse(alloc, tc.q, .{}) catch |e| {
|
||||||
|
std.debug.print("parse, query: {s}\n", .{tc.q});
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
defer s.deinit(alloc);
|
||||||
|
|
||||||
|
const node = Node{ .node = parser.documentHTMLToNode(doc) };
|
||||||
|
|
||||||
|
_ = css.matchAll(&s, node, &matcher) catch |e| {
|
||||||
|
std.debug.print("match, query: {s}\n", .{tc.q});
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
||||||
|
std.debug.print("expectation, query: {s}\n", .{tc.q});
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,330 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 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 css = @import("css.zig");
|
|
||||||
const Node = @import("libdom.zig").Node;
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const Matcher = struct {
|
|
||||||
const Nodes = std.ArrayListUnmanaged(Node);
|
|
||||||
|
|
||||||
nodes: Nodes,
|
|
||||||
allocator: Allocator,
|
|
||||||
|
|
||||||
fn init(allocator: Allocator) Matcher {
|
|
||||||
return .{
|
|
||||||
.nodes = .empty,
|
|
||||||
.allocator = allocator,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(m: *Matcher) void {
|
|
||||||
m.nodes.deinit(m.allocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(m: *Matcher) void {
|
|
||||||
m.nodes.clearRetainingCapacity();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match(m: *Matcher, n: Node) !void {
|
|
||||||
try m.nodes.append(m.allocator, n);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
test "matchFirst" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
var matcher = Matcher.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
q: []const u8,
|
|
||||||
html: []const u8,
|
|
||||||
exp: usize,
|
|
||||||
}{
|
|
||||||
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
|
||||||
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
|
||||||
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
|
||||||
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
|
||||||
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
|
||||||
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
|
||||||
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
|
||||||
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
|
||||||
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
|
||||||
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
|
||||||
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
|
||||||
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
|
||||||
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
|
||||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
|
||||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
|
||||||
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
|
||||||
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
|
||||||
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
|
||||||
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
|
||||||
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
|
||||||
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
|
||||||
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
|
||||||
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
|
||||||
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
|
||||||
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
|
||||||
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 1 },
|
|
||||||
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 1 },
|
|
||||||
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 1 },
|
|
||||||
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
|
||||||
// .{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
// .{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
|
||||||
// .{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
|
||||||
// .{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
|
||||||
// .{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 1 },
|
|
||||||
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
|
||||||
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
|
||||||
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 1 },
|
|
||||||
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 1 },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
matcher.reset();
|
|
||||||
|
|
||||||
const doc = try parser.documentHTMLParseFromStr(tc.html);
|
|
||||||
defer parser.documentHTMLClose(doc) catch {};
|
|
||||||
|
|
||||||
const s = css.parse(alloc, tc.q, .{}) catch |e| {
|
|
||||||
std.debug.print("parse, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
defer s.deinit(alloc);
|
|
||||||
|
|
||||||
const node = Node{ .node = parser.documentHTMLToNode(doc) };
|
|
||||||
|
|
||||||
_ = css.matchFirst(s, node, &matcher) catch |e| {
|
|
||||||
std.debug.print("match, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("expectation, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "matchAll" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
var matcher = Matcher.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
q: []const u8,
|
|
||||||
html: []const u8,
|
|
||||||
exp: usize,
|
|
||||||
}{
|
|
||||||
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
|
||||||
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 3 },
|
|
||||||
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 3 },
|
|
||||||
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
|
||||||
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
|
||||||
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
|
||||||
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
|
||||||
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
|
||||||
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
|
||||||
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
|
||||||
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 2 },
|
|
||||||
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 2 },
|
|
||||||
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
|
||||||
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
|
||||||
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
|
||||||
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
|
||||||
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
|
||||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
|
||||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
|
||||||
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
|
||||||
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
|
||||||
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
|
||||||
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
|
||||||
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
|
||||||
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
|
||||||
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
|
||||||
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
|
||||||
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
|
||||||
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
|
||||||
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 3 },
|
|
||||||
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 2 },
|
|
||||||
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 2 },
|
|
||||||
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 2 },
|
|
||||||
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 3 },
|
|
||||||
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
|
||||||
// .{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
.{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
|
||||||
// .{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
|
||||||
// .{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 2 },
|
|
||||||
// .{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
|
||||||
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
|
||||||
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
|
||||||
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 3 },
|
|
||||||
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
|
||||||
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 2 },
|
|
||||||
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 2 },
|
|
||||||
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
|
||||||
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 5 },
|
|
||||||
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 2 },
|
|
||||||
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 2 },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
matcher.reset();
|
|
||||||
|
|
||||||
const doc = try parser.documentHTMLParseFromStr(tc.html);
|
|
||||||
defer parser.documentHTMLClose(doc) catch {};
|
|
||||||
|
|
||||||
const s = css.parse(alloc, tc.q, .{}) catch |e| {
|
|
||||||
std.debug.print("parse, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
defer s.deinit(alloc);
|
|
||||||
|
|
||||||
const node = Node{ .node = parser.documentHTMLToNode(doc) };
|
|
||||||
|
|
||||||
_ = css.matchAll(s, node, &matcher) catch |e| {
|
|
||||||
std.debug.print("match, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("expectation, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,591 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 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 css = @import("css.zig");
|
|
||||||
|
|
||||||
// Node mock implementation for test only.
|
|
||||||
pub const Node = struct {
|
|
||||||
child: ?*const Node = null,
|
|
||||||
last: ?*const Node = null,
|
|
||||||
sibling: ?*const Node = null,
|
|
||||||
prev: ?*const Node = null,
|
|
||||||
par: ?*const Node = null,
|
|
||||||
|
|
||||||
name: []const u8 = "",
|
|
||||||
att: ?[]const u8 = null,
|
|
||||||
|
|
||||||
pub fn firstChild(n: *const Node) !?*const Node {
|
|
||||||
return n.child;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lastChild(n: *const Node) !?*const Node {
|
|
||||||
return n.last;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn nextSibling(n: *const Node) !?*const Node {
|
|
||||||
return n.sibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prevSibling(n: *const Node) !?*const Node {
|
|
||||||
return n.prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parent(n: *const Node) !?*const Node {
|
|
||||||
return n.par;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isElement(_: *const Node) bool {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isDocument(_: *const Node) bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isComment(_: *const Node) bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isText(_: *const Node) bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isEmptyText(_: *const Node) !bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tag(n: *const Node) ![]const u8 {
|
|
||||||
return n.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn attr(n: *const Node, _: []const u8) !?[]const u8 {
|
|
||||||
return n.att;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eql(a: *const Node, b: *const Node) bool {
|
|
||||||
return a == b;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Matcher = struct {
|
|
||||||
const Nodes = std.ArrayListUnmanaged(*const Node);
|
|
||||||
|
|
||||||
nodes: Nodes,
|
|
||||||
allocator: Allocator,
|
|
||||||
|
|
||||||
fn init(allocator: Allocator) Matcher {
|
|
||||||
return .{
|
|
||||||
.nodes = .empty,
|
|
||||||
.allocator = allocator,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(m: *Matcher) void {
|
|
||||||
m.nodes.deinit(self.allocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(m: *Matcher) void {
|
|
||||||
m.nodes.clearRetainingCapacity();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match(m: *Matcher, n: *const Node) !void {
|
|
||||||
try m.nodes.append(self.allocator, n);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
test "matchFirst" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
var matcher = Matcher.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
q: []const u8,
|
|
||||||
n: Node,
|
|
||||||
exp: usize,
|
|
||||||
}{
|
|
||||||
.{
|
|
||||||
.q = "address",
|
|
||||||
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "#foo",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = ".t1",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = ".t1",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo=baz]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo!=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo!=baz]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo~=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo~=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo^=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo$=baz]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo*=rb]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo|=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo|=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo|=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "strong, a",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p a",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p a",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
|
|
||||||
.name = "a",
|
|
||||||
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
|
|
||||||
} } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = ":not(p)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:has(a)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:has(strong)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:haschild(a)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:haschild(strong)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:lang(en)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "a:lang(en)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
matcher.reset();
|
|
||||||
|
|
||||||
const s = try css.parse(alloc, tc.q, .{});
|
|
||||||
defer s.deinit(alloc);
|
|
||||||
|
|
||||||
_ = css.matchFirst(s, &tc.n, &matcher) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "matchAll" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
var matcher = Matcher.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
q: []const u8,
|
|
||||||
n: Node,
|
|
||||||
exp: usize,
|
|
||||||
}{
|
|
||||||
.{
|
|
||||||
.q = "address",
|
|
||||||
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "#foo",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = ".t1",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = ".t1",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo=baz]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo!=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo!=baz]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 2,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo~=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo~=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo^=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo$=baz]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo*=rb]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo|=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo|=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo|=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "strong, a",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 2,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p a",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p a",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
|
|
||||||
.name = "a",
|
|
||||||
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
|
|
||||||
} } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = ":not(p)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 2,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:has(a)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:has(strong)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:haschild(a)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:haschild(strong)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:lang(en)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "a:lang(en)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
matcher.reset();
|
|
||||||
|
|
||||||
const s = try css.parse(alloc, tc.q, .{});
|
|
||||||
defer s.deinit(alloc);
|
|
||||||
|
|
||||||
css.matchAll(s, &tc.n, &matcher) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "pseudo class" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
var matcher = Matcher.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
var p1: Node = .{ .name = "p" };
|
|
||||||
var p2: Node = .{ .name = "p" };
|
|
||||||
var a1: Node = .{ .name = "a" };
|
|
||||||
|
|
||||||
p1.sibling = &p2;
|
|
||||||
p2.prev = &p1;
|
|
||||||
|
|
||||||
p2.sibling = &a1;
|
|
||||||
a1.prev = &p2;
|
|
||||||
|
|
||||||
var root: Node = .{ .child = &p1, .last = &a1 };
|
|
||||||
p1.par = &root;
|
|
||||||
p2.par = &root;
|
|
||||||
a1.par = &root;
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
q: []const u8,
|
|
||||||
n: Node,
|
|
||||||
exp: ?*const Node,
|
|
||||||
}{
|
|
||||||
.{ .q = "p:only-child", .n = root, .exp = null },
|
|
||||||
.{ .q = "a:only-of-type", .n = root, .exp = &a1 },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
matcher.reset();
|
|
||||||
|
|
||||||
const s = try css.parse(alloc, tc.q, .{});
|
|
||||||
defer s.deinit(alloc);
|
|
||||||
|
|
||||||
css.matchAll(s, &tc.n, &matcher) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (tc.exp) |exp_n| {
|
|
||||||
const exp: usize = 1;
|
|
||||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const exp: usize = 0;
|
|
||||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "nth pseudo class" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
var matcher = Matcher.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
var p1: Node = .{ .name = "p" };
|
|
||||||
var p2: Node = .{ .name = "p" };
|
|
||||||
|
|
||||||
p1.sibling = &p2;
|
|
||||||
p2.prev = &p1;
|
|
||||||
|
|
||||||
var root: Node = .{ .child = &p1, .last = &p2 };
|
|
||||||
p1.par = &root;
|
|
||||||
p2.par = &root;
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
q: []const u8,
|
|
||||||
n: Node,
|
|
||||||
exp: ?*const Node,
|
|
||||||
}{
|
|
||||||
.{ .q = "a:nth-of-type(1)", .n = root, .exp = null },
|
|
||||||
.{ .q = "p:nth-of-type(1)", .n = root, .exp = &p1 },
|
|
||||||
.{ .q = "p:nth-of-type(2)", .n = root, .exp = &p2 },
|
|
||||||
.{ .q = "p:nth-of-type(0)", .n = root, .exp = null },
|
|
||||||
.{ .q = "p:nth-of-type(2n)", .n = root, .exp = &p2 },
|
|
||||||
.{ .q = "p:nth-last-child(1)", .n = root, .exp = &p2 },
|
|
||||||
.{ .q = "p:nth-last-child(2)", .n = root, .exp = &p1 },
|
|
||||||
.{ .q = "p:nth-child(1)", .n = root, .exp = &p1 },
|
|
||||||
.{ .q = "p:nth-child(2)", .n = root, .exp = &p2 },
|
|
||||||
.{ .q = "p:nth-child(odd)", .n = root, .exp = &p1 },
|
|
||||||
.{ .q = "p:nth-child(even)", .n = root, .exp = &p2 },
|
|
||||||
.{ .q = "p:nth-child(n+2)", .n = root, .exp = &p2 },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
matcher.reset();
|
|
||||||
|
|
||||||
const s = try css.parse(alloc, tc.q, .{});
|
|
||||||
defer s.deinit(alloc);
|
|
||||||
|
|
||||||
css.matchAll(s, &tc.n, &matcher) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (tc.exp) |exp_n| {
|
|
||||||
const exp: usize = 1;
|
|
||||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const exp: usize = 0;
|
|
||||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -557,8 +557,6 @@ pub const Parser = struct {
|
|||||||
const val = try buf.toOwnedSlice(allocator);
|
const val = try buf.toOwnedSlice(allocator);
|
||||||
errdefer allocator.free(val);
|
errdefer allocator.free(val);
|
||||||
|
|
||||||
lowerstr(val);
|
|
||||||
|
|
||||||
return .{ .pseudo_class_contains = .{ .own = pseudo_class == .containsown, .val = val } };
|
return .{ .pseudo_class_contains = .{ .own = pseudo_class == .containsown, .val = val } };
|
||||||
},
|
},
|
||||||
.matches, .matchesown => {
|
.matches, .matchesown => {
|
||||||
@@ -823,7 +821,8 @@ pub const Parser = struct {
|
|||||||
// nameStart returns whether c can be the first character of an identifier
|
// nameStart returns whether c can be the first character of an identifier
|
||||||
// (not counting an initial hyphen, or an escape sequence).
|
// (not counting an initial hyphen, or an escape sequence).
|
||||||
fn nameStart(c: u8) bool {
|
fn nameStart(c: u8) bool {
|
||||||
return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127;
|
return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127 or
|
||||||
|
'0' <= c and c <= '9';
|
||||||
}
|
}
|
||||||
|
|
||||||
// nameChar returns whether c can be a character within an identifier
|
// nameChar returns whether c can be a character within an identifier
|
||||||
@@ -892,7 +891,7 @@ test "parser.parseIdentifier" {
|
|||||||
err: bool = false,
|
err: bool = false,
|
||||||
}{
|
}{
|
||||||
.{ .s = "x", .exp = "x" },
|
.{ .s = "x", .exp = "x" },
|
||||||
.{ .s = "96", .exp = "", .err = true },
|
.{ .s = "96", .exp = "96", .err = false },
|
||||||
.{ .s = "-x", .exp = "-x" },
|
.{ .s = "-x", .exp = "-x" },
|
||||||
.{ .s = "r\\e9 sumé", .exp = "résumé" },
|
.{ .s = "r\\e9 sumé", .exp = "résumé" },
|
||||||
.{ .s = "r\\0000e9 sumé", .exp = "résumé" },
|
.{ .s = "r\\0000e9 sumé", .exp = "résumé" },
|
||||||
@@ -977,6 +976,7 @@ test "parser.parse" {
|
|||||||
.{ .s = ":root", .exp = .{ .pseudo_class = .root } },
|
.{ .s = ":root", .exp = .{ .pseudo_class = .root } },
|
||||||
.{ .s = ".\\:bar", .exp = .{ .class = ":bar" } },
|
.{ .s = ".\\:bar", .exp = .{ .class = ":bar" } },
|
||||||
.{ .s = ".foo\\:bar", .exp = .{ .class = "foo:bar" } },
|
.{ .s = ".foo\\:bar", .exp = .{ .class = "foo:bar" } },
|
||||||
|
.{ .s = "[class=75c0fa18a94b9e3a6b8e14d6cbe688a27f5da10a]", .exp = .{ .attribute = .{ .key = "class", .val = "75c0fa18a94b9e3a6b8e14d6cbe688a27f5da10a", .op = .eql } } },
|
||||||
};
|
};
|
||||||
|
|
||||||
for (testcases) |tc| {
|
for (testcases) |tc| {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
// 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 Allocator = std.mem.Allocator;
|
||||||
|
const css = @import("css.zig");
|
||||||
|
|
||||||
pub const AttributeOP = enum {
|
pub const AttributeOP = enum {
|
||||||
eql, // =
|
eql, // =
|
||||||
@@ -332,41 +334,39 @@ pub const Selector = union(enum) {
|
|||||||
if (!try v.second.match(n)) return false;
|
if (!try v.second.match(n)) return false;
|
||||||
|
|
||||||
// The first must match a ascendent.
|
// The first must match a ascendent.
|
||||||
var p = try n.parent();
|
var parent = n.parent();
|
||||||
while (p != null) {
|
while (parent) |p| {
|
||||||
if (try v.first.match(p.?)) {
|
if (try v.first.match(p)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
p = try p.?.parent();
|
parent = p.parent();
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
.child => {
|
.child => {
|
||||||
const p = try n.parent();
|
const p = n.parent() orelse return false;
|
||||||
if (p == null) return false;
|
return try v.second.match(n) and try v.first.match(p);
|
||||||
|
|
||||||
return try v.second.match(n) and try v.first.match(p.?);
|
|
||||||
},
|
},
|
||||||
.next_sibling => {
|
.next_sibling => {
|
||||||
if (!try v.second.match(n)) return false;
|
if (!try v.second.match(n)) return false;
|
||||||
var c = try n.prevSibling();
|
var child = n.prevSibling();
|
||||||
while (c != null) {
|
while (child) |c| {
|
||||||
if (c.?.isText() or c.?.isComment()) {
|
if (c.isText() or c.isComment()) {
|
||||||
c = try c.?.prevSibling();
|
child = c.prevSibling();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return try v.first.match(c.?);
|
return try v.first.match(c);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
.subsequent_sibling => {
|
.subsequent_sibling => {
|
||||||
if (!try v.second.match(n)) return false;
|
if (!try v.second.match(n)) return false;
|
||||||
|
|
||||||
var c = try n.prevSibling();
|
var child = n.prevSibling();
|
||||||
while (c != null) {
|
while (child) |c| {
|
||||||
if (try v.first.match(c.?)) return true;
|
if (try v.first.match(c)) return true;
|
||||||
c = try c.?.prevSibling();
|
child = c.prevSibling();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
@@ -432,7 +432,25 @@ pub const Selector = union(enum) {
|
|||||||
else => Error.UnsupportedRelativePseudoClass,
|
else => Error.UnsupportedRelativePseudoClass,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
.pseudo_class_contains => return Error.UnsupportedContainsPseudoClass, // TODO, need mem allocation.
|
.pseudo_class_contains => |v| {
|
||||||
|
// Only containsOwn is implemented.
|
||||||
|
if (v.own == false) return Error.UnsupportedContainsPseudoClass;
|
||||||
|
|
||||||
|
var child = n.firstChild();
|
||||||
|
while (child) |c| {
|
||||||
|
if (c.isText()) {
|
||||||
|
const text = c.text();
|
||||||
|
if (text) |_text| {
|
||||||
|
if (contains(_text, v.val, false)) { // we are case sensitive. Is this correct behavior?
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
child = c.nextSibling();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
.pseudo_class_regexp => return Error.UnsupportedRegexpPseudoClass, // TODO need mem allocation.
|
.pseudo_class_regexp => return Error.UnsupportedRegexpPseudoClass, // TODO need mem allocation.
|
||||||
.pseudo_class_nth => |v| {
|
.pseudo_class_nth => |v| {
|
||||||
if (v.a == 0) {
|
if (v.a == 0) {
|
||||||
@@ -457,16 +475,16 @@ pub const Selector = union(enum) {
|
|||||||
.empty => {
|
.empty => {
|
||||||
if (!n.isElement()) return false;
|
if (!n.isElement()) return false;
|
||||||
|
|
||||||
var c = try n.firstChild();
|
var child = n.firstChild();
|
||||||
while (c != null) {
|
while (child) |c| {
|
||||||
if (c.?.isElement()) return false;
|
if (c.isElement()) return false;
|
||||||
|
|
||||||
if (c.?.isText()) {
|
if (c.isText()) {
|
||||||
if (try c.?.isEmptyText()) continue;
|
if (c.isEmptyText()) continue;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
c = try c.?.nextSibling();
|
child = c.nextSibling();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -474,7 +492,7 @@ pub const Selector = union(enum) {
|
|||||||
.root => {
|
.root => {
|
||||||
if (!n.isElement()) return false;
|
if (!n.isElement()) return false;
|
||||||
|
|
||||||
const p = try n.parent();
|
const p = n.parent();
|
||||||
return (p != null and p.?.isDocument());
|
return (p != null and p.?.isDocument());
|
||||||
},
|
},
|
||||||
.link => {
|
.link => {
|
||||||
@@ -544,7 +562,7 @@ pub const Selector = union(enum) {
|
|||||||
|
|
||||||
const ntag = try n.tag();
|
const ntag = try n.tag();
|
||||||
|
|
||||||
if (std.ascii.eqlIgnoreCase("intput", ntag)) {
|
if (std.ascii.eqlIgnoreCase("input", ntag)) {
|
||||||
const ntype = try n.attr("type");
|
const ntype = try n.attr("type");
|
||||||
if (ntype == null) return false;
|
if (ntype == null) return false;
|
||||||
|
|
||||||
@@ -589,24 +607,23 @@ pub const Selector = union(enum) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn hasLegendInPreviousSiblings(n: anytype) anyerror!bool {
|
fn hasLegendInPreviousSiblings(n: anytype) anyerror!bool {
|
||||||
var c = try n.prevSibling();
|
var child = n.prevSibling();
|
||||||
while (c != null) {
|
while (child) |c| {
|
||||||
const ctag = try c.?.tag();
|
const ctag = try c.tag();
|
||||||
if (std.ascii.eqlIgnoreCase("legend", ctag)) return true;
|
if (std.ascii.eqlIgnoreCase("legend", ctag)) return true;
|
||||||
c = try c.?.prevSibling();
|
child = c.prevSibling();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inDisabledFieldset(n: anytype) anyerror!bool {
|
fn inDisabledFieldset(n: anytype) anyerror!bool {
|
||||||
const p = try n.parent();
|
const p = n.parent() orelse return false;
|
||||||
if (p == null) return false;
|
|
||||||
|
|
||||||
const ntag = try n.tag();
|
const ntag = try n.tag();
|
||||||
const ptag = try p.?.tag();
|
const ptag = try p.tag();
|
||||||
|
|
||||||
if (std.ascii.eqlIgnoreCase("fieldset", ptag) and
|
if (std.ascii.eqlIgnoreCase("fieldset", ptag) and
|
||||||
try p.?.attr("disabled") != null and
|
try p.attr("disabled") != null and
|
||||||
(!std.ascii.eqlIgnoreCase("legend", ntag) or try hasLegendInPreviousSiblings(n)))
|
(!std.ascii.eqlIgnoreCase("legend", ntag) or try hasLegendInPreviousSiblings(n)))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
@@ -622,7 +639,7 @@ pub const Selector = union(enum) {
|
|||||||
// ```
|
// ```
|
||||||
// https://github.com/andybalholm/cascadia/blob/master/pseudo_classes.go#L434
|
// https://github.com/andybalholm/cascadia/blob/master/pseudo_classes.go#L434
|
||||||
|
|
||||||
return try inDisabledFieldset(p.?);
|
return try inDisabledFieldset(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn langMatch(lang: []const u8, n: anytype) anyerror!bool {
|
fn langMatch(lang: []const u8, n: anytype) anyerror!bool {
|
||||||
@@ -636,10 +653,8 @@ pub const Selector = union(enum) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if the tag doesn't match, try the parent.
|
// if the tag doesn't match, try the parent.
|
||||||
const p = try n.parent();
|
const p = n.parent() orelse return false;
|
||||||
if (p == null) return false;
|
return langMatch(lang, p);
|
||||||
|
|
||||||
return langMatch(lang, p.?);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// onlyChildMatch implements :only-child
|
// onlyChildMatch implements :only-child
|
||||||
@@ -647,25 +662,24 @@ pub const Selector = union(enum) {
|
|||||||
fn onlyChildMatch(of_type: bool, n: anytype) anyerror!bool {
|
fn onlyChildMatch(of_type: bool, n: anytype) anyerror!bool {
|
||||||
if (!n.isElement()) return false;
|
if (!n.isElement()) return false;
|
||||||
|
|
||||||
const p = try n.parent();
|
const p = n.parent() orelse return false;
|
||||||
if (p == null) return false;
|
|
||||||
|
|
||||||
const ntag = try n.tag();
|
const ntag = try n.tag();
|
||||||
|
|
||||||
var count: usize = 0;
|
var count: usize = 0;
|
||||||
var c = try p.?.firstChild();
|
var child = p.firstChild();
|
||||||
// loop hover all n siblings.
|
// loop hover all n siblings.
|
||||||
while (c != null) {
|
while (child) |c| {
|
||||||
// ignore non elements or others tags if of-type is true.
|
// ignore non elements or others tags if of-type is true.
|
||||||
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
|
if (!c.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.tag()))) {
|
||||||
c = try c.?.nextSibling();
|
child = c.nextSibling();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
count += 1;
|
count += 1;
|
||||||
if (count > 1) return false;
|
if (count > 1) return false;
|
||||||
|
|
||||||
c = try c.?.nextSibling();
|
child = c.nextSibling();
|
||||||
}
|
}
|
||||||
|
|
||||||
return count == 1;
|
return count == 1;
|
||||||
@@ -676,27 +690,25 @@ pub const Selector = union(enum) {
|
|||||||
fn simpleNthLastChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool {
|
fn simpleNthLastChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool {
|
||||||
if (!n.isElement()) return false;
|
if (!n.isElement()) return false;
|
||||||
|
|
||||||
const p = try n.parent();
|
const p = n.parent() orelse return false;
|
||||||
if (p == null) return false;
|
|
||||||
|
|
||||||
const ntag = try n.tag();
|
const ntag = try n.tag();
|
||||||
|
|
||||||
var count: isize = 0;
|
var count: isize = 0;
|
||||||
var c = try p.?.lastChild();
|
var child = p.lastChild();
|
||||||
// loop hover all n siblings.
|
// loop hover all n siblings.
|
||||||
while (c != null) {
|
while (child) |c| {
|
||||||
// ignore non elements or others tags if of-type is true.
|
// ignore non elements or others tags if of-type is true.
|
||||||
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
|
if (!c.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.tag()))) {
|
||||||
c = try c.?.prevSibling();
|
child = c.prevSibling();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
count += 1;
|
count += 1;
|
||||||
|
|
||||||
if (n.eql(c.?)) return count == b;
|
if (n.eql(c)) return count == b;
|
||||||
if (count >= b) return false;
|
if (count >= b) return false;
|
||||||
|
|
||||||
c = try c.?.prevSibling();
|
child = c.prevSibling();
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -707,27 +719,25 @@ pub const Selector = union(enum) {
|
|||||||
fn simpleNthChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool {
|
fn simpleNthChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool {
|
||||||
if (!n.isElement()) return false;
|
if (!n.isElement()) return false;
|
||||||
|
|
||||||
const p = try n.parent();
|
const p = n.parent() orelse return false;
|
||||||
if (p == null) return false;
|
|
||||||
|
|
||||||
const ntag = try n.tag();
|
const ntag = try n.tag();
|
||||||
|
|
||||||
var count: isize = 0;
|
var count: isize = 0;
|
||||||
var c = try p.?.firstChild();
|
var child = p.firstChild();
|
||||||
// loop hover all n siblings.
|
// loop hover all n siblings.
|
||||||
while (c != null) {
|
while (child) |c| {
|
||||||
// ignore non elements or others tags if of-type is true.
|
// ignore non elements or others tags if of-type is true.
|
||||||
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
|
if (!c.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.tag()))) {
|
||||||
c = try c.?.nextSibling();
|
child = c.nextSibling();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
count += 1;
|
count += 1;
|
||||||
|
|
||||||
if (n.eql(c.?)) return count == b;
|
if (n.eql(c)) return count == b;
|
||||||
if (count >= b) return false;
|
if (count >= b) return false;
|
||||||
|
|
||||||
c = try c.?.nextSibling();
|
child = c.nextSibling();
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -739,29 +749,27 @@ pub const Selector = union(enum) {
|
|||||||
fn nthChildMatch(a: isize, b: isize, last: bool, of_type: bool, n: anytype) anyerror!bool {
|
fn nthChildMatch(a: isize, b: isize, last: bool, of_type: bool, n: anytype) anyerror!bool {
|
||||||
if (!n.isElement()) return false;
|
if (!n.isElement()) return false;
|
||||||
|
|
||||||
const p = try n.parent();
|
const p = n.parent() orelse return false;
|
||||||
if (p == null) return false;
|
|
||||||
|
|
||||||
const ntag = try n.tag();
|
const ntag = try n.tag();
|
||||||
|
|
||||||
var i: isize = -1;
|
var i: isize = -1;
|
||||||
var count: isize = 0;
|
var count: isize = 0;
|
||||||
var c = try p.?.firstChild();
|
var child = p.firstChild();
|
||||||
// loop hover all n siblings.
|
// loop hover all n siblings.
|
||||||
while (c != null) {
|
while (child) |c| {
|
||||||
// ignore non elements or others tags if of-type is true.
|
// ignore non elements or others tags if of-type is true.
|
||||||
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
|
if (!c.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.tag()))) {
|
||||||
c = try c.?.nextSibling();
|
child = c.nextSibling();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
count += 1;
|
count += 1;
|
||||||
|
|
||||||
if (n.eql(c.?)) {
|
if (n.eql(c)) {
|
||||||
i = count;
|
i = count;
|
||||||
if (!last) break;
|
if (!last) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
c = try c.?.nextSibling();
|
child = c.nextSibling();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i == -1) return false;
|
if (i == -1) return false;
|
||||||
@@ -774,21 +782,21 @@ pub const Selector = union(enum) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn hasDescendantMatch(s: *const Selector, n: anytype) anyerror!bool {
|
fn hasDescendantMatch(s: *const Selector, n: anytype) anyerror!bool {
|
||||||
var c = try n.firstChild();
|
var child = n.firstChild();
|
||||||
while (c != null) {
|
while (child) |c| {
|
||||||
if (try s.match(c.?)) return true;
|
if (try s.match(c)) return true;
|
||||||
if (c.?.isElement() and try hasDescendantMatch(s, c.?)) return true;
|
if (c.isElement() and try hasDescendantMatch(s, c)) return true;
|
||||||
c = try c.?.nextSibling();
|
child = c.nextSibling();
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hasChildMatch(s: *const Selector, n: anytype) anyerror!bool {
|
fn hasChildMatch(s: *const Selector, n: anytype) anyerror!bool {
|
||||||
var c = try n.firstChild();
|
var child = n.firstChild();
|
||||||
while (c != null) {
|
while (child) |c| {
|
||||||
if (try s.match(c.?)) return true;
|
if (try s.match(c)) return true;
|
||||||
c = try c.?.nextSibling();
|
child = c.nextSibling();
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -827,3 +835,583 @@ pub const Selector = union(enum) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// NodeTest mock implementation for test only.
|
||||||
|
pub const NodeTest = struct {
|
||||||
|
child: ?*const NodeTest = null,
|
||||||
|
last: ?*const NodeTest = null,
|
||||||
|
sibling: ?*const NodeTest = null,
|
||||||
|
prev: ?*const NodeTest = null,
|
||||||
|
par: ?*const NodeTest = null,
|
||||||
|
|
||||||
|
name: []const u8 = "",
|
||||||
|
att: ?[]const u8 = null,
|
||||||
|
|
||||||
|
pub fn firstChild(n: *const NodeTest) ?*const NodeTest {
|
||||||
|
return n.child;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lastChild(n: *const NodeTest) ?*const NodeTest {
|
||||||
|
return n.last;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nextSibling(n: *const NodeTest) ?*const NodeTest {
|
||||||
|
return n.sibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prevSibling(n: *const NodeTest) ?*const NodeTest {
|
||||||
|
return n.prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parent(n: *const NodeTest) ?*const NodeTest {
|
||||||
|
return n.par;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isElement(_: *const NodeTest) bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isDocument(_: *const NodeTest) bool {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isComment(_: *const NodeTest) bool {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text(_: *const NodeTest) ?[]const u8 {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isText(_: *const NodeTest) bool {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isEmptyText(_: *const NodeTest) bool {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tag(n: *const NodeTest) ![]const u8 {
|
||||||
|
return n.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn attr(n: *const NodeTest, _: []const u8) !?[]const u8 {
|
||||||
|
return n.att;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eql(a: *const NodeTest, b: *const NodeTest) bool {
|
||||||
|
return a == b;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const MatcherTest = struct {
|
||||||
|
const NodeTests = std.ArrayListUnmanaged(*const NodeTest);
|
||||||
|
|
||||||
|
nodes: NodeTests,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
fn init(allocator: Allocator) MatcherTest {
|
||||||
|
return .{
|
||||||
|
.nodes = .empty,
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(m: *MatcherTest) void {
|
||||||
|
m.nodes.deinit(m.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(m: *MatcherTest) void {
|
||||||
|
m.nodes.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn match(m: *MatcherTest, n: *const NodeTest) !void {
|
||||||
|
try m.nodes.append(m.allocator, n);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "Browser.CSS.Selector: matchFirst" {
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
|
||||||
|
var matcher = MatcherTest.init(alloc);
|
||||||
|
defer matcher.deinit();
|
||||||
|
|
||||||
|
const testcases = [_]struct {
|
||||||
|
q: []const u8,
|
||||||
|
n: NodeTest,
|
||||||
|
exp: usize,
|
||||||
|
}{
|
||||||
|
.{
|
||||||
|
.q = "address",
|
||||||
|
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "#foo",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = ".t1",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = ".t1",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
|
||||||
|
.exp = 0,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo=bar]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo=baz]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||||
|
.exp = 0,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo=1baz]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||||
|
.exp = 0,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo!=bar]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo!=baz]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo~=bar]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo~=bar]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||||
|
.exp = 0,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo^=bar]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo$=baz]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo*=rb]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo|=bar]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo|=bar]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo|=bar]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
|
||||||
|
.exp = 0,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "strong, a",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "p a",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "p a",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
|
||||||
|
.name = "a",
|
||||||
|
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
|
||||||
|
} } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = ":not(p)",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "p:has(a)",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "p:has(strong)",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||||
|
.exp = 0,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "p:haschild(a)",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "p:haschild(strong)",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||||
|
.exp = 0,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "p:lang(en)",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "a:lang(en)",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (testcases) |tc| {
|
||||||
|
matcher.reset();
|
||||||
|
|
||||||
|
const s = try css.parse(alloc, tc.q, .{});
|
||||||
|
defer s.deinit(alloc);
|
||||||
|
|
||||||
|
_ = css.matchFirst(&s, &tc.n, &matcher) catch |e| {
|
||||||
|
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
|
||||||
|
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
||||||
|
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Browser.CSS.Selector: matchAll" {
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
|
||||||
|
var matcher = MatcherTest.init(alloc);
|
||||||
|
defer matcher.deinit();
|
||||||
|
|
||||||
|
const testcases = [_]struct {
|
||||||
|
q: []const u8,
|
||||||
|
n: NodeTest,
|
||||||
|
exp: usize,
|
||||||
|
}{
|
||||||
|
.{
|
||||||
|
.q = "address",
|
||||||
|
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "#foo",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = ".t1",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = ".t1",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
|
||||||
|
.exp = 0,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo=bar]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo=baz]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||||
|
.exp = 0,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo!=bar]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo!=baz]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||||
|
.exp = 2,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo~=bar]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo~=bar]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||||
|
.exp = 0,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo^=bar]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo$=baz]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo*=rb]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo|=bar]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo|=bar]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "[foo|=bar]",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
|
||||||
|
.exp = 0,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "strong, a",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||||
|
.exp = 2,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "p a",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "p a",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
|
||||||
|
.name = "a",
|
||||||
|
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
|
||||||
|
} } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = ":not(p)",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||||
|
.exp = 2,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "p:has(a)",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "p:has(strong)",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||||
|
.exp = 0,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "p:haschild(a)",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "p:haschild(strong)",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||||
|
.exp = 0,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "p:lang(en)",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.q = "a:lang(en)",
|
||||||
|
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
|
||||||
|
.exp = 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (testcases) |tc| {
|
||||||
|
matcher.reset();
|
||||||
|
|
||||||
|
const s = try css.parse(alloc, tc.q, .{});
|
||||||
|
defer s.deinit(alloc);
|
||||||
|
|
||||||
|
css.matchAll(&s, &tc.n, &matcher) catch |e| {
|
||||||
|
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
|
||||||
|
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
||||||
|
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Browser.CSS.Selector: pseudo class" {
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
|
||||||
|
var matcher = MatcherTest.init(alloc);
|
||||||
|
defer matcher.deinit();
|
||||||
|
|
||||||
|
var p1: NodeTest = .{ .name = "p" };
|
||||||
|
var p2: NodeTest = .{ .name = "p" };
|
||||||
|
var a1: NodeTest = .{ .name = "a" };
|
||||||
|
|
||||||
|
p1.sibling = &p2;
|
||||||
|
p2.prev = &p1;
|
||||||
|
|
||||||
|
p2.sibling = &a1;
|
||||||
|
a1.prev = &p2;
|
||||||
|
|
||||||
|
var root: NodeTest = .{ .child = &p1, .last = &a1 };
|
||||||
|
p1.par = &root;
|
||||||
|
p2.par = &root;
|
||||||
|
a1.par = &root;
|
||||||
|
|
||||||
|
const testcases = [_]struct {
|
||||||
|
q: []const u8,
|
||||||
|
n: NodeTest,
|
||||||
|
exp: ?*const NodeTest,
|
||||||
|
}{
|
||||||
|
.{ .q = "p:only-child", .n = root, .exp = null },
|
||||||
|
.{ .q = "a:only-of-type", .n = root, .exp = &a1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (testcases) |tc| {
|
||||||
|
matcher.reset();
|
||||||
|
|
||||||
|
const s = try css.parse(alloc, tc.q, .{});
|
||||||
|
defer s.deinit(alloc);
|
||||||
|
|
||||||
|
css.matchAll(&s, &tc.n, &matcher) catch |e| {
|
||||||
|
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tc.exp) |exp_n| {
|
||||||
|
const exp: usize = 1;
|
||||||
|
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
||||||
|
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
|
||||||
|
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
|
||||||
|
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exp: usize = 0;
|
||||||
|
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
||||||
|
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Browser.CSS.Selector: nth pseudo class" {
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
|
||||||
|
var matcher = MatcherTest.init(alloc);
|
||||||
|
defer matcher.deinit();
|
||||||
|
|
||||||
|
var p1: NodeTest = .{ .name = "p" };
|
||||||
|
var p2: NodeTest = .{ .name = "p" };
|
||||||
|
|
||||||
|
p1.sibling = &p2;
|
||||||
|
p2.prev = &p1;
|
||||||
|
|
||||||
|
var root: NodeTest = .{ .child = &p1, .last = &p2 };
|
||||||
|
p1.par = &root;
|
||||||
|
p2.par = &root;
|
||||||
|
|
||||||
|
const testcases = [_]struct {
|
||||||
|
q: []const u8,
|
||||||
|
n: NodeTest,
|
||||||
|
exp: ?*const NodeTest,
|
||||||
|
}{
|
||||||
|
.{ .q = "a:nth-of-type(1)", .n = root, .exp = null },
|
||||||
|
.{ .q = "p:nth-of-type(1)", .n = root, .exp = &p1 },
|
||||||
|
.{ .q = "p:nth-of-type(2)", .n = root, .exp = &p2 },
|
||||||
|
.{ .q = "p:nth-of-type(0)", .n = root, .exp = null },
|
||||||
|
.{ .q = "p:nth-of-type(2n)", .n = root, .exp = &p2 },
|
||||||
|
.{ .q = "p:nth-last-child(1)", .n = root, .exp = &p2 },
|
||||||
|
.{ .q = "p:nth-last-child(2)", .n = root, .exp = &p1 },
|
||||||
|
.{ .q = "p:nth-child(1)", .n = root, .exp = &p1 },
|
||||||
|
.{ .q = "p:nth-child(2)", .n = root, .exp = &p2 },
|
||||||
|
.{ .q = "p:nth-child(odd)", .n = root, .exp = &p1 },
|
||||||
|
.{ .q = "p:nth-child(even)", .n = root, .exp = &p2 },
|
||||||
|
.{ .q = "p:nth-child(n+2)", .n = root, .exp = &p2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (testcases) |tc| {
|
||||||
|
matcher.reset();
|
||||||
|
|
||||||
|
const s = try css.parse(alloc, tc.q, .{});
|
||||||
|
defer s.deinit(alloc);
|
||||||
|
|
||||||
|
css.matchAll(&s, &tc.n, &matcher) catch |e| {
|
||||||
|
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tc.exp) |exp_n| {
|
||||||
|
const exp: usize = 1;
|
||||||
|
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
||||||
|
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
|
||||||
|
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
|
||||||
|
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exp: usize = 0;
|
||||||
|
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
||||||
|
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const CSSRule = @import("CSSRule.zig");
|
const CSSRule = @import("CSSRule.zig");
|
||||||
const StyleSheet = @import("StyleSheet.zig").StyleSheet;
|
|
||||||
|
|
||||||
const CSSImportRule = CSSRule.CSSImportRule;
|
const CSSImportRule = CSSRule.CSSImportRule;
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const Env = @import("../env.zig").Env;
|
const js = @import("../js/js.zig");
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
const StyleSheet = @import("StyleSheet.zig");
|
const StyleSheet = @import("StyleSheet.zig");
|
||||||
const CSSRuleList = @import("CSSRuleList.zig");
|
const CSSRuleList = @import("CSSRuleList.zig");
|
||||||
@@ -73,15 +73,13 @@ pub fn _deleteRule(self: *CSSStyleSheet, index: usize) !void {
|
|||||||
_ = self.css_rules.list.orderedRemove(index);
|
_ = self.css_rules.list.orderedRemove(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !Env.Promise {
|
pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise {
|
||||||
_ = self;
|
_ = self;
|
||||||
_ = text;
|
_ = text;
|
||||||
// TODO: clear self.css_rules
|
// TODO: clear self.css_rules
|
||||||
// parse text and re-populate self.css_rules
|
// parse text and re-populate self.css_rules
|
||||||
|
|
||||||
const resolver = page.main_context.createPromiseResolver();
|
return page.js.resolvePromise({});
|
||||||
try resolver.resolve({});
|
|
||||||
return resolver.promise();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _replaceSync(self: *CSSStyleSheet, text: []const u8) !void {
|
pub fn _replaceSync(self: *CSSStyleSheet, text: []const u8) !void {
|
||||||
|
|||||||
@@ -18,19 +18,17 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
const JsObject = @import("../env.zig").JsObject;
|
|
||||||
const Promise = @import("../env.zig").Promise;
|
|
||||||
const PromiseResolver = @import("../env.zig").PromiseResolver;
|
|
||||||
|
|
||||||
const Animation = @This();
|
const Animation = @This();
|
||||||
|
|
||||||
effect: ?JsObject,
|
effect: ?js.Object,
|
||||||
timeline: ?JsObject,
|
timeline: ?js.Object,
|
||||||
ready_resolver: ?PromiseResolver,
|
ready_resolver: ?js.PromiseResolver,
|
||||||
finished_resolver: ?PromiseResolver,
|
finished_resolver: ?js.PromiseResolver,
|
||||||
|
|
||||||
pub fn constructor(effect: ?JsObject, timeline: ?JsObject) !Animation {
|
pub fn constructor(effect: ?js.Object, timeline: ?js.Object) !Animation {
|
||||||
return .{
|
return .{
|
||||||
.effect = if (effect) |eo| try eo.persist() else null,
|
.effect = if (effect) |eo| try eo.persist() else null,
|
||||||
.timeline = if (timeline) |to| try to.persist() else null,
|
.timeline = if (timeline) |to| try to.persist() else null,
|
||||||
@@ -49,37 +47,37 @@ pub fn get_pending(self: *const Animation) bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_finished(self: *Animation, page: *Page) !Promise {
|
pub fn get_finished(self: *Animation, page: *Page) !js.Promise {
|
||||||
if (self.finished_resolver == null) {
|
if (self.finished_resolver == null) {
|
||||||
const resolver = page.main_context.createPromiseResolver();
|
const resolver = page.js.createPromiseResolver(.none);
|
||||||
try resolver.resolve(self);
|
try resolver.resolve(self);
|
||||||
self.finished_resolver = resolver;
|
self.finished_resolver = resolver;
|
||||||
}
|
}
|
||||||
return self.finished_resolver.?.promise();
|
return self.finished_resolver.?.promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_ready(self: *Animation, page: *Page) !Promise {
|
pub fn get_ready(self: *Animation, page: *Page) !js.Promise {
|
||||||
// never resolved, because we're always "finished"
|
// never resolved, because we're always "finished"
|
||||||
if (self.ready_resolver == null) {
|
if (self.ready_resolver == null) {
|
||||||
const resolver = page.main_context.createPromiseResolver();
|
const resolver = page.js.createPromiseResolver(.none);
|
||||||
self.ready_resolver = resolver;
|
self.ready_resolver = resolver;
|
||||||
}
|
}
|
||||||
return self.ready_resolver.?.promise();
|
return self.ready_resolver.?.promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_effect(self: *const Animation) ?JsObject {
|
pub fn get_effect(self: *const Animation) ?js.Object {
|
||||||
return self.effect;
|
return self.effect;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_effect(self: *Animation, effect: JsObject) !void {
|
pub fn set_effect(self: *Animation, effect: js.Object) !void {
|
||||||
self.effect = try effect.persist();
|
self.effect = try effect.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_timeline(self: *const Animation) ?JsObject {
|
pub fn get_timeline(self: *const Animation) ?js.Object {
|
||||||
return self.timeline;
|
return self.timeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_timeline(self: *Animation, timeline: JsObject) !void {
|
pub fn set_timeline(self: *Animation, timeline: js.Object) !void {
|
||||||
self.timeline = try timeline.persist();
|
self.timeline = try timeline.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
329
src/browser/dom/IntersectionObserver.zig
Normal file
329
src/browser/dom/IntersectionObserver.zig
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
const parser = @import("../netsurf.zig");
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
const Node = @import("node.zig").Node;
|
||||||
|
const Element = @import("element.zig").Element;
|
||||||
|
|
||||||
|
pub const Interfaces = .{
|
||||||
|
IntersectionObserver,
|
||||||
|
Entry,
|
||||||
|
};
|
||||||
|
|
||||||
|
// This implementation attempts to be as less wrong as possible. Since we don't
|
||||||
|
// render, or know how things are positioned, our best guess isn't very good.
|
||||||
|
const IntersectionObserver = @This();
|
||||||
|
page: *Page,
|
||||||
|
root: *parser.Node,
|
||||||
|
callback: js.Function,
|
||||||
|
event_node: parser.EventNode,
|
||||||
|
observed_entries: std.ArrayList(Entry),
|
||||||
|
pending_elements: std.ArrayList(*parser.Element),
|
||||||
|
ready_elements: std.ArrayList(*parser.Element),
|
||||||
|
|
||||||
|
pub fn constructor(callback: js.Function, opts_: ?IntersectionObserverOptions, page: *Page) !*IntersectionObserver {
|
||||||
|
const opts = opts_ orelse IntersectionObserverOptions{};
|
||||||
|
|
||||||
|
const self = try page.arena.create(IntersectionObserver);
|
||||||
|
self.* = .{
|
||||||
|
.page = page,
|
||||||
|
.callback = callback,
|
||||||
|
.ready_elements = .{},
|
||||||
|
.observed_entries = .{},
|
||||||
|
.pending_elements = .{},
|
||||||
|
.event_node = .{ .func = mutationCallback },
|
||||||
|
.root = opts.root orelse parser.documentToNode(parser.documentHTMLToDocument(page.window.document)),
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = try parser.eventTargetAddEventListener(
|
||||||
|
parser.toEventTarget(parser.Node, self.root),
|
||||||
|
"DOMNodeInserted",
|
||||||
|
&self.event_node,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
_ = try parser.eventTargetAddEventListener(
|
||||||
|
parser.toEventTarget(parser.Node, self.root),
|
||||||
|
"DOMNodeRemoved",
|
||||||
|
&self.event_node,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _disconnect(self: *IntersectionObserver) !void {
|
||||||
|
// We don't free as it is on an arena
|
||||||
|
self.ready_elements = .{};
|
||||||
|
self.observed_entries = .{};
|
||||||
|
self.pending_elements = .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _observe(self: *IntersectionObserver, target_element: *parser.Element, page: *Page) !void {
|
||||||
|
for (self.observed_entries.items) |*observer| {
|
||||||
|
if (observer.target == target_element) {
|
||||||
|
return; // Already observed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.isPending(target_element)) {
|
||||||
|
return; // Already pending
|
||||||
|
}
|
||||||
|
|
||||||
|
for (self.ready_elements.items) |element| {
|
||||||
|
if (element == target_element) {
|
||||||
|
return; // Already primed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can never fire callbacks synchronously. Code like React expects any
|
||||||
|
// callback to fire in the future (e.g. via microtasks).
|
||||||
|
try self.ready_elements.append(self.page.arena, target_element);
|
||||||
|
if (self.ready_elements.items.len == 1) {
|
||||||
|
// this is our first ready entry, schedule a callback
|
||||||
|
try page.scheduler.add(self, processReady, 0, .{
|
||||||
|
.name = "intersection ready",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _unobserve(self: *IntersectionObserver, target: *parser.Element) !void {
|
||||||
|
if (self.removeObserved(target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (self.ready_elements.items, 0..) |el, index| {
|
||||||
|
if (el == target) {
|
||||||
|
_ = self.ready_elements.swapRemove(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (self.pending_elements.items, 0..) |el, index| {
|
||||||
|
if (el == target) {
|
||||||
|
_ = self.pending_elements.swapRemove(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _takeRecords(self: *IntersectionObserver) []Entry {
|
||||||
|
return self.observed_entries.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processReady(ctx: *anyopaque) ?u32 {
|
||||||
|
const self: *IntersectionObserver = @ptrCast(@alignCast(ctx));
|
||||||
|
self._processReady() catch |err| {
|
||||||
|
log.err(.web_api, "intersection ready", .{ .err = err });
|
||||||
|
};
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _processReady(self: *IntersectionObserver) !void {
|
||||||
|
defer self.ready_elements.clearRetainingCapacity();
|
||||||
|
for (self.ready_elements.items) |element| {
|
||||||
|
// IntersectionObserver probably doesn't work like what your intuition
|
||||||
|
// thinks. As long as a node has a parent, even if that parent isn't
|
||||||
|
// connected and even if the two nodes don't intersect, it'll fire the
|
||||||
|
// callback once.
|
||||||
|
if (try Node.get_parentNode(@ptrCast(element)) == null) {
|
||||||
|
if (!self.isPending(element)) {
|
||||||
|
try self.pending_elements.append(self.page.arena, element);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try self.forceObserve(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isPending(self: *IntersectionObserver, element: *parser.Element) bool {
|
||||||
|
for (self.pending_elements.items) |el| {
|
||||||
|
if (el == element) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mutationCallback(en: *parser.EventNode, event: *parser.Event) void {
|
||||||
|
const mutation_event = parser.eventToMutationEvent(event);
|
||||||
|
const self: *IntersectionObserver = @fieldParentPtr("event_node", en);
|
||||||
|
self._mutationCallback(mutation_event) catch |err| {
|
||||||
|
log.err(.web_api, "mutation callback", .{ .err = err, .source = "intersection observer" });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _mutationCallback(self: *IntersectionObserver, event: *parser.MutationEvent) !void {
|
||||||
|
const event_type = parser.eventType(@ptrCast(event));
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, event_type, "DOMNodeInserted")) {
|
||||||
|
const node = parser.mutationEventRelatedNode(event) catch return orelse return;
|
||||||
|
if (parser.nodeType(node) != .element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const el: *parser.Element = @ptrCast(node);
|
||||||
|
if (self.removePending(el)) {
|
||||||
|
// It was pending (because it wasn't in the root), but now it is
|
||||||
|
// we should observe it.
|
||||||
|
try self.forceObserve(el);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, event_type, "DOMNodeRemoved")) {
|
||||||
|
const node = parser.mutationEventRelatedNode(event) catch return orelse return;
|
||||||
|
if (parser.nodeType(node) != .element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const el: *parser.Element = @ptrCast(node);
|
||||||
|
if (self.removeObserved(el)) {
|
||||||
|
// It _was_ observed, it no longer is in our root, but if it was
|
||||||
|
// to get re-added, it should be observed again (I think), so
|
||||||
|
// we add it to our pending list
|
||||||
|
try self.pending_elements.append(self.page.arena, el);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// impossible event type
|
||||||
|
unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists to skip the checks made _observe when called from a DOMNodeInserted
|
||||||
|
// event. In such events, the event handler has alread done the necessary
|
||||||
|
// checks.
|
||||||
|
fn forceObserve(self: *IntersectionObserver, target: *parser.Element) !void {
|
||||||
|
try self.observed_entries.append(self.page.arena, .{
|
||||||
|
.page = self.page,
|
||||||
|
.root = self.root,
|
||||||
|
.target = target,
|
||||||
|
});
|
||||||
|
|
||||||
|
var result: js.Function.Result = undefined;
|
||||||
|
self.callback.tryCall(void, .{self.observed_entries.items}, &result) catch {
|
||||||
|
log.debug(.user_script, "callback error", .{
|
||||||
|
.err = result.exception,
|
||||||
|
.stack = result.stack,
|
||||||
|
.source = "intersection observer",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn removeObserved(self: *IntersectionObserver, target: *parser.Element) bool {
|
||||||
|
for (self.observed_entries.items, 0..) |*observer, index| {
|
||||||
|
if (observer.target == target) {
|
||||||
|
_ = self.observed_entries.swapRemove(index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn removePending(self: *IntersectionObserver, target: *parser.Element) bool {
|
||||||
|
for (self.pending_elements.items, 0..) |el, index| {
|
||||||
|
if (el == target) {
|
||||||
|
_ = self.pending_elements.swapRemove(index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IntersectionObserverOptions = struct {
|
||||||
|
root: ?*parser.Node = null, // Element or Document
|
||||||
|
rootMargin: ?[]const u8 = "0px 0px 0px 0px",
|
||||||
|
threshold: ?Threshold = .{ .single = 0.0 },
|
||||||
|
|
||||||
|
const Threshold = union(enum) {
|
||||||
|
single: f32,
|
||||||
|
list: []const f32,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Entry
|
||||||
|
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
|
||||||
|
pub const Entry = struct {
|
||||||
|
page: *Page,
|
||||||
|
root: *parser.Node,
|
||||||
|
target: *parser.Element,
|
||||||
|
|
||||||
|
// Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect().
|
||||||
|
pub fn get_boundingClientRect(self: *const Entry) !Element.DOMRect {
|
||||||
|
return Element._getBoundingClientRect(self.target, self.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the ratio of the intersectionRect to the boundingClientRect.
|
||||||
|
pub fn get_intersectionRatio(_: *const Entry) f32 {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a DOMRectReadOnly representing the target's visible area.
|
||||||
|
pub fn get_intersectionRect(self: *const Entry) !Element.DOMRect {
|
||||||
|
return Element._getBoundingClientRect(self.target, self.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Boolean value which is true if the target element intersects with the
|
||||||
|
// intersection observer's root. If this is true, then, the
|
||||||
|
// Entry describes a transition into a state of
|
||||||
|
// intersection; if it's false, then you know the transition is from
|
||||||
|
// intersecting to not-intersecting.
|
||||||
|
pub fn get_isIntersecting(_: *const Entry) bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a DOMRectReadOnly for the intersection observer's root.
|
||||||
|
pub fn get_rootBounds(self: *const Entry) !Element.DOMRect {
|
||||||
|
const root = self.root;
|
||||||
|
if (@intFromPtr(root) == @intFromPtr(self.page.window.document)) {
|
||||||
|
return self.page.renderer.boundingRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
const root_type = parser.nodeType(root);
|
||||||
|
|
||||||
|
var element: *parser.Element = undefined;
|
||||||
|
switch (root_type) {
|
||||||
|
.element => element = parser.nodeToElement(root),
|
||||||
|
.document => {
|
||||||
|
const doc = parser.nodeToDocument(root);
|
||||||
|
element = (try parser.documentGetDocumentElement(doc)).?;
|
||||||
|
},
|
||||||
|
else => return error.InvalidState,
|
||||||
|
}
|
||||||
|
|
||||||
|
return Element._getBoundingClientRect(element, self.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Element whose intersection with the root changed.
|
||||||
|
pub fn get_target(self: *const Entry) *parser.Element {
|
||||||
|
return self.target;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: pub fn get_time(self: *const Entry)
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "Browser: DOM.IntersectionObserver" {
|
||||||
|
try testing.htmlRunner("dom/intersection_observer.html");
|
||||||
|
}
|
||||||
@@ -20,13 +20,11 @@ const std = @import("std");
|
|||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
|
|
||||||
const Env = @import("../env.zig").Env;
|
const js = @import("../js/js.zig");
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||||
|
|
||||||
const JsObject = Env.JsObject;
|
|
||||||
const Function = Env.Function;
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const MAX_QUEUE_SIZE = 10;
|
const MAX_QUEUE_SIZE = 10;
|
||||||
@@ -72,29 +70,28 @@ pub const MessagePort = struct {
|
|||||||
pair: *MessagePort,
|
pair: *MessagePort,
|
||||||
closed: bool = false,
|
closed: bool = false,
|
||||||
started: bool = false,
|
started: bool = false,
|
||||||
onmessage_cbk: ?Function = null,
|
onmessage_cbk: ?js.Function = null,
|
||||||
onmessageerror_cbk: ?Function = null,
|
onmessageerror_cbk: ?js.Function = null,
|
||||||
// This is the queue of messages to dispatch to THIS MessagePort when the
|
// This is the queue of messages to dispatch to THIS MessagePort when the
|
||||||
// MessagePort is started.
|
// MessagePort is started.
|
||||||
queue: std.ArrayListUnmanaged(JsObject) = .empty,
|
queue: std.ArrayListUnmanaged(js.Object) = .empty,
|
||||||
|
|
||||||
pub const PostMessageOption = union(enum) {
|
pub const PostMessageOption = union(enum) {
|
||||||
transfer: JsObject,
|
transfer: js.Object,
|
||||||
options: Opts,
|
options: Opts,
|
||||||
|
|
||||||
pub const Opts = struct {
|
pub const Opts = struct {
|
||||||
transfer: JsObject,
|
transfer: js.Object,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn _postMessage(self: *MessagePort, obj: JsObject, opts_: ?PostMessageOption, page: *Page) !void {
|
pub fn _postMessage(self: *MessagePort, obj: js.Object, opts_: ?PostMessageOption, page: *Page) !void {
|
||||||
if (self.closed) {
|
if (self.closed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts_ != null) {
|
if (opts_ != null) {
|
||||||
log.warn(.web_api, "not implemented", .{ .feature = "MessagePort postMessage options" });
|
log.warn(.web_api, "not implemented", .{ .feature = "MessagePort postMessage options" });
|
||||||
return error.NotImplemented;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try self.pair.dispatchOrQueue(obj, page.arena);
|
try self.pair.dispatchOrQueue(obj, page.arena);
|
||||||
@@ -125,10 +122,10 @@ pub const MessagePort = struct {
|
|||||||
self.pair.closed = true;
|
self.pair.closed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_onmessage(self: *MessagePort) ?Function {
|
pub fn get_onmessage(self: *MessagePort) ?js.Function {
|
||||||
return self.onmessage_cbk;
|
return self.onmessage_cbk;
|
||||||
}
|
}
|
||||||
pub fn get_onmessageerror(self: *MessagePort) ?Function {
|
pub fn get_onmessageerror(self: *MessagePort) ?js.Function {
|
||||||
return self.onmessageerror_cbk;
|
return self.onmessageerror_cbk;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +150,7 @@ pub const MessagePort = struct {
|
|||||||
|
|
||||||
// called from our pair. If port1.postMessage("x") is called, then this
|
// called from our pair. If port1.postMessage("x") is called, then this
|
||||||
// will be called on port2.
|
// will be called on port2.
|
||||||
fn dispatchOrQueue(self: *MessagePort, obj: JsObject, arena: Allocator) !void {
|
fn dispatchOrQueue(self: *MessagePort, obj: js.Object, arena: Allocator) !void {
|
||||||
// our pair should have checked this already
|
// our pair should have checked this already
|
||||||
std.debug.assert(self.closed == false);
|
std.debug.assert(self.closed == false);
|
||||||
|
|
||||||
@@ -168,7 +165,7 @@ pub const MessagePort = struct {
|
|||||||
return self.queue.append(arena, try obj.persist());
|
return self.queue.append(arena, try obj.persist());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch(self: *MessagePort, obj: JsObject) !void {
|
fn dispatch(self: *MessagePort, obj: js.Object) !void {
|
||||||
// obj is already persisted, don't use `MessageEvent.constructor`, but
|
// obj is already persisted, don't use `MessageEvent.constructor`, but
|
||||||
// go directly to `init`, which assumes persisted objects.
|
// go directly to `init`, which assumes persisted objects.
|
||||||
var evt = try MessageEvent.init(.{ .data = obj });
|
var evt = try MessageEvent.init(.{ .data = obj });
|
||||||
@@ -183,7 +180,7 @@ pub const MessagePort = struct {
|
|||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
typ: []const u8,
|
typ: []const u8,
|
||||||
listener: EventHandler.Listener,
|
listener: EventHandler.Listener,
|
||||||
) !?Function {
|
) !?js.Function {
|
||||||
const target = @as(*parser.EventTarget, @ptrCast(self));
|
const target = @as(*parser.EventTarget, @ptrCast(self));
|
||||||
const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable;
|
const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable;
|
||||||
return eh.callback;
|
return eh.callback;
|
||||||
@@ -208,12 +205,12 @@ pub const MessageEvent = struct {
|
|||||||
pub const union_make_copy = true;
|
pub const union_make_copy = true;
|
||||||
|
|
||||||
proto: parser.Event,
|
proto: parser.Event,
|
||||||
data: ?JsObject,
|
data: ?js.Object,
|
||||||
|
|
||||||
// You would think if port1 sends to port2, the source would be port2
|
// You would think if port1 sends to port2, the source would be port2
|
||||||
// (which is how I read the documentation), but it appears to always be
|
// (which is how I read the documentation), but it appears to always be
|
||||||
// null. It can always be set explicitly via the constructor;
|
// null. It can always be set explicitly via the constructor;
|
||||||
source: ?JsObject,
|
source: ?js.Object,
|
||||||
|
|
||||||
origin: []const u8,
|
origin: []const u8,
|
||||||
|
|
||||||
@@ -227,8 +224,8 @@ pub const MessageEvent = struct {
|
|||||||
ports: []*MessagePort,
|
ports: []*MessagePort,
|
||||||
|
|
||||||
const Options = struct {
|
const Options = struct {
|
||||||
data: ?JsObject = null,
|
data: ?js.Object = null,
|
||||||
source: ?JsObject = null,
|
source: ?js.Object = null,
|
||||||
origin: []const u8 = "",
|
origin: []const u8 = "",
|
||||||
lastEventId: []const u8 = "",
|
lastEventId: []const u8 = "",
|
||||||
ports: []*MessagePort = &.{},
|
ports: []*MessagePort = &.{},
|
||||||
@@ -244,7 +241,7 @@ pub const MessageEvent = struct {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is like "constructor", but it assumes JsObjects have already been
|
// This is like "constructor", but it assumes js.Objects have already been
|
||||||
// persisted. Necessary because this `new MessageEvent()` can be called
|
// persisted. Necessary because this `new MessageEvent()` can be called
|
||||||
// directly from JS OR from a port.postMessage. In the latter case, data
|
// directly from JS OR from a port.postMessage. In the latter case, data
|
||||||
// may have already been persisted (as it might need to be queued);
|
// may have already been persisted (as it might need to be queued);
|
||||||
@@ -252,7 +249,7 @@ pub const MessageEvent = struct {
|
|||||||
const event = try parser.eventCreate();
|
const event = try parser.eventCreate();
|
||||||
defer parser.eventDestroy(event);
|
defer parser.eventDestroy(event);
|
||||||
try parser.eventInit(event, "message", .{});
|
try parser.eventInit(event, "message", .{});
|
||||||
try parser.eventSetInternalType(event, .message_event);
|
parser.eventSetInternalType(event, .message_event);
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.proto = event.*,
|
.proto = event.*,
|
||||||
@@ -264,7 +261,7 @@ pub const MessageEvent = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_data(self: *const MessageEvent) !?JsObject {
|
pub fn get_data(self: *const MessageEvent) !?js.Object {
|
||||||
return self.data;
|
return self.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +269,7 @@ pub const MessageEvent = struct {
|
|||||||
return self.origin;
|
return self.origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_source(self: *const MessageEvent) ?JsObject {
|
pub fn get_source(self: *const MessageEvent) ?js.Object {
|
||||||
return self.source;
|
return self.source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,24 +25,24 @@ pub const Attr = struct {
|
|||||||
pub const prototype = *Node;
|
pub const prototype = *Node;
|
||||||
pub const subtype = .node;
|
pub const subtype = .node;
|
||||||
|
|
||||||
pub fn get_namespaceURI(self: *parser.Attribute) !?[]const u8 {
|
pub fn get_namespaceURI(self: *parser.Attribute) ?[]const u8 {
|
||||||
return try parser.nodeGetNamespace(parser.attributeToNode(self));
|
return parser.nodeGetNamespace(parser.attributeToNode(self));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_prefix(self: *parser.Attribute) !?[]const u8 {
|
pub fn get_prefix(self: *parser.Attribute) ?[]const u8 {
|
||||||
return try parser.nodeGetPrefix(parser.attributeToNode(self));
|
return parser.nodeGetPrefix(parser.attributeToNode(self));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_localName(self: *parser.Attribute) ![]const u8 {
|
pub fn get_localName(self: *parser.Attribute) ![]const u8 {
|
||||||
return try parser.nodeLocalName(parser.attributeToNode(self));
|
return parser.nodeLocalName(parser.attributeToNode(self));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_name(self: *parser.Attribute) ![]const u8 {
|
pub fn get_name(self: *parser.Attribute) ![]const u8 {
|
||||||
return try parser.attributeGetName(self);
|
return parser.attributeGetName(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_value(self: *parser.Attribute) !?[]const u8 {
|
pub fn get_value(self: *parser.Attribute) !?[]const u8 {
|
||||||
return try parser.attributeGetValue(self);
|
return parser.attributeGetValue(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_value(self: *parser.Attribute, v: []const u8) !?[]const u8 {
|
pub fn set_value(self: *parser.Attribute, v: []const u8) !?[]const u8 {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ pub const CharacterData = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_nextElementSibling(self: *parser.CharacterData) !?ElementUnion {
|
pub fn get_nextElementSibling(self: *parser.CharacterData) !?ElementUnion {
|
||||||
const res = try parser.nodeNextElementSibling(parser.characterDataToNode(self));
|
const res = parser.nodeNextElementSibling(parser.characterDataToNode(self));
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ pub const CharacterData = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_previousElementSibling(self: *parser.CharacterData) !?ElementUnion {
|
pub fn get_previousElementSibling(self: *parser.CharacterData) !?ElementUnion {
|
||||||
const res = try parser.nodePreviousElementSibling(parser.characterDataToNode(self));
|
const res = parser.nodePreviousElementSibling(parser.characterDataToNode(self));
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -68,8 +68,8 @@ pub const CharacterData = struct {
|
|||||||
|
|
||||||
// Read/Write attributes
|
// Read/Write attributes
|
||||||
|
|
||||||
pub fn get_data(self: *parser.CharacterData) ![]const u8 {
|
pub fn get_data(self: *parser.CharacterData) []const u8 {
|
||||||
return try parser.characterDataData(self);
|
return parser.characterDataData(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_data(self: *parser.CharacterData, data: []const u8) !void {
|
pub fn set_data(self: *parser.CharacterData, data: []const u8) !void {
|
||||||
@@ -96,18 +96,18 @@ pub const CharacterData = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn _substringData(self: *parser.CharacterData, offset: u32, count: u32) ![]const u8 {
|
pub fn _substringData(self: *parser.CharacterData, offset: u32, count: u32) ![]const u8 {
|
||||||
return try parser.characterDataSubstringData(self, offset, count);
|
return parser.characterDataSubstringData(self, offset, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
// netsurf's CharacterData (text, comment) doesn't implement the
|
// netsurf's CharacterData (text, comment) doesn't implement the
|
||||||
// dom_node_get_attributes and thus will crash if we try to call nodeIsEqualNode.
|
// dom_node_get_attributes and thus will crash if we try to call nodeIsEqualNode.
|
||||||
pub fn _isEqualNode(self: *parser.CharacterData, other_node: *parser.Node) !bool {
|
pub fn _isEqualNode(self: *parser.CharacterData, other_node: *parser.Node) bool {
|
||||||
if (try parser.nodeType(@ptrCast(@alignCast(self))) != try parser.nodeType(other_node)) {
|
if (parser.nodeType(@ptrCast(@alignCast(self))) != parser.nodeType(other_node)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const other: *parser.CharacterData = @ptrCast(other_node);
|
const other: *parser.CharacterData = @ptrCast(other_node);
|
||||||
if (std.mem.eql(u8, try get_data(self), try get_data(other)) == false) {
|
if (std.mem.eql(u8, get_data(self), get_data(other)) == false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,9 @@
|
|||||||
// 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 log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
@@ -38,8 +39,6 @@ const Range = @import("range.zig").Range;
|
|||||||
|
|
||||||
const CustomEvent = @import("../events/custom_event.zig").CustomEvent;
|
const CustomEvent = @import("../events/custom_event.zig").CustomEvent;
|
||||||
|
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
|
|
||||||
const DOMImplementation = @import("implementation.zig").DOMImplementation;
|
const DOMImplementation = @import("implementation.zig").DOMImplementation;
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#document
|
// WEB IDL https://dom.spec.whatwg.org/#document
|
||||||
@@ -116,7 +115,9 @@ pub const Document = struct {
|
|||||||
base: *parser.Event,
|
base: *parser.Event,
|
||||||
custom: CustomEvent,
|
custom: CustomEvent,
|
||||||
} {
|
} {
|
||||||
if (std.ascii.eqlIgnoreCase(eventCstr, "Event") or std.ascii.eqlIgnoreCase(eventCstr, "Events")) {
|
const eqlIgnoreCase = std.ascii.eqlIgnoreCase;
|
||||||
|
|
||||||
|
if (eqlIgnoreCase(eventCstr, "Event") or eqlIgnoreCase(eventCstr, "Events") or eqlIgnoreCase(eventCstr, "HTMLEvents")) {
|
||||||
return .{ .base = try parser.eventCreate() };
|
return .{ .base = try parser.eventCreate() };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,22 +155,14 @@ pub const Document = struct {
|
|||||||
// the spec changed to return an HTMLCollection instead.
|
// the spec changed to return an HTMLCollection instead.
|
||||||
// That's why we reimplemented getElementsByTagName by using an
|
// That's why we reimplemented getElementsByTagName by using an
|
||||||
// HTMLCollection in zig here.
|
// HTMLCollection in zig here.
|
||||||
pub fn _getElementsByTagName(
|
pub fn _getElementsByTagName(self: *parser.Document, tag_name: js.String) !collection.HTMLCollection {
|
||||||
self: *parser.Document,
|
return collection.HTMLCollectionByTagName(parser.documentToNode(self), tag_name.string, .{
|
||||||
tag_name: []const u8,
|
|
||||||
page: *Page,
|
|
||||||
) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentToNode(self), tag_name, .{
|
|
||||||
.include_root = true,
|
.include_root = true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _getElementsByClassName(
|
pub fn _getElementsByClassName(self: *parser.Document, class_names: js.String) !collection.HTMLCollection {
|
||||||
self: *parser.Document,
|
return collection.HTMLCollectionByClassName(parser.documentToNode(self), class_names.string, .{
|
||||||
classNames: []const u8,
|
|
||||||
page: *Page,
|
|
||||||
) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionByClassName(page.arena, parser.documentToNode(self), classNames, .{
|
|
||||||
.include_root = true,
|
.include_root = true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -306,21 +299,26 @@ pub const Document = struct {
|
|||||||
return &.{};
|
return &.{};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !Env.JsObject {
|
pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !js.Object {
|
||||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||||
if (state.adopted_style_sheets) |obj| {
|
if (state.adopted_style_sheets) |obj| {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
const obj = try page.main_context.newArray(0).persist();
|
const obj = try page.js.createArray(0).persist();
|
||||||
state.adopted_style_sheets = obj;
|
state.adopted_style_sheets = obj;
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: Env.JsObject, page: *Page) !void {
|
pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: js.Object, page: *Page) !void {
|
||||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||||
state.adopted_style_sheets = try sheets.persist();
|
state.adopted_style_sheets = try sheets.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn _hasFocus(_: *parser.Document) bool {
|
||||||
|
log.debug(.web_api, "not implemented", .{ .feature = "Document hasFocus" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ pub const DocumentFragment = struct {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _isEqualNode(self: *parser.DocumentFragment, other_node: *parser.Node) !bool {
|
pub fn _isEqualNode(self: *parser.DocumentFragment, other_node: *parser.Node) bool {
|
||||||
const other_type = try parser.nodeType(other_node);
|
const other_type = parser.nodeType(other_node);
|
||||||
if (other_type != .document_fragment) {
|
if (other_type != .document_fragment) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,21 +29,21 @@ pub const DocumentType = struct {
|
|||||||
pub const subtype = .node;
|
pub const subtype = .node;
|
||||||
|
|
||||||
pub fn get_name(self: *parser.DocumentType) ![]const u8 {
|
pub fn get_name(self: *parser.DocumentType) ![]const u8 {
|
||||||
return try parser.documentTypeGetName(self);
|
return parser.documentTypeGetName(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_publicId(self: *parser.DocumentType) ![]const u8 {
|
pub fn get_publicId(self: *parser.DocumentType) []const u8 {
|
||||||
return try parser.documentTypeGetPublicId(self);
|
return parser.documentTypeGetPublicId(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_systemId(self: *parser.DocumentType) ![]const u8 {
|
pub fn get_systemId(self: *parser.DocumentType) []const u8 {
|
||||||
return try parser.documentTypeGetSystemId(self);
|
return parser.documentTypeGetSystemId(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
// netsurf's DocumentType doesn't implement the dom_node_get_attributes
|
// netsurf's DocumentType doesn't implement the dom_node_get_attributes
|
||||||
// and thus will crash if we try to call nodeIsEqualNode.
|
// and thus will crash if we try to call nodeIsEqualNode.
|
||||||
pub fn _isEqualNode(self: *parser.DocumentType, other_node: *parser.Node) !bool {
|
pub fn _isEqualNode(self: *parser.DocumentType, other_node: *parser.Node) !bool {
|
||||||
if (try parser.nodeType(other_node) != .document_type) {
|
if (parser.nodeType(other_node) != .document_type) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,10 +51,10 @@ pub const DocumentType = struct {
|
|||||||
if (std.mem.eql(u8, try get_name(self), try get_name(other)) == false) {
|
if (std.mem.eql(u8, try get_name(self), try get_name(other)) == false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (std.mem.eql(u8, try get_publicId(self), try get_publicId(other)) == false) {
|
if (std.mem.eql(u8, get_publicId(self), get_publicId(other)) == false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (std.mem.eql(u8, try get_systemId(self), try get_systemId(other)) == false) {
|
if (std.mem.eql(u8, get_systemId(self), get_systemId(other)) == false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ const NodeList = @import("nodelist.zig");
|
|||||||
const Node = @import("node.zig");
|
const Node = @import("node.zig");
|
||||||
const ResizeObserver = @import("resize_observer.zig");
|
const ResizeObserver = @import("resize_observer.zig");
|
||||||
const MutationObserver = @import("mutation_observer.zig");
|
const MutationObserver = @import("mutation_observer.zig");
|
||||||
const IntersectionObserver = @import("intersection_observer.zig");
|
|
||||||
const DOMParser = @import("dom_parser.zig").DOMParser;
|
const DOMParser = @import("dom_parser.zig").DOMParser;
|
||||||
const TreeWalker = @import("tree_walker.zig").TreeWalker;
|
const TreeWalker = @import("tree_walker.zig").TreeWalker;
|
||||||
const NodeIterator = @import("node_iterator.zig").NodeIterator;
|
const NodeIterator = @import("node_iterator.zig").NodeIterator;
|
||||||
@@ -44,7 +43,6 @@ pub const Interfaces = .{
|
|||||||
Node.Interfaces,
|
Node.Interfaces,
|
||||||
ResizeObserver.Interfaces,
|
ResizeObserver.Interfaces,
|
||||||
MutationObserver.Interfaces,
|
MutationObserver.Interfaces,
|
||||||
IntersectionObserver.Interfaces,
|
|
||||||
DOMParser,
|
DOMParser,
|
||||||
TreeWalker,
|
TreeWalker,
|
||||||
NodeIterator,
|
NodeIterator,
|
||||||
@@ -54,4 +52,5 @@ pub const Interfaces = .{
|
|||||||
@import("range.zig").Interfaces,
|
@import("range.zig").Interfaces,
|
||||||
@import("Animation.zig"),
|
@import("Animation.zig"),
|
||||||
@import("MessageChannel.zig").Interfaces,
|
@import("MessageChannel.zig").Interfaces,
|
||||||
|
@import("IntersectionObserver.zig").Interfaces,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
@@ -33,7 +34,6 @@ const HTMLElem = @import("../html/elements.zig");
|
|||||||
const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot;
|
const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot;
|
||||||
|
|
||||||
const Animation = @import("Animation.zig");
|
const Animation = @import("Animation.zig");
|
||||||
const JsObject = @import("../env.zig").JsObject;
|
|
||||||
|
|
||||||
pub const Union = @import("../html/elements.zig").Union;
|
pub const Union = @import("../html/elements.zig").Union;
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ pub const Element = struct {
|
|||||||
pub fn toInterfaceT(comptime T: type, e: *parser.Element) !T {
|
pub fn toInterfaceT(comptime T: type, e: *parser.Element) !T {
|
||||||
const tagname = try parser.elementGetTagName(e) orelse {
|
const tagname = try parser.elementGetTagName(e) orelse {
|
||||||
// If the owner's document is HTML, assume we have an HTMLElement.
|
// If the owner's document is HTML, assume we have an HTMLElement.
|
||||||
const doc = try parser.nodeOwnerDocument(parser.elementToNode(e));
|
const doc = parser.nodeOwnerDocument(parser.elementToNode(e));
|
||||||
if (doc != null and !doc.?.is_html) {
|
if (doc != null and !doc.?.is_html) {
|
||||||
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
|
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ pub const Element = struct {
|
|||||||
|
|
||||||
const tag = parser.Tag.fromString(tagname) catch {
|
const tag = parser.Tag.fromString(tagname) catch {
|
||||||
// If the owner's document is HTML, assume we have an HTMLElement.
|
// If the owner's document is HTML, assume we have an HTMLElement.
|
||||||
const doc = try parser.nodeOwnerDocument(parser.elementToNode(e));
|
const doc = parser.nodeOwnerDocument(parser.elementToNode(e));
|
||||||
if (doc != null and doc.?.is_html) {
|
if (doc != null and doc.?.is_html) {
|
||||||
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
|
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
|
||||||
}
|
}
|
||||||
@@ -87,12 +87,12 @@ pub const Element = struct {
|
|||||||
// JS funcs
|
// JS funcs
|
||||||
// --------
|
// --------
|
||||||
|
|
||||||
pub fn get_namespaceURI(self: *parser.Element) !?[]const u8 {
|
pub fn get_namespaceURI(self: *parser.Element) ?[]const u8 {
|
||||||
return try parser.nodeGetNamespace(parser.elementToNode(self));
|
return parser.nodeGetNamespace(parser.elementToNode(self));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_prefix(self: *parser.Element) !?[]const u8 {
|
pub fn get_prefix(self: *parser.Element) ?[]const u8 {
|
||||||
return try parser.nodeGetPrefix(parser.elementToNode(self));
|
return parser.nodeGetPrefix(parser.elementToNode(self));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_localName(self: *parser.Element) ![]const u8 {
|
pub fn get_localName(self: *parser.Element) ![]const u8 {
|
||||||
@@ -103,6 +103,14 @@ pub const Element = struct {
|
|||||||
return try parser.nodeName(parser.elementToNode(self));
|
return try parser.nodeName(parser.elementToNode(self));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_dir(self: *parser.Element) ![]const u8 {
|
||||||
|
return try parser.elementGetAttribute(self, "dir") orelse "";
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_dir(self: *parser.Element, dir: []const u8) !void {
|
||||||
|
return parser.elementSetAttribute(self, "dir", dir);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_id(self: *parser.Element) ![]const u8 {
|
pub fn get_id(self: *parser.Element) ![]const u8 {
|
||||||
return try parser.elementGetAttribute(self, "id") orelse "";
|
return try parser.elementGetAttribute(self, "id") orelse "";
|
||||||
}
|
}
|
||||||
@@ -127,6 +135,10 @@ pub const Element = struct {
|
|||||||
return try parser.elementSetAttribute(self, "slot", slot);
|
return try parser.elementSetAttribute(self, "slot", slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_assignedSlot(self: *parser.Element, page: *const Page) !?*parser.Slot {
|
||||||
|
return @import("../SlotChangeMonitor.zig").findSlot(self, page);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_classList(self: *parser.Element) !*parser.TokenList {
|
pub fn get_classList(self: *parser.Element) !*parser.TokenList {
|
||||||
return try parser.tokenListCreate(self, "class");
|
return try parser.tokenListCreate(self, "class");
|
||||||
}
|
}
|
||||||
@@ -150,7 +162,7 @@ pub const Element = struct {
|
|||||||
|
|
||||||
pub fn set_innerHTML(self: *parser.Element, str: []const u8, page: *Page) !void {
|
pub fn set_innerHTML(self: *parser.Element, str: []const u8, page: *Page) !void {
|
||||||
const node = parser.elementToNode(self);
|
const node = parser.elementToNode(self);
|
||||||
const doc = try parser.nodeOwnerDocument(node) orelse return parser.DOMError.WrongDocument;
|
const doc = parser.nodeOwnerDocument(node) orelse return parser.DOMError.WrongDocument;
|
||||||
// parse the fragment
|
// parse the fragment
|
||||||
const fragment = try parser.documentParseFragmentFromStr(doc, str);
|
const fragment = try parser.documentParseFragmentFromStr(doc, str);
|
||||||
|
|
||||||
@@ -168,9 +180,9 @@ pub const Element = struct {
|
|||||||
// or an actual document. In a blank page, something like:
|
// or an actual document. In a blank page, something like:
|
||||||
// x.innerHTML = '<script></script>';
|
// x.innerHTML = '<script></script>';
|
||||||
// does _not_ create an empty script, but in a real page, it does. Weird.
|
// does _not_ create an empty script, but in a real page, it does. Weird.
|
||||||
const html = try parser.nodeFirstChild(fragment_node) orelse return;
|
const html = parser.nodeFirstChild(fragment_node) orelse return;
|
||||||
const head = try parser.nodeFirstChild(html) orelse return;
|
const head = parser.nodeFirstChild(html) orelse return;
|
||||||
const body = try parser.nodeNextSibling(head) orelse return;
|
const body = parser.nodeNextSibling(head) orelse return;
|
||||||
|
|
||||||
if (try parser.elementTag(self) == .template) {
|
if (try parser.elementTag(self) == .template) {
|
||||||
// HTMLElementTemplate is special. We don't append these as children
|
// HTMLElementTemplate is special. We don't append these as children
|
||||||
@@ -179,11 +191,9 @@ pub const Element = struct {
|
|||||||
// a new fragment
|
// a new fragment
|
||||||
const clean = try parser.documentCreateDocumentFragment(doc);
|
const clean = try parser.documentCreateDocumentFragment(doc);
|
||||||
const children = try parser.nodeGetChildNodes(body);
|
const children = try parser.nodeGetChildNodes(body);
|
||||||
const ln = try parser.nodeListLength(children);
|
// always index 0, because nodeAppendChild moves the node out of
|
||||||
for (0..ln) |_| {
|
// the nodeList and into the new tree
|
||||||
// always index 0, because nodeAppendChild moves the node out of
|
while (parser.nodeListItem(children, 0)) |child| {
|
||||||
// the nodeList and into the new tree
|
|
||||||
const child = try parser.nodeListItem(children, 0) orelse continue;
|
|
||||||
_ = try parser.nodeAppendChild(@ptrCast(@alignCast(clean)), child);
|
_ = try parser.nodeAppendChild(@ptrCast(@alignCast(clean)), child);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,27 +207,102 @@ pub const Element = struct {
|
|||||||
{
|
{
|
||||||
// First, copy some of the head element
|
// First, copy some of the head element
|
||||||
const children = try parser.nodeGetChildNodes(head);
|
const children = try parser.nodeGetChildNodes(head);
|
||||||
const ln = try parser.nodeListLength(children);
|
// always index 0, because nodeAppendChild moves the node out of
|
||||||
for (0..ln) |_| {
|
// the nodeList and into the new tree
|
||||||
// always index 0, because nodeAppendChild moves the node out of
|
while (parser.nodeListItem(children, 0)) |child| {
|
||||||
// the nodeList and into the new tree
|
|
||||||
const child = try parser.nodeListItem(children, 0) orelse continue;
|
|
||||||
_ = try parser.nodeAppendChild(node, child);
|
_ = try parser.nodeAppendChild(node, child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const children = try parser.nodeGetChildNodes(body);
|
const children = try parser.nodeGetChildNodes(body);
|
||||||
const ln = try parser.nodeListLength(children);
|
// always index 0, because nodeAppendChild moves the node out of
|
||||||
for (0..ln) |_| {
|
// the nodeList and into the new tree
|
||||||
// always index 0, because nodeAppendChild moves the node out of
|
while (parser.nodeListItem(children, 0)) |child| {
|
||||||
// the nodeList and into the new tree
|
|
||||||
const child = try parser.nodeListItem(children, 0) orelse continue;
|
|
||||||
_ = try parser.nodeAppendChild(node, child);
|
_ = try parser.nodeAppendChild(node, child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses the given `input` string and inserts its children to an element at given `position`.
|
||||||
|
/// https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
|
||||||
|
///
|
||||||
|
/// TODO: Support for XML parsing and `TrustedHTML` instances.
|
||||||
|
pub fn _insertAdjacentHTML(self: *parser.Element, position: []const u8, input: []const u8) !void {
|
||||||
|
const self_node = parser.elementToNode(self);
|
||||||
|
const doc = parser.nodeOwnerDocument(self_node) orelse {
|
||||||
|
return parser.DOMError.WrongDocument;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the fragment.
|
||||||
|
// Should return error.Syntax on fail?
|
||||||
|
const fragment = try parser.documentParseFragmentFromStr(doc, input);
|
||||||
|
const fragment_node = parser.documentFragmentToNode(fragment);
|
||||||
|
|
||||||
|
// We always get it wrapped like so:
|
||||||
|
// <html><head></head><body>{ ... }</body></html>
|
||||||
|
// None of the following can be null.
|
||||||
|
const maybe_html = parser.nodeFirstChild(fragment_node);
|
||||||
|
std.debug.assert(maybe_html != null);
|
||||||
|
const html = maybe_html orelse return;
|
||||||
|
|
||||||
|
const maybe_body = parser.nodeLastChild(html);
|
||||||
|
std.debug.assert(maybe_body != null);
|
||||||
|
const body = maybe_body orelse return;
|
||||||
|
|
||||||
|
const children = try parser.nodeGetChildNodes(body);
|
||||||
|
|
||||||
|
// * `target_node` is `*Node` (where we actually insert),
|
||||||
|
// * `prev_node` is `?*Node`.
|
||||||
|
const target_node, const prev_node = blk: {
|
||||||
|
// Prefer case-sensitive match.
|
||||||
|
// "beforeend" was the most common case in my tests; we might adjust the order
|
||||||
|
// depending on which ones websites prefer most.
|
||||||
|
if (std.mem.eql(u8, position, "beforeend")) {
|
||||||
|
break :blk .{ self_node, null };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, position, "afterbegin")) {
|
||||||
|
// Get the first child; null indicates there are no children.
|
||||||
|
const first_child = parser.nodeFirstChild(self_node);
|
||||||
|
break :blk .{ self_node, first_child };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, position, "beforebegin")) {
|
||||||
|
// The node must have a parent node in order to use this variant.
|
||||||
|
const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed;
|
||||||
|
// Parent cannot be Document.
|
||||||
|
// Should have checks for document_fragment and document_type?
|
||||||
|
if (parser.nodeType(parent) == .document) {
|
||||||
|
return error.NoModificationAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk .{ parent, self_node };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, position, "afterend")) {
|
||||||
|
// The node must have a parent node in order to use this variant.
|
||||||
|
const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed;
|
||||||
|
// Parent cannot be Document.
|
||||||
|
if (parser.nodeType(parent) == .document) {
|
||||||
|
return error.NoModificationAllowed;
|
||||||
|
}
|
||||||
|
// Get the next sibling or null; null indicates our node is the only one.
|
||||||
|
const sibling = parser.nodeNextSibling(self_node);
|
||||||
|
break :blk .{ parent, sibling };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thrown if:
|
||||||
|
// * position is not one of the four listed values.
|
||||||
|
// * The input is XML that is not well-formed.
|
||||||
|
return error.Syntax;
|
||||||
|
};
|
||||||
|
|
||||||
|
while (parser.nodeListItem(children, 0)) |child| {
|
||||||
|
_ = try parser.nodeInsertBefore(target_node, child, prev_node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The closest() method of the Element interface traverses the element and its parents (heading toward the document root) until it finds a node that matches the specified CSS selector.
|
// The closest() method of the Element interface traverses the element and its parents (heading toward the document root) until it finds a node that matches the specified CSS selector.
|
||||||
// Returns the closest ancestor Element or itself, which matches the selectors. If there are no such element, null.
|
// Returns the closest ancestor Element or itself, which matches the selectors. If there are no such element, null.
|
||||||
pub fn _closest(self: *parser.Element, selector: []const u8, page: *Page) !?*parser.Element {
|
pub fn _closest(self: *parser.Element, selector: []const u8, page: *Page) !?*parser.Element {
|
||||||
@@ -234,7 +319,7 @@ pub const Element = struct {
|
|||||||
}
|
}
|
||||||
return parser.nodeToElement(current.node);
|
return parser.nodeToElement(current.node);
|
||||||
}
|
}
|
||||||
current = try current.parent() orelse return null;
|
current = current.parent() orelse return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,28 +435,18 @@ pub const Element = struct {
|
|||||||
return try parser.elementRemoveAttributeNode(self, attr);
|
return try parser.elementRemoveAttributeNode(self, attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _getElementsByTagName(
|
pub fn _getElementsByTagName(self: *parser.Element, tag_name: js.String) !collection.HTMLCollection {
|
||||||
self: *parser.Element,
|
return collection.HTMLCollectionByTagName(
|
||||||
tag_name: []const u8,
|
|
||||||
page: *Page,
|
|
||||||
) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionByTagName(
|
|
||||||
page.arena,
|
|
||||||
parser.elementToNode(self),
|
parser.elementToNode(self),
|
||||||
tag_name,
|
tag_name.string,
|
||||||
.{ .include_root = false },
|
.{ .include_root = false },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _getElementsByClassName(
|
pub fn _getElementsByClassName(self: *parser.Element, class_names: js.String) !collection.HTMLCollection {
|
||||||
self: *parser.Element,
|
|
||||||
classNames: []const u8,
|
|
||||||
page: *Page,
|
|
||||||
) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionByClassName(
|
return try collection.HTMLCollectionByClassName(
|
||||||
page.arena,
|
|
||||||
parser.elementToNode(self),
|
parser.elementToNode(self),
|
||||||
classNames,
|
class_names.string,
|
||||||
.{ .include_root = false },
|
.{ .include_root = false },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -407,13 +482,13 @@ pub const Element = struct {
|
|||||||
// NonDocumentTypeChildNode
|
// NonDocumentTypeChildNode
|
||||||
// https://dom.spec.whatwg.org/#interface-nondocumenttypechildnode
|
// https://dom.spec.whatwg.org/#interface-nondocumenttypechildnode
|
||||||
pub fn get_previousElementSibling(self: *parser.Element) !?Union {
|
pub fn get_previousElementSibling(self: *parser.Element) !?Union {
|
||||||
const res = try parser.nodePreviousElementSibling(parser.elementToNode(self));
|
const res = parser.nodePreviousElementSibling(parser.elementToNode(self));
|
||||||
if (res == null) return null;
|
if (res == null) return null;
|
||||||
return try toInterface(res.?);
|
return try toInterface(res.?);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_nextElementSibling(self: *parser.Element) !?Union {
|
pub fn get_nextElementSibling(self: *parser.Element) !?Union {
|
||||||
const res = try parser.nodeNextElementSibling(parser.elementToNode(self));
|
const res = parser.nodeNextElementSibling(parser.elementToNode(self));
|
||||||
if (res == null) return null;
|
if (res == null) return null;
|
||||||
return try toInterface(res.?);
|
return try toInterface(res.?);
|
||||||
}
|
}
|
||||||
@@ -426,7 +501,7 @@ pub const Element = struct {
|
|||||||
while (true) {
|
while (true) {
|
||||||
next = try walker.get_next(root, next) orelse return null;
|
next = try walker.get_next(root, next) orelse return null;
|
||||||
// ignore non-element nodes.
|
// ignore non-element nodes.
|
||||||
if (try parser.nodeType(next.?) != .element) {
|
if (parser.nodeType(next.?) != .element) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const e = parser.nodeToElement(next.?);
|
const e = parser.nodeToElement(next.?);
|
||||||
@@ -474,7 +549,7 @@ pub const Element = struct {
|
|||||||
// Returns a 0 DOMRect object if the element is eventually detached from the main window
|
// Returns a 0 DOMRect object if the element is eventually detached from the main window
|
||||||
pub fn _getBoundingClientRect(self: *parser.Element, page: *Page) !DOMRect {
|
pub fn _getBoundingClientRect(self: *parser.Element, page: *Page) !DOMRect {
|
||||||
// Since we are lazy rendering we need to do this check. We could store the renderer in a viewport such that it could cache these, but it would require tracking changes.
|
// Since we are lazy rendering we need to do this check. We could store the renderer in a viewport such that it could cache these, but it would require tracking changes.
|
||||||
if (!try page.isNodeAttached(parser.elementToNode(self))) {
|
if (!page.isNodeAttached(parser.elementToNode(self))) {
|
||||||
return DOMRect{
|
return DOMRect{
|
||||||
.x = 0,
|
.x = 0,
|
||||||
.y = 0,
|
.y = 0,
|
||||||
@@ -493,7 +568,7 @@ pub const Element = struct {
|
|||||||
// We do not render so it only always return the element's bounding rect.
|
// We do not render so it only always return the element's bounding rect.
|
||||||
// Returns an empty array if the element is eventually detached from the main window
|
// Returns an empty array if the element is eventually detached from the main window
|
||||||
pub fn _getClientRects(self: *parser.Element, page: *Page) ![]DOMRect {
|
pub fn _getClientRects(self: *parser.Element, page: *Page) ![]DOMRect {
|
||||||
if (!try page.isNodeAttached(parser.elementToNode(self))) {
|
if (!page.isNodeAttached(parser.elementToNode(self))) {
|
||||||
return &.{};
|
return &.{};
|
||||||
}
|
}
|
||||||
const heap_ptr = try page.call_arena.create(DOMRect);
|
const heap_ptr = try page.call_arena.create(DOMRect);
|
||||||
@@ -524,6 +599,8 @@ pub const Element = struct {
|
|||||||
contentVisibilityAuto: bool,
|
contentVisibilityAuto: bool,
|
||||||
opacityProperty: bool,
|
opacityProperty: bool,
|
||||||
visibilityProperty: bool,
|
visibilityProperty: bool,
|
||||||
|
checkVisibilityCSS: bool,
|
||||||
|
checkOpacity: bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn _checkVisibility(self: *parser.Element, opts: ?CheckVisibilityOpts) bool {
|
pub fn _checkVisibility(self: *parser.Element, opts: ?CheckVisibilityOpts) bool {
|
||||||
@@ -549,7 +626,7 @@ pub const Element = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Not sure what to do if there is no owner document
|
// Not sure what to do if there is no owner document
|
||||||
const doc = try parser.nodeOwnerDocument(@ptrCast(self)) orelse return error.InvalidArgument;
|
const doc = parser.nodeOwnerDocument(@ptrCast(self)) orelse return error.InvalidArgument;
|
||||||
const fragment = try parser.documentCreateDocumentFragment(doc);
|
const fragment = try parser.documentCreateDocumentFragment(doc);
|
||||||
const sr = try page.arena.create(ShadowRoot);
|
const sr = try page.arena.create(ShadowRoot);
|
||||||
sr.* = .{
|
sr.* = .{
|
||||||
@@ -583,7 +660,7 @@ pub const Element = struct {
|
|||||||
return sr;
|
return sr;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _animate(self: *parser.Element, effect: JsObject, opts: JsObject) !Animation {
|
pub fn _animate(self: *parser.Element, effect: js.Object, opts: js.Object) !Animation {
|
||||||
_ = self;
|
_ = self;
|
||||||
_ = opts;
|
_ = opts;
|
||||||
return Animation.constructor(effect, null);
|
return Animation.constructor(effect, null);
|
||||||
@@ -595,7 +672,7 @@ pub const Element = struct {
|
|||||||
// for related elements JIT by walking the tree, but there could be
|
// for related elements JIT by walking the tree, but there could be
|
||||||
// cases in libdom or the Zig WebAPI where this reference is kept
|
// cases in libdom or the Zig WebAPI where this reference is kept
|
||||||
const as_node: *parser.Node = @ptrCast(self);
|
const as_node: *parser.Node = @ptrCast(self);
|
||||||
const parent = try parser.nodeParentNode(as_node) orelse return;
|
const parent = parser.nodeParentNode(as_node) orelse return;
|
||||||
_ = try Node._removeChild(parent, as_node);
|
_ = try Node._removeChild(parent, as_node);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
@@ -35,6 +34,7 @@ pub const Union = union(enum) {
|
|||||||
screen_orientation: *@import("../html/screen.zig").ScreenOrientation,
|
screen_orientation: *@import("../html/screen.zig").ScreenOrientation,
|
||||||
performance: *@import("performance.zig").Performance,
|
performance: *@import("performance.zig").Performance,
|
||||||
media_query_list: *@import("../html/media_query_list.zig").MediaQueryList,
|
media_query_list: *@import("../html/media_query_list.zig").MediaQueryList,
|
||||||
|
navigation: *@import("../navigation/Navigation.zig"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// EventTarget implementation
|
// EventTarget implementation
|
||||||
@@ -48,7 +48,7 @@ pub const EventTarget = struct {
|
|||||||
pub fn toInterface(et: *parser.EventTarget, page: *Page) !Union {
|
pub fn toInterface(et: *parser.EventTarget, page: *Page) !Union {
|
||||||
// libdom assumes that all event targets are libdom nodes. They are not.
|
// libdom assumes that all event targets are libdom nodes. They are not.
|
||||||
|
|
||||||
switch (try parser.eventTargetInternalType(et)) {
|
switch (parser.eventTargetInternalType(et)) {
|
||||||
.libdom_node => {
|
.libdom_node => {
|
||||||
return .{ .node = try nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))) };
|
return .{ .node = try nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))) };
|
||||||
},
|
},
|
||||||
@@ -83,6 +83,11 @@ pub const EventTarget = struct {
|
|||||||
.media_query_list => {
|
.media_query_list => {
|
||||||
return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||||
},
|
},
|
||||||
|
.navigation => {
|
||||||
|
const NavigationEventTarget = @import("../navigation/NavigationEventTarget.zig");
|
||||||
|
const base: *NavigationEventTarget = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et)));
|
||||||
|
return .{ .navigation = @fieldParentPtr("proto", base) };
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +106,9 @@ pub const EventTarget = struct {
|
|||||||
page: *Page,
|
page: *Page,
|
||||||
) !void {
|
) !void {
|
||||||
_ = try EventHandler.register(page.arena, self, typ, listener, opts);
|
_ = try EventHandler.register(page.arena, self, typ, listener, opts);
|
||||||
|
if (std.mem.eql(u8, typ, "slotchange")) {
|
||||||
|
try page.registerSlotChangeMonitor();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const RemoveEventListenerOpts = union(enum) {
|
const RemoveEventListenerOpts = union(enum) {
|
||||||
@@ -148,8 +156,15 @@ pub const EventTarget = struct {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool {
|
pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event, page: *Page) !bool {
|
||||||
return try parser.eventTargetDispatchEvent(self, event);
|
const res = try parser.eventTargetDispatchEvent(self, event);
|
||||||
|
|
||||||
|
if (!parser.eventBubbles(event) or parser.eventIsStopped(event)) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
try page.window.dispatchForDocumentTarget(event);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ const parser = @import("../netsurf.zig");
|
|||||||
|
|
||||||
const Element = @import("element.zig").Element;
|
const Element = @import("element.zig").Element;
|
||||||
const Union = @import("element.zig").Union;
|
const Union = @import("element.zig").Union;
|
||||||
const JsThis = @import("../env.zig").JsThis;
|
|
||||||
const Walker = @import("walker.zig").Walker;
|
const Walker = @import("walker.zig").Walker;
|
||||||
|
|
||||||
const Matcher = union(enum) {
|
const Matcher = union(enum) {
|
||||||
@@ -52,13 +51,13 @@ pub const MatchByTagName = struct {
|
|||||||
tag: []const u8,
|
tag: []const u8,
|
||||||
is_wildcard: bool,
|
is_wildcard: bool,
|
||||||
|
|
||||||
fn init(arena: Allocator, tag_name: []const u8) !MatchByTagName {
|
fn init(tag_name: []const u8) MatchByTagName {
|
||||||
if (std.mem.eql(u8, tag_name, "*")) {
|
if (std.mem.eql(u8, tag_name, "*")) {
|
||||||
return .{ .tag = "*", .is_wildcard = true };
|
return .{ .tag = "*", .is_wildcard = true };
|
||||||
}
|
}
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.tag = try arena.dupe(u8, tag_name),
|
.tag = tag_name,
|
||||||
.is_wildcard = false,
|
.is_wildcard = false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -69,15 +68,14 @@ pub const MatchByTagName = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn HTMLCollectionByTagName(
|
pub fn HTMLCollectionByTagName(
|
||||||
arena: Allocator,
|
|
||||||
root: ?*parser.Node,
|
root: ?*parser.Node,
|
||||||
tag_name: []const u8,
|
tag_name: []const u8,
|
||||||
opts: Opts,
|
opts: Opts,
|
||||||
) !HTMLCollection {
|
) HTMLCollection {
|
||||||
return HTMLCollection{
|
return .{
|
||||||
.root = root,
|
.root = root,
|
||||||
.walker = .{ .walkerDepthFirst = .{} },
|
.walker = .{ .walkerDepthFirst = .{} },
|
||||||
.matcher = .{ .matchByTagName = try MatchByTagName.init(arena, tag_name) },
|
.matcher = .{ .matchByTagName = MatchByTagName.init(tag_name) },
|
||||||
.mutable = opts.mutable,
|
.mutable = opts.mutable,
|
||||||
.include_root = opts.include_root,
|
.include_root = opts.include_root,
|
||||||
};
|
};
|
||||||
@@ -86,9 +84,9 @@ pub fn HTMLCollectionByTagName(
|
|||||||
pub const MatchByClassName = struct {
|
pub const MatchByClassName = struct {
|
||||||
class_names: []const u8,
|
class_names: []const u8,
|
||||||
|
|
||||||
fn init(arena: Allocator, class_names: []const u8) !MatchByClassName {
|
fn init(class_names: []const u8) !MatchByClassName {
|
||||||
return .{
|
return .{
|
||||||
.class_names = try arena.dupe(u8, class_names),
|
.class_names = class_names,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,15 +105,14 @@ pub const MatchByClassName = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn HTMLCollectionByClassName(
|
pub fn HTMLCollectionByClassName(
|
||||||
arena: Allocator,
|
|
||||||
root: ?*parser.Node,
|
root: ?*parser.Node,
|
||||||
classNames: []const u8,
|
class_names: []const u8,
|
||||||
opts: Opts,
|
opts: Opts,
|
||||||
) !HTMLCollection {
|
) !HTMLCollection {
|
||||||
return HTMLCollection{
|
return HTMLCollection{
|
||||||
.root = root,
|
.root = root,
|
||||||
.walker = .{ .walkerDepthFirst = .{} },
|
.walker = .{ .walkerDepthFirst = .{} },
|
||||||
.matcher = .{ .matchByClassName = try MatchByClassName.init(arena, classNames) },
|
.matcher = .{ .matchByClassName = try MatchByClassName.init(class_names) },
|
||||||
.mutable = opts.mutable,
|
.mutable = opts.mutable,
|
||||||
.include_root = opts.include_root,
|
.include_root = opts.include_root,
|
||||||
};
|
};
|
||||||
@@ -124,10 +121,8 @@ pub fn HTMLCollectionByClassName(
|
|||||||
pub const MatchByName = struct {
|
pub const MatchByName = struct {
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
|
|
||||||
fn init(arena: Allocator, name: []const u8) !MatchByName {
|
fn init(name: []const u8) !MatchByName {
|
||||||
return .{
|
return .{ .name = name };
|
||||||
.name = try arena.dupe(u8, name),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn match(self: MatchByName, node: *parser.Node) !bool {
|
pub fn match(self: MatchByName, node: *parser.Node) !bool {
|
||||||
@@ -138,7 +133,6 @@ pub const MatchByName = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn HTMLCollectionByName(
|
pub fn HTMLCollectionByName(
|
||||||
arena: Allocator,
|
|
||||||
root: ?*parser.Node,
|
root: ?*parser.Node,
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
opts: Opts,
|
opts: Opts,
|
||||||
@@ -146,7 +140,7 @@ pub fn HTMLCollectionByName(
|
|||||||
return HTMLCollection{
|
return HTMLCollection{
|
||||||
.root = root,
|
.root = root,
|
||||||
.walker = .{ .walkerDepthFirst = .{} },
|
.walker = .{ .walkerDepthFirst = .{} },
|
||||||
.matcher = .{ .matchByName = try MatchByName.init(arena, name) },
|
.matcher = .{ .matchByName = try MatchByName.init(name) },
|
||||||
.mutable = opts.mutable,
|
.mutable = opts.mutable,
|
||||||
.include_root = opts.include_root,
|
.include_root = opts.include_root,
|
||||||
};
|
};
|
||||||
@@ -203,8 +197,8 @@ pub fn HTMLCollectionChildren(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn HTMLCollectionEmpty() !HTMLCollection {
|
pub fn HTMLCollectionEmpty() HTMLCollection {
|
||||||
return HTMLCollection{
|
return .{
|
||||||
.root = null,
|
.root = null,
|
||||||
.walker = .{ .walkerNone = .{} },
|
.walker = .{ .walkerNone = .{} },
|
||||||
.matcher = .{ .matchFalse = .{} },
|
.matcher = .{ .matchFalse = .{} },
|
||||||
@@ -226,14 +220,11 @@ pub const MatchByLinks = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn HTMLCollectionByLinks(
|
pub fn HTMLCollectionByLinks(root: ?*parser.Node, opts: Opts) HTMLCollection {
|
||||||
root: ?*parser.Node,
|
return .{
|
||||||
opts: Opts,
|
|
||||||
) !HTMLCollection {
|
|
||||||
return HTMLCollection{
|
|
||||||
.root = root,
|
.root = root,
|
||||||
.walker = .{ .walkerDepthFirst = .{} },
|
.walker = .{ .walkerDepthFirst = .{} },
|
||||||
.matcher = .{ .matchByLinks = MatchByLinks{} },
|
.matcher = .{ .matchByLinks = .{} },
|
||||||
.mutable = opts.mutable,
|
.mutable = opts.mutable,
|
||||||
.include_root = opts.include_root,
|
.include_root = opts.include_root,
|
||||||
};
|
};
|
||||||
@@ -252,14 +243,11 @@ pub const MatchByAnchors = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn HTMLCollectionByAnchors(
|
pub fn HTMLCollectionByAnchors(root: ?*parser.Node, opts: Opts) HTMLCollection {
|
||||||
root: ?*parser.Node,
|
return .{
|
||||||
opts: Opts,
|
|
||||||
) !HTMLCollection {
|
|
||||||
return HTMLCollection{
|
|
||||||
.root = root,
|
.root = root,
|
||||||
.walker = .{ .walkerDepthFirst = .{} },
|
.walker = .{ .walkerDepthFirst = .{} },
|
||||||
.matcher = .{ .matchByAnchors = MatchByAnchors{} },
|
.matcher = .{ .matchByAnchors = .{} },
|
||||||
.mutable = opts.mutable,
|
.mutable = opts.mutable,
|
||||||
.include_root = opts.include_root,
|
.include_root = opts.include_root,
|
||||||
};
|
};
|
||||||
@@ -298,7 +286,7 @@ const Opts = struct {
|
|||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#htmlcollection
|
// WEB IDL https://dom.spec.whatwg.org/#htmlcollection
|
||||||
// HTMLCollection is re implemented in zig here because libdom
|
// HTMLCollection is re implemented in zig here because libdom
|
||||||
// dom_html_collection expects a comparison function callback as arguement.
|
// dom_html_collection expects a comparison function callback as argument.
|
||||||
// But we wanted a dynamically comparison here, according to the match tagname.
|
// But we wanted a dynamically comparison here, according to the match tagname.
|
||||||
pub const HTMLCollection = struct {
|
pub const HTMLCollection = struct {
|
||||||
matcher: Matcher,
|
matcher: Matcher,
|
||||||
@@ -344,7 +332,7 @@ pub const HTMLCollection = struct {
|
|||||||
var node = try self.start() orelse return 0;
|
var node = try self.start() orelse return 0;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (try parser.nodeType(node) == .element) {
|
if (parser.nodeType(node) == .element) {
|
||||||
if (try self.matcher.match(node)) {
|
if (try self.matcher.match(node)) {
|
||||||
len += 1;
|
len += 1;
|
||||||
}
|
}
|
||||||
@@ -371,7 +359,7 @@ pub const HTMLCollection = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (try parser.nodeType(node) == .element) {
|
if (parser.nodeType(node) == .element) {
|
||||||
if (try self.matcher.match(node)) {
|
if (try self.matcher.match(node)) {
|
||||||
// check if we found the searched element.
|
// check if we found the searched element.
|
||||||
if (i == index) {
|
if (i == index) {
|
||||||
@@ -405,7 +393,7 @@ pub const HTMLCollection = struct {
|
|||||||
var node = try self.start() orelse return null;
|
var node = try self.start() orelse return null;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (try parser.nodeType(node) == .element) {
|
if (parser.nodeType(node) == .element) {
|
||||||
if (try self.matcher.match(node)) {
|
if (try self.matcher.match(node)) {
|
||||||
const elem = @as(*parser.Element, @ptrCast(node));
|
const elem = @as(*parser.Element, @ptrCast(node));
|
||||||
|
|
||||||
@@ -440,24 +428,23 @@ pub const HTMLCollection = struct {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn postAttach(self: *HTMLCollection, js_this: JsThis) !void {
|
pub fn indexed_get(self: *HTMLCollection, index: u32, has_value: *bool) !?Union {
|
||||||
const len = try self.get_length();
|
return (try _item(self, index)) orelse {
|
||||||
for (0..len) |i| {
|
has_value.* = false;
|
||||||
const node = try self.item(@intCast(i)) orelse unreachable;
|
return undefined;
|
||||||
const e = @as(*parser.Element, @ptrCast(node));
|
};
|
||||||
const as_interface = try Element.toInterface(e);
|
}
|
||||||
try js_this.setIndex(@intCast(i), as_interface, .{});
|
|
||||||
|
|
||||||
if (try item_name(e)) |name| {
|
pub fn named_get(self: *const HTMLCollection, name: []const u8, has_value: *bool) !?Union {
|
||||||
// Even though an entry might have an empty id, the spec says
|
// Even though an entry might have an empty id, the spec says
|
||||||
// that namedItem("") should always return null
|
// that namedItem("") should always return null
|
||||||
if (name.len > 0) {
|
if (name.len == 0) {
|
||||||
// Named fields should not be enumerable (it is defined with
|
return null;
|
||||||
// the LegacyUnenumerableNamedProperties flag.)
|
|
||||||
try js_this.set(name, as_interface, .{ .DONT_ENUM = true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return (try _namedItem(self, name)) orelse {
|
||||||
|
has_value.* = false;
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,186 +0,0 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const Element = @import("element.zig").Element;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
IntersectionObserver,
|
|
||||||
IntersectionObserverEntry,
|
|
||||||
};
|
|
||||||
|
|
||||||
// This is supposed to listen to change between the root and observation targets.
|
|
||||||
// However, our rendered stores everything as 1 pixel sized boxes in a long row that never changes.
|
|
||||||
// As such, there are no changes to intersections between the root and any target.
|
|
||||||
// Instead we keep a list of all entries that are being observed.
|
|
||||||
// The callback is called with all entries everytime a new entry is added(observed).
|
|
||||||
// Potentially we should also call the callback at a regular interval.
|
|
||||||
// The returned Entries are phony, they always indicate full intersection.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
|
|
||||||
pub const IntersectionObserver = struct {
|
|
||||||
page: *Page,
|
|
||||||
callback: Env.Function,
|
|
||||||
options: IntersectionObserverOptions,
|
|
||||||
|
|
||||||
observed_entries: std.ArrayListUnmanaged(IntersectionObserverEntry),
|
|
||||||
|
|
||||||
// new IntersectionObserver(callback)
|
|
||||||
// new IntersectionObserver(callback, options) [not supported yet]
|
|
||||||
pub fn constructor(callback: Env.Function, options_: ?IntersectionObserverOptions, page: *Page) !IntersectionObserver {
|
|
||||||
var options = IntersectionObserverOptions{
|
|
||||||
.root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document)),
|
|
||||||
.rootMargin = "0px 0px 0px 0px",
|
|
||||||
.threshold = .{ .single = 0.0 },
|
|
||||||
};
|
|
||||||
if (options_) |*o| {
|
|
||||||
if (o.root) |root| {
|
|
||||||
options.root = root;
|
|
||||||
} // Other properties are not used due to the way we render
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.page = page,
|
|
||||||
.callback = callback,
|
|
||||||
.options = options,
|
|
||||||
.observed_entries = .{},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _disconnect(self: *IntersectionObserver) !void {
|
|
||||||
self.observed_entries = .{}; // We don't free as it is on an arena
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _observe(self: *IntersectionObserver, target_element: *parser.Element) !void {
|
|
||||||
for (self.observed_entries.items) |*observer| {
|
|
||||||
if (observer.target == target_element) {
|
|
||||||
return; // Already observed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.observed_entries.append(self.page.arena, .{
|
|
||||||
.page = self.page,
|
|
||||||
.target = target_element,
|
|
||||||
.options = &self.options,
|
|
||||||
});
|
|
||||||
|
|
||||||
var result: Env.Function.Result = undefined;
|
|
||||||
self.callback.tryCall(void, .{self.observed_entries.items}, &result) catch {
|
|
||||||
log.debug(.user_script, "callback error", .{
|
|
||||||
.err = result.exception,
|
|
||||||
.stack = result.stack,
|
|
||||||
.source = "intersection observer",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _unobserve(self: *IntersectionObserver, target: *parser.Element) !void {
|
|
||||||
for (self.observed_entries.items, 0..) |*observer, index| {
|
|
||||||
if (observer.target == target) {
|
|
||||||
_ = self.observed_entries.swapRemove(index);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _takeRecords(self: *IntersectionObserver) []IntersectionObserverEntry {
|
|
||||||
return self.observed_entries.items;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const IntersectionObserverOptions = struct {
|
|
||||||
root: ?*parser.Node, // Element or Document
|
|
||||||
rootMargin: ?[]const u8,
|
|
||||||
threshold: ?Threshold,
|
|
||||||
|
|
||||||
const Threshold = union(enum) {
|
|
||||||
single: f32,
|
|
||||||
list: []const f32,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry
|
|
||||||
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
|
|
||||||
pub const IntersectionObserverEntry = struct {
|
|
||||||
page: *Page,
|
|
||||||
target: *parser.Element,
|
|
||||||
options: *IntersectionObserverOptions,
|
|
||||||
|
|
||||||
// Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect().
|
|
||||||
pub fn get_boundingClientRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
|
||||||
return Element._getBoundingClientRect(self.target, self.page);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the ratio of the intersectionRect to the boundingClientRect.
|
|
||||||
pub fn get_intersectionRatio(_: *const IntersectionObserverEntry) f32 {
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a DOMRectReadOnly representing the target's visible area.
|
|
||||||
pub fn get_intersectionRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
|
||||||
return Element._getBoundingClientRect(self.target, self.page);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A Boolean value which is true if the target element intersects with the
|
|
||||||
// intersection observer's root. If this is true, then, the
|
|
||||||
// IntersectionObserverEntry describes a transition into a state of
|
|
||||||
// intersection; if it's false, then you know the transition is from
|
|
||||||
// intersecting to not-intersecting.
|
|
||||||
pub fn get_isIntersecting(_: *const IntersectionObserverEntry) bool {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a DOMRectReadOnly for the intersection observer's root.
|
|
||||||
pub fn get_rootBounds(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
|
||||||
const root = self.options.root.?;
|
|
||||||
if (@intFromPtr(root) == @intFromPtr(self.page.window.document)) {
|
|
||||||
return self.page.renderer.boundingRect();
|
|
||||||
}
|
|
||||||
|
|
||||||
const root_type = try parser.nodeType(root);
|
|
||||||
|
|
||||||
var element: *parser.Element = undefined;
|
|
||||||
switch (root_type) {
|
|
||||||
.element => element = parser.nodeToElement(root),
|
|
||||||
.document => {
|
|
||||||
const doc = parser.nodeToDocument(root);
|
|
||||||
element = (try parser.documentGetDocumentElement(doc)).?;
|
|
||||||
},
|
|
||||||
else => return error.InvalidState,
|
|
||||||
}
|
|
||||||
|
|
||||||
return Element._getBoundingClientRect(element, self.page);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Element whose intersection with the root changed.
|
|
||||||
pub fn get_target(self: *const IntersectionObserverEntry) *parser.Element {
|
|
||||||
return self.target;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: pub fn get_time(self: *const IntersectionObserverEntry)
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.IntersectionObserver" {
|
|
||||||
try testing.htmlRunner("dom/intersection_observer.html");
|
|
||||||
}
|
|
||||||
@@ -17,13 +17,12 @@
|
|||||||
// 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 Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const NodeList = @import("nodelist.zig").NodeList;
|
const NodeList = @import("nodelist.zig").NodeList;
|
||||||
|
|
||||||
pub const Interfaces = .{
|
pub const Interfaces = .{
|
||||||
@@ -36,21 +35,21 @@ const Walker = @import("../dom/walker.zig").WalkerChildren;
|
|||||||
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
|
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
|
||||||
pub const MutationObserver = struct {
|
pub const MutationObserver = struct {
|
||||||
page: *Page,
|
page: *Page,
|
||||||
cbk: Env.Function,
|
cbk: js.Function,
|
||||||
connected: bool,
|
|
||||||
scheduled: bool,
|
scheduled: bool,
|
||||||
|
observers: std.ArrayListUnmanaged(*Observer),
|
||||||
|
|
||||||
// List of records which were observed. When the call scope ends, we need to
|
// List of records which were observed. When the call scope ends, we need to
|
||||||
// execute our callback with it.
|
// execute our callback with it.
|
||||||
observed: std.ArrayListUnmanaged(MutationRecord),
|
observed: std.ArrayListUnmanaged(MutationRecord),
|
||||||
|
|
||||||
pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver {
|
pub fn constructor(cbk: js.Function, page: *Page) !MutationObserver {
|
||||||
return .{
|
return .{
|
||||||
.cbk = cbk,
|
.cbk = cbk,
|
||||||
.page = page,
|
.page = page,
|
||||||
.observed = .{},
|
.observed = .{},
|
||||||
.connected = true,
|
|
||||||
.scheduled = false,
|
.scheduled = false,
|
||||||
|
.observers = .empty,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,15 +68,17 @@ pub const MutationObserver = struct {
|
|||||||
.event_node = .{ .id = self.cbk.id, .func = Observer.handle },
|
.event_node = .{ .id = self.cbk.id, .func = Observer.handle },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try self.observers.append(arena, observer);
|
||||||
|
|
||||||
// register node's events
|
// register node's events
|
||||||
if (options.childList or options.subtree) {
|
if (options.childList or options.subtree) {
|
||||||
_ = try parser.eventTargetAddEventListener(
|
observer.dom_node_inserted_listener = try parser.eventTargetAddEventListener(
|
||||||
parser.toEventTarget(parser.Node, node),
|
parser.toEventTarget(parser.Node, node),
|
||||||
"DOMNodeInserted",
|
"DOMNodeInserted",
|
||||||
&observer.event_node,
|
&observer.event_node,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
_ = try parser.eventTargetAddEventListener(
|
observer.dom_node_removed_listener = try parser.eventTargetAddEventListener(
|
||||||
parser.toEventTarget(parser.Node, node),
|
parser.toEventTarget(parser.Node, node),
|
||||||
"DOMNodeRemoved",
|
"DOMNodeRemoved",
|
||||||
&observer.event_node,
|
&observer.event_node,
|
||||||
@@ -85,7 +86,7 @@ pub const MutationObserver = struct {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (options.attr()) {
|
if (options.attr()) {
|
||||||
_ = try parser.eventTargetAddEventListener(
|
observer.dom_node_attribute_modified_listener = try parser.eventTargetAddEventListener(
|
||||||
parser.toEventTarget(parser.Node, node),
|
parser.toEventTarget(parser.Node, node),
|
||||||
"DOMAttrModified",
|
"DOMAttrModified",
|
||||||
&observer.event_node,
|
&observer.event_node,
|
||||||
@@ -93,7 +94,7 @@ pub const MutationObserver = struct {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (options.cdata()) {
|
if (options.cdata()) {
|
||||||
_ = try parser.eventTargetAddEventListener(
|
observer.dom_cdata_modified_listener = try parser.eventTargetAddEventListener(
|
||||||
parser.toEventTarget(parser.Node, node),
|
parser.toEventTarget(parser.Node, node),
|
||||||
"DOMCharacterDataModified",
|
"DOMCharacterDataModified",
|
||||||
&observer.event_node,
|
&observer.event_node,
|
||||||
@@ -101,7 +102,7 @@ pub const MutationObserver = struct {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (options.subtree) {
|
if (options.subtree) {
|
||||||
_ = try parser.eventTargetAddEventListener(
|
observer.dom_subtree_modified_listener = try parser.eventTargetAddEventListener(
|
||||||
parser.toEventTarget(parser.Node, node),
|
parser.toEventTarget(parser.Node, node),
|
||||||
"DOMSubtreeModified",
|
"DOMSubtreeModified",
|
||||||
&observer.event_node,
|
&observer.event_node,
|
||||||
@@ -112,10 +113,6 @@ pub const MutationObserver = struct {
|
|||||||
|
|
||||||
fn callback(ctx: *anyopaque) ?u32 {
|
fn callback(ctx: *anyopaque) ?u32 {
|
||||||
const self: *MutationObserver = @ptrCast(@alignCast(ctx));
|
const self: *MutationObserver = @ptrCast(@alignCast(ctx));
|
||||||
if (self.connected == false) {
|
|
||||||
self.scheduled = true;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
self.scheduled = false;
|
self.scheduled = false;
|
||||||
|
|
||||||
const records = self.observed.items;
|
const records = self.observed.items;
|
||||||
@@ -125,8 +122,8 @@ pub const MutationObserver = struct {
|
|||||||
|
|
||||||
defer self.observed.clearRetainingCapacity();
|
defer self.observed.clearRetainingCapacity();
|
||||||
|
|
||||||
var result: Env.Function.Result = undefined;
|
var result: js.Function.Result = undefined;
|
||||||
self.cbk.tryCall(void, .{records}, &result) catch {
|
self.cbk.tryCallWithThis(void, self, .{records}, &result) catch {
|
||||||
log.debug(.user_script, "callback error", .{
|
log.debug(.user_script, "callback error", .{
|
||||||
.err = result.exception,
|
.err = result.exception,
|
||||||
.stack = result.stack,
|
.stack = result.stack,
|
||||||
@@ -136,9 +133,55 @@ pub const MutationObserver = struct {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
|
||||||
pub fn _disconnect(self: *MutationObserver) !void {
|
pub fn _disconnect(self: *MutationObserver) !void {
|
||||||
self.connected = false;
|
for (self.observers.items) |observer| {
|
||||||
|
const event_target = parser.toEventTarget(parser.Node, observer.node);
|
||||||
|
if (observer.dom_node_inserted_listener) |listener| {
|
||||||
|
try parser.eventTargetRemoveEventListener(
|
||||||
|
event_target,
|
||||||
|
"DOMNodeInserted",
|
||||||
|
listener,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (observer.dom_node_removed_listener) |listener| {
|
||||||
|
try parser.eventTargetRemoveEventListener(
|
||||||
|
event_target,
|
||||||
|
"DOMNodeRemoved",
|
||||||
|
listener,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (observer.dom_node_attribute_modified_listener) |listener| {
|
||||||
|
try parser.eventTargetRemoveEventListener(
|
||||||
|
event_target,
|
||||||
|
"DOMAttrModified",
|
||||||
|
listener,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (observer.dom_cdata_modified_listener) |listener| {
|
||||||
|
try parser.eventTargetRemoveEventListener(
|
||||||
|
event_target,
|
||||||
|
"DOMCharacterDataModified",
|
||||||
|
listener,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (observer.dom_subtree_modified_listener) |listener| {
|
||||||
|
try parser.eventTargetRemoveEventListener(
|
||||||
|
event_target,
|
||||||
|
"DOMSubtreeModified",
|
||||||
|
listener,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.observers.clearRetainingCapacity();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
@@ -223,6 +266,12 @@ const Observer = struct {
|
|||||||
|
|
||||||
event_node: parser.EventNode,
|
event_node: parser.EventNode,
|
||||||
|
|
||||||
|
dom_node_inserted_listener: ?*parser.EventListener = null,
|
||||||
|
dom_node_removed_listener: ?*parser.EventListener = null,
|
||||||
|
dom_node_attribute_modified_listener: ?*parser.EventListener = null,
|
||||||
|
dom_cdata_modified_listener: ?*parser.EventListener = null,
|
||||||
|
dom_subtree_modified_listener: ?*parser.EventListener = null,
|
||||||
|
|
||||||
fn appliesTo(
|
fn appliesTo(
|
||||||
self: *const Observer,
|
self: *const Observer,
|
||||||
target: *parser.Node,
|
target: *parser.Node,
|
||||||
@@ -278,13 +327,13 @@ const Observer = struct {
|
|||||||
var mutation_observer = self.mutation_observer;
|
var mutation_observer = self.mutation_observer;
|
||||||
|
|
||||||
const node = blk: {
|
const node = blk: {
|
||||||
const event_target = try parser.eventTarget(event) orelse return;
|
const event_target = parser.eventTarget(event) orelse return;
|
||||||
break :blk parser.eventTargetToNode(event_target);
|
break :blk parser.eventTargetToNode(event_target);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mutation_event = parser.eventToMutationEvent(event);
|
const mutation_event = parser.eventToMutationEvent(event);
|
||||||
const event_type = blk: {
|
const event_type = blk: {
|
||||||
const t = try parser.eventType(event);
|
const t = parser.eventType(event);
|
||||||
break :blk std.meta.stringToEnum(MutationEventType, t) orelse return;
|
break :blk std.meta.stringToEnum(MutationEventType, t) orelse return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -302,12 +351,12 @@ const Observer = struct {
|
|||||||
.DOMAttrModified => {
|
.DOMAttrModified => {
|
||||||
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
|
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
|
||||||
if (self.options.attributeOldValue) {
|
if (self.options.attributeOldValue) {
|
||||||
record.old_value = parser.mutationEventPrevValue(mutation_event) catch null;
|
record.old_value = parser.mutationEventPrevValue(mutation_event);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.DOMCharacterDataModified => {
|
.DOMCharacterDataModified => {
|
||||||
if (self.options.characterDataOldValue) {
|
if (self.options.characterDataOldValue) {
|
||||||
record.old_value = parser.mutationEventPrevValue(mutation_event) catch null;
|
record.old_value = parser.mutationEventPrevValue(mutation_event);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.DOMNodeInserted => {
|
.DOMNodeInserted => {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const std = @import("std");
|
|||||||
|
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const generate = @import("../../runtime/generate.zig");
|
const generate = @import("../js/generate.zig");
|
||||||
|
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
const EventTarget = @import("event_target.zig").EventTarget;
|
const EventTarget = @import("event_target.zig").EventTarget;
|
||||||
@@ -67,7 +67,7 @@ pub const Node = struct {
|
|||||||
pub const subtype = .node;
|
pub const subtype = .node;
|
||||||
|
|
||||||
pub fn toInterface(node: *parser.Node) !Union {
|
pub fn toInterface(node: *parser.Node) !Union {
|
||||||
return switch (try parser.nodeType(node)) {
|
return switch (parser.nodeType(node)) {
|
||||||
.element => try Element.toInterfaceT(
|
.element => try Element.toInterfaceT(
|
||||||
Union,
|
Union,
|
||||||
@as(*parser.Element, @ptrCast(node)),
|
@as(*parser.Element, @ptrCast(node)),
|
||||||
@@ -124,7 +124,7 @@ pub const Node = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_firstChild(self: *parser.Node) !?Union {
|
pub fn get_firstChild(self: *parser.Node) !?Union {
|
||||||
const res = try parser.nodeFirstChild(self);
|
const res = parser.nodeFirstChild(self);
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,7 @@ pub const Node = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_lastChild(self: *parser.Node) !?Union {
|
pub fn get_lastChild(self: *parser.Node) !?Union {
|
||||||
const res = try parser.nodeLastChild(self);
|
const res = parser.nodeLastChild(self);
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -140,7 +140,7 @@ pub const Node = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_nextSibling(self: *parser.Node) !?Union {
|
pub fn get_nextSibling(self: *parser.Node) !?Union {
|
||||||
const res = try parser.nodeNextSibling(self);
|
const res = parser.nodeNextSibling(self);
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -148,7 +148,7 @@ pub const Node = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_previousSibling(self: *parser.Node) !?Union {
|
pub fn get_previousSibling(self: *parser.Node) !?Union {
|
||||||
const res = try parser.nodePreviousSibling(self);
|
const res = parser.nodePreviousSibling(self);
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@ pub const Node = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_parentNode(self: *parser.Node) !?Union {
|
pub fn get_parentNode(self: *parser.Node) !?Union {
|
||||||
const res = try parser.nodeParentNode(self);
|
const res = parser.nodeParentNode(self);
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -164,7 +164,7 @@ pub const Node = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_parentElement(self: *parser.Node) !?ElementUnion {
|
pub fn get_parentElement(self: *parser.Node) !?ElementUnion {
|
||||||
const res = try parser.nodeParentElement(self);
|
const res = parser.nodeParentElement(self);
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -176,11 +176,11 @@ pub const Node = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_nodeType(self: *parser.Node) !u8 {
|
pub fn get_nodeType(self: *parser.Node) !u8 {
|
||||||
return @intFromEnum(try parser.nodeType(self));
|
return @intFromEnum(parser.nodeType(self));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_ownerDocument(self: *parser.Node) !?*parser.DocumentHTML {
|
pub fn get_ownerDocument(self: *parser.Node) !?*parser.DocumentHTML {
|
||||||
const res = try parser.nodeOwnerDocument(self);
|
const res = parser.nodeOwnerDocument(self);
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -190,12 +190,12 @@ pub const Node = struct {
|
|||||||
pub fn get_isConnected(self: *parser.Node) !bool {
|
pub fn get_isConnected(self: *parser.Node) !bool {
|
||||||
var node = self;
|
var node = self;
|
||||||
while (true) {
|
while (true) {
|
||||||
const node_type = try parser.nodeType(node);
|
const node_type = parser.nodeType(node);
|
||||||
if (node_type == .document) {
|
if (node_type == .document) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (try parser.nodeParentNode(node)) |parent| {
|
if (parser.nodeParentNode(node)) |parent| {
|
||||||
// didn't find a document, but node has a parent, let's see
|
// didn't find a document, but node has a parent, let's see
|
||||||
// if it's connected;
|
// if it's connected;
|
||||||
node = parent;
|
node = parent;
|
||||||
@@ -222,15 +222,15 @@ pub const Node = struct {
|
|||||||
// Read/Write attributes
|
// Read/Write attributes
|
||||||
|
|
||||||
pub fn get_nodeValue(self: *parser.Node) !?[]const u8 {
|
pub fn get_nodeValue(self: *parser.Node) !?[]const u8 {
|
||||||
return try parser.nodeValue(self);
|
return parser.nodeValue(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_nodeValue(self: *parser.Node, data: []u8) !void {
|
pub fn set_nodeValue(self: *parser.Node, data: []u8) !void {
|
||||||
try parser.nodeSetValue(self, data);
|
try parser.nodeSetValue(self, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_textContent(self: *parser.Node) !?[]const u8 {
|
pub fn get_textContent(self: *parser.Node) ?[]const u8 {
|
||||||
return try parser.nodeTextContent(self);
|
return parser.nodeTextContent(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_textContent(self: *parser.Node, data: []u8) !void {
|
pub fn set_textContent(self: *parser.Node, data: []u8) !void {
|
||||||
@@ -240,8 +240,8 @@ pub const Node = struct {
|
|||||||
// Methods
|
// Methods
|
||||||
|
|
||||||
pub fn _appendChild(self: *parser.Node, child: *parser.Node) !Union {
|
pub fn _appendChild(self: *parser.Node, child: *parser.Node) !Union {
|
||||||
const self_owner = try parser.nodeOwnerDocument(self);
|
const self_owner = parser.nodeOwnerDocument(self);
|
||||||
const child_owner = try parser.nodeOwnerDocument(child);
|
const child_owner = parser.nodeOwnerDocument(child);
|
||||||
|
|
||||||
// If the node to be inserted has a different ownerDocument than the parent node,
|
// If the node to be inserted has a different ownerDocument than the parent node,
|
||||||
// modern browsers automatically adopt the node and its descendants into
|
// modern browsers automatically adopt the node and its descendants into
|
||||||
@@ -272,14 +272,14 @@ pub const Node = struct {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const docself = try parser.nodeOwnerDocument(self) orelse blk: {
|
const docself = parser.nodeOwnerDocument(self) orelse blk: {
|
||||||
if (try parser.nodeType(self) == .document) {
|
if (parser.nodeType(self) == .document) {
|
||||||
break :blk @as(*parser.Document, @ptrCast(self));
|
break :blk @as(*parser.Document, @ptrCast(self));
|
||||||
}
|
}
|
||||||
break :blk null;
|
break :blk null;
|
||||||
};
|
};
|
||||||
const docother = try parser.nodeOwnerDocument(other) orelse blk: {
|
const docother = parser.nodeOwnerDocument(other) orelse blk: {
|
||||||
if (try parser.nodeType(other) == .document) {
|
if (parser.nodeType(other) == .document) {
|
||||||
break :blk @as(*parser.Document, @ptrCast(other));
|
break :blk @as(*parser.Document, @ptrCast(other));
|
||||||
}
|
}
|
||||||
break :blk null;
|
break :blk null;
|
||||||
@@ -299,8 +299,8 @@ pub const Node = struct {
|
|||||||
@intFromEnum(parser.DocumentPosition.contained_by);
|
@intFromEnum(parser.DocumentPosition.contained_by);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootself = try parser.nodeGetRootNode(self);
|
const rootself = parser.nodeGetRootNode(self);
|
||||||
const rootother = try parser.nodeGetRootNode(other);
|
const rootother = parser.nodeGetRootNode(other);
|
||||||
if (rootself != rootother) {
|
if (rootself != rootother) {
|
||||||
return @intFromEnum(parser.DocumentPosition.disconnected) +
|
return @intFromEnum(parser.DocumentPosition.disconnected) +
|
||||||
@intFromEnum(parser.DocumentPosition.implementation_specific) +
|
@intFromEnum(parser.DocumentPosition.implementation_specific) +
|
||||||
@@ -347,8 +347,8 @@ pub const Node = struct {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _contains(self: *parser.Node, other: *parser.Node) !bool {
|
pub fn _contains(self: *parser.Node, other: *parser.Node) bool {
|
||||||
return try parser.nodeContains(self, other);
|
return parser.nodeContains(self, other);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns itself or ancestor object inheriting from Node.
|
// Returns itself or ancestor object inheriting from Node.
|
||||||
@@ -360,32 +360,44 @@ pub const Node = struct {
|
|||||||
node: Union,
|
node: Union,
|
||||||
};
|
};
|
||||||
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult {
|
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult {
|
||||||
if (options) |options_| if (options_.composed) {
|
const composed = if (options) |opts| opts.composed else false;
|
||||||
log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const root = try parser.nodeGetRootNode(self);
|
var current_root = parser.nodeGetRootNode(self);
|
||||||
if (page.getNodeState(root)) |state| {
|
|
||||||
if (state.shadow_root) |sr| {
|
while (true) {
|
||||||
return .{ .shadow_root = sr };
|
const node_type = parser.nodeType(current_root);
|
||||||
|
|
||||||
|
if (node_type == .document_fragment) {
|
||||||
|
if (parser.documentFragmentGetHost(@ptrCast(current_root))) |host| {
|
||||||
|
if (page.getNodeState(host)) |state| {
|
||||||
|
if (state.shadow_root) |sr| {
|
||||||
|
if (!composed) {
|
||||||
|
return .{ .shadow_root = sr };
|
||||||
|
}
|
||||||
|
current_root = parser.nodeGetRootNode(@ptrCast(sr.host));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return .{ .node = try Node.toInterface(root) };
|
return .{ .node = try Node.toInterface(current_root) };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _hasChildNodes(self: *parser.Node) !bool {
|
pub fn _hasChildNodes(self: *parser.Node) bool {
|
||||||
return try parser.nodeHasChildNodes(self);
|
return parser.nodeHasChildNodes(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList {
|
pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList {
|
||||||
const allocator = page.arena;
|
const allocator = page.arena;
|
||||||
var list: NodeList = .{};
|
var list: NodeList = .{};
|
||||||
|
|
||||||
var n = try parser.nodeFirstChild(self) orelse return list;
|
var n = parser.nodeFirstChild(self) orelse return list;
|
||||||
while (true) {
|
while (true) {
|
||||||
try list.append(allocator, n);
|
try list.append(allocator, n);
|
||||||
n = try parser.nodeNextSibling(n) orelse return list;
|
n = parser.nodeNextSibling(n) orelse return list;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,8 +406,8 @@ pub const Node = struct {
|
|||||||
return _appendChild(self, new_node);
|
return _appendChild(self, new_node);
|
||||||
}
|
}
|
||||||
|
|
||||||
const self_owner = try parser.nodeOwnerDocument(self);
|
const self_owner = parser.nodeOwnerDocument(self);
|
||||||
const new_node_owner = try parser.nodeOwnerDocument(new_node);
|
const new_node_owner = parser.nodeOwnerDocument(new_node);
|
||||||
|
|
||||||
// If the node to be inserted has a different ownerDocument than the parent node,
|
// If the node to be inserted has a different ownerDocument than the parent node,
|
||||||
// modern browsers automatically adopt the node and its descendants into
|
// modern browsers automatically adopt the node and its descendants into
|
||||||
@@ -415,7 +427,7 @@ pub const Node = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn _isDefaultNamespace(self: *parser.Node, namespace: ?[]const u8) !bool {
|
pub fn _isDefaultNamespace(self: *parser.Node, namespace: ?[]const u8) !bool {
|
||||||
return try parser.nodeIsDefaultNamespace(self, namespace);
|
return parser.nodeIsDefaultNamespace(self, namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _isEqualNode(self: *parser.Node, other: *parser.Node) !bool {
|
pub fn _isEqualNode(self: *parser.Node, other: *parser.Node) !bool {
|
||||||
@@ -423,10 +435,10 @@ pub const Node = struct {
|
|||||||
return try parser.nodeIsEqualNode(self, other);
|
return try parser.nodeIsEqualNode(self, other);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _isSameNode(self: *parser.Node, other: *parser.Node) !bool {
|
pub fn _isSameNode(self: *parser.Node, other: *parser.Node) bool {
|
||||||
// TODO: other is not an optional parameter, but can be null.
|
// TODO: other is not an optional parameter, but can be null.
|
||||||
// NOTE: there is no need to use isSameNode(); instead use the === strict equality operator
|
// NOTE: there is no need to use isSameNode(); instead use the === strict equality operator
|
||||||
return try parser.nodeIsSameNode(self, other);
|
return parser.nodeIsSameNode(self, other);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _lookupPrefix(self: *parser.Node, namespace: ?[]const u8) !?[]const u8 {
|
pub fn _lookupPrefix(self: *parser.Node, namespace: ?[]const u8) !?[]const u8 {
|
||||||
@@ -461,7 +473,7 @@ pub const Node = struct {
|
|||||||
|
|
||||||
// Check if the hierarchy node tree constraints are respected.
|
// Check if the hierarchy node tree constraints are respected.
|
||||||
// For now, it checks only if new nodes are not self.
|
// For now, it checks only if new nodes are not self.
|
||||||
// TODO implements the others contraints.
|
// TODO implements the others constraints.
|
||||||
// see https://dom.spec.whatwg.org/#concept-node-tree
|
// see https://dom.spec.whatwg.org/#concept-node-tree
|
||||||
pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool {
|
pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool {
|
||||||
for (nodes) |n| {
|
for (nodes) |n| {
|
||||||
@@ -482,9 +494,9 @@ pub const Node = struct {
|
|||||||
return parser.DOMError.HierarchyRequest;
|
return parser.DOMError.HierarchyRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
const doc = (try parser.nodeOwnerDocument(self)) orelse return;
|
const doc = (parser.nodeOwnerDocument(self)) orelse return;
|
||||||
|
|
||||||
if (try parser.nodeFirstChild(self)) |first| {
|
if (parser.nodeFirstChild(self)) |first| {
|
||||||
for (nodes) |node| {
|
for (nodes) |node| {
|
||||||
_ = try parser.nodeInsertBefore(self, try node.toNode(doc), first);
|
_ = try parser.nodeInsertBefore(self, try node.toNode(doc), first);
|
||||||
}
|
}
|
||||||
@@ -506,7 +518,7 @@ pub const Node = struct {
|
|||||||
return parser.DOMError.HierarchyRequest;
|
return parser.DOMError.HierarchyRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
const doc = (try parser.nodeOwnerDocument(self)) orelse return;
|
const doc = (parser.nodeOwnerDocument(self)) orelse return;
|
||||||
for (nodes) |node| {
|
for (nodes) |node| {
|
||||||
_ = try parser.nodeAppendChild(self, try node.toNode(doc));
|
_ = try parser.nodeAppendChild(self, try node.toNode(doc));
|
||||||
}
|
}
|
||||||
@@ -525,7 +537,7 @@ pub const Node = struct {
|
|||||||
// remove existing children
|
// remove existing children
|
||||||
try removeChildren(self);
|
try removeChildren(self);
|
||||||
|
|
||||||
const doc = (try parser.nodeOwnerDocument(self)) orelse return;
|
const doc = (parser.nodeOwnerDocument(self)) orelse return;
|
||||||
// add new children
|
// add new children
|
||||||
for (nodes) |node| {
|
for (nodes) |node| {
|
||||||
_ = try parser.nodeAppendChild(self, try node.toNode(doc));
|
_ = try parser.nodeAppendChild(self, try node.toNode(doc));
|
||||||
@@ -533,30 +545,30 @@ pub const Node = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn removeChildren(self: *parser.Node) !void {
|
pub fn removeChildren(self: *parser.Node) !void {
|
||||||
if (!try parser.nodeHasChildNodes(self)) return;
|
if (!parser.nodeHasChildNodes(self)) return;
|
||||||
|
|
||||||
const children = try parser.nodeGetChildNodes(self);
|
const children = try parser.nodeGetChildNodes(self);
|
||||||
const ln = try parser.nodeListLength(children);
|
const ln = parser.nodeListLength(children);
|
||||||
var i: u32 = 0;
|
var i: u32 = 0;
|
||||||
while (i < ln) {
|
while (i < ln) {
|
||||||
defer i += 1;
|
defer i += 1;
|
||||||
// we always retrieve the 0 index child on purpose: libdom nodelist
|
// we always retrieve the 0 index child on purpose: libdom nodelist
|
||||||
// are dynamic. So the next child to remove is always as pos 0.
|
// are dynamic. So the next child to remove is always as pos 0.
|
||||||
const child = try parser.nodeListItem(children, 0) orelse continue;
|
const child = parser.nodeListItem(children, 0) orelse continue;
|
||||||
_ = try parser.nodeRemoveChild(self, child);
|
_ = try parser.nodeRemoveChild(self, child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn before(self: *parser.Node, nodes: []const NodeOrText) !void {
|
pub fn before(self: *parser.Node, nodes: []const NodeOrText) !void {
|
||||||
const parent = try parser.nodeParentNode(self) orelse return;
|
const parent = parser.nodeParentNode(self) orelse return;
|
||||||
const doc = (try parser.nodeOwnerDocument(parent)) orelse return;
|
const doc = (parser.nodeOwnerDocument(parent)) orelse return;
|
||||||
|
|
||||||
var sibling: ?*parser.Node = self;
|
var sibling: ?*parser.Node = self;
|
||||||
// have to find the first sibling that isn't in nodes
|
// have to find the first sibling that isn't in nodes
|
||||||
CHECK: while (sibling) |s| {
|
CHECK: while (sibling) |s| {
|
||||||
for (nodes) |n| {
|
for (nodes) |n| {
|
||||||
if (n.is(s)) {
|
if (n.is(s)) {
|
||||||
sibling = try parser.nodePreviousSibling(s);
|
sibling = parser.nodePreviousSibling(s);
|
||||||
continue :CHECK;
|
continue :CHECK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -564,7 +576,7 @@ pub const Node = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sibling == null) {
|
if (sibling == null) {
|
||||||
sibling = try parser.nodeFirstChild(parent);
|
sibling = parser.nodeFirstChild(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sibling) |ref_node| {
|
if (sibling) |ref_node| {
|
||||||
@@ -578,15 +590,15 @@ pub const Node = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn after(self: *parser.Node, nodes: []const NodeOrText) !void {
|
pub fn after(self: *parser.Node, nodes: []const NodeOrText) !void {
|
||||||
const parent = try parser.nodeParentNode(self) orelse return;
|
const parent = parser.nodeParentNode(self) orelse return;
|
||||||
const doc = (try parser.nodeOwnerDocument(parent)) orelse return;
|
const doc = (parser.nodeOwnerDocument(parent)) orelse return;
|
||||||
|
|
||||||
// have to find the first sibling that isn't in nodes
|
// have to find the first sibling that isn't in nodes
|
||||||
var sibling = try parser.nodeNextSibling(self);
|
var sibling = parser.nodeNextSibling(self);
|
||||||
CHECK: while (sibling) |s| {
|
CHECK: while (sibling) |s| {
|
||||||
for (nodes) |n| {
|
for (nodes) |n| {
|
||||||
if (n.is(s)) {
|
if (n.is(s)) {
|
||||||
sibling = try parser.nodeNextSibling(s);
|
sibling = parser.nodeNextSibling(s);
|
||||||
continue :CHECK;
|
continue :CHECK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -631,262 +643,7 @@ pub const Node = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.DOM.node" {
|
test "Browser: DOM.Node" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
try testing.htmlRunner("dom/node.html");
|
||||||
defer runner.deinit();
|
try testing.htmlRunner("dom/node_owner.html");
|
||||||
|
|
||||||
{
|
|
||||||
var err_out: ?[]const u8 = null;
|
|
||||||
try runner.exec(
|
|
||||||
\\ function trimAndReplace(str) {
|
|
||||||
\\ str = str.replace(/(\r\n|\n|\r)/gm,'');
|
|
||||||
\\ str = str.replace(/\s+/g, ' ');
|
|
||||||
\\ str = str.trim();
|
|
||||||
\\ return str;
|
|
||||||
\\ }
|
|
||||||
, "trimAndReplace", &err_out);
|
|
||||||
}
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.body.compareDocumentPosition(document.firstChild); ", "10" },
|
|
||||||
.{ "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"content\"));", "10" },
|
|
||||||
.{ "document.getElementById(\"content\").compareDocumentPosition(document.getElementById(\"para-empty\"));", "20" },
|
|
||||||
.{ "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"link\"));", "0" },
|
|
||||||
.{ "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"link\"));", "2" },
|
|
||||||
.{ "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"para-empty\"));", "4" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.getElementById('content').getRootNode().__proto__.constructor.name", "HTMLDocument" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
// for next test cases
|
|
||||||
.{ "let content = document.getElementById('content')", "undefined" },
|
|
||||||
.{ "let link = document.getElementById('link')", "undefined" },
|
|
||||||
.{ "let first_child = content.firstChild.nextSibling", "undefined" }, // nextSibling because of line return \n
|
|
||||||
|
|
||||||
.{ "let body_first_child = document.body.firstChild", "undefined" },
|
|
||||||
.{ "body_first_child.localName", "div" },
|
|
||||||
.{ "body_first_child.__proto__.constructor.name", "HTMLDivElement" },
|
|
||||||
.{ "document.getElementById('para-empty').firstChild.firstChild", "null" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let last_child = content.lastChild.previousSibling", "undefined" }, // previousSibling because of line return \n
|
|
||||||
.{ "last_child.__proto__.constructor.name", "Comment" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let next_sibling = link.nextSibling.nextSibling", "undefined" },
|
|
||||||
.{ "next_sibling.localName", "p" },
|
|
||||||
.{ "next_sibling.__proto__.constructor.name", "HTMLParagraphElement" },
|
|
||||||
.{ "content.nextSibling.nextSibling", "null" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let prev_sibling = document.getElementById('para-empty').previousSibling.previousSibling", "undefined" },
|
|
||||||
.{ "prev_sibling.localName", "a" },
|
|
||||||
.{ "prev_sibling.__proto__.constructor.name", "HTMLAnchorElement" },
|
|
||||||
.{ "content.previousSibling", "null" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let parent = document.getElementById('para').parentElement", "undefined" },
|
|
||||||
.{ "parent.localName", "div" },
|
|
||||||
.{ "parent.__proto__.constructor.name", "HTMLDivElement" },
|
|
||||||
.{ "let h = content.parentElement.parentElement", "undefined" },
|
|
||||||
.{ "h.parentElement", "null" },
|
|
||||||
.{ "h.parentNode.__proto__.constructor.name", "HTMLDocument" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "first_child.nodeName === 'A'", "true" },
|
|
||||||
.{ "link.firstChild.nodeName === '#text'", "true" },
|
|
||||||
.{ "last_child.nodeName === '#comment'", "true" },
|
|
||||||
.{ "document.nodeName === '#document'", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "first_child.nodeType === 1", "true" },
|
|
||||||
.{ "link.firstChild.nodeType === 3", "true" },
|
|
||||||
.{ "last_child.nodeType === 8", "true" },
|
|
||||||
.{ "document.nodeType === 9", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let owner = content.ownerDocument", "undefined" },
|
|
||||||
.{ "owner.__proto__.constructor.name", "HTMLDocument" },
|
|
||||||
.{ "document.ownerDocument", "null" },
|
|
||||||
.{ "let owner2 = document.createElement('div').ownerDocument", "undefined" },
|
|
||||||
.{ "owner2.__proto__.constructor.name", "HTMLDocument" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "content.isConnected", "true" },
|
|
||||||
.{ "document.isConnected", "true" },
|
|
||||||
.{ "const connDiv = document.createElement('div')", null },
|
|
||||||
.{ "connDiv.isConnected", "false" },
|
|
||||||
.{ "const connParentDiv = document.createElement('div')", null },
|
|
||||||
.{ "connParentDiv.appendChild(connDiv)", null },
|
|
||||||
.{ "connDiv.isConnected", "false" },
|
|
||||||
.{ "content.appendChild(connParentDiv)", null },
|
|
||||||
.{ "connDiv.isConnected", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "last_child.nodeValue === 'comment'", "true" },
|
|
||||||
.{ "link.nodeValue === null", "true" },
|
|
||||||
.{ "let text = link.firstChild", "undefined" },
|
|
||||||
.{ "text.nodeValue === 'OK'", "true" },
|
|
||||||
.{ "text.nodeValue = 'OK modified'", "OK modified" },
|
|
||||||
.{ "text.nodeValue === 'OK modified'", "true" },
|
|
||||||
.{ "link.nodeValue = 'nothing'", "nothing" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "text.textContent === 'OK modified'", "true" },
|
|
||||||
.{ "trimAndReplace(content.textContent) === 'OK modified And'", "true" },
|
|
||||||
.{ "text.textContent = 'OK'", "OK" },
|
|
||||||
.{ "text.textContent", "OK" },
|
|
||||||
.{ "trimAndReplace(document.getElementById('para-empty').textContent)", "" },
|
|
||||||
.{ "document.getElementById('para-empty').textContent = 'OK'", "OK" },
|
|
||||||
.{ "document.getElementById('para-empty').firstChild.nodeName === '#text'", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let append = document.createElement('h1')", "undefined" },
|
|
||||||
.{ "content.appendChild(append).toString()", "[object HTMLHeadingElement]" },
|
|
||||||
.{ "content.lastChild.__proto__.constructor.name", "HTMLHeadingElement" },
|
|
||||||
.{ "content.appendChild(link).toString()", "[object HTMLAnchorElement]" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let clone = link.cloneNode()", "undefined" },
|
|
||||||
.{ "clone.toString()", "[object HTMLAnchorElement]" },
|
|
||||||
.{ "clone.parentNode === null", "true" },
|
|
||||||
.{ "clone.firstChild === null", "true" },
|
|
||||||
.{ "let clone_deep = link.cloneNode(true)", "undefined" },
|
|
||||||
.{ "clone_deep.firstChild.nodeName === '#text'", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "link.contains(text)", "true" },
|
|
||||||
.{ "text.contains(link)", "false" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "link.hasChildNodes()", "true" },
|
|
||||||
.{ "text.hasChildNodes()", "false" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "link.childNodes.length", "1" },
|
|
||||||
.{ "text.childNodes.length", "0" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let insertBefore = document.createElement('a')", "undefined" },
|
|
||||||
.{ "link.insertBefore(insertBefore, text) !== undefined", "true" },
|
|
||||||
.{ "link.firstChild.localName === 'a'", "true" },
|
|
||||||
|
|
||||||
.{ "let insertBefore2 = document.createElement('b')", null },
|
|
||||||
.{ "link.insertBefore(insertBefore2, null).localName", "b" },
|
|
||||||
.{ "link.childNodes[link.childNodes.length - 1].localName", "b" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
// TODO: does not seems to work
|
|
||||||
// .{ "link.isDefaultNamespace('')", "true" },
|
|
||||||
.{ "link.isDefaultNamespace('false')", "false" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let equal1 = document.createElement('a')", "undefined" },
|
|
||||||
.{ "let equal2 = document.createElement('a')", "undefined" },
|
|
||||||
.{ "equal1.textContent = 'is equal'", "is equal" },
|
|
||||||
.{ "equal2.textContent = 'is equal'", "is equal" },
|
|
||||||
// TODO: does not seems to work
|
|
||||||
// .{ "equal1.isEqualNode(equal2)", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.body.isSameNode(document.body)", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
// TODO: no test
|
|
||||||
.{ "link.normalize()", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "link.baseURI", "https://lightpanda.io/opensource-browser/" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "content.removeChild(append) !== undefined", "true" },
|
|
||||||
.{ "last_child.__proto__.constructor.name !== 'HTMLHeadingElement'", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let replace = document.createElement('div')", "undefined" },
|
|
||||||
.{ "link.replaceChild(replace, insertBefore) !== undefined", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "Node.ELEMENT_NODE", "1" },
|
|
||||||
.{ "Node.ATTRIBUTE_NODE", "2" },
|
|
||||||
.{ "Node.TEXT_NODE", "3" },
|
|
||||||
.{ "Node.CDATA_SECTION_NODE", "4" },
|
|
||||||
.{ "Node.PROCESSING_INSTRUCTION_NODE", "7" },
|
|
||||||
.{ "Node.COMMENT_NODE", "8" },
|
|
||||||
.{ "Node.DOCUMENT_NODE", "9" },
|
|
||||||
.{ "Node.DOCUMENT_TYPE_NODE", "10" },
|
|
||||||
.{ "Node.DOCUMENT_FRAGMENT_NODE", "11" },
|
|
||||||
.{ "Node.ENTITY_REFERENCE_NODE", "5" },
|
|
||||||
.{ "Node.ENTITY_NODE", "6" },
|
|
||||||
.{ "Node.NOTATION_NODE", "12" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser.DOM.node.owner" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
|
|
||||||
\\ <div id="target-container">
|
|
||||||
\\ <p id="reference-node">
|
|
||||||
\\ I am the original reference node.
|
|
||||||
\\ </p>
|
|
||||||
\\ </div>"
|
|
||||||
});
|
|
||||||
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{
|
|
||||||
\\ const parser = new DOMParser();
|
|
||||||
\\ const newDoc = parser.parseFromString('<div id="new-node"><p>Hey</p><span>Marked</span></div>', 'text/html');
|
|
||||||
\\ const newNode = newDoc.getElementById('new-node');
|
|
||||||
\\ const parent = document.getElementById('target-container');
|
|
||||||
\\ const referenceNode = document.getElementById('reference-node');
|
|
||||||
\\ parent.insertBefore(newNode, referenceNode);
|
|
||||||
\\ const k = document.getElementById('new-node');
|
|
||||||
\\ const ptag = k.querySelector('p');
|
|
||||||
\\ const spanTag = k.querySelector('span');
|
|
||||||
\\ const anotherDoc = parser.parseFromString('<div id="another-new-node"></div>', 'text/html');
|
|
||||||
\\ const anotherNewNode = anotherDoc.getElementById('another-new-node');
|
|
||||||
\\
|
|
||||||
\\ parent.appendChild(anotherNewNode)
|
|
||||||
,
|
|
||||||
"[object HTMLDivElement]",
|
|
||||||
},
|
|
||||||
|
|
||||||
.{ "parent.ownerDocument === newNode.ownerDocument", "true" },
|
|
||||||
.{ "parent.ownerDocument === anotherNewNode.ownerDocument", "true" },
|
|
||||||
.{ "newNode.firstChild.nodeName", "P" },
|
|
||||||
.{ "ptag.ownerDocument === parent.ownerDocument", "true" },
|
|
||||||
.{ "spanTag.ownerDocument === parent.ownerDocument", "true" },
|
|
||||||
.{ "parent.contains(newNode)", "true" },
|
|
||||||
.{ "parent.contains(anotherNewNode)", "true" },
|
|
||||||
.{ "anotherDoc.contains(anotherNewNode)", "false" },
|
|
||||||
.{ "newDoc.contains(newNode)", "false" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
// 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 js = @import("../js/js.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const Node = @import("node.zig").Node;
|
const Node = @import("node.zig").Node;
|
||||||
|
|
||||||
pub const NodeFilter = struct {
|
pub const NodeFilter = struct {
|
||||||
@@ -43,10 +43,13 @@ pub const NodeFilter = struct {
|
|||||||
|
|
||||||
const VerifyResult = enum { accept, skip, reject };
|
const VerifyResult = enum { accept, skip, reject };
|
||||||
|
|
||||||
pub fn verify(what_to_show: u32, filter: ?Env.Function, node: *parser.Node) !VerifyResult {
|
pub fn verify(what_to_show: u32, filter: ?js.Function, node: *parser.Node) !VerifyResult {
|
||||||
const node_type = try parser.nodeType(node);
|
const node_type = parser.nodeType(node);
|
||||||
|
|
||||||
// Verify that we can show this node type.
|
// Verify that we can show this node type.
|
||||||
|
// Per the DOM spec, what_to_show filters which nodes to return, but should
|
||||||
|
// still traverse children. So we return .skip (not .reject) when the node
|
||||||
|
// type doesn't match.
|
||||||
if (!switch (node_type) {
|
if (!switch (node_type) {
|
||||||
.attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0,
|
.attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0,
|
||||||
.cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0,
|
.cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0,
|
||||||
@@ -60,7 +63,7 @@ pub fn verify(what_to_show: u32, filter: ?Env.Function, node: *parser.Node) !Ver
|
|||||||
.notation => what_to_show & NodeFilter._SHOW_NOTATION != 0,
|
.notation => what_to_show & NodeFilter._SHOW_NOTATION != 0,
|
||||||
.processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0,
|
.processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0,
|
||||||
.text => what_to_show & NodeFilter._SHOW_TEXT != 0,
|
.text => what_to_show & NodeFilter._SHOW_TEXT != 0,
|
||||||
}) return .reject;
|
}) return .skip;
|
||||||
|
|
||||||
// Verify that we aren't filtering it out.
|
// Verify that we aren't filtering it out.
|
||||||
if (filter) |f| {
|
if (filter) |f| {
|
||||||
@@ -75,15 +78,6 @@ pub fn verify(what_to_show: u32, filter: ?Env.Function, node: *parser.Node) !Ver
|
|||||||
}
|
}
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.DOM.NodeFilter" {
|
test "Browser: DOM.NodeFilter" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
try testing.htmlRunner("dom/node_filter.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "NodeFilter.FILTER_ACCEPT", "1" },
|
|
||||||
.{ "NodeFilter.FILTER_REJECT", "2" },
|
|
||||||
.{ "NodeFilter.FILTER_SKIP", "3" },
|
|
||||||
.{ "NodeFilter.SHOW_ALL", "4294967295" },
|
|
||||||
.{ "NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT", "129" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const NodeFilter = @import("node_filter.zig");
|
const NodeFilter = @import("node_filter.zig");
|
||||||
const Node = @import("node.zig").Node;
|
const Node = @import("node.zig").Node;
|
||||||
const NodeUnion = @import("node.zig").Union;
|
const NodeUnion = @import("node.zig").Union;
|
||||||
@@ -37,7 +37,7 @@ pub const NodeIterator = struct {
|
|||||||
reference_node: *parser.Node,
|
reference_node: *parser.Node,
|
||||||
what_to_show: u32,
|
what_to_show: u32,
|
||||||
filter: ?NodeIteratorOpts,
|
filter: ?NodeIteratorOpts,
|
||||||
filter_func: ?Env.Function,
|
filter_func: ?js.Function,
|
||||||
pointer_before_current: bool = true,
|
pointer_before_current: bool = true,
|
||||||
// used to track / block recursive filters
|
// used to track / block recursive filters
|
||||||
is_in_callback: bool = false,
|
is_in_callback: bool = false,
|
||||||
@@ -45,15 +45,15 @@ pub const NodeIterator = struct {
|
|||||||
// One of the few cases where null and undefined resolve to different default.
|
// One of the few cases where null and undefined resolve to different default.
|
||||||
// We need the raw JsObject so that we can probe the tri state:
|
// We need the raw JsObject so that we can probe the tri state:
|
||||||
// null, undefined or i32.
|
// null, undefined or i32.
|
||||||
pub const WhatToShow = Env.JsObject;
|
pub const WhatToShow = js.Object;
|
||||||
|
|
||||||
pub const NodeIteratorOpts = union(enum) {
|
pub const NodeIteratorOpts = union(enum) {
|
||||||
function: Env.Function,
|
function: js.Function,
|
||||||
object: struct { acceptNode: Env.Function },
|
object: struct { acceptNode: js.Function },
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?NodeIteratorOpts) !NodeIterator {
|
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?NodeIteratorOpts) !NodeIterator {
|
||||||
var filter_func: ?Env.Function = null;
|
var filter_func: ?js.Function = null;
|
||||||
if (filter) |f| {
|
if (filter) |f| {
|
||||||
filter_func = switch (f) {
|
filter_func = switch (f) {
|
||||||
.function => |func| func,
|
.function => |func| func,
|
||||||
@@ -74,10 +74,10 @@ pub const NodeIterator = struct {
|
|||||||
|
|
||||||
return .{
|
return .{
|
||||||
.root = node,
|
.root = node,
|
||||||
.reference_node = node,
|
|
||||||
.what_to_show = what_to_show,
|
|
||||||
.filter = filter,
|
.filter = filter,
|
||||||
|
.reference_node = node,
|
||||||
.filter_func = filter_func,
|
.filter_func = filter_func,
|
||||||
|
.what_to_show = what_to_show,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,17 +115,30 @@ pub const NodeIterator = struct {
|
|||||||
|
|
||||||
if (try self.firstChild(self.reference_node)) |child| {
|
if (try self.firstChild(self.reference_node)) |child| {
|
||||||
self.reference_node = child;
|
self.reference_node = child;
|
||||||
|
self.pointer_before_current = false;
|
||||||
return try Node.toInterface(child);
|
return try Node.toInterface(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
var current = self.reference_node;
|
var current = self.reference_node;
|
||||||
while (current != self.root) {
|
while (current != self.root) {
|
||||||
if (try self.nextSibling(current)) |sibling| {
|
// Try to get next sibling (including .skip/.reject nodes we need to descend into)
|
||||||
self.reference_node = sibling;
|
if (try self.nextSiblingOrSkipReject(current)) |result| {
|
||||||
return try Node.toInterface(sibling);
|
if (result.should_descend) {
|
||||||
|
// This is a .skip/.reject node - try to find acceptable children within it
|
||||||
|
if (try self.firstChild(result.node)) |child| {
|
||||||
|
self.reference_node = child;
|
||||||
|
return try Node.toInterface(child);
|
||||||
|
}
|
||||||
|
// No acceptable children, continue looking at this node's siblings
|
||||||
|
current = result.node;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// This is an .accept node - return it
|
||||||
|
self.reference_node = result.node;
|
||||||
|
return try Node.toInterface(result.node);
|
||||||
}
|
}
|
||||||
|
|
||||||
current = (try parser.nodeParentNode(current)) orelse break;
|
current = (parser.nodeParentNode(current)) orelse break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -147,7 +160,7 @@ pub const NodeIterator = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var current = self.reference_node;
|
var current = self.reference_node;
|
||||||
while (try parser.nodePreviousSibling(current)) |previous| {
|
while (parser.nodePreviousSibling(current)) |previous| {
|
||||||
current = previous;
|
current = previous;
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||||
@@ -189,11 +202,11 @@ pub const NodeIterator = struct {
|
|||||||
|
|
||||||
fn firstChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
fn firstChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
||||||
const children = try parser.nodeGetChildNodes(node);
|
const children = try parser.nodeGetChildNodes(node);
|
||||||
const child_count = try parser.nodeListLength(children);
|
const child_count = parser.nodeListLength(children);
|
||||||
|
|
||||||
for (0..child_count) |i| {
|
for (0..child_count) |i| {
|
||||||
const index: u32 = @intCast(i);
|
const index: u32 = @intCast(i);
|
||||||
const child = (try parser.nodeListItem(children, index)) orelse return null;
|
const child = (parser.nodeListItem(children, index)) orelse return null;
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
||||||
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
|
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
|
||||||
@@ -206,12 +219,12 @@ pub const NodeIterator = struct {
|
|||||||
|
|
||||||
fn lastChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
fn lastChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
||||||
const children = try parser.nodeGetChildNodes(node);
|
const children = try parser.nodeGetChildNodes(node);
|
||||||
const child_count = try parser.nodeListLength(children);
|
const child_count = parser.nodeListLength(children);
|
||||||
|
|
||||||
var index: u32 = child_count;
|
var index: u32 = child_count;
|
||||||
while (index > 0) {
|
while (index > 0) {
|
||||||
index -= 1;
|
index -= 1;
|
||||||
const child = (try parser.nodeListItem(children, index)) orelse return null;
|
const child = (parser.nodeListItem(children, index)) orelse return null;
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
||||||
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
|
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
|
||||||
@@ -229,7 +242,7 @@ pub const NodeIterator = struct {
|
|||||||
var current = node;
|
var current = node;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (current == self.root) return null;
|
if (current == self.root) return null;
|
||||||
current = (try parser.nodeParentNode(current)) orelse return null;
|
current = (parser.nodeParentNode(current)) orelse return null;
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||||
.accept => return current,
|
.accept => return current,
|
||||||
@@ -243,7 +256,7 @@ pub const NodeIterator = struct {
|
|||||||
var current = node;
|
var current = node;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
current = (try parser.nodeNextSibling(current)) orelse return null;
|
current = (parser.nodeNextSibling(current)) orelse return null;
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||||
.accept => return current,
|
.accept => return current,
|
||||||
@@ -254,6 +267,22 @@ pub const NodeIterator = struct {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the next sibling that is either acceptable or should be descended into (skip/reject)
|
||||||
|
fn nextSiblingOrSkipReject(self: *const NodeIterator, node: *parser.Node) !?struct { node: *parser.Node, should_descend: bool } {
|
||||||
|
var current = node;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
current = (parser.nodeNextSibling(current)) orelse return null;
|
||||||
|
|
||||||
|
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||||
|
.accept => return .{ .node = current, .should_descend = false },
|
||||||
|
.skip, .reject => return .{ .node = current, .should_descend = true },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
fn callbackStart(self: *NodeIterator) !void {
|
fn callbackStart(self: *NodeIterator) !void {
|
||||||
if (self.is_in_callback) {
|
if (self.is_in_callback) {
|
||||||
// this is the correct DOMExeption
|
// this is the correct DOMExeption
|
||||||
@@ -268,71 +297,6 @@ pub const NodeIterator = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.DOM.NodeFilter" {
|
test "Browser: DOM.NodeIterator" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
try testing.htmlRunner("dom/node_iterator.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{
|
|
||||||
\\ const nodeIterator = document.createNodeIterator(
|
|
||||||
\\ document.body,
|
|
||||||
\\ NodeFilter.SHOW_ELEMENT,
|
|
||||||
\\ {
|
|
||||||
\\ acceptNode(node) {
|
|
||||||
\\ return NodeFilter.FILTER_ACCEPT;
|
|
||||||
\\ },
|
|
||||||
\\ },
|
|
||||||
\\ );
|
|
||||||
\\ nodeIterator.nextNode().nodeName;
|
|
||||||
,
|
|
||||||
"BODY",
|
|
||||||
},
|
|
||||||
.{ "nodeIterator.nextNode().nodeName", "DIV" },
|
|
||||||
.{ "nodeIterator.nextNode().nodeName", "A" },
|
|
||||||
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
|
|
||||||
.{ "nodeIterator.nextNode().nodeName", "A" }, // pointer_before_current flips
|
|
||||||
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
|
|
||||||
.{ "nodeIterator.previousNode().nodeName", "DIV" },
|
|
||||||
.{ "nodeIterator.previousNode().nodeName", "BODY" },
|
|
||||||
.{ "nodeIterator.previousNode()", "null" }, // Not HEAD since body is root
|
|
||||||
.{ "nodeIterator.previousNode()", "null" }, // Keeps returning null
|
|
||||||
.{ "nodeIterator.nextNode().nodeName", "BODY" },
|
|
||||||
|
|
||||||
.{ "nodeIterator.nextNode().nodeName", null },
|
|
||||||
.{ "nodeIterator.nextNode().nodeName", null },
|
|
||||||
.{ "nodeIterator.nextNode().nodeName", null },
|
|
||||||
.{ "nodeIterator.nextNode().nodeName", "SPAN" },
|
|
||||||
.{ "nodeIterator.nextNode().nodeName", "P" },
|
|
||||||
.{ "nodeIterator.nextNode()", "null" }, // Just the last one
|
|
||||||
.{ "nodeIterator.nextNode()", "null" }, // Keeps returning null
|
|
||||||
.{ "nodeIterator.previousNode().nodeName", "P" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{
|
|
||||||
\\ const notationIterator = document.createNodeIterator(
|
|
||||||
\\ document.body,
|
|
||||||
\\ NodeFilter.SHOW_NOTATION,
|
|
||||||
\\ );
|
|
||||||
\\ notationIterator.nextNode();
|
|
||||||
,
|
|
||||||
"null",
|
|
||||||
},
|
|
||||||
.{ "notationIterator.previousNode()", "null" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "nodeIterator.filter.acceptNode(document.body)", "1" },
|
|
||||||
.{ "notationIterator.filter", "null" },
|
|
||||||
.{
|
|
||||||
\\ const rejectIterator = document.createNodeIterator(
|
|
||||||
\\ document.body,
|
|
||||||
\\ NodeFilter.SHOW_ALL,
|
|
||||||
\\ (e => { return NodeFilter.FILTER_REJECT}),
|
|
||||||
\\ );
|
|
||||||
\\ rejectIterator.filter(document.body);
|
|
||||||
,
|
|
||||||
"2",
|
|
||||||
},
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,12 @@
|
|||||||
// 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 Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
|
|
||||||
const JsThis = @import("../env.zig").JsThis;
|
|
||||||
const Function = @import("../env.zig").Function;
|
|
||||||
|
|
||||||
const NodeUnion = @import("node.zig").Union;
|
const NodeUnion = @import("node.zig").Union;
|
||||||
const Node = @import("node.zig").Node;
|
const Node = @import("node.zig").Node;
|
||||||
|
|
||||||
@@ -101,13 +100,20 @@ pub const NodeList = struct {
|
|||||||
|
|
||||||
nodes: NodesArrayList = .{},
|
nodes: NodesArrayList = .{},
|
||||||
|
|
||||||
pub fn deinit(self: *NodeList, alloc: std.mem.Allocator) void {
|
pub fn deinit(self: *NodeList, allocator: Allocator) void {
|
||||||
// TODO unref all nodes
|
self.nodes.deinit(allocator);
|
||||||
self.nodes.deinit(alloc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn append(self: *NodeList, alloc: std.mem.Allocator, node: *parser.Node) !void {
|
pub fn ensureTotalCapacity(self: *NodeList, allocator: Allocator, n: usize) !void {
|
||||||
try self.nodes.append(alloc, node);
|
return self.nodes.ensureTotalCapacity(allocator, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn append(self: *NodeList, allocator: Allocator, node: *parser.Node) !void {
|
||||||
|
try self.nodes.append(allocator, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn appendAssumeCapacity(self: *NodeList, node: *parser.Node) void {
|
||||||
|
self.nodes.appendAssumeCapacity(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_length(self: *const NodeList) u32 {
|
pub fn get_length(self: *const NodeList) u32 {
|
||||||
@@ -140,10 +146,10 @@ pub const NodeList = struct {
|
|||||||
// };
|
// };
|
||||||
// }
|
// }
|
||||||
|
|
||||||
pub fn _forEach(self: *NodeList, cbk: Function) !void { // TODO handle thisArg
|
pub fn _forEach(self: *NodeList, cbk: js.Function) !void { // TODO handle thisArg
|
||||||
for (self.nodes.items, 0..) |n, i| {
|
for (self.nodes.items, 0..) |n, i| {
|
||||||
const ii: u32 = @intCast(i);
|
const ii: u32 = @intCast(i);
|
||||||
var result: Function.Result = undefined;
|
var result: js.Function.Result = undefined;
|
||||||
cbk.tryCall(void, .{ n, ii, self }, &result) catch {
|
cbk.tryCall(void, .{ n, ii, self }, &result) catch {
|
||||||
log.debug(.user_script, "forEach callback", .{ .err = result.exception, .stack = result.stack });
|
log.debug(.user_script, "forEach callback", .{ .err = result.exception, .stack = result.stack });
|
||||||
};
|
};
|
||||||
@@ -167,7 +173,7 @@ pub const NodeList = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
|
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
|
||||||
pub fn postAttach(self: *NodeList, js_this: JsThis) !void {
|
pub fn postAttach(self: *NodeList, js_this: js.This) !void {
|
||||||
const len = self.get_length();
|
const len = self.get_length();
|
||||||
for (0..len) |i| {
|
for (0..len) |i| {
|
||||||
const node = try self._item(@intCast(i)) orelse unreachable;
|
const node = try self._item(@intCast(i)) orelse unreachable;
|
||||||
@@ -177,22 +183,6 @@ pub const NodeList = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.DOM.NodeList" {
|
test "Browser: DOM.NodeList" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
try testing.htmlRunner("dom/node_list.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let list = document.getElementById('content').childNodes", "undefined" },
|
|
||||||
.{ "list.length", "9" },
|
|
||||||
.{ "list[0].__proto__.constructor.name", "Text" },
|
|
||||||
.{
|
|
||||||
\\ let i = 0;
|
|
||||||
\\ list.forEach(function (n, idx) {
|
|
||||||
\\ i += idx;
|
|
||||||
\\ });
|
|
||||||
\\ i;
|
|
||||||
,
|
|
||||||
"36",
|
|
||||||
},
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
|
const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
|
||||||
@@ -61,7 +61,7 @@ pub const Performance = struct {
|
|||||||
return milliTimestamp() - self.time_origin;
|
return milliTimestamp() - self.time_origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _mark(_: *Performance, name: []const u8, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark {
|
pub fn _mark(_: *Performance, name: js.String, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark {
|
||||||
const mark: PerformanceMark = try .constructor(name, _options, page);
|
const mark: PerformanceMark = try .constructor(name, _options, page);
|
||||||
// TODO: Should store this in an entries list
|
// TODO: Should store this in an entries list
|
||||||
return mark;
|
return mark;
|
||||||
@@ -148,14 +148,14 @@ pub const PerformanceMark = struct {
|
|||||||
pub const prototype = *PerformanceEntry;
|
pub const prototype = *PerformanceEntry;
|
||||||
|
|
||||||
proto: PerformanceEntry,
|
proto: PerformanceEntry,
|
||||||
detail: ?Env.JsObject,
|
detail: ?js.Object,
|
||||||
|
|
||||||
const Options = struct {
|
const Options = struct {
|
||||||
detail: ?Env.JsObject = null,
|
detail: ?js.Object = null,
|
||||||
startTime: ?f64 = null,
|
startTime: ?f64 = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn constructor(name: []const u8, _options: ?Options, page: *Page) !PerformanceMark {
|
pub fn constructor(name: js.String, _options: ?Options, page: *Page) !PerformanceMark {
|
||||||
const perf = &page.window.performance;
|
const perf = &page.window.performance;
|
||||||
|
|
||||||
const options = _options orelse Options{};
|
const options = _options orelse Options{};
|
||||||
@@ -166,14 +166,12 @@ pub const PerformanceMark = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const detail = if (options.detail) |d| try d.persist() else null;
|
const detail = if (options.detail) |d| try d.persist() else null;
|
||||||
|
const proto = PerformanceEntry{ .name = name.string, .entry_type = .mark, .start_time = start_time };
|
||||||
const duped_name = try page.arena.dupe(u8, name);
|
|
||||||
const proto = PerformanceEntry{ .name = duped_name, .entry_type = .mark, .start_time = start_time };
|
|
||||||
|
|
||||||
return .{ .proto = proto, .detail = detail };
|
return .{ .proto = proto, .detail = detail };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_detail(self: *const PerformanceMark) ?Env.JsObject {
|
pub fn get_detail(self: *const PerformanceMark) ?js.Object {
|
||||||
return self.detail;
|
return self.detail;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -197,28 +195,12 @@ test "Performance: now" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var after = perf._now();
|
var after = perf._now();
|
||||||
while (after <= now) { // Loop untill after > now
|
while (after <= now) { // Loop until after > now
|
||||||
try testing.expectEqual(after, now);
|
try testing.expectEqual(after, now);
|
||||||
after = perf._now();
|
after = perf._now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Browser.Performance.Mark" {
|
test "Browser: Performance.Mark" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
try testing.htmlRunner("dom/performance.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let performance = window.performance", null },
|
|
||||||
.{ "performance instanceof Performance", "true" },
|
|
||||||
|
|
||||||
.{ "let mark1 = performance.mark(\"start\")", null },
|
|
||||||
.{ "mark1 instanceof PerformanceMark", "true" },
|
|
||||||
.{ "mark1.name", "start" },
|
|
||||||
.{ "mark1.entryType", "mark" },
|
|
||||||
.{ "mark1.duration", "0" },
|
|
||||||
.{ "mark1.detail", "null" },
|
|
||||||
|
|
||||||
.{ "let mark2 = performance.mark(\"start\", {startTime: 32939393.9})", null },
|
|
||||||
.{ "mark2.startTime", "32939393.9" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
// 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 Env = @import("../env.zig").Env;
|
const js = @import("../js/js.zig");
|
||||||
|
|
||||||
const PerformanceEntry = @import("performance.zig").PerformanceEntry;
|
const PerformanceEntry = @import("performance.zig").PerformanceEntry;
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ const PerformanceEntry = @import("performance.zig").PerformanceEntry;
|
|||||||
pub const PerformanceObserver = struct {
|
pub const PerformanceObserver = struct {
|
||||||
pub const _supportedEntryTypes = [0][]const u8{};
|
pub const _supportedEntryTypes = [0][]const u8{};
|
||||||
|
|
||||||
pub fn constructor(cbk: Env.Function) PerformanceObserver {
|
pub fn constructor(cbk: js.Function) PerformanceObserver {
|
||||||
_ = cbk;
|
_ = cbk;
|
||||||
return .{};
|
return .{};
|
||||||
}
|
}
|
||||||
@@ -53,11 +53,6 @@ const Options = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.DOM.PerformanceObserver" {
|
test "Browser: DOM.PerformanceObserver" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
try testing.htmlRunner("dom/performance_observer.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "PerformanceObserver.supportedEntryTypes.length", "0" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ pub const ProcessingInstruction = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_data(self: *parser.ProcessingInstruction) !?[]const u8 {
|
pub fn get_data(self: *parser.ProcessingInstruction) !?[]const u8 {
|
||||||
return try parser.nodeValue(parser.processingInstructionToNode(self));
|
return parser.nodeValue(parser.processingInstructionToNode(self));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_data(self: *parser.ProcessingInstruction, data: []u8) !void {
|
pub fn set_data(self: *parser.ProcessingInstruction, data: []u8) !void {
|
||||||
@@ -58,7 +58,7 @@ pub const ProcessingInstruction = struct {
|
|||||||
// netsurf's ProcessInstruction doesn't implement the dom_node_get_attributes
|
// netsurf's ProcessInstruction doesn't implement the dom_node_get_attributes
|
||||||
// and thus will crash if we try to call nodeIsEqualNode.
|
// and thus will crash if we try to call nodeIsEqualNode.
|
||||||
pub fn _isEqualNode(self: *parser.ProcessingInstruction, other_node: *parser.Node) !bool {
|
pub fn _isEqualNode(self: *parser.ProcessingInstruction, other_node: *parser.Node) !bool {
|
||||||
if (try parser.nodeType(other_node) != .processing_instruction) {
|
if (parser.nodeType(other_node) != .processing_instruction) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,30 +87,6 @@ pub const ProcessingInstruction = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.DOM.ProcessingInstruction" {
|
test "Browser: DOM.ProcessingInstruction" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
try testing.htmlRunner("dom/processing_instruction.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let pi = document.createProcessingInstruction('foo', 'bar')", "undefined" },
|
|
||||||
.{ "pi.target", "foo" },
|
|
||||||
.{ "pi.data", "bar" },
|
|
||||||
.{ "pi.data = 'foo'", "foo" },
|
|
||||||
.{ "pi.data", "foo" },
|
|
||||||
|
|
||||||
.{ "let pi2 = pi.cloneNode()", "undefined" },
|
|
||||||
.{ "pi2.nodeType", "7" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let pi11 = document.createProcessingInstruction('target1', 'data1');", "undefined" },
|
|
||||||
.{ "let pi12 = document.createProcessingInstruction('target2', 'data2');", "undefined" },
|
|
||||||
.{ "let pi13 = document.createProcessingInstruction('target1', 'data1');", "undefined" },
|
|
||||||
.{ "pi11.isEqualNode(pi11)", "true" },
|
|
||||||
.{ "pi11.isEqualNode(pi13)", "true" },
|
|
||||||
.{ "pi11.isEqualNode(pi12)", "false" },
|
|
||||||
.{ "pi12.isEqualNode(pi13)", "false" },
|
|
||||||
.{ "pi11.isEqualNode(document)", "false" },
|
|
||||||
.{ "document.isEqualNode(pi11)", "false" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ pub const Range = struct {
|
|||||||
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
|
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
|
||||||
try ensureValidOffset(node, offset_);
|
try ensureValidOffset(node, offset_);
|
||||||
const offset: u32 = @intCast(offset_);
|
const offset: u32 = @intCast(offset_);
|
||||||
const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) {
|
const position = compare(node, offset, self.proto.end_node, self.proto.end_offset) catch |err| switch (err) {
|
||||||
error.WrongDocument => blk: {
|
error.WrongDocument => blk: {
|
||||||
// allow a node with a different root than the current, or
|
// allow a node with a different root than the current, or
|
||||||
// a disconnected one. Treat it as if it's "after", so that
|
// a disconnected one. Treat it as if it's "after", so that
|
||||||
@@ -103,7 +103,7 @@ pub const Range = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (position == 1) {
|
if (position == 1) {
|
||||||
// if we're setting the node after the current start, the end must
|
// if we're setting the node after the current end, the end must
|
||||||
// be set too.
|
// be set too.
|
||||||
self.proto.end_offset = offset;
|
self.proto.end_offset = offset;
|
||||||
self.proto.end_node = node;
|
self.proto.end_node = node;
|
||||||
@@ -176,10 +176,10 @@ pub const Range = struct {
|
|||||||
self.proto.end_node = node;
|
self.proto.end_node = node;
|
||||||
|
|
||||||
// Set end_offset
|
// Set end_offset
|
||||||
switch (try parser.nodeType(node)) {
|
switch (parser.nodeType(node)) {
|
||||||
.text, .cdata_section, .comment, .processing_instruction => {
|
.text, .cdata_section, .comment, .processing_instruction => {
|
||||||
// For text-like nodes, end_offset should be the length of the text data
|
// For text-like nodes, end_offset should be the length of the text data
|
||||||
if (try parser.nodeValue(node)) |text_data| {
|
if (parser.nodeValue(node)) |text_data| {
|
||||||
self.proto.end_offset = @intCast(text_data.len);
|
self.proto.end_offset = @intCast(text_data.len);
|
||||||
} else {
|
} else {
|
||||||
self.proto.end_offset = 0;
|
self.proto.end_offset = 0;
|
||||||
@@ -188,7 +188,7 @@ pub const Range = struct {
|
|||||||
else => {
|
else => {
|
||||||
// For element and other nodes, end_offset is the number of children
|
// For element and other nodes, end_offset is the number of children
|
||||||
const child_nodes = try parser.nodeGetChildNodes(node);
|
const child_nodes = try parser.nodeGetChildNodes(node);
|
||||||
const child_count = try parser.nodeListLength(child_nodes);
|
const child_count = parser.nodeListLength(child_nodes);
|
||||||
self.proto.end_offset = @intCast(child_count);
|
self.proto.end_offset = @intCast(child_count);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -211,7 +211,7 @@ pub const Range = struct {
|
|||||||
|
|
||||||
pub fn _comparePoint(self: *const Range, node: *parser.Node, offset_: i32) !i32 {
|
pub fn _comparePoint(self: *const Range, node: *parser.Node, offset_: i32) !i32 {
|
||||||
const start = self.proto.start_node;
|
const start = self.proto.start_node;
|
||||||
if (try parser.nodeGetRootNode(start) != try parser.nodeGetRootNode(node)) {
|
if (parser.nodeGetRootNode(start) != parser.nodeGetRootNode(node)) {
|
||||||
// WPT really wants this error to be first. Later, when we check
|
// WPT really wants this error to be first. Later, when we check
|
||||||
// if the relative position is 'disconnected', it'll also catch this
|
// if the relative position is 'disconnected', it'll also catch this
|
||||||
// case, but WPT will complain because it sometimes also sends
|
// case, but WPT will complain because it sometimes also sends
|
||||||
@@ -219,7 +219,7 @@ pub const Range = struct {
|
|||||||
return error.WrongDocument;
|
return error.WrongDocument;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (try parser.nodeType(node) == .document_type) {
|
if (parser.nodeType(node) == .document_type) {
|
||||||
return error.InvalidNodeType;
|
return error.InvalidNodeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,8 +245,8 @@ pub const Range = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn _intersectsNode(self: *const Range, node: *parser.Node) !bool {
|
pub fn _intersectsNode(self: *const Range, node: *parser.Node) !bool {
|
||||||
const start_root = try parser.nodeGetRootNode(self.proto.start_node);
|
const start_root = parser.nodeGetRootNode(self.proto.start_node);
|
||||||
const node_root = try parser.nodeGetRootNode(node);
|
const node_root = parser.nodeGetRootNode(node);
|
||||||
if (start_root != node_root) {
|
if (start_root != node_root) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -299,29 +299,29 @@ fn ensureValidOffset(node: *parser.Node, offset: i32) !void {
|
|||||||
|
|
||||||
fn nodeLength(node: *parser.Node) !usize {
|
fn nodeLength(node: *parser.Node) !usize {
|
||||||
switch (try isTextual(node)) {
|
switch (try isTextual(node)) {
|
||||||
true => return ((try parser.nodeTextContent(node)) orelse "").len,
|
true => return ((parser.nodeTextContent(node)) orelse "").len,
|
||||||
false => {
|
false => {
|
||||||
const children = try parser.nodeGetChildNodes(node);
|
const children = try parser.nodeGetChildNodes(node);
|
||||||
return @intCast(try parser.nodeListLength(children));
|
return @intCast(parser.nodeListLength(children));
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isTextual(node: *parser.Node) !bool {
|
fn isTextual(node: *parser.Node) !bool {
|
||||||
return switch (try parser.nodeType(node)) {
|
return switch (parser.nodeType(node)) {
|
||||||
.text, .comment, .cdata_section => true,
|
.text, .comment, .cdata_section => true,
|
||||||
else => false,
|
else => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getParentAndIndex(child: *parser.Node) !struct { *parser.Node, u32 } {
|
fn getParentAndIndex(child: *parser.Node) !struct { *parser.Node, u32 } {
|
||||||
const parent = (try parser.nodeParentNode(child)) orelse return error.InvalidNodeType;
|
const parent = (parser.nodeParentNode(child)) orelse return error.InvalidNodeType;
|
||||||
const children = try parser.nodeGetChildNodes(parent);
|
const children = try parser.nodeGetChildNodes(parent);
|
||||||
const ln = try parser.nodeListLength(children);
|
const ln = parser.nodeListLength(children);
|
||||||
var i: u32 = 0;
|
var i: u32 = 0;
|
||||||
while (i < ln) {
|
while (i < ln) {
|
||||||
defer i += 1;
|
defer i += 1;
|
||||||
const c = try parser.nodeListItem(children, i) orelse continue;
|
const c = parser.nodeListItem(children, i) orelse continue;
|
||||||
if (c == child) {
|
if (c == child) {
|
||||||
return .{ parent, i };
|
return .{ parent, i };
|
||||||
}
|
}
|
||||||
@@ -363,7 +363,7 @@ fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b:
|
|||||||
if (position & @intFromEnum(parser.DocumentPosition.contains) == @intFromEnum(parser.DocumentPosition.contains)) {
|
if (position & @intFromEnum(parser.DocumentPosition.contains) == @intFromEnum(parser.DocumentPosition.contains)) {
|
||||||
// node_a contains node_b
|
// node_a contains node_b
|
||||||
var child = node_b;
|
var child = node_b;
|
||||||
while (try parser.nodeParentNode(child)) |parent| {
|
while (parser.nodeParentNode(child)) |parent| {
|
||||||
if (parent == node_a) {
|
if (parent == node_a) {
|
||||||
// child.parentNode == node_a
|
// child.parentNode == node_a
|
||||||
break;
|
break;
|
||||||
@@ -378,51 +378,13 @@ fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b:
|
|||||||
|
|
||||||
const child_parent, const child_index = try getParentAndIndex(child);
|
const child_parent, const child_index = try getParentAndIndex(child);
|
||||||
std.debug.assert(node_a == child_parent);
|
std.debug.assert(node_a == child_parent);
|
||||||
return if (child_index < offset_a) -1 else 1;
|
return if (offset_a <= child_index) -1 else 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.Range" {
|
test "Browser: Range" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
try testing.htmlRunner("dom/range.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
// Test Range constructor
|
|
||||||
.{ "let range = new Range()", "undefined" },
|
|
||||||
.{ "range instanceof Range", "true" },
|
|
||||||
.{ "range instanceof AbstractRange", "true" },
|
|
||||||
|
|
||||||
// Test initial state - collapsed range
|
|
||||||
.{ "range.collapsed", "true" },
|
|
||||||
.{ "range.startOffset", "0" },
|
|
||||||
.{ "range.endOffset", "0" },
|
|
||||||
.{ "range.startContainer instanceof HTMLDocument", "true" },
|
|
||||||
.{ "range.endContainer instanceof HTMLDocument", "true" },
|
|
||||||
|
|
||||||
// Test document.createRange()
|
|
||||||
.{ "let docRange = document.createRange()", "undefined" },
|
|
||||||
.{ "docRange instanceof Range", "true" },
|
|
||||||
.{ "docRange.collapsed", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const container = document.getElementById('content');", null },
|
|
||||||
|
|
||||||
// Test text range
|
|
||||||
.{ "const commentNode = container.childNodes[7];", null },
|
|
||||||
.{ "commentNode.nodeValue", "comment" },
|
|
||||||
.{ "const textRange = document.createRange();", null },
|
|
||||||
.{ "textRange.selectNodeContents(commentNode)", "undefined" },
|
|
||||||
.{ "textRange.startOffset", "0" },
|
|
||||||
.{ "textRange.endOffset", "7" }, // length of `comment`
|
|
||||||
|
|
||||||
// Test Node range
|
|
||||||
.{ "const nodeRange = document.createRange();", null },
|
|
||||||
.{ "nodeRange.selectNodeContents(container)", "undefined" },
|
|
||||||
.{ "nodeRange.startOffset", "0" },
|
|
||||||
.{ "nodeRange.endOffset", "9" }, // length of container.childNodes
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const Env = @import("../env.zig").Env;
|
const js = @import("../js/js.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
|
|
||||||
pub const Interfaces = .{
|
pub const Interfaces = .{
|
||||||
@@ -25,7 +25,7 @@ pub const Interfaces = .{
|
|||||||
|
|
||||||
// WEB IDL https://drafts.csswg.org/resize-observer/#resize-observer-interface
|
// WEB IDL https://drafts.csswg.org/resize-observer/#resize-observer-interface
|
||||||
pub const ResizeObserver = struct {
|
pub const ResizeObserver = struct {
|
||||||
pub fn constructor(cbk: Env.Function) ResizeObserver {
|
pub fn constructor(cbk: js.Function) ResizeObserver {
|
||||||
_ = cbk;
|
_ = cbk;
|
||||||
return .{};
|
return .{};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const std = @import("std");
|
|||||||
const dump = @import("../dump.zig");
|
const dump = @import("../dump.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
|
|
||||||
const Env = @import("../env.zig").Env;
|
const js = @import(".././js/js.zig");
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
const Node = @import("node.zig").Node;
|
const Node = @import("node.zig").Node;
|
||||||
const Element = @import("element.zig").Element;
|
const Element = @import("element.zig").Element;
|
||||||
@@ -34,7 +34,7 @@ pub const ShadowRoot = struct {
|
|||||||
mode: Mode,
|
mode: Mode,
|
||||||
host: *parser.Element,
|
host: *parser.Element,
|
||||||
proto: *parser.DocumentFragment,
|
proto: *parser.DocumentFragment,
|
||||||
adopted_style_sheets: ?Env.JsObject = null,
|
adopted_style_sheets: ?js.Object = null,
|
||||||
|
|
||||||
pub const Mode = enum {
|
pub const Mode = enum {
|
||||||
open,
|
open,
|
||||||
@@ -45,17 +45,17 @@ pub const ShadowRoot = struct {
|
|||||||
return Element.toInterface(self.host);
|
return Element.toInterface(self.host);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !Env.JsObject {
|
pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !js.Object {
|
||||||
if (self.adopted_style_sheets) |obj| {
|
if (self.adopted_style_sheets) |obj| {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
const obj = try page.main_context.newArray(0).persist();
|
const obj = try page.js.createArray(0).persist();
|
||||||
self.adopted_style_sheets = obj;
|
self.adopted_style_sheets = obj;
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: Env.JsObject) !void {
|
pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: js.Object) !void {
|
||||||
self.adopted_style_sheets = try sheets.persist();
|
self.adopted_style_sheets = try sheets.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ pub const ShadowRoot = struct {
|
|||||||
|
|
||||||
pub fn set_innerHTML(self: *ShadowRoot, str_: ?[]const u8) !void {
|
pub fn set_innerHTML(self: *ShadowRoot, str_: ?[]const u8) !void {
|
||||||
const sr_doc = parser.documentFragmentToNode(self.proto);
|
const sr_doc = parser.documentFragmentToNode(self.proto);
|
||||||
const doc = try parser.nodeOwnerDocument(sr_doc) orelse return parser.DOMError.WrongDocument;
|
const doc = parser.nodeOwnerDocument(sr_doc) orelse return parser.DOMError.WrongDocument;
|
||||||
try Node.removeChildren(sr_doc);
|
try Node.removeChildren(sr_doc);
|
||||||
const str = str_ orelse return;
|
const str = str_ orelse return;
|
||||||
|
|
||||||
@@ -80,76 +80,22 @@ pub const ShadowRoot = struct {
|
|||||||
// element.
|
// element.
|
||||||
// For ShadowRoot, it appears the only the children within the body should
|
// For ShadowRoot, it appears the only the children within the body should
|
||||||
// be set.
|
// be set.
|
||||||
const html = try parser.nodeFirstChild(fragment_node) orelse return;
|
const html = parser.nodeFirstChild(fragment_node) orelse return;
|
||||||
const head = try parser.nodeFirstChild(html) orelse return;
|
const head = parser.nodeFirstChild(html) orelse return;
|
||||||
const body = try parser.nodeNextSibling(head) orelse return;
|
const body = parser.nodeNextSibling(head) orelse return;
|
||||||
|
|
||||||
const children = try parser.nodeGetChildNodes(body);
|
const children = try parser.nodeGetChildNodes(body);
|
||||||
const ln = try parser.nodeListLength(children);
|
const ln = parser.nodeListLength(children);
|
||||||
for (0..ln) |_| {
|
for (0..ln) |_| {
|
||||||
// always index 0, because nodeAppendChild moves the node out of
|
// always index 0, because nodeAppendChild moves the node out of
|
||||||
// the nodeList and into the new tree
|
// the nodeList and into the new tree
|
||||||
const child = try parser.nodeListItem(children, 0) orelse continue;
|
const child = parser.nodeListItem(children, 0) orelse continue;
|
||||||
_ = try parser.nodeAppendChild(sr_doc, child);
|
_ = try parser.nodeAppendChild(sr_doc, child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.DOM.ShadowRoot" {
|
test "Browser: DOM.ShadowRoot" {
|
||||||
defer testing.reset();
|
try testing.htmlRunner("dom/shadow_root.html");
|
||||||
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
|
|
||||||
\\ <div id=conflict>nope</div>
|
|
||||||
});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const div1 = document.createElement('div');", null },
|
|
||||||
.{ "let sr1 = div1.attachShadow({mode: 'open'})", null },
|
|
||||||
.{ "sr1.host == div1", "true" },
|
|
||||||
.{ "div1.attachShadow({mode: 'open'}) == sr1", "true" },
|
|
||||||
.{ "div1.shadowRoot == sr1", "true" },
|
|
||||||
|
|
||||||
.{ "try { div1.attachShadow({mode: 'closed'}) } catch (e) { e }", "Error: NotSupportedError" },
|
|
||||||
|
|
||||||
.{ " sr1.append(document.createElement('div'))", null },
|
|
||||||
.{ " sr1.append(document.createElement('span'))", null },
|
|
||||||
.{ "sr1.childElementCount", "2" },
|
|
||||||
// re-attaching clears it
|
|
||||||
.{ "div1.attachShadow({mode: 'open'}) == sr1", "true" },
|
|
||||||
.{ "sr1.childElementCount", "0" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const div2 = document.createElement('di2');", null },
|
|
||||||
.{ "let sr2 = div2.attachShadow({mode: 'closed'})", null },
|
|
||||||
.{ "sr2.host == div2", "true" },
|
|
||||||
.{ "div2.shadowRoot", "null" }, // null when attached with 'closed'
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "sr2.getElementById('conflict')", "null" },
|
|
||||||
.{ "const n1 = document.createElement('div')", null },
|
|
||||||
.{ "n1.id = 'conflict'", null },
|
|
||||||
.{ "sr2.append(n1)", null },
|
|
||||||
.{ "sr2.getElementById('conflict') == n1", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const acss = sr2.adoptedStyleSheets", null },
|
|
||||||
.{ "acss.length", "0" },
|
|
||||||
.{ "acss.push(new CSSStyleSheet())", null },
|
|
||||||
.{ "sr2.adoptedStyleSheets.length", "1" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "sr1.innerHTML = '<p>hello</p>'", null },
|
|
||||||
.{ "sr1.innerHTML", "<p>hello</p>" },
|
|
||||||
.{ "sr1.querySelector('*')", "[object HTMLParagraphElement]" },
|
|
||||||
|
|
||||||
.{ "sr1.innerHTML = null", null },
|
|
||||||
.{ "sr1.innerHTML", "" },
|
|
||||||
.{ "sr1.querySelector('*')", "null" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,31 +56,7 @@ pub const Text = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.DOM.Text" {
|
test "Browser: DOM.Text" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
try testing.htmlRunner("dom/text.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let t = new Text('foo')", "undefined" },
|
|
||||||
.{ "t.data", "foo" },
|
|
||||||
|
|
||||||
.{ "let emptyt = new Text()", "undefined" },
|
|
||||||
.{ "emptyt.data", "" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let text = document.getElementById('link').firstChild", "undefined" },
|
|
||||||
.{ "text.wholeText === 'OK'", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "text.data = 'OK modified'", "OK modified" },
|
|
||||||
.{ "let split = text.splitText('OK'.length)", "undefined" },
|
|
||||||
.{ "split.data === ' modified'", "true" },
|
|
||||||
.{ "text.data === 'OK'", "true" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,11 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const iterator = @import("../iterator/iterator.zig");
|
const iterator = @import("../iterator/iterator.zig");
|
||||||
|
|
||||||
const Function = @import("../env.zig").Function;
|
|
||||||
const JsObject = @import("../env.zig").JsObject;
|
|
||||||
const DOMException = @import("exceptions.zig").DOMException;
|
const DOMException = @import("exceptions.zig").DOMException;
|
||||||
|
|
||||||
pub const Interfaces = .{
|
pub const Interfaces = .{
|
||||||
@@ -137,10 +136,10 @@ pub const DOMTokenList = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO handle thisArg
|
// TODO handle thisArg
|
||||||
pub fn _forEach(self: *parser.TokenList, cbk: Function, this_arg: JsObject) !void {
|
pub fn _forEach(self: *parser.TokenList, cbk: js.Function, this_arg: js.Object) !void {
|
||||||
var entries = _entries(self);
|
var entries = _entries(self);
|
||||||
while (try entries._next()) |entry| {
|
while (try entries._next()) |entry| {
|
||||||
var result: Function.Result = undefined;
|
var result: js.Function.Result = undefined;
|
||||||
cbk.tryCallWithThis(void, this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch {
|
cbk.tryCallWithThis(void, this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch {
|
||||||
log.debug(.user_script, "callback error", .{
|
log.debug(.user_script, "callback error", .{
|
||||||
.err = result.exception,
|
.err = result.exception,
|
||||||
@@ -169,77 +168,7 @@ pub const Iterator = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.DOM.TokenList" {
|
test "Browser: DOM.TokenList" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
try testing.htmlRunner("dom/token_list.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let gs = document.getElementById('para-empty')", "undefined" },
|
|
||||||
.{ "let cl = gs.classList", "undefined" },
|
|
||||||
.{ "gs.className", "ok empty" },
|
|
||||||
.{ "cl.value", "ok empty" },
|
|
||||||
.{ "cl.length", "2" },
|
|
||||||
.{ "gs.className = 'foo bar baz'", "foo bar baz" },
|
|
||||||
.{ "gs.className", "foo bar baz" },
|
|
||||||
.{ "cl.length", "3" },
|
|
||||||
.{ "gs.className = 'ok empty'", "ok empty" },
|
|
||||||
.{ "cl.length", "2" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let cl2 = gs.classList", "undefined" },
|
|
||||||
.{ "cl2.length", "2" },
|
|
||||||
.{ "cl2.item(0)", "ok" },
|
|
||||||
.{ "cl2.item(1)", "empty" },
|
|
||||||
.{ "cl2.contains('ok')", "true" },
|
|
||||||
.{ "cl2.contains('nok')", "false" },
|
|
||||||
.{ "cl2.add('foo', 'bar', 'baz')", "undefined" },
|
|
||||||
.{ "cl2.length", "5" },
|
|
||||||
.{ "cl2.remove('foo', 'bar', 'baz')", "undefined" },
|
|
||||||
.{ "cl2.length", "2" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let cl3 = gs.classList", "undefined" },
|
|
||||||
.{ "cl3.toggle('ok')", "false" },
|
|
||||||
.{ "cl3.toggle('ok')", "true" },
|
|
||||||
.{ "cl3.length", "2" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let cl4 = gs.classList", "undefined" },
|
|
||||||
.{ "cl4.replace('ok', 'nok')", "true" },
|
|
||||||
.{ "cl4.value", "empty nok" },
|
|
||||||
.{ "cl4.replace('nok', 'ok')", "true" },
|
|
||||||
.{ "cl4.value", "empty ok" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let cl5 = gs.classList", "undefined" },
|
|
||||||
.{ "let keys = [...cl5.keys()]", "undefined" },
|
|
||||||
.{ "keys.length", "2" },
|
|
||||||
.{ "keys[0]", "0" },
|
|
||||||
.{ "keys[1]", "1" },
|
|
||||||
|
|
||||||
.{ "let values = [...cl5.values()]", "undefined" },
|
|
||||||
.{ "values.length", "2" },
|
|
||||||
.{ "values[0]", "empty" },
|
|
||||||
.{ "values[1]", "ok" },
|
|
||||||
|
|
||||||
.{ "let entries = [...cl5.entries()]", "undefined" },
|
|
||||||
.{ "entries.length", "2" },
|
|
||||||
.{ "entries[0]", "0,empty" },
|
|
||||||
.{ "entries[1]", "1,ok" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let cl6 = gs.classList", "undefined" },
|
|
||||||
.{ "cl6.value = 'a b ccc'", "a b ccc" },
|
|
||||||
.{ "cl6.value", "a b ccc" },
|
|
||||||
.{ "cl6.toString()", "a b ccc" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,10 @@
|
|||||||
// 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 js = @import("../js/js.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
|
|
||||||
const NodeFilter = @import("node_filter.zig");
|
const NodeFilter = @import("node_filter.zig");
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const Node = @import("node.zig").Node;
|
const Node = @import("node.zig").Node;
|
||||||
const NodeUnion = @import("node.zig").Union;
|
const NodeUnion = @import("node.zig").Union;
|
||||||
|
|
||||||
@@ -31,20 +30,20 @@ pub const TreeWalker = struct {
|
|||||||
current_node: *parser.Node,
|
current_node: *parser.Node,
|
||||||
what_to_show: u32,
|
what_to_show: u32,
|
||||||
filter: ?TreeWalkerOpts,
|
filter: ?TreeWalkerOpts,
|
||||||
filter_func: ?Env.Function,
|
filter_func: ?js.Function,
|
||||||
|
|
||||||
// One of the few cases where null and undefined resolve to different default.
|
// One of the few cases where null and undefined resolve to different default.
|
||||||
// We need the raw JsObject so that we can probe the tri state:
|
// We need the raw JsObject so that we can probe the tri state:
|
||||||
// null, undefined or i32.
|
// null, undefined or i32.
|
||||||
pub const WhatToShow = Env.JsObject;
|
pub const WhatToShow = js.Object;
|
||||||
|
|
||||||
pub const TreeWalkerOpts = union(enum) {
|
pub const TreeWalkerOpts = union(enum) {
|
||||||
function: Env.Function,
|
function: js.Function,
|
||||||
object: struct { acceptNode: Env.Function },
|
object: struct { acceptNode: js.Function },
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?TreeWalkerOpts) !TreeWalker {
|
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?TreeWalkerOpts) !TreeWalker {
|
||||||
var filter_func: ?Env.Function = null;
|
var filter_func: ?js.Function = null;
|
||||||
|
|
||||||
if (filter) |f| {
|
if (filter) |f| {
|
||||||
filter_func = switch (f) {
|
filter_func = switch (f) {
|
||||||
@@ -95,11 +94,11 @@ pub const TreeWalker = struct {
|
|||||||
|
|
||||||
fn firstChild(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
fn firstChild(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
||||||
const children = try parser.nodeGetChildNodes(node);
|
const children = try parser.nodeGetChildNodes(node);
|
||||||
const child_count = try parser.nodeListLength(children);
|
const child_count = parser.nodeListLength(children);
|
||||||
|
|
||||||
for (0..child_count) |i| {
|
for (0..child_count) |i| {
|
||||||
const index: u32 = @intCast(i);
|
const index: u32 = @intCast(i);
|
||||||
const child = (try parser.nodeListItem(children, index)) orelse return null;
|
const child = (parser.nodeListItem(children, index)) orelse return null;
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
||||||
.accept => return child,
|
.accept => return child,
|
||||||
@@ -113,12 +112,12 @@ pub const TreeWalker = struct {
|
|||||||
|
|
||||||
fn lastChild(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
fn lastChild(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
||||||
const children = try parser.nodeGetChildNodes(node);
|
const children = try parser.nodeGetChildNodes(node);
|
||||||
const child_count = try parser.nodeListLength(children);
|
const child_count = parser.nodeListLength(children);
|
||||||
|
|
||||||
var index: u32 = child_count;
|
var index: u32 = child_count;
|
||||||
while (index > 0) {
|
while (index > 0) {
|
||||||
index -= 1;
|
index -= 1;
|
||||||
const child = (try parser.nodeListItem(children, index)) orelse return null;
|
const child = (parser.nodeListItem(children, index)) orelse return null;
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
||||||
.accept => return child,
|
.accept => return child,
|
||||||
@@ -134,7 +133,7 @@ pub const TreeWalker = struct {
|
|||||||
var current = node;
|
var current = node;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
current = (try parser.nodeNextSibling(current)) orelse return null;
|
current = (parser.nodeNextSibling(current)) orelse return null;
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||||
.accept => return current,
|
.accept => return current,
|
||||||
@@ -145,11 +144,28 @@ pub const TreeWalker = struct {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the next sibling that is either acceptable or should be descended into (skip)
|
||||||
|
fn nextSiblingOrSkip(self: *const TreeWalker, node: *parser.Node) !?struct { node: *parser.Node, should_descend: bool } {
|
||||||
|
var current = node;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
current = (parser.nodeNextSibling(current)) orelse return null;
|
||||||
|
|
||||||
|
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||||
|
.accept => return .{ .node = current, .should_descend = false },
|
||||||
|
.skip => return .{ .node = current, .should_descend = true },
|
||||||
|
.reject => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
fn previousSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
fn previousSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
||||||
var current = node;
|
var current = node;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
current = (try parser.nodePreviousSibling(current)) orelse return null;
|
current = (parser.nodePreviousSibling(current)) orelse return null;
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||||
.accept => return current,
|
.accept => return current,
|
||||||
@@ -166,7 +182,7 @@ pub const TreeWalker = struct {
|
|||||||
var current = node;
|
var current = node;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (current == self.root) return null;
|
if (current == self.root) return null;
|
||||||
current = (try parser.nodeParentNode(current)) orelse return null;
|
current = (parser.nodeParentNode(current)) orelse return null;
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||||
.accept => return current,
|
.accept => return current,
|
||||||
@@ -194,19 +210,36 @@ pub const TreeWalker = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn _nextNode(self: *TreeWalker) !?NodeUnion {
|
pub fn _nextNode(self: *TreeWalker) !?NodeUnion {
|
||||||
if (try self.firstChild(self.current_node)) |child| {
|
var current = self.current_node;
|
||||||
|
|
||||||
|
// First, try to go to first child of current node
|
||||||
|
if (try self.firstChild(current)) |child| {
|
||||||
self.current_node = child;
|
self.current_node = child;
|
||||||
return try Node.toInterface(child);
|
return try Node.toInterface(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
var current = self.current_node;
|
// No acceptable children, move to next node in tree
|
||||||
while (current != self.root) {
|
while (current != self.root) {
|
||||||
if (try self.nextSibling(current)) |sibling| {
|
const result = try self.nextSiblingOrSkip(current) orelse {
|
||||||
self.current_node = sibling;
|
// No next sibling, go up to parent and continue
|
||||||
return try Node.toInterface(sibling);
|
// or, if there is no parent, we're done
|
||||||
|
current = (parser.nodeParentNode(current)) orelse break;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!result.should_descend) {
|
||||||
|
// This is an .accept node - return it
|
||||||
|
self.current_node = result.node;
|
||||||
|
return try Node.toInterface(result.node);
|
||||||
}
|
}
|
||||||
|
|
||||||
current = (try parser.nodeParentNode(current)) orelse break;
|
// This is a .skip node - try to find acceptable children within it
|
||||||
|
if (try self.firstChild(result.node)) |child| {
|
||||||
|
self.current_node = child;
|
||||||
|
return try Node.toInterface(child);
|
||||||
|
}
|
||||||
|
// No acceptable children, continue looking at this node's siblings
|
||||||
|
current = result.node;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -234,7 +267,7 @@ pub const TreeWalker = struct {
|
|||||||
if (self.current_node == self.root) return null;
|
if (self.current_node == self.root) return null;
|
||||||
|
|
||||||
var current = self.current_node;
|
var current = self.current_node;
|
||||||
while (try parser.nodePreviousSibling(current)) |previous| {
|
while (parser.nodePreviousSibling(current)) |previous| {
|
||||||
current = previous;
|
current = previous;
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||||
|
|||||||
@@ -44,39 +44,39 @@ pub const WalkerDepthFirst = struct {
|
|||||||
var n = cur orelse root;
|
var n = cur orelse root;
|
||||||
|
|
||||||
// TODO deinit next
|
// TODO deinit next
|
||||||
if (try parser.nodeFirstChild(n)) |next| {
|
if (parser.nodeFirstChild(n)) |next| {
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO deinit next
|
// TODO deinit next
|
||||||
if (try parser.nodeNextSibling(n)) |next| {
|
if (parser.nodeNextSibling(n)) |next| {
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO deinit parent
|
// TODO deinit parent
|
||||||
// Back to the parent of cur.
|
// Back to the parent of cur.
|
||||||
// If cur has no parent, then the iteration is over.
|
// If cur has no parent, then the iteration is over.
|
||||||
var parent = try parser.nodeParentNode(n) orelse return null;
|
var parent = parser.nodeParentNode(n) orelse return null;
|
||||||
|
|
||||||
// TODO deinit lastchild
|
// TODO deinit lastchild
|
||||||
var lastchild = try parser.nodeLastChild(parent);
|
var lastchild = parser.nodeLastChild(parent);
|
||||||
while (n != root and n == lastchild) {
|
while (n != root and n == lastchild) {
|
||||||
n = parent;
|
n = parent;
|
||||||
|
|
||||||
// TODO deinit parent
|
// TODO deinit parent
|
||||||
// Back to the prev's parent.
|
// Back to the prev's parent.
|
||||||
// If prev has no parent, then the loop must stop.
|
// If prev has no parent, then the loop must stop.
|
||||||
parent = try parser.nodeParentNode(n) orelse break;
|
parent = parser.nodeParentNode(n) orelse break;
|
||||||
|
|
||||||
// TODO deinit lastchild
|
// TODO deinit lastchild
|
||||||
lastchild = try parser.nodeLastChild(parent);
|
lastchild = parser.nodeLastChild(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (n == root) {
|
if (n == root) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return try parser.nodeNextSibling(n);
|
return parser.nodeNextSibling(n);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,14 +84,14 @@ pub const WalkerDepthFirst = struct {
|
|||||||
pub const WalkerChildren = struct {
|
pub const WalkerChildren = struct {
|
||||||
pub fn get_next(_: WalkerChildren, root: *parser.Node, cur: ?*parser.Node) !?*parser.Node {
|
pub fn get_next(_: WalkerChildren, root: *parser.Node, cur: ?*parser.Node) !?*parser.Node {
|
||||||
// On walk start, we return the first root's child.
|
// On walk start, we return the first root's child.
|
||||||
if (cur == null) return try parser.nodeFirstChild(root);
|
if (cur == null) return parser.nodeFirstChild(root);
|
||||||
|
|
||||||
// If cur is root, then return null.
|
// If cur is root, then return null.
|
||||||
// This is a special case, if the root is included in the walk, we
|
// This is a special case, if the root is included in the walk, we
|
||||||
// don't want to go further to find children.
|
// don't want to go further to find children.
|
||||||
if (root == cur.?) return null;
|
if (root == cur.?) return null;
|
||||||
|
|
||||||
return try parser.nodeNextSibling(cur.?);
|
return parser.nodeNextSibling(cur.?);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,13 @@ pub const Opts = struct {
|
|||||||
// set to include element shadowroots in the dump
|
// set to include element shadowroots in the dump
|
||||||
page: ?*const Page = null,
|
page: ?*const Page = null,
|
||||||
|
|
||||||
exclude_scripts: bool = false,
|
strip_mode: StripMode = .{},
|
||||||
|
|
||||||
|
pub const StripMode = struct {
|
||||||
|
js: bool = false,
|
||||||
|
ui: bool = false,
|
||||||
|
css: bool = false,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// writer must be a std.io.Writer
|
// writer must be a std.io.Writer
|
||||||
@@ -41,8 +47,8 @@ pub fn writeDocType(doc_type: *parser.DocumentType, writer: *std.Io.Writer) !voi
|
|||||||
try writer.writeAll("<!DOCTYPE ");
|
try writer.writeAll("<!DOCTYPE ");
|
||||||
try writer.writeAll(try parser.documentTypeGetName(doc_type));
|
try writer.writeAll(try parser.documentTypeGetName(doc_type));
|
||||||
|
|
||||||
const public_id = try parser.documentTypeGetPublicId(doc_type);
|
const public_id = parser.documentTypeGetPublicId(doc_type);
|
||||||
const system_id = try parser.documentTypeGetSystemId(doc_type);
|
const system_id = parser.documentTypeGetSystemId(doc_type);
|
||||||
if (public_id.len != 0 and system_id.len != 0) {
|
if (public_id.len != 0 and system_id.len != 0) {
|
||||||
try writer.writeAll(" PUBLIC \"");
|
try writer.writeAll(" PUBLIC \"");
|
||||||
try writeEscapedAttributeValue(writer, public_id);
|
try writeEscapedAttributeValue(writer, public_id);
|
||||||
@@ -63,11 +69,11 @@ pub fn writeDocType(doc_type: *parser.DocumentType, writer: *std.Io.Writer) !voi
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerror!void {
|
pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerror!void {
|
||||||
switch (try parser.nodeType(node)) {
|
switch (parser.nodeType(node)) {
|
||||||
.element => {
|
.element => {
|
||||||
// open the tag
|
// open the tag
|
||||||
const tag_type = try parser.nodeHTMLGetTagType(node) orelse .undef;
|
const tag_type = try parser.nodeHTMLGetTagType(node) orelse .undef;
|
||||||
if (opts.exclude_scripts and try isScriptOrRelated(tag_type, node)) {
|
if (try isStripped(tag_type, node, opts.strip_mode)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +110,7 @@ pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerro
|
|||||||
if (try isVoid(parser.nodeToElement(node))) return;
|
if (try isVoid(parser.nodeToElement(node))) return;
|
||||||
|
|
||||||
if (tag_type == .script) {
|
if (tag_type == .script) {
|
||||||
try writer.writeAll(try parser.nodeTextContent(node) orelse "");
|
try writer.writeAll(parser.nodeTextContent(node) orelse "");
|
||||||
} else {
|
} else {
|
||||||
// write the children
|
// write the children
|
||||||
// TODO avoid recursion
|
// TODO avoid recursion
|
||||||
@@ -117,17 +123,17 @@ pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerro
|
|||||||
try writer.writeAll(">");
|
try writer.writeAll(">");
|
||||||
},
|
},
|
||||||
.text => {
|
.text => {
|
||||||
const v = try parser.nodeValue(node) orelse return;
|
const v = parser.nodeValue(node) orelse return;
|
||||||
try writeEscapedTextNode(writer, v);
|
try writeEscapedTextNode(writer, v);
|
||||||
},
|
},
|
||||||
.cdata_section => {
|
.cdata_section => {
|
||||||
const v = try parser.nodeValue(node) orelse return;
|
const v = parser.nodeValue(node) orelse return;
|
||||||
try writer.writeAll("<![CDATA[");
|
try writer.writeAll("<![CDATA[");
|
||||||
try writer.writeAll(v);
|
try writer.writeAll(v);
|
||||||
try writer.writeAll("]]>");
|
try writer.writeAll("]]>");
|
||||||
},
|
},
|
||||||
.comment => {
|
.comment => {
|
||||||
const v = try parser.nodeValue(node) orelse return;
|
const v = parser.nodeValue(node) orelse return;
|
||||||
try writer.writeAll("<!--");
|
try writer.writeAll("<!--");
|
||||||
try writer.writeAll(v);
|
try writer.writeAll(v);
|
||||||
try writer.writeAll("-->");
|
try writer.writeAll("-->");
|
||||||
@@ -159,9 +165,22 @@ pub fn writeChildren(root: *parser.Node, opts: Opts, writer: *std.Io.Writer) !vo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// When `exclude_scripts` is passed to dump, we don't include <script> tags.
|
fn isStripped(tag_type: parser.Tag, node: *parser.Node, strip_mode: Opts.StripMode) !bool {
|
||||||
// We also want to omit <link rel=preload as=ascript>
|
if (strip_mode.js and try isJsRelated(tag_type, node)) {
|
||||||
fn isScriptOrRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strip_mode.css and try isCssRelated(tag_type, node)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strip_mode.ui and try isUIRelated(tag_type, node)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isJsRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
|
||||||
if (tag_type == .script) {
|
if (tag_type == .script) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -178,6 +197,34 @@ fn isScriptOrRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn isCssRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
|
||||||
|
if (tag_type == .style) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tag_type == .link) {
|
||||||
|
const el = parser.nodeToElement(node);
|
||||||
|
const rel = try parser.elementGetAttribute(el, "rel") orelse return false;
|
||||||
|
return std.ascii.eqlIgnoreCase(rel, "stylesheet");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isUIRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
|
||||||
|
if (try isCssRelated(tag_type, node)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tag_type == .img or tag_type == .picture or tag_type == .video) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tag_type == .undef) {
|
||||||
|
const name = try parser.nodeLocalName(node);
|
||||||
|
if (std.mem.eql(u8, name, "svg")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
|
// area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
|
||||||
// https://html.spec.whatwg.org/#void-elements
|
// https://html.spec.whatwg.org/#void-elements
|
||||||
fn isVoid(elem: *parser.Element) !bool {
|
fn isVoid(elem: *parser.Element) !bool {
|
||||||
@@ -189,10 +236,10 @@ fn isVoid(elem: *parser.Element) !bool {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
|
fn writeEscapedTextNode(writer: *std.Io.Writer, value: []const u8) !void {
|
||||||
var v = value;
|
var v = value;
|
||||||
while (v.len > 0) {
|
while (v.len > 0) {
|
||||||
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>' }) orelse {
|
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', 194 }) orelse {
|
||||||
return writer.writeAll(v);
|
return writer.writeAll(v);
|
||||||
};
|
};
|
||||||
try writer.writeAll(v[0..index]);
|
try writer.writeAll(v[0..index]);
|
||||||
@@ -200,13 +247,22 @@ fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
|
|||||||
'&' => try writer.writeAll("&"),
|
'&' => try writer.writeAll("&"),
|
||||||
'<' => try writer.writeAll("<"),
|
'<' => try writer.writeAll("<"),
|
||||||
'>' => try writer.writeAll(">"),
|
'>' => try writer.writeAll(">"),
|
||||||
|
194 => {
|
||||||
|
// non breaking space
|
||||||
|
if (v.len > index + 1 and v[index + 1] == 160) {
|
||||||
|
try writer.writeAll(" ");
|
||||||
|
v = v[index + 2 ..];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try writer.writeByte(194);
|
||||||
|
},
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
}
|
}
|
||||||
v = v[index + 1 ..];
|
v = v[index + 1 ..];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
|
fn writeEscapedAttributeValue(writer: *std.Io.Writer, value: []const u8) !void {
|
||||||
var v = value;
|
var v = value;
|
||||||
while (v.len > 0) {
|
while (v.len > 0) {
|
||||||
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', '"' }) orelse {
|
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', '"' }) orelse {
|
||||||
@@ -226,7 +282,7 @@ fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
|
|||||||
|
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
test "dump.writeHTML" {
|
test "dump.writeHTML" {
|
||||||
try parser.init();
|
parser.init();
|
||||||
defer parser.deinit();
|
defer parser.deinit();
|
||||||
|
|
||||||
try testWriteHTML(
|
try testWriteHTML(
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
const Env = @import("../env.zig").Env;
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
// https://encoding.spec.whatwg.org/#interface-textdecoder
|
// https://encoding.spec.whatwg.org/#interface-textdecoder
|
||||||
const TextDecoder = @This();
|
const TextDecoder = @This();
|
||||||
@@ -37,6 +37,7 @@ const Options = struct {
|
|||||||
|
|
||||||
fatal: bool,
|
fatal: bool,
|
||||||
ignore_bom: bool,
|
ignore_bom: bool,
|
||||||
|
stream: std.ArrayList(u8),
|
||||||
|
|
||||||
pub fn constructor(label_: ?[]const u8, opts_: ?Options) !TextDecoder {
|
pub fn constructor(label_: ?[]const u8, opts_: ?Options) !TextDecoder {
|
||||||
if (label_) |l| {
|
if (label_) |l| {
|
||||||
@@ -47,6 +48,7 @@ pub fn constructor(label_: ?[]const u8, opts_: ?Options) !TextDecoder {
|
|||||||
}
|
}
|
||||||
const opts = opts_ orelse Options{};
|
const opts = opts_ orelse Options{};
|
||||||
return .{
|
return .{
|
||||||
|
.stream = .empty,
|
||||||
.fatal = opts.fatal,
|
.fatal = opts.fatal,
|
||||||
.ignore_bom = opts.ignoreBOM,
|
.ignore_bom = opts.ignoreBOM,
|
||||||
};
|
};
|
||||||
@@ -64,18 +66,34 @@ pub fn get_fatal(self: *const TextDecoder) bool {
|
|||||||
return self.fatal;
|
return self.fatal;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Should accept an ArrayBuffer, TypedArray or DataView
|
const DecodeOptions = struct {
|
||||||
// js.zig will currently only map a TypedArray to our []const u8.
|
stream: bool = false,
|
||||||
pub fn _decode(self: *const TextDecoder, v: []const u8) ![]const u8 {
|
};
|
||||||
if (self.fatal and !std.unicode.utf8ValidateSlice(v)) {
|
pub fn _decode(self: *TextDecoder, str_: ?[]const u8, opts_: ?DecodeOptions, page: *Page) ![]const u8 {
|
||||||
|
var str = str_ orelse return "";
|
||||||
|
const opts: DecodeOptions = opts_ orelse .{};
|
||||||
|
|
||||||
|
if (self.stream.items.len > 0) {
|
||||||
|
try self.stream.appendSlice(page.arena, str);
|
||||||
|
str = self.stream.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.fatal and !std.unicode.utf8ValidateSlice(str)) {
|
||||||
|
if (opts.stream) {
|
||||||
|
if (self.stream.items.len == 0) {
|
||||||
|
try self.stream.appendSlice(page.arena, str);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
return error.InvalidUtf8;
|
return error.InvalidUtf8;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.ignore_bom == false and std.mem.startsWith(u8, v, &.{ 0xEF, 0xBB, 0xBF })) {
|
self.stream.clearRetainingCapacity();
|
||||||
return v[3..];
|
if (self.ignore_bom == false and std.mem.startsWith(u8, str, &.{ 0xEF, 0xBB, 0xBF })) {
|
||||||
|
return str[3..];
|
||||||
}
|
}
|
||||||
|
|
||||||
return v;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const Env = @import("../env.zig").Env;
|
const js = @import("../js/js.zig");
|
||||||
|
|
||||||
// https://encoding.spec.whatwg.org/#interface-textencoder
|
// https://encoding.spec.whatwg.org/#interface-textencoder
|
||||||
const TextEncoder = @This();
|
const TextEncoder = @This();
|
||||||
@@ -31,7 +31,7 @@ pub fn get_encoding(_: *const TextEncoder) []const u8 {
|
|||||||
return "utf-8";
|
return "utf-8";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) {
|
pub fn _encode(_: *const TextEncoder, v: []const u8) !js.TypedArray(u8) {
|
||||||
// Ensure the input is a valid utf-8
|
// Ensure the input is a valid utf-8
|
||||||
// It seems chrome accepts invalid utf-8 sequence.
|
// It seems chrome accepts invalid utf-8 sequence.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Page = @import("page.zig").Page;
|
|
||||||
const js = @import("../runtime/js.zig");
|
|
||||||
const generate = @import("../runtime/generate.zig");
|
|
||||||
|
|
||||||
const WebApis = struct {
|
|
||||||
// Wrapped like this for debug ergonomics.
|
|
||||||
// When we create our Env, a few lines down, we define it as:
|
|
||||||
// pub const Env = js.Env(*Page, WebApis);
|
|
||||||
//
|
|
||||||
// If there's a compile time error witht he Env, it's type will be readable,
|
|
||||||
// i.e.: runtime.js.Env(*browser.env.Page, browser.env.WebApis)
|
|
||||||
//
|
|
||||||
// But if we didn't wrap it in the struct, like we once didn't, and defined
|
|
||||||
// env as:
|
|
||||||
// pub const Env = js.Env(*Page, Interfaces);
|
|
||||||
//
|
|
||||||
// Because Interfaces is an anynoumous type, it doesn't have a friendly name
|
|
||||||
// and errors would be something like:
|
|
||||||
// runtime.js.Env(*browser.Page, .{...A HUNDRED TYPES...})
|
|
||||||
pub const Interfaces = generate.Tuple(.{
|
|
||||||
@import("crypto/crypto.zig").Crypto,
|
|
||||||
@import("console/console.zig").Console,
|
|
||||||
@import("css/css.zig").Interfaces,
|
|
||||||
@import("cssom/cssom.zig").Interfaces,
|
|
||||||
@import("dom/dom.zig").Interfaces,
|
|
||||||
@import("dom/shadow_root.zig").ShadowRoot,
|
|
||||||
@import("encoding/encoding.zig").Interfaces,
|
|
||||||
@import("events/event.zig").Interfaces,
|
|
||||||
@import("html/html.zig").Interfaces,
|
|
||||||
@import("iterator/iterator.zig").Interfaces,
|
|
||||||
@import("storage/storage.zig").Interfaces,
|
|
||||||
@import("url/url.zig").Interfaces,
|
|
||||||
@import("xhr/xhr.zig").Interfaces,
|
|
||||||
@import("xhr/form_data.zig").Interfaces,
|
|
||||||
@import("xhr/File.zig"),
|
|
||||||
@import("xmlserializer/xmlserializer.zig").Interfaces,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const JsThis = Env.JsThis;
|
|
||||||
pub const JsObject = Env.JsObject;
|
|
||||||
pub const Function = Env.Function;
|
|
||||||
pub const Promise = Env.Promise;
|
|
||||||
pub const PromiseResolver = Env.PromiseResolver;
|
|
||||||
|
|
||||||
pub const Env = js.Env(*Page, WebApis);
|
|
||||||
pub const Global = @import("html/window.zig").Window;
|
|
||||||
57
src/browser/events/composition_event.zig
Normal file
57
src/browser/events/composition_event.zig
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// 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 parser = @import("../netsurf.zig");
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
|
||||||
|
pub const CompositionEvent = struct {
|
||||||
|
data: []const u8,
|
||||||
|
proto: parser.Event,
|
||||||
|
|
||||||
|
pub const union_make_copy = true;
|
||||||
|
pub const prototype = *parser.Event;
|
||||||
|
|
||||||
|
pub const ConstructorOptions = struct {
|
||||||
|
data: []const u8 = "",
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn constructor(event_type: []const u8, options_: ?ConstructorOptions) !CompositionEvent {
|
||||||
|
const options: ConstructorOptions = options_ orelse .{};
|
||||||
|
|
||||||
|
const event = try parser.eventCreate();
|
||||||
|
defer parser.eventDestroy(event);
|
||||||
|
try parser.eventInit(event, event_type, .{});
|
||||||
|
parser.eventSetInternalType(event, .composition_event);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.proto = event.*,
|
||||||
|
.data = options.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_data(self: *const CompositionEvent) []const u8 {
|
||||||
|
return self.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "Browser: Events.Composition" {
|
||||||
|
try testing.htmlRunner("events/composition.html");
|
||||||
|
}
|
||||||
@@ -16,9 +16,10 @@
|
|||||||
// 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 js = @import("../js/js.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Event = @import("event.zig").Event;
|
const Event = @import("event.zig").Event;
|
||||||
const JsObject = @import("../env.zig").JsObject;
|
|
||||||
const netsurf = @import("../netsurf.zig");
|
const netsurf = @import("../netsurf.zig");
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#interface-customevent
|
// https://dom.spec.whatwg.org/#interface-customevent
|
||||||
@@ -27,13 +28,13 @@ pub const CustomEvent = struct {
|
|||||||
pub const union_make_copy = true;
|
pub const union_make_copy = true;
|
||||||
|
|
||||||
proto: parser.Event,
|
proto: parser.Event,
|
||||||
detail: ?JsObject,
|
detail: ?js.Object,
|
||||||
|
|
||||||
const CustomEventInit = struct {
|
const CustomEventInit = struct {
|
||||||
bubbles: bool = false,
|
bubbles: bool = false,
|
||||||
cancelable: bool = false,
|
cancelable: bool = false,
|
||||||
composed: bool = false,
|
composed: bool = false,
|
||||||
detail: ?JsObject = null,
|
detail: ?js.Object = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn constructor(event_type: []const u8, opts_: ?CustomEventInit) !CustomEvent {
|
pub fn constructor(event_type: []const u8, opts_: ?CustomEventInit) !CustomEvent {
|
||||||
@@ -53,7 +54,7 @@ pub const CustomEvent = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_detail(self: *CustomEvent) ?JsObject {
|
pub fn get_detail(self: *CustomEvent) ?js.Object {
|
||||||
return self.detail;
|
return self.detail;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ pub const CustomEvent = struct {
|
|||||||
event_type: []const u8,
|
event_type: []const u8,
|
||||||
can_bubble: bool,
|
can_bubble: bool,
|
||||||
cancelable: bool,
|
cancelable: bool,
|
||||||
maybe_detail: ?JsObject,
|
maybe_detail: ?js.Object,
|
||||||
) !void {
|
) !void {
|
||||||
// This function can only be called after the constructor has called.
|
// This function can only be called after the constructor has called.
|
||||||
// So we assume proto is initialized already by constructor.
|
// So we assume proto is initialized already by constructor.
|
||||||
|
|||||||
@@ -17,11 +17,12 @@
|
|||||||
// 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 js = @import("../js/js.zig");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const generate = @import("../../runtime/generate.zig");
|
const generate = @import("../js/generate.zig");
|
||||||
|
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
const Node = @import("../dom/node.zig").Node;
|
const Node = @import("../dom/node.zig").Node;
|
||||||
@@ -33,11 +34,26 @@ const AbortSignal = @import("../html/AbortController.zig").AbortSignal;
|
|||||||
const CustomEvent = @import("custom_event.zig").CustomEvent;
|
const CustomEvent = @import("custom_event.zig").CustomEvent;
|
||||||
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
|
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
|
||||||
const MouseEvent = @import("mouse_event.zig").MouseEvent;
|
const MouseEvent = @import("mouse_event.zig").MouseEvent;
|
||||||
|
const KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent;
|
||||||
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
|
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
|
||||||
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
|
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
|
||||||
|
const PopStateEvent = @import("../html/History.zig").PopStateEvent;
|
||||||
|
const CompositionEvent = @import("composition_event.zig").CompositionEvent;
|
||||||
|
const NavigationCurrentEntryChangeEvent = @import("../navigation/root.zig").NavigationCurrentEntryChangeEvent;
|
||||||
|
|
||||||
// Event interfaces
|
// Event interfaces
|
||||||
pub const Interfaces = .{ Event, CustomEvent, ProgressEvent, MouseEvent, ErrorEvent, MessageEvent };
|
pub const Interfaces = .{
|
||||||
|
Event,
|
||||||
|
CustomEvent,
|
||||||
|
ProgressEvent,
|
||||||
|
MouseEvent,
|
||||||
|
KeyboardEvent,
|
||||||
|
ErrorEvent,
|
||||||
|
MessageEvent,
|
||||||
|
PopStateEvent,
|
||||||
|
CompositionEvent,
|
||||||
|
NavigationCurrentEntryChangeEvent,
|
||||||
|
};
|
||||||
|
|
||||||
pub const Union = generate.Union(Interfaces);
|
pub const Union = generate.Union(Interfaces);
|
||||||
|
|
||||||
@@ -55,14 +71,20 @@ pub const Event = struct {
|
|||||||
pub const _AT_TARGET = 2;
|
pub const _AT_TARGET = 2;
|
||||||
pub const _BUBBLING_PHASE = 3;
|
pub const _BUBBLING_PHASE = 3;
|
||||||
|
|
||||||
pub fn toInterface(evt: *parser.Event) !Union {
|
pub fn toInterface(evt: *parser.Event) Union {
|
||||||
return switch (try parser.eventGetInternalType(evt)) {
|
return switch (parser.eventGetInternalType(evt)) {
|
||||||
.event, .abort_signal, .xhr_event => .{ .Event = evt },
|
.event, .abort_signal, .xhr_event => .{ .Event = evt },
|
||||||
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
|
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
|
||||||
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
|
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
|
||||||
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
|
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
|
||||||
.error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* },
|
.error_event => .{ .ErrorEvent = (@as(*ErrorEvent, @fieldParentPtr("proto", evt))).* },
|
||||||
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
|
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
|
||||||
|
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
|
||||||
|
.pop_state => .{ .PopStateEvent = @as(*PopStateEvent, @ptrCast(evt)).* },
|
||||||
|
.composition_event => .{ .CompositionEvent = (@as(*CompositionEvent, @fieldParentPtr("proto", evt))).* },
|
||||||
|
.navigation_current_entry_change_event => .{
|
||||||
|
.NavigationCurrentEntryChangeEvent = @as(*NavigationCurrentEntryChangeEvent, @ptrCast(evt)).*,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,46 +96,46 @@ pub const Event = struct {
|
|||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
|
|
||||||
pub fn get_type(self: *parser.Event) ![]const u8 {
|
pub fn get_type(self: *parser.Event) []const u8 {
|
||||||
return try parser.eventType(self);
|
return parser.eventType(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_target(self: *parser.Event, page: *Page) !?EventTargetUnion {
|
pub fn get_target(self: *parser.Event, page: *Page) !?EventTargetUnion {
|
||||||
const et = try parser.eventTarget(self);
|
const et = parser.eventTarget(self);
|
||||||
if (et == null) return null;
|
if (et == null) return null;
|
||||||
return try EventTarget.toInterface(et.?, page);
|
return try EventTarget.toInterface(et.?, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_currentTarget(self: *parser.Event, page: *Page) !?EventTargetUnion {
|
pub fn get_currentTarget(self: *parser.Event, page: *Page) !?EventTargetUnion {
|
||||||
const et = try parser.eventCurrentTarget(self);
|
const et = parser.eventCurrentTarget(self);
|
||||||
if (et == null) return null;
|
if (et == null) return null;
|
||||||
return try EventTarget.toInterface(et.?, page);
|
return try EventTarget.toInterface(et.?, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_eventPhase(self: *parser.Event) !u8 {
|
pub fn get_eventPhase(self: *parser.Event) u8 {
|
||||||
return try parser.eventPhase(self);
|
return parser.eventPhase(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_bubbles(self: *parser.Event) !bool {
|
pub fn get_bubbles(self: *parser.Event) bool {
|
||||||
return try parser.eventBubbles(self);
|
return parser.eventBubbles(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_cancelable(self: *parser.Event) !bool {
|
pub fn get_cancelable(self: *parser.Event) bool {
|
||||||
return try parser.eventCancelable(self);
|
return parser.eventCancelable(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_defaultPrevented(self: *parser.Event) !bool {
|
pub fn get_defaultPrevented(self: *parser.Event) bool {
|
||||||
return try parser.eventDefaultPrevented(self);
|
return parser.eventDefaultPrevented(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_isTrusted(self: *parser.Event) !bool {
|
pub fn get_isTrusted(self: *parser.Event) bool {
|
||||||
return try parser.eventIsTrusted(self);
|
return parser.eventIsTrusted(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Even though this is supposed to to provide microsecond resolution, browser
|
// Even though this is supposed to to provide microsecond resolution, browser
|
||||||
// return coarser values to protect against fingerprinting. libdom returns
|
// return coarser values to protect against fingerprinting. libdom returns
|
||||||
// seconds, which is good enough.
|
// seconds, which is good enough.
|
||||||
pub fn get_timeStamp(self: *parser.Event) !u32 {
|
pub fn get_timeStamp(self: *parser.Event) u64 {
|
||||||
return parser.eventTimestamp(self);
|
return parser.eventTimestamp(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,22 +155,22 @@ pub const Event = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn _stopPropagation(self: *parser.Event) !void {
|
pub fn _stopPropagation(self: *parser.Event) !void {
|
||||||
return try parser.eventStopPropagation(self);
|
return parser.eventStopPropagation(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _stopImmediatePropagation(self: *parser.Event) !void {
|
pub fn _stopImmediatePropagation(self: *parser.Event) !void {
|
||||||
return try parser.eventStopImmediatePropagation(self);
|
return parser.eventStopImmediatePropagation(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _preventDefault(self: *parser.Event) !void {
|
pub fn _preventDefault(self: *parser.Event) !void {
|
||||||
return try parser.eventPreventDefault(self);
|
return parser.eventPreventDefault(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _composedPath(self: *parser.Event, page: *Page) ![]const EventTargetUnion {
|
pub fn _composedPath(self: *parser.Event, page: *Page) ![]const EventTargetUnion {
|
||||||
const et_ = try parser.eventTarget(self);
|
const et_ = parser.eventTarget(self);
|
||||||
const et = et_ orelse return &.{};
|
const et = et_ orelse return &.{};
|
||||||
|
|
||||||
var node: ?*parser.Node = switch (try parser.eventTargetInternalType(et)) {
|
var node: ?*parser.Node = switch (parser.eventTargetInternalType(et)) {
|
||||||
.libdom_node => @as(*parser.Node, @ptrCast(et)),
|
.libdom_node => @as(*parser.Node, @ptrCast(et)),
|
||||||
.plain => parser.eventTargetToNode(et),
|
.plain => parser.eventTargetToNode(et),
|
||||||
else => {
|
else => {
|
||||||
@@ -164,8 +186,8 @@ pub const Event = struct {
|
|||||||
.node = try Node.toInterface(n),
|
.node = try Node.toInterface(n),
|
||||||
});
|
});
|
||||||
|
|
||||||
node = try parser.nodeParentNode(n);
|
node = parser.nodeParentNode(n);
|
||||||
if (node == null and try parser.nodeType(n) == .document_fragment) {
|
if (node == null and parser.nodeType(n) == .document_fragment) {
|
||||||
// we have a non-continuous hook from a shadowroot to its host (
|
// we have a non-continuous hook from a shadowroot to its host (
|
||||||
// it's parent element). libdom doesn't really support ShdowRoots
|
// it's parent element). libdom doesn't really support ShdowRoots
|
||||||
// and, for the most part, that works out well since it naturally
|
// and, for the most part, that works out well since it naturally
|
||||||
@@ -206,18 +228,15 @@ pub const Event = struct {
|
|||||||
pub const EventHandler = struct {
|
pub const EventHandler = struct {
|
||||||
once: bool,
|
once: bool,
|
||||||
capture: bool,
|
capture: bool,
|
||||||
callback: Function,
|
callback: js.Function,
|
||||||
node: parser.EventNode,
|
node: parser.EventNode,
|
||||||
listener: *parser.EventListener,
|
listener: *parser.EventListener,
|
||||||
|
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const Function = Env.Function;
|
|
||||||
|
|
||||||
pub const Listener = union(enum) {
|
pub const Listener = union(enum) {
|
||||||
function: Function,
|
function: js.Function,
|
||||||
object: Env.JsObject,
|
object: js.Object,
|
||||||
|
|
||||||
pub fn callback(self: Listener, target: *parser.EventTarget) !?Function {
|
pub fn callback(self: Listener, target: *parser.EventTarget) !?js.Function {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.function => |func| try func.withThis(target),
|
.function => |func| try func.withThis(target),
|
||||||
.object => |obj| blk: {
|
.object => |obj| blk: {
|
||||||
@@ -316,13 +335,9 @@ pub const EventHandler = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle(node: *parser.EventNode, event: *parser.Event) void {
|
fn handle(node: *parser.EventNode, event: *parser.Event) void {
|
||||||
const ievent = Event.toInterface(event) catch |err| {
|
const ievent = Event.toInterface(event);
|
||||||
log.err(.app, "toInterface error", .{ .err = err });
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
const self: *EventHandler = @fieldParentPtr("node", node);
|
const self: *EventHandler = @fieldParentPtr("node", node);
|
||||||
var result: Function.Result = undefined;
|
var result: js.Function.Result = undefined;
|
||||||
self.callback.tryCall(void, .{ievent}, &result) catch {
|
self.callback.tryCall(void, .{ievent}, &result) catch {
|
||||||
log.debug(.user_script, "callback error", .{
|
log.debug(.user_script, "callback error", .{
|
||||||
.err = result.exception,
|
.err = result.exception,
|
||||||
@@ -332,8 +347,8 @@ pub const EventHandler = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (self.once) {
|
if (self.once) {
|
||||||
const target = (parser.eventTarget(event) catch return).?;
|
const target = parser.eventTarget(event).?;
|
||||||
const typ = parser.eventType(event) catch return;
|
const typ = parser.eventType(event);
|
||||||
parser.eventTargetRemoveEventListener(
|
parser.eventTargetRemoveEventListener(
|
||||||
target,
|
target,
|
||||||
typ,
|
typ,
|
||||||
@@ -388,6 +403,40 @@ const SignalCallback = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub fn DirectEventHandler(
|
||||||
|
comptime TargetT: type,
|
||||||
|
target: *TargetT,
|
||||||
|
event_type: []const u8,
|
||||||
|
maybe_listener: ?EventHandler.Listener,
|
||||||
|
cb: *?js.Function,
|
||||||
|
page_arena: std.mem.Allocator,
|
||||||
|
) !void {
|
||||||
|
const event_target = parser.toEventTarget(TargetT, target);
|
||||||
|
|
||||||
|
// Check if we have a listener set.
|
||||||
|
if (cb.*) |callback| {
|
||||||
|
const listener = try parser.eventTargetHasListener(event_target, event_type, false, callback.id);
|
||||||
|
std.debug.assert(listener != null);
|
||||||
|
try parser.eventTargetRemoveEventListener(event_target, event_type, listener.?, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maybe_listener) |listener| {
|
||||||
|
switch (listener) {
|
||||||
|
// If an object is given as listener, do nothing.
|
||||||
|
.object => {},
|
||||||
|
.function => |callback| {
|
||||||
|
_ = try EventHandler.register(page_arena, event_target, event_type, listener, null) orelse unreachable;
|
||||||
|
cb.* = callback;
|
||||||
|
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just unset the listener.
|
||||||
|
cb.* = null;
|
||||||
|
}
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser: Event" {
|
test "Browser: Event" {
|
||||||
try testing.htmlRunner("events/event.html");
|
try testing.htmlRunner("events/event.html");
|
||||||
|
|||||||
159
src/browser/events/keyboard_event.zig
Normal file
159
src/browser/events/keyboard_event.zig
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
// Copyright (C) 2023-2024 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 builtin = @import("builtin");
|
||||||
|
|
||||||
|
const parser = @import("../netsurf.zig");
|
||||||
|
const Event = @import("event.zig").Event;
|
||||||
|
|
||||||
|
// TODO: We currently don't have a UIEvent interface so we skip it in the prototype chain.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
|
||||||
|
const UIEvent = Event;
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
|
||||||
|
pub const KeyboardEvent = struct {
|
||||||
|
pub const Self = parser.KeyboardEvent;
|
||||||
|
pub const prototype = *UIEvent;
|
||||||
|
|
||||||
|
pub const ConstructorOptions = struct {
|
||||||
|
key: []const u8 = "",
|
||||||
|
code: []const u8 = "",
|
||||||
|
location: parser.KeyboardEventOpts.LocationCode = .standard,
|
||||||
|
repeat: bool = false,
|
||||||
|
isComposing: bool = false,
|
||||||
|
// Currently not supported but we take as argument.
|
||||||
|
charCode: u32 = 0,
|
||||||
|
// Currently not supported but we take as argument.
|
||||||
|
keyCode: u32 = 0,
|
||||||
|
// Currently not supported but we take as argument.
|
||||||
|
which: u32 = 0,
|
||||||
|
ctrlKey: bool = false,
|
||||||
|
shiftKey: bool = false,
|
||||||
|
altKey: bool = false,
|
||||||
|
metaKey: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn constructor(event_type: []const u8, maybe_options: ?ConstructorOptions) !*parser.KeyboardEvent {
|
||||||
|
const options: ConstructorOptions = maybe_options orelse .{};
|
||||||
|
|
||||||
|
const event = try parser.keyboardEventCreate();
|
||||||
|
parser.eventSetInternalType(@ptrCast(event), .keyboard_event);
|
||||||
|
|
||||||
|
try parser.keyboardEventInit(
|
||||||
|
event,
|
||||||
|
event_type,
|
||||||
|
.{
|
||||||
|
.key = options.key,
|
||||||
|
.code = options.code,
|
||||||
|
.location = options.location,
|
||||||
|
.repeat = options.repeat,
|
||||||
|
.is_composing = options.isComposing,
|
||||||
|
.ctrl_key = options.ctrlKey,
|
||||||
|
.shift_key = options.shiftKey,
|
||||||
|
.alt_key = options.altKey,
|
||||||
|
.meta_key = options.metaKey,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the modifier state for given modifier key.
|
||||||
|
pub fn _getModifierState(self: *Self, key: []const u8) bool {
|
||||||
|
// Chrome and Firefox do case-sensitive match, here we prefer the same.
|
||||||
|
if (std.mem.eql(u8, key, "Alt")) {
|
||||||
|
return get_altKey(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, key, "AltGraph")) {
|
||||||
|
return (get_altKey(self) and get_ctrlKey(self));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, key, "Control")) {
|
||||||
|
return get_ctrlKey(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, key, "Shift")) {
|
||||||
|
return get_shiftKey(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, key, "Meta") or std.mem.eql(u8, key, "OS")) {
|
||||||
|
return get_metaKey(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case for IE.
|
||||||
|
if (comptime builtin.os.tag == .windows) {
|
||||||
|
if (std.mem.eql(u8, key, "Win")) {
|
||||||
|
return get_metaKey(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getModifierState() also accepts a deprecated virtual modifier named "Accel".
|
||||||
|
// event.getModifierState("Accel") returns true when at least one of
|
||||||
|
// KeyboardEvent.ctrlKey or KeyboardEvent.metaKey is true.
|
||||||
|
//
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState#accel_virtual_modifier
|
||||||
|
if (std.mem.eql(u8, key, "Accel")) {
|
||||||
|
return (get_ctrlKey(self) or get_metaKey(self));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add support for "CapsLock", "ScrollLock".
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters.
|
||||||
|
|
||||||
|
pub fn get_altKey(self: *Self) bool {
|
||||||
|
return parser.keyboardEventKeyIsSet(self, .alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_ctrlKey(self: *Self) bool {
|
||||||
|
return parser.keyboardEventKeyIsSet(self, .ctrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_metaKey(self: *Self) bool {
|
||||||
|
return parser.keyboardEventKeyIsSet(self, .meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_shiftKey(self: *Self) bool {
|
||||||
|
return parser.keyboardEventKeyIsSet(self, .shift);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_isComposing(self: *Self) bool {
|
||||||
|
return self.is_composing;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_location(self: *Self) u32 {
|
||||||
|
return self.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_key(self: *Self) ![]const u8 {
|
||||||
|
return parser.keyboardEventGetKey(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_repeat(self: *Self) bool {
|
||||||
|
return self.repeat;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "Browser: Events.Keyboard" {
|
||||||
|
try testing.htmlRunner("events/keyboard.html");
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@ const log = @import("../../log.zig");
|
|||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Event = @import("event.zig").Event;
|
const Event = @import("event.zig").Event;
|
||||||
const JsObject = @import("../env.zig").JsObject;
|
|
||||||
|
|
||||||
// TODO: We currently don't have a UIEvent interface so we skip it in the prototype chain.
|
// TODO: We currently don't have a UIEvent interface so we skip it in the prototype chain.
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
|
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
|
||||||
@@ -55,8 +54,8 @@ pub const MouseEvent = struct {
|
|||||||
pub fn constructor(event_type: []const u8, opts_: ?MouseEventInit) !*parser.MouseEvent {
|
pub fn constructor(event_type: []const u8, opts_: ?MouseEventInit) !*parser.MouseEvent {
|
||||||
const opts = opts_ orelse MouseEventInit{};
|
const opts = opts_ orelse MouseEventInit{};
|
||||||
|
|
||||||
var mouse_event = try parser.mouseEventCreate();
|
const mouse_event = try parser.mouseEventCreate();
|
||||||
try parser.eventSetInternalType(@ptrCast(&mouse_event), .mouse_event);
|
parser.eventSetInternalType(@ptrCast(mouse_event), .mouse_event);
|
||||||
|
|
||||||
try parser.mouseEventInit(mouse_event, event_type, .{
|
try parser.mouseEventInit(mouse_event, event_type, .{
|
||||||
.x = opts.clientX,
|
.x = opts.clientX,
|
||||||
@@ -69,7 +68,7 @@ pub const MouseEvent = struct {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!std.mem.eql(u8, event_type, "click")) {
|
if (!std.mem.eql(u8, event_type, "click")) {
|
||||||
log.warn(.mouse_event, "unsupported mouse event", .{ .event = event_type });
|
log.warn(.browser, "unsupported mouse event", .{ .event = event_type });
|
||||||
}
|
}
|
||||||
|
|
||||||
return mouse_event;
|
return mouse_event;
|
||||||
|
|||||||
225
src/browser/fetch/Headers.zig
Normal file
225
src/browser/fetch/Headers.zig
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
const URL = @import("../../url.zig").URL;
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
|
const iterator = @import("../iterator/iterator.zig");
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Headers
|
||||||
|
const Headers = @This();
|
||||||
|
|
||||||
|
// Case-Insensitive String HashMap.
|
||||||
|
// This allows us to avoid having to allocate lowercase keys all the time.
|
||||||
|
const HeaderHashMap = std.HashMapUnmanaged([]const u8, []const u8, struct {
|
||||||
|
pub fn hash(_: @This(), s: []const u8) u64 {
|
||||||
|
var buf: [64]u8 = undefined;
|
||||||
|
var hasher = std.hash.Wyhash.init(s.len);
|
||||||
|
|
||||||
|
var key = s;
|
||||||
|
while (key.len >= 64) {
|
||||||
|
const lower = std.ascii.lowerString(buf[0..], key[0..64]);
|
||||||
|
hasher.update(lower);
|
||||||
|
key = key[64..];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.len > 0) {
|
||||||
|
const lower = std.ascii.lowerString(buf[0..key.len], key);
|
||||||
|
hasher.update(lower);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasher.final();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eql(_: @This(), a: []const u8, b: []const u8) bool {
|
||||||
|
return std.ascii.eqlIgnoreCase(a, b);
|
||||||
|
}
|
||||||
|
}, 80);
|
||||||
|
|
||||||
|
headers: HeaderHashMap = .empty,
|
||||||
|
|
||||||
|
// They can either be:
|
||||||
|
//
|
||||||
|
// 1. An array of string pairs.
|
||||||
|
// 2. An object with string keys to string values.
|
||||||
|
// 3. Another Headers object.
|
||||||
|
pub const HeadersInit = union(enum) {
|
||||||
|
// List of Pairs of []const u8
|
||||||
|
strings: []const [2][]const u8,
|
||||||
|
// Headers
|
||||||
|
headers: *Headers,
|
||||||
|
// Mappings
|
||||||
|
object: js.Object,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn constructor(_init: ?HeadersInit, page: *Page) !Headers {
|
||||||
|
const arena = page.arena;
|
||||||
|
var headers: HeaderHashMap = .empty;
|
||||||
|
|
||||||
|
if (_init) |init| {
|
||||||
|
switch (init) {
|
||||||
|
.strings => |kvs| {
|
||||||
|
for (kvs) |pair| {
|
||||||
|
const key = try arena.dupe(u8, pair[0]);
|
||||||
|
const value = try arena.dupe(u8, pair[1]);
|
||||||
|
|
||||||
|
try headers.put(arena, key, value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.headers => |hdrs| {
|
||||||
|
var iter = hdrs.headers.iterator();
|
||||||
|
while (iter.next()) |entry| {
|
||||||
|
try headers.put(arena, entry.key_ptr.*, entry.value_ptr.*);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.object => |obj| {
|
||||||
|
var iter = obj.nameIterator();
|
||||||
|
while (try iter.next()) |name_value| {
|
||||||
|
const name = try name_value.toString(arena);
|
||||||
|
const value = try obj.get(name);
|
||||||
|
const value_string = try value.toString(arena);
|
||||||
|
|
||||||
|
try headers.put(arena, name, value_string);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.headers = headers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn append(self: *Headers, name: []const u8, value: []const u8, allocator: std.mem.Allocator) !void {
|
||||||
|
const key = try allocator.dupe(u8, name);
|
||||||
|
const gop = try self.headers.getOrPut(allocator, key);
|
||||||
|
|
||||||
|
if (gop.found_existing) {
|
||||||
|
// If we found it, append the value.
|
||||||
|
const new_value = try std.fmt.allocPrint(allocator, "{s}, {s}", .{ gop.value_ptr.*, value });
|
||||||
|
gop.value_ptr.* = new_value;
|
||||||
|
} else {
|
||||||
|
// Otherwise, we should just put it in.
|
||||||
|
gop.value_ptr.* = try allocator.dupe(u8, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
|
||||||
|
const arena = page.arena;
|
||||||
|
try self.append(name, value, arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _delete(self: *Headers, name: []const u8) void {
|
||||||
|
_ = self.headers.remove(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const HeadersEntryIterator = struct {
|
||||||
|
slot: [2][]const u8,
|
||||||
|
iter: HeaderHashMap.Iterator,
|
||||||
|
|
||||||
|
// TODO: these SHOULD be in lexigraphical order but I'm not sure how actually
|
||||||
|
// important that is.
|
||||||
|
pub fn _next(self: *HeadersEntryIterator) ?[2][]const u8 {
|
||||||
|
if (self.iter.next()) |entry| {
|
||||||
|
self.slot[0] = entry.key_ptr.*;
|
||||||
|
self.slot[1] = entry.value_ptr.*;
|
||||||
|
return self.slot;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn _entries(self: *const Headers) HeadersEntryIterable {
|
||||||
|
return .{
|
||||||
|
.inner = .{
|
||||||
|
.slot = undefined,
|
||||||
|
.iter = self.headers.iterator(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _forEach(self: *Headers, callback_fn: js.Function, this_arg: ?js.Object) !void {
|
||||||
|
var iter = self.headers.iterator();
|
||||||
|
|
||||||
|
const cb = if (this_arg) |this| try callback_fn.withThis(this) else callback_fn;
|
||||||
|
|
||||||
|
while (iter.next()) |entry| {
|
||||||
|
try cb.call(void, .{ entry.key_ptr.*, entry.value_ptr.*, self });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _get(self: *const Headers, name: []const u8) ?[]const u8 {
|
||||||
|
return self.headers.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _has(self: *const Headers, name: []const u8) bool {
|
||||||
|
return self.headers.contains(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const HeadersKeyIterator = struct {
|
||||||
|
iter: HeaderHashMap.KeyIterator,
|
||||||
|
|
||||||
|
pub fn _next(self: *HeadersKeyIterator) ?[]const u8 {
|
||||||
|
if (self.iter.next()) |key| {
|
||||||
|
return key.*;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn _keys(self: *const Headers) HeadersKeyIterable {
|
||||||
|
return .{ .inner = .{ .iter = self.headers.keyIterator() } };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _set(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
|
||||||
|
const arena = page.arena;
|
||||||
|
|
||||||
|
const key = try arena.dupe(u8, name);
|
||||||
|
const gop = try self.headers.getOrPut(arena, key);
|
||||||
|
gop.value_ptr.* = try arena.dupe(u8, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const HeadersValueIterator = struct {
|
||||||
|
iter: HeaderHashMap.ValueIterator,
|
||||||
|
|
||||||
|
pub fn _next(self: *HeadersValueIterator) ?[]const u8 {
|
||||||
|
if (self.iter.next()) |value| {
|
||||||
|
return value.*;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn _values(self: *const Headers) HeadersValueIterable {
|
||||||
|
return .{ .inner = .{ .iter = self.headers.valueIterator() } };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const HeadersKeyIterable = iterator.Iterable(HeadersKeyIterator, "HeadersKeyIterator");
|
||||||
|
pub const HeadersValueIterable = iterator.Iterable(HeadersValueIterator, "HeadersValueIterator");
|
||||||
|
pub const HeadersEntryIterable = iterator.Iterable(HeadersEntryIterator, "HeadersEntryIterator");
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "fetch: Headers" {
|
||||||
|
try testing.htmlRunner("fetch/headers.html");
|
||||||
|
}
|
||||||
283
src/browser/fetch/Request.zig
Normal file
283
src/browser/fetch/Request.zig
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
|
const URL = @import("../../url.zig").URL;
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
|
const Response = @import("./Response.zig");
|
||||||
|
const Http = @import("../../http/Http.zig");
|
||||||
|
const ReadableStream = @import("../streams/ReadableStream.zig");
|
||||||
|
|
||||||
|
const Headers = @import("Headers.zig");
|
||||||
|
const HeadersInit = @import("Headers.zig").HeadersInit;
|
||||||
|
|
||||||
|
pub const RequestInput = union(enum) {
|
||||||
|
string: []const u8,
|
||||||
|
request: *Request,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RequestCache = enum {
|
||||||
|
default,
|
||||||
|
@"no-store",
|
||||||
|
reload,
|
||||||
|
@"no-cache",
|
||||||
|
@"force-cache",
|
||||||
|
@"only-if-cached",
|
||||||
|
|
||||||
|
pub fn fromString(str: []const u8) ?RequestCache {
|
||||||
|
for (std.enums.values(RequestCache)) |cache| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toString(self: RequestCache) []const u8 {
|
||||||
|
return @tagName(self);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RequestCredentials = enum {
|
||||||
|
omit,
|
||||||
|
@"same-origin",
|
||||||
|
include,
|
||||||
|
|
||||||
|
pub fn fromString(str: []const u8) ?RequestCredentials {
|
||||||
|
for (std.enums.values(RequestCredentials)) |cache| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toString(self: RequestCredentials) []const u8 {
|
||||||
|
return @tagName(self);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RequestMode = enum {
|
||||||
|
cors,
|
||||||
|
@"no-cors",
|
||||||
|
@"same-origin",
|
||||||
|
navigate,
|
||||||
|
|
||||||
|
pub fn fromString(str: []const u8) ?RequestMode {
|
||||||
|
for (std.enums.values(RequestMode)) |cache| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toString(self: RequestMode) []const u8 {
|
||||||
|
return @tagName(self);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
|
||||||
|
pub const RequestInit = struct {
|
||||||
|
body: ?[]const u8 = null,
|
||||||
|
cache: ?[]const u8 = null,
|
||||||
|
credentials: ?[]const u8 = null,
|
||||||
|
headers: ?HeadersInit = null,
|
||||||
|
integrity: ?[]const u8 = null,
|
||||||
|
method: ?[]const u8 = null,
|
||||||
|
mode: ?[]const u8 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
|
||||||
|
const Request = @This();
|
||||||
|
|
||||||
|
method: Http.Method,
|
||||||
|
url: [:0]const u8,
|
||||||
|
cache: RequestCache,
|
||||||
|
credentials: RequestCredentials,
|
||||||
|
// no-cors is default is not built with constructor.
|
||||||
|
mode: RequestMode = .@"no-cors",
|
||||||
|
headers: Headers,
|
||||||
|
body: ?[]const u8,
|
||||||
|
body_used: bool = false,
|
||||||
|
integrity: []const u8,
|
||||||
|
|
||||||
|
pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Request {
|
||||||
|
const arena = page.arena;
|
||||||
|
const options: RequestInit = _options orelse .{};
|
||||||
|
|
||||||
|
const url: [:0]const u8 = blk: switch (input) {
|
||||||
|
.string => |str| {
|
||||||
|
break :blk try URL.stitch(arena, str, page.url.raw, .{ .null_terminated = true });
|
||||||
|
},
|
||||||
|
.request => |req| {
|
||||||
|
break :blk try arena.dupeZ(u8, req.url);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const cache = (if (options.cache) |cache| RequestCache.fromString(cache) else null) orelse RequestCache.default;
|
||||||
|
const credentials = (if (options.credentials) |creds| RequestCredentials.fromString(creds) else null) orelse RequestCredentials.@"same-origin";
|
||||||
|
const integrity = if (options.integrity) |integ| try arena.dupe(u8, integ) else "";
|
||||||
|
const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{};
|
||||||
|
const mode = (if (options.mode) |mode| RequestMode.fromString(mode) else null) orelse RequestMode.cors;
|
||||||
|
|
||||||
|
const method: Http.Method = blk: {
|
||||||
|
if (options.method) |given_method| {
|
||||||
|
for (std.enums.values(Http.Method)) |method| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(given_method, @tagName(method))) {
|
||||||
|
break :blk method;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break :blk Http.Method.GET;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Can't have a body on .GET or .HEAD.
|
||||||
|
const body: ?[]const u8 = blk: {
|
||||||
|
if (method == .GET or method == .HEAD) {
|
||||||
|
break :blk null;
|
||||||
|
} else break :blk if (options.body) |body| try arena.dupe(u8, body) else null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.method = method,
|
||||||
|
.url = url,
|
||||||
|
.cache = cache,
|
||||||
|
.credentials = credentials,
|
||||||
|
.mode = mode,
|
||||||
|
.headers = headers,
|
||||||
|
.body = body,
|
||||||
|
.integrity = integrity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_body(self: *const Request, page: *Page) !?*ReadableStream {
|
||||||
|
if (self.body) |body| {
|
||||||
|
const stream = try ReadableStream.constructor(null, null, page);
|
||||||
|
try stream.queue.append(page.arena, .{ .string = body });
|
||||||
|
return stream;
|
||||||
|
} else return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_bodyUsed(self: *const Request) bool {
|
||||||
|
return self.body_used;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_cache(self: *const Request) RequestCache {
|
||||||
|
return self.cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_credentials(self: *const Request) RequestCredentials {
|
||||||
|
return self.credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_headers(self: *Request) *Headers {
|
||||||
|
return &self.headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_integrity(self: *const Request) []const u8 {
|
||||||
|
return self.integrity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: If we ever support the Navigation API, we need isHistoryNavigation
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Request/isHistoryNavigation
|
||||||
|
|
||||||
|
pub fn get_method(self: *const Request) []const u8 {
|
||||||
|
return @tagName(self.method);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mode(self: *const Request) RequestMode {
|
||||||
|
return self.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_url(self: *const Request) []const u8 {
|
||||||
|
return self.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _clone(self: *Request) !Request {
|
||||||
|
// Not allowed to clone if the body was used.
|
||||||
|
if (self.body_used) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK to just return the same fields BECAUSE
|
||||||
|
// all of these fields are read-only and can't be modified.
|
||||||
|
return Request{
|
||||||
|
.body = self.body,
|
||||||
|
.body_used = self.body_used,
|
||||||
|
.cache = self.cache,
|
||||||
|
.credentials = self.credentials,
|
||||||
|
.headers = self.headers,
|
||||||
|
.method = self.method,
|
||||||
|
.integrity = self.integrity,
|
||||||
|
.url = self.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _bytes(self: *Response, page: *Page) !js.Promise {
|
||||||
|
if (self.body_used) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
self.body_used = true;
|
||||||
|
return page.js.resolvePromise(self.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _json(self: *Response, page: *Page) !js.Promise {
|
||||||
|
if (self.body_used) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
self.body_used = true;
|
||||||
|
|
||||||
|
if (self.body) |body| {
|
||||||
|
const p = std.json.parseFromSliceLeaky(
|
||||||
|
std.json.Value,
|
||||||
|
page.call_arena,
|
||||||
|
body,
|
||||||
|
.{},
|
||||||
|
) catch |e| {
|
||||||
|
log.info(.browser, "invalid json", .{ .err = e, .source = "Request" });
|
||||||
|
return error.SyntaxError;
|
||||||
|
};
|
||||||
|
|
||||||
|
return page.js.resolvePromise(p);
|
||||||
|
}
|
||||||
|
return page.js.resolvePromise(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _text(self: *Response, page: *Page) !js.Promise {
|
||||||
|
if (self.body_used) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
self.body_used = true;
|
||||||
|
return page.js.resolvePromise(self.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "fetch: Request" {
|
||||||
|
try testing.htmlRunner("fetch/request.html");
|
||||||
|
}
|
||||||
209
src/browser/fetch/Response.zig
Normal file
209
src/browser/fetch/Response.zig
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
|
const HttpClient = @import("../../http/Client.zig");
|
||||||
|
const Http = @import("../../http/Http.zig");
|
||||||
|
const URL = @import("../../url.zig").URL;
|
||||||
|
|
||||||
|
const ReadableStream = @import("../streams/ReadableStream.zig");
|
||||||
|
const Headers = @import("Headers.zig");
|
||||||
|
const HeadersInit = @import("Headers.zig").HeadersInit;
|
||||||
|
|
||||||
|
const Mime = @import("../mime.zig").Mime;
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Response
|
||||||
|
const Response = @This();
|
||||||
|
|
||||||
|
status: u16 = 200,
|
||||||
|
status_text: []const u8 = "",
|
||||||
|
headers: Headers,
|
||||||
|
mime: ?Mime = null,
|
||||||
|
url: []const u8 = "",
|
||||||
|
body: ?[]const u8 = null,
|
||||||
|
body_used: bool = false,
|
||||||
|
redirected: bool = false,
|
||||||
|
type: ResponseType = .basic,
|
||||||
|
|
||||||
|
const ResponseBody = union(enum) {
|
||||||
|
string: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ResponseOptions = struct {
|
||||||
|
status: u16 = 200,
|
||||||
|
statusText: ?[]const u8 = null,
|
||||||
|
headers: ?HeadersInit = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ResponseType = enum {
|
||||||
|
basic,
|
||||||
|
cors,
|
||||||
|
@"error",
|
||||||
|
@"opaque",
|
||||||
|
opaqueredirect,
|
||||||
|
|
||||||
|
pub fn fromString(str: []const u8) ?ResponseType {
|
||||||
|
for (std.enums.values(ResponseType)) |cache| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toString(self: ResponseType) []const u8 {
|
||||||
|
return @tagName(self);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Page) !Response {
|
||||||
|
const arena = page.arena;
|
||||||
|
|
||||||
|
const options: ResponseOptions = _options orelse .{};
|
||||||
|
|
||||||
|
const body = blk: {
|
||||||
|
if (_input) |input| {
|
||||||
|
switch (input) {
|
||||||
|
.string => |str| {
|
||||||
|
break :blk try arena.dupe(u8, str);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break :blk null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{};
|
||||||
|
const status_text = if (options.statusText) |st| try arena.dupe(u8, st) else "";
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.body = body,
|
||||||
|
.headers = headers,
|
||||||
|
.status = options.status,
|
||||||
|
.status_text = status_text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_body(self: *const Response, page: *Page) !*ReadableStream {
|
||||||
|
const stream = try ReadableStream.constructor(null, null, page);
|
||||||
|
if (self.body) |body| {
|
||||||
|
try stream.queue.append(page.arena, .{ .string = body });
|
||||||
|
}
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_bodyUsed(self: *const Response) bool {
|
||||||
|
return self.body_used;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_headers(self: *Response) *Headers {
|
||||||
|
return &self.headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_ok(self: *const Response) bool {
|
||||||
|
return self.status >= 200 and self.status <= 299;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_redirected(self: *const Response) bool {
|
||||||
|
return self.redirected;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_status(self: *const Response) u16 {
|
||||||
|
return self.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_statusText(self: *const Response) []const u8 {
|
||||||
|
return self.status_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_type(self: *const Response) ResponseType {
|
||||||
|
return self.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_url(self: *const Response) []const u8 {
|
||||||
|
return self.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _clone(self: *const Response) !Response {
|
||||||
|
if (self.body_used) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK to just return the same fields BECAUSE
|
||||||
|
// all of these fields are read-only and can't be modified.
|
||||||
|
return Response{
|
||||||
|
.body = self.body,
|
||||||
|
.body_used = self.body_used,
|
||||||
|
.mime = self.mime,
|
||||||
|
.headers = self.headers,
|
||||||
|
.redirected = self.redirected,
|
||||||
|
.status = self.status,
|
||||||
|
.url = self.url,
|
||||||
|
.type = self.type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _bytes(self: *Response, page: *Page) !js.Promise {
|
||||||
|
if (self.body_used) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.body_used = true;
|
||||||
|
return page.js.resolvePromise(self.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _json(self: *Response, page: *Page) !js.Promise {
|
||||||
|
if (self.body_used) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.body) |body| {
|
||||||
|
self.body_used = true;
|
||||||
|
const p = std.json.parseFromSliceLeaky(
|
||||||
|
std.json.Value,
|
||||||
|
page.call_arena,
|
||||||
|
body,
|
||||||
|
.{},
|
||||||
|
) catch |e| {
|
||||||
|
log.info(.browser, "invalid json", .{ .err = e, .source = "Response" });
|
||||||
|
return error.SyntaxError;
|
||||||
|
};
|
||||||
|
|
||||||
|
return page.js.resolvePromise(p);
|
||||||
|
}
|
||||||
|
return page.js.resolvePromise(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _text(self: *Response, page: *Page) !js.Promise {
|
||||||
|
if (self.body_used) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
self.body_used = true;
|
||||||
|
|
||||||
|
return page.js.resolvePromise(self.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "fetch: Response" {
|
||||||
|
try testing.htmlRunner("fetch/response.html");
|
||||||
|
}
|
||||||
243
src/browser/fetch/fetch.zig
Normal file
243
src/browser/fetch/fetch.zig
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
// Copyright (C) 2023-2024 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 js = @import("../js/js.zig");
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
|
const Http = @import("../../http/Http.zig");
|
||||||
|
const HttpClient = @import("../../http/Client.zig");
|
||||||
|
const Mime = @import("../mime.zig").Mime;
|
||||||
|
|
||||||
|
const Headers = @import("Headers.zig");
|
||||||
|
|
||||||
|
const RequestInput = @import("Request.zig").RequestInput;
|
||||||
|
const RequestInit = @import("Request.zig").RequestInit;
|
||||||
|
const Request = @import("Request.zig");
|
||||||
|
const Response = @import("Response.zig");
|
||||||
|
|
||||||
|
pub const Interfaces = .{
|
||||||
|
@import("Headers.zig"),
|
||||||
|
@import("Headers.zig").HeadersEntryIterable,
|
||||||
|
@import("Headers.zig").HeadersKeyIterable,
|
||||||
|
@import("Headers.zig").HeadersValueIterable,
|
||||||
|
@import("Request.zig"),
|
||||||
|
@import("Response.zig"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const FetchContext = struct {
|
||||||
|
page: *Page,
|
||||||
|
arena: std.mem.Allocator,
|
||||||
|
promise_resolver: js.PersistentPromiseResolver,
|
||||||
|
|
||||||
|
method: Http.Method,
|
||||||
|
url: []const u8,
|
||||||
|
body: std.ArrayListUnmanaged(u8) = .empty,
|
||||||
|
headers: std.ArrayListUnmanaged([]const u8) = .empty,
|
||||||
|
status: u16 = 0,
|
||||||
|
mime: ?Mime = null,
|
||||||
|
mode: Request.RequestMode,
|
||||||
|
transfer: ?*HttpClient.Transfer = null,
|
||||||
|
|
||||||
|
/// This effectively takes ownership of the FetchContext.
|
||||||
|
///
|
||||||
|
/// We just return the underlying slices used for `headers`
|
||||||
|
/// and for `body` here to avoid an allocation.
|
||||||
|
pub fn toResponse(self: *const FetchContext) !Response {
|
||||||
|
var headers: Headers = .{};
|
||||||
|
|
||||||
|
// seems to be the highest priority
|
||||||
|
const same_origin = try self.page.isSameOrigin(self.url);
|
||||||
|
|
||||||
|
// If the mode is "no-cors", we need to return this opaque/stripped Response.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Response/type
|
||||||
|
if (!same_origin and self.mode == .@"no-cors") {
|
||||||
|
return Response{
|
||||||
|
.status = 0,
|
||||||
|
.headers = headers,
|
||||||
|
.mime = self.mime,
|
||||||
|
.body = null,
|
||||||
|
.url = self.url,
|
||||||
|
.type = .@"opaque",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert into Headers
|
||||||
|
for (self.headers.items) |hdr| {
|
||||||
|
var iter = std.mem.splitScalar(u8, hdr, ':');
|
||||||
|
const name = iter.next() orelse "";
|
||||||
|
const value = iter.next() orelse "";
|
||||||
|
try headers.append(name, value, self.arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp_type: Response.ResponseType = blk: {
|
||||||
|
if (same_origin or std.mem.startsWith(u8, self.url, "data:")) {
|
||||||
|
break :blk .basic;
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk switch (self.mode) {
|
||||||
|
.cors => .cors,
|
||||||
|
.@"same-origin", .navigate => .basic,
|
||||||
|
.@"no-cors" => unreachable,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return Response{
|
||||||
|
.status = self.status,
|
||||||
|
.headers = headers,
|
||||||
|
.mime = self.mime,
|
||||||
|
.body = self.body.items,
|
||||||
|
.url = self.url,
|
||||||
|
.type = resp_type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
|
||||||
|
pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !js.Promise {
|
||||||
|
const arena = page.arena;
|
||||||
|
|
||||||
|
const req = try Request.constructor(input, options, page);
|
||||||
|
var headers = try page.http_client.newHeaders();
|
||||||
|
|
||||||
|
// Copy our headers into the HTTP headers.
|
||||||
|
var header_iter = req.headers.headers.iterator();
|
||||||
|
while (header_iter.next()) |entry| {
|
||||||
|
const combined = try std.fmt.allocPrintSentinel(
|
||||||
|
page.arena,
|
||||||
|
"{s}: {s}",
|
||||||
|
.{ entry.key_ptr.*, entry.value_ptr.* },
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
try headers.add(combined.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
try page.requestCookie(.{}).headersForRequest(arena, req.url, &headers);
|
||||||
|
|
||||||
|
const resolver = try page.js.createPromiseResolver(.page);
|
||||||
|
|
||||||
|
const fetch_ctx = try arena.create(FetchContext);
|
||||||
|
fetch_ctx.* = .{
|
||||||
|
.page = page,
|
||||||
|
.arena = arena,
|
||||||
|
.promise_resolver = resolver,
|
||||||
|
.method = req.method,
|
||||||
|
.url = req.url,
|
||||||
|
.mode = req.mode,
|
||||||
|
};
|
||||||
|
|
||||||
|
try page.http_client.request(.{
|
||||||
|
.ctx = @ptrCast(fetch_ctx),
|
||||||
|
.url = req.url,
|
||||||
|
.method = req.method,
|
||||||
|
.headers = headers,
|
||||||
|
.body = req.body,
|
||||||
|
.cookie_jar = page.cookie_jar,
|
||||||
|
.resource_type = .fetch,
|
||||||
|
|
||||||
|
.start_callback = struct {
|
||||||
|
fn startCallback(transfer: *HttpClient.Transfer) !void {
|
||||||
|
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
|
||||||
|
log.debug(.fetch, "request start", .{ .method = self.method, .url = self.url, .source = "fetch" });
|
||||||
|
|
||||||
|
self.transfer = transfer;
|
||||||
|
}
|
||||||
|
}.startCallback,
|
||||||
|
.header_callback = struct {
|
||||||
|
fn headerCallback(transfer: *HttpClient.Transfer) !void {
|
||||||
|
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
|
||||||
|
|
||||||
|
const header = &transfer.response_header.?;
|
||||||
|
|
||||||
|
log.debug(.fetch, "request header", .{
|
||||||
|
.source = "fetch",
|
||||||
|
.method = self.method,
|
||||||
|
.url = self.url,
|
||||||
|
.status = header.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (header.contentType()) |ct| {
|
||||||
|
self.mime = Mime.parse(ct) catch {
|
||||||
|
return error.MimeParsing;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transfer.getContentLength()) |cl| {
|
||||||
|
try self.body.ensureTotalCapacity(self.arena, cl);
|
||||||
|
}
|
||||||
|
|
||||||
|
var it = transfer.responseHeaderIterator();
|
||||||
|
while (it.next()) |hdr| {
|
||||||
|
const joined = try std.fmt.allocPrint(self.arena, "{s}: {s}", .{ hdr.name, hdr.value });
|
||||||
|
try self.headers.append(self.arena, joined);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.status = header.status;
|
||||||
|
}
|
||||||
|
}.headerCallback,
|
||||||
|
.data_callback = struct {
|
||||||
|
fn dataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
|
||||||
|
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
|
||||||
|
try self.body.appendSlice(self.arena, data);
|
||||||
|
}
|
||||||
|
}.dataCallback,
|
||||||
|
.done_callback = struct {
|
||||||
|
fn doneCallback(ctx: *anyopaque) !void {
|
||||||
|
const self: *FetchContext = @ptrCast(@alignCast(ctx));
|
||||||
|
self.transfer = null;
|
||||||
|
|
||||||
|
log.info(.fetch, "request complete", .{
|
||||||
|
.source = "fetch",
|
||||||
|
.method = self.method,
|
||||||
|
.url = self.url,
|
||||||
|
.status = self.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = try self.toResponse();
|
||||||
|
try self.promise_resolver.resolve(response);
|
||||||
|
}
|
||||||
|
}.doneCallback,
|
||||||
|
.error_callback = struct {
|
||||||
|
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||||
|
const self: *FetchContext = @ptrCast(@alignCast(ctx));
|
||||||
|
self.transfer = null;
|
||||||
|
|
||||||
|
log.err(.fetch, "error", .{
|
||||||
|
.url = self.url,
|
||||||
|
.err = err,
|
||||||
|
.source = "fetch error",
|
||||||
|
});
|
||||||
|
|
||||||
|
// We throw an Abort error when the page is getting closed so,
|
||||||
|
// in this case, we don't need to reject the promise.
|
||||||
|
if (err != error.Abort) {
|
||||||
|
self.promise_resolver.reject(@errorName(err)) catch unreachable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.errorCallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
return resolver.promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "fetch: fetch" {
|
||||||
|
try testing.htmlRunner("fetch/fetch.html");
|
||||||
|
}
|
||||||
@@ -17,9 +17,9 @@
|
|||||||
// 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 js = @import("../js/js.zig");
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ pub const AbortSignal = struct {
|
|||||||
self.reason = reason_ orelse DEFAULT_REASON;
|
self.reason = reason_ orelse DEFAULT_REASON;
|
||||||
|
|
||||||
const abort_event = try parser.eventCreate();
|
const abort_event = try parser.eventCreate();
|
||||||
try parser.eventSetInternalType(abort_event, .abort_signal);
|
parser.eventSetInternalType(abort_event, .abort_signal);
|
||||||
|
|
||||||
defer parser.eventDestroy(abort_event);
|
defer parser.eventDestroy(abort_event);
|
||||||
try parser.eventInit(abort_event, "abort", .{});
|
try parser.eventInit(abort_event, "abort", .{});
|
||||||
@@ -113,12 +113,12 @@ pub const AbortSignal = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ThrowIfAborted = union(enum) {
|
const ThrowIfAborted = union(enum) {
|
||||||
exception: Env.Exception,
|
exception: js.Exception,
|
||||||
undefined: void,
|
undefined: void,
|
||||||
};
|
};
|
||||||
pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted {
|
pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted {
|
||||||
if (self.aborted) {
|
if (self.aborted) {
|
||||||
const ex = page.main_context.throw(self.reason orelse DEFAULT_REASON);
|
const ex = page.js.throw(self.reason orelse DEFAULT_REASON);
|
||||||
return .{ .exception = ex };
|
return .{ .exception = ex };
|
||||||
}
|
}
|
||||||
return .{ .undefined = {} };
|
return .{ .undefined = {} };
|
||||||
@@ -138,44 +138,6 @@ const TimeoutCallback = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.HTML.AbortController" {
|
test "Browser: HTML.AbortController" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
try testing.htmlRunner("html/abort_controller.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "var called = 0", null },
|
|
||||||
.{ "var a1 = new AbortController()", null },
|
|
||||||
.{ "var s1 = a1.signal", null },
|
|
||||||
.{ "s1.throwIfAborted()", "undefined" },
|
|
||||||
.{ "s1.reason", "undefined" },
|
|
||||||
.{ "var target;", null },
|
|
||||||
.{
|
|
||||||
\\ s1.addEventListener('abort', (e) => {
|
|
||||||
\\ called += 1;
|
|
||||||
\\ target = e.target;
|
|
||||||
\\
|
|
||||||
\\ });
|
|
||||||
,
|
|
||||||
null,
|
|
||||||
},
|
|
||||||
.{ "a1.abort()", null },
|
|
||||||
.{ "s1.aborted", "true" },
|
|
||||||
.{ "target == s1", "true" },
|
|
||||||
.{ "s1.reason", "AbortError" },
|
|
||||||
.{ "called", "1" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "var s2 = AbortSignal.abort('over 9000')", null },
|
|
||||||
.{ "s2.aborted", "true" },
|
|
||||||
.{ "s2.reason", "over 9000" },
|
|
||||||
.{ "AbortSignal.abort().reason", "AbortError" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "var s3 = AbortSignal.timeout(10)", null },
|
|
||||||
.{ "s3.aborted", "true" },
|
|
||||||
.{ "s3.reason", "TimeoutError" },
|
|
||||||
.{ "try { s3.throwIfAborted() } catch (e) { e }", "Error: TimeoutError" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
// 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 parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Env = @import("../env.zig").Env;
|
const js = @import("../js/js.zig");
|
||||||
|
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
@@ -26,7 +27,7 @@ const DataSet = @This();
|
|||||||
|
|
||||||
element: *parser.Element,
|
element: *parser.Element,
|
||||||
|
|
||||||
pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !Env.UndefinedOr([]const u8) {
|
pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !js.UndefinedOr([]const u8) {
|
||||||
const normalized_name = try normalize(page.call_arena, name);
|
const normalized_name = try normalize(page.call_arena, name);
|
||||||
if (try parser.elementGetAttribute(self.element, normalized_name)) |value| {
|
if (try parser.elementGetAttribute(self.element, normalized_name)) |value| {
|
||||||
return .{ .value = value };
|
return .{ .value = value };
|
||||||
@@ -76,22 +77,6 @@ fn normalize(allocator: Allocator, name: []const u8) ![]const u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.HTML.DataSet" {
|
test "Browser: HTML.DataSet" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
|
try testing.htmlRunner("html/dataset.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let el1 = document.createElement('div')", null },
|
|
||||||
.{ "el1.dataset.x", "undefined" },
|
|
||||||
.{ "el1.dataset.x = '123'", "123" },
|
|
||||||
.{ "delete el1.dataset.x", "true" },
|
|
||||||
.{ "el1.dataset.x", "undefined" },
|
|
||||||
.{ "delete el1.dataset.other", "true" }, // yes, this is right
|
|
||||||
|
|
||||||
.{ "let ds1 = el1.dataset", null },
|
|
||||||
.{ "ds1.helloWorld = 'yes'", null },
|
|
||||||
.{ "el1.getAttribute('data-hello-world')", "yes" },
|
|
||||||
.{ "el1.setAttribute('data-this-will-work', 'positive')", null },
|
|
||||||
.{ "ds1.thisWillWork", "positive" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
183
src/browser/html/History.zig
Normal file
183
src/browser/html/History.zig
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
// Copyright (C) 2023-2024 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 js = @import("../js/js.zig");
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
const Window = @import("window.zig").Window;
|
||||||
|
|
||||||
|
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
|
||||||
|
const History = @This();
|
||||||
|
|
||||||
|
const ScrollRestorationMode = enum {
|
||||||
|
pub const ENUM_JS_USE_TAG = true;
|
||||||
|
|
||||||
|
auto,
|
||||||
|
manual,
|
||||||
|
};
|
||||||
|
|
||||||
|
scroll_restoration: ScrollRestorationMode = .auto,
|
||||||
|
|
||||||
|
pub fn get_length(_: *History, page: *Page) u32 {
|
||||||
|
return @intCast(page.session.navigation.entries.items.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_scrollRestoration(self: *History) ScrollRestorationMode {
|
||||||
|
return self.scroll_restoration;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_scrollRestoration(self: *History, mode: ScrollRestorationMode) void {
|
||||||
|
self.scroll_restoration = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_state(_: *History, page: *Page) !?js.Value {
|
||||||
|
if (page.session.navigation.currentEntry().state) |state| {
|
||||||
|
const value = try js.Value.fromJson(page.js, state);
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _pushState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
|
||||||
|
const arena = page.session.arena;
|
||||||
|
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
|
||||||
|
|
||||||
|
const json = state.toJson(arena) catch return error.DataClone;
|
||||||
|
_ = try page.session.navigation.pushEntry(url, json, page, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _replaceState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
|
||||||
|
const arena = page.session.arena;
|
||||||
|
|
||||||
|
const entry = page.session.navigation.currentEntry();
|
||||||
|
const json = try state.toJson(arena);
|
||||||
|
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
|
||||||
|
|
||||||
|
entry.state = json;
|
||||||
|
entry.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn go(_: *const History, delta: i32, page: *Page) !void {
|
||||||
|
// 0 behaves the same as no argument, both reloading the page.
|
||||||
|
|
||||||
|
const current = page.session.navigation.index;
|
||||||
|
const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta)));
|
||||||
|
if (index_s < 0 or index_s > page.session.navigation.entries.items.len - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = @as(usize, @intCast(index_s));
|
||||||
|
const entry = page.session.navigation.entries.items[index];
|
||||||
|
|
||||||
|
if (entry.url) |url| {
|
||||||
|
if (try page.isSameOrigin(url)) {
|
||||||
|
PopStateEvent.dispatch(entry.state, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = try page.session.navigation.navigate(entry.url, .{ .traverse = index }, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _go(self: *History, _delta: ?i32, page: *Page) !void {
|
||||||
|
try self.go(_delta orelse 0, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _back(self: *History, page: *Page) !void {
|
||||||
|
try self.go(-1, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _forward(self: *History, page: *Page) !void {
|
||||||
|
try self.go(1, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = @import("../netsurf.zig");
|
||||||
|
const Event = @import("../events/event.zig").Event;
|
||||||
|
|
||||||
|
pub const PopStateEvent = struct {
|
||||||
|
pub const prototype = *Event;
|
||||||
|
pub const union_make_copy = true;
|
||||||
|
|
||||||
|
pub const EventInit = struct {
|
||||||
|
state: ?[]const u8 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
proto: parser.Event,
|
||||||
|
state: ?[]const u8,
|
||||||
|
|
||||||
|
pub fn constructor(event_type: []const u8, opts: ?EventInit) !PopStateEvent {
|
||||||
|
const event = try parser.eventCreate();
|
||||||
|
defer parser.eventDestroy(event);
|
||||||
|
try parser.eventInit(event, event_type, .{});
|
||||||
|
parser.eventSetInternalType(event, .pop_state);
|
||||||
|
|
||||||
|
const o = opts orelse EventInit{};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.proto = event.*,
|
||||||
|
.state = o.state,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// `hasUAVisualTransition` is not implemented. It isn't baseline so this is okay.
|
||||||
|
|
||||||
|
pub fn get_state(self: *const PopStateEvent, page: *Page) !?js.Value {
|
||||||
|
if (self.state) |state| {
|
||||||
|
const value = try js.Value.fromJson(page.js, state);
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch(state: ?[]const u8, page: *Page) void {
|
||||||
|
log.debug(.script_event, "dispatch popstate event", .{
|
||||||
|
.type = "popstate",
|
||||||
|
.source = "history",
|
||||||
|
});
|
||||||
|
|
||||||
|
var evt = PopStateEvent.constructor("popstate", .{ .state = state }) catch |err| {
|
||||||
|
log.err(.app, "event constructor error", .{
|
||||||
|
.err = err,
|
||||||
|
.type = "popstate",
|
||||||
|
.source = "history",
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = parser.eventTargetDispatchEvent(
|
||||||
|
parser.toEventTarget(Window, &page.window),
|
||||||
|
&evt.proto,
|
||||||
|
) catch |err| {
|
||||||
|
log.err(.app, "dispatch popstate event error", .{
|
||||||
|
.err = err,
|
||||||
|
.type = "popstate",
|
||||||
|
.source = "history",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "Browser: HTML.History" {
|
||||||
|
try testing.htmlRunner("html/history/history.html");
|
||||||
|
try testing.htmlRunner("html/history/history2.html");
|
||||||
|
}
|
||||||
@@ -42,12 +42,12 @@ pub const HTMLDocument = struct {
|
|||||||
// JS funcs
|
// JS funcs
|
||||||
// --------
|
// --------
|
||||||
|
|
||||||
pub fn get_domain(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
pub fn get_domain(self: *parser.DocumentHTML) ![]const u8 {
|
||||||
// libdom's document_html get_domain always returns null, this is
|
// libdom's document_html get_domain always returns null, this is
|
||||||
// the way MDN recommends getting the domain anyways, since document.domain
|
// the way MDN recommends getting the domain anyways, since document.domain
|
||||||
// is deprecated.
|
// is deprecated.
|
||||||
const location = try parser.documentHTMLGetLocation(Location, self) orelse return "";
|
const location = try parser.documentHTMLGetLocation(Location, self) orelse return "";
|
||||||
return location.get_host(page);
|
return location.get_host();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
|
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
|
||||||
@@ -115,67 +115,69 @@ pub const HTMLDocument = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, page: *Page) !NodeList {
|
pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, page: *Page) !NodeList {
|
||||||
const arena = page.arena;
|
|
||||||
var list: NodeList = .{};
|
var list: NodeList = .{};
|
||||||
|
|
||||||
if (name.len == 0) return list;
|
if (name.len == 0) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
const root = parser.documentHTMLToNode(self);
|
const root = parser.documentHTMLToNode(self);
|
||||||
var c = try collection.HTMLCollectionByName(arena, root, name, .{
|
var c = try collection.HTMLCollectionByName(root, name, .{
|
||||||
.include_root = false,
|
.include_root = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ln = try c.get_length();
|
const ln = try c.get_length();
|
||||||
|
try list.ensureTotalCapacity(page.arena, ln);
|
||||||
|
|
||||||
var i: u32 = 0;
|
var i: u32 = 0;
|
||||||
while (i < ln) {
|
while (i < ln) : (i += 1) {
|
||||||
const n = try c.item(i) orelse break;
|
const n = try c.item(i) orelse break;
|
||||||
try list.append(arena, n);
|
list.appendAssumeCapacity(n);
|
||||||
i += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_images(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
pub fn get_images(self: *parser.DocumentHTML) collection.HTMLCollection {
|
||||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "img", .{
|
return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "img", .{
|
||||||
.include_root = false,
|
.include_root = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_embeds(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
pub fn get_embeds(self: *parser.DocumentHTML) collection.HTMLCollection {
|
||||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "embed", .{
|
return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "embed", .{
|
||||||
.include_root = false,
|
.include_root = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_plugins(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
pub fn get_plugins(self: *parser.DocumentHTML) collection.HTMLCollection {
|
||||||
return get_embeds(self, page);
|
return get_embeds(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_forms(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
pub fn get_forms(self: *parser.DocumentHTML) collection.HTMLCollection {
|
||||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "form", .{
|
return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "form", .{
|
||||||
.include_root = false,
|
.include_root = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_scripts(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
pub fn get_scripts(self: *parser.DocumentHTML) collection.HTMLCollection {
|
||||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "script", .{
|
return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "script", .{
|
||||||
.include_root = false,
|
.include_root = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_applets(_: *parser.DocumentHTML) !collection.HTMLCollection {
|
pub fn get_applets(_: *parser.DocumentHTML) collection.HTMLCollection {
|
||||||
return try collection.HTMLCollectionEmpty();
|
return collection.HTMLCollectionEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_links(self: *parser.DocumentHTML) !collection.HTMLCollection {
|
pub fn get_links(self: *parser.DocumentHTML) collection.HTMLCollection {
|
||||||
return try collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), .{
|
return collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), .{
|
||||||
.include_root = false,
|
.include_root = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_anchors(self: *parser.DocumentHTML) !collection.HTMLCollection {
|
pub fn get_anchors(self: *parser.DocumentHTML) collection.HTMLCollection {
|
||||||
return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), .{
|
return collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), .{
|
||||||
.include_root = false,
|
.include_root = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -193,7 +195,7 @@ pub const HTMLDocument = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void {
|
pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void {
|
||||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {
|
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {
|
||||||
@@ -314,116 +316,7 @@ pub const HTMLDocument = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
|
test "Browser: HTML.Document" {
|
||||||
test "Browser.HTML.Document" {
|
try testing.htmlRunner("html/document.html");
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.__proto__.constructor.name", "HTMLDocument" },
|
|
||||||
.{ "document.__proto__.__proto__.constructor.name", "Document" },
|
|
||||||
.{ "document.body.localName == 'body'", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.domain", "lightpanda.io" },
|
|
||||||
.{ "document.referrer", "" },
|
|
||||||
.{ "document.title", "" },
|
|
||||||
.{ "document.body.localName", "body" },
|
|
||||||
.{ "document.head.localName", "head" },
|
|
||||||
.{ "document.images.length", "0" },
|
|
||||||
.{ "document.embeds.length", "0" },
|
|
||||||
.{ "document.plugins.length", "0" },
|
|
||||||
.{ "document.scripts.length", "0" },
|
|
||||||
.{ "document.forms.length", "0" },
|
|
||||||
.{ "document.links.length", "1" },
|
|
||||||
.{ "document.applets.length", "0" },
|
|
||||||
.{ "document.anchors.length", "0" },
|
|
||||||
.{ "document.all.length", "8" },
|
|
||||||
.{ "document.currentScript", "null" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.title = 'foo'", "foo" },
|
|
||||||
.{ "document.title", "foo" },
|
|
||||||
.{ "document.title = ''", "" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.getElementById('link').setAttribute('name', 'foo')", "undefined" },
|
|
||||||
.{ "let list = document.getElementsByName('foo')", "undefined" },
|
|
||||||
.{ "list.length", "1" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.cookie", "" },
|
|
||||||
.{ "document.cookie = 'name=Oeschger; SameSite=None; Secure'", "name=Oeschger; SameSite=None; Secure" },
|
|
||||||
.{ "document.cookie = 'favorite_food=tripe; SameSite=None; Secure'", "favorite_food=tripe; SameSite=None; Secure" },
|
|
||||||
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
|
|
||||||
.{ "document.cookie = 'IgnoreMy=Ghost; HttpOnly'", null }, // "" should be returned, but the framework overrules it atm
|
|
||||||
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.elementFromPoint(0.5, 0.5)", "null" }, // Return null since we only return element s when they have previously been localized
|
|
||||||
.{ "document.elementsFromPoint(0.5, 0.5)", "" },
|
|
||||||
.{
|
|
||||||
\\ let div1 = document.createElement('div');
|
|
||||||
\\ document.body.appendChild(div1);
|
|
||||||
\\ div1.getClientRects();
|
|
||||||
,
|
|
||||||
null,
|
|
||||||
},
|
|
||||||
.{ "document.elementFromPoint(0.5, 0.5)", "[object HTMLDivElement]" },
|
|
||||||
.{ "let elems = document.elementsFromPoint(0.5, 0.5)", null },
|
|
||||||
.{ "elems.length", "3" },
|
|
||||||
.{ "elems[0]", "[object HTMLDivElement]" },
|
|
||||||
.{ "elems[1]", "[object HTMLBodyElement]" },
|
|
||||||
.{ "elems[2]", "[object HTMLHtmlElement]" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{
|
|
||||||
\\ let a = document.createElement('a');
|
|
||||||
\\ a.href = "https://lightpanda.io";
|
|
||||||
\\ document.body.appendChild(a);
|
|
||||||
\\ a.getClientRects();
|
|
||||||
, // Note this will be placed after the div of previous test
|
|
||||||
null,
|
|
||||||
},
|
|
||||||
.{ "let a_again = document.elementFromPoint(1.5, 0.5)", null },
|
|
||||||
.{ "a_again", "[object HTMLAnchorElement]" },
|
|
||||||
.{ "a_again.href", "https://lightpanda.io" },
|
|
||||||
.{ "let a_agains = document.elementsFromPoint(1.5, 0.5)", null },
|
|
||||||
.{ "a_agains[0].href", "https://lightpanda.io" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "!document.all", "true" },
|
|
||||||
.{ "!!document.all", "false" },
|
|
||||||
.{ "document.all(5)", "[object HTMLParagraphElement]" },
|
|
||||||
.{ "document.all('content')", "[object HTMLDivElement]" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.defaultView.document == document", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.readyState", "loading" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try HTMLDocument.documentIsLoaded(runner.page.window.document, runner.page);
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.readyState", "interactive" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try HTMLDocument.documentIsComplete(runner.page.window.document, runner.page);
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.readyState", "complete" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const generate = @import("../../runtime/generate.zig");
|
const generate = @import("../js/generate.zig");
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
const urlStitch = @import("../../url.zig").URL.stitch;
|
const urlStitch = @import("../../url.zig").URL.stitch;
|
||||||
@@ -133,14 +133,14 @@ pub const HTMLElement = struct {
|
|||||||
|
|
||||||
pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 {
|
pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 {
|
||||||
const n = @as(*parser.Node, @ptrCast(e));
|
const n = @as(*parser.Node, @ptrCast(e));
|
||||||
return try parser.nodeTextContent(n) orelse "";
|
return parser.nodeTextContent(n) orelse "";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_innerText(e: *parser.ElementHTML, s: []const u8) !void {
|
pub fn set_innerText(e: *parser.ElementHTML, s: []const u8) !void {
|
||||||
const n = @as(*parser.Node, @ptrCast(e));
|
const n = @as(*parser.Node, @ptrCast(e));
|
||||||
|
|
||||||
// create text node.
|
// create text node.
|
||||||
const doc = try parser.nodeOwnerDocument(n) orelse return error.NoDocument;
|
const doc = parser.nodeOwnerDocument(n) orelse return error.NoDocument;
|
||||||
const t = try parser.documentCreateTextNode(doc, s);
|
const t = try parser.documentCreateTextNode(doc, s);
|
||||||
|
|
||||||
// remove existing children.
|
// remove existing children.
|
||||||
@@ -167,12 +167,12 @@ pub const HTMLElement = struct {
|
|||||||
focusVisible: bool,
|
focusVisible: bool,
|
||||||
};
|
};
|
||||||
pub fn _focus(e: *parser.ElementHTML, _: ?FocusOpts, page: *Page) !void {
|
pub fn _focus(e: *parser.ElementHTML, _: ?FocusOpts, page: *Page) !void {
|
||||||
if (!try page.isNodeAttached(@ptrCast(e))) {
|
if (!page.isNodeAttached(@ptrCast(e))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Document = @import("../dom/document.zig").Document;
|
const Document = @import("../dom/document.zig").Document;
|
||||||
const root_node = try parser.nodeGetRootNode(@ptrCast(e));
|
const root_node = parser.nodeGetRootNode(@ptrCast(e));
|
||||||
try Document.setFocus(@ptrCast(root_node), e, page);
|
try Document.setFocus(@ptrCast(root_node), e, page);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -218,40 +218,40 @@ pub const HTMLAnchorElement = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_href(self: *parser.Anchor) ![]const u8 {
|
pub fn get_href(self: *parser.Anchor) ![]const u8 {
|
||||||
return try parser.anchorGetHref(self);
|
return parser.anchorGetHref(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void {
|
pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void {
|
||||||
const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
|
const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
|
||||||
return try parser.anchorSetHref(self, full);
|
return parser.anchorSetHref(self, full);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_hreflang(self: *parser.Anchor) ![]const u8 {
|
pub fn get_hreflang(self: *parser.Anchor) ![]const u8 {
|
||||||
return try parser.anchorGetHrefLang(self);
|
return parser.anchorGetHrefLang(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void {
|
pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void {
|
||||||
return try parser.anchorSetHrefLang(self, href);
|
return parser.anchorSetHrefLang(self, href);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_type(self: *parser.Anchor) ![]const u8 {
|
pub fn get_type(self: *parser.Anchor) ![]const u8 {
|
||||||
return try parser.anchorGetType(self);
|
return parser.anchorGetType(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_type(self: *parser.Anchor, t: []const u8) !void {
|
pub fn set_type(self: *parser.Anchor, t: []const u8) !void {
|
||||||
return try parser.anchorSetType(self, t);
|
return parser.anchorSetType(self, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rel(self: *parser.Anchor) ![]const u8 {
|
pub fn get_rel(self: *parser.Anchor) ![]const u8 {
|
||||||
return try parser.anchorGetRel(self);
|
return parser.anchorGetRel(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_rel(self: *parser.Anchor, t: []const u8) !void {
|
pub fn set_rel(self: *parser.Anchor, t: []const u8) !void {
|
||||||
return try parser.anchorSetRel(self, t);
|
return parser.anchorSetRel(self, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_text(self: *parser.Anchor) !?[]const u8 {
|
pub fn get_text(self: *parser.Anchor) !?[]const u8 {
|
||||||
return try parser.nodeTextContent(parser.anchorToNode(self));
|
return parser.nodeTextContent(parser.anchorToNode(self));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_text(self: *parser.Anchor, v: []const u8) !void {
|
pub fn set_text(self: *parser.Anchor, v: []const u8) !void {
|
||||||
@@ -269,182 +269,175 @@ pub const HTMLAnchorElement = struct {
|
|||||||
if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| {
|
if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| {
|
||||||
return URL.constructor(.{ .string = href }, null, page); // TODO inject base url
|
return URL.constructor(.{ .string = href }, null, page); // TODO inject base url
|
||||||
}
|
}
|
||||||
return .empty;
|
return error.NotProvided;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO return a disposable string
|
|
||||||
pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
return try u.get_origin(page);
|
defer u.destructor();
|
||||||
|
return u.get_origin(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO return a disposable string
|
|
||||||
pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
return u.get_protocol(page);
|
defer u.destructor();
|
||||||
|
|
||||||
|
return page.call_arena.dupe(u8, u.get_protocol());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void {
|
pub fn set_protocol(self: *parser.Anchor, protocol: []const u8, page: *Page) !void {
|
||||||
const arena = page.arena;
|
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
|
defer u.destructor();
|
||||||
|
try u.set_protocol(protocol);
|
||||||
|
|
||||||
u.uri.scheme = v;
|
const href = try u._toString(page);
|
||||||
const href = try u.toString(arena);
|
return parser.anchorSetHref(self, href);
|
||||||
try parser.anchorSetHref(self, href);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO return a disposable string
|
|
||||||
pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return try u.get_host(page);
|
defer u.destructor();
|
||||||
|
|
||||||
|
return page.call_arena.dupe(u8, u.get_host());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_host(self: *parser.Anchor, v: []const u8, page: *Page) !void {
|
pub fn set_host(self: *parser.Anchor, host: []const u8, page: *Page) !void {
|
||||||
// search : separator
|
|
||||||
var p: ?u16 = null;
|
|
||||||
var h: []const u8 = undefined;
|
|
||||||
for (v, 0..) |c, i| {
|
|
||||||
if (c == ':') {
|
|
||||||
h = v[0..i];
|
|
||||||
p = try std.fmt.parseInt(u16, v[i + 1 ..], 10);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const arena = page.arena;
|
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
|
defer u.destructor();
|
||||||
|
try u.set_host(host);
|
||||||
|
|
||||||
if (p) |pp| {
|
const href = try u._toString(page);
|
||||||
u.uri.host = .{ .raw = h };
|
return parser.anchorSetHref(self, href);
|
||||||
u.uri.port = pp;
|
|
||||||
} else {
|
|
||||||
u.uri.host = .{ .raw = v };
|
|
||||||
u.uri.port = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const href = try u.toString(arena);
|
|
||||||
try parser.anchorSetHref(self, href);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return u.get_hostname();
|
defer u.destructor();
|
||||||
|
return page.call_arena.dupe(u8, u.get_hostname());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
|
pub fn set_hostname(self: *parser.Anchor, hostname: []const u8, page: *Page) !void {
|
||||||
const arena = page.arena;
|
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
u.uri.host = .{ .raw = v };
|
defer u.destructor();
|
||||||
const href = try u.toString(arena);
|
try u.set_hostname(hostname);
|
||||||
try parser.anchorSetHref(self, href);
|
|
||||||
|
const href = try u._toString(page);
|
||||||
|
return parser.anchorSetHref(self, href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO return a disposable string
|
|
||||||
pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return try u.get_port(page);
|
defer u.destructor();
|
||||||
|
return page.call_arena.dupe(u8, u.get_port());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
pub fn set_port(self: *parser.Anchor, maybe_port: ?[]const u8, page: *Page) !void {
|
||||||
const arena = page.arena;
|
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
|
defer u.destructor();
|
||||||
|
|
||||||
if (v != null and v.?.len > 0) {
|
if (maybe_port) |port| {
|
||||||
u.uri.port = try std.fmt.parseInt(u16, v.?, 10);
|
try u.set_port(port);
|
||||||
} else {
|
} else {
|
||||||
u.uri.port = null;
|
u.clearPort();
|
||||||
}
|
}
|
||||||
|
|
||||||
const href = try u.toString(arena);
|
const href = try u._toString(page);
|
||||||
try parser.anchorSetHref(self, href);
|
return parser.anchorSetHref(self, href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO return a disposable string
|
|
||||||
pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return u.get_username();
|
defer u.destructor();
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
const username = u.get_username();
|
||||||
const arena = page.arena;
|
if (username.len == 0) {
|
||||||
var u = try url(self, page);
|
return "";
|
||||||
|
|
||||||
if (v) |vv| {
|
|
||||||
u.uri.user = .{ .raw = vv };
|
|
||||||
} else {
|
|
||||||
u.uri.user = null;
|
|
||||||
}
|
}
|
||||||
const href = try u.toString(arena);
|
|
||||||
|
|
||||||
try parser.anchorSetHref(self, href);
|
return page.call_arena.dupe(u8, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_username(self: *parser.Anchor, maybe_username: ?[]const u8, page: *Page) !void {
|
||||||
|
var u = try url(self, page);
|
||||||
|
defer u.destructor();
|
||||||
|
|
||||||
|
const username = if (maybe_username) |username| username else "";
|
||||||
|
try u.set_username(username);
|
||||||
|
|
||||||
|
const href = try u._toString(page);
|
||||||
|
return parser.anchorSetHref(self, href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO return a disposable string
|
|
||||||
pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return try page.arena.dupe(u8, u.get_password());
|
defer u.destructor();
|
||||||
|
|
||||||
|
return page.call_arena.dupe(u8, u.get_password());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
pub fn set_password(self: *parser.Anchor, maybe_password: ?[]const u8, page: *Page) !void {
|
||||||
const arena = page.arena;
|
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
|
defer u.destructor();
|
||||||
|
|
||||||
if (v) |vv| {
|
const password = if (maybe_password) |password| password else "";
|
||||||
u.uri.password = .{ .raw = vv };
|
try u.set_password(password);
|
||||||
} else {
|
|
||||||
u.uri.password = null;
|
|
||||||
}
|
|
||||||
const href = try u.toString(arena);
|
|
||||||
|
|
||||||
try parser.anchorSetHref(self, href);
|
const href = try u._toString(page);
|
||||||
|
return parser.anchorSetHref(self, href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO return a disposable string
|
|
||||||
pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return u.get_pathname();
|
defer u.destructor();
|
||||||
|
|
||||||
|
return page.call_arena.dupe(u8, u.get_pathname());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
|
pub fn set_pathname(self: *parser.Anchor, pathname: []const u8, page: *Page) !void {
|
||||||
const arena = page.arena;
|
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
u.uri.path = .{ .raw = v };
|
defer u.destructor();
|
||||||
const href = try u.toString(arena);
|
|
||||||
|
|
||||||
try parser.anchorSetHref(self, href);
|
try u.set_pathname(pathname);
|
||||||
|
|
||||||
|
const href = try u._toString(page);
|
||||||
|
return parser.anchorSetHref(self, href);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return try u.get_search(page);
|
defer u.destructor();
|
||||||
|
// This allocates in page arena so no need to dupe.
|
||||||
|
return u.get_search(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
|
defer u.destructor();
|
||||||
try u.set_search(v, page);
|
try u.set_search(v, page);
|
||||||
|
|
||||||
const href = try u.toString(page.call_arena);
|
const href = try u._toString(page);
|
||||||
try parser.anchorSetHref(self, href);
|
return parser.anchorSetHref(self, href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO return a disposable string
|
|
||||||
pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return try u.get_hash(page);
|
defer u.destructor();
|
||||||
|
|
||||||
|
return page.call_arena.dupe(u8, u.get_hash());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
pub fn set_hash(self: *parser.Anchor, maybe_hash: ?[]const u8, page: *Page) !void {
|
||||||
const arena = page.arena;
|
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
|
defer u.destructor();
|
||||||
|
|
||||||
if (v) |vv| {
|
if (maybe_hash) |hash| {
|
||||||
u.uri.fragment = .{ .raw = vv };
|
try u.set_hash(hash);
|
||||||
} else {
|
} else {
|
||||||
u.uri.fragment = null;
|
u.clearHash();
|
||||||
}
|
}
|
||||||
const href = try u.toString(arena);
|
|
||||||
|
|
||||||
try parser.anchorSetHref(self, href);
|
const href = try u._toString(page);
|
||||||
|
return parser.anchorSetHref(self, href);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -732,6 +725,9 @@ pub const HTMLInputElement = struct {
|
|||||||
pub fn set_value(self: *parser.Input, value: []const u8) !void {
|
pub fn set_value(self: *parser.Input, value: []const u8) !void {
|
||||||
try parser.inputSetValue(self, value);
|
try parser.inputSetValue(self, value);
|
||||||
}
|
}
|
||||||
|
pub fn _select(_: *parser.Input) void {
|
||||||
|
log.debug(.web_api, "not implemented", .{ .feature = "HTMLInputElement select" });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const HTMLLIElement = struct {
|
pub const HTMLLIElement = struct {
|
||||||
@@ -757,13 +753,21 @@ pub const HTMLLinkElement = struct {
|
|||||||
pub const prototype = *HTMLElement;
|
pub const prototype = *HTMLElement;
|
||||||
pub const subtype = .node;
|
pub const subtype = .node;
|
||||||
|
|
||||||
|
pub fn get_rel(self: *parser.Link) ![]const u8 {
|
||||||
|
return parser.linkGetRel(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_rel(self: *parser.Link, rel: []const u8) !void {
|
||||||
|
return parser.linkSetRel(self, rel);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_href(self: *parser.Link) ![]const u8 {
|
pub fn get_href(self: *parser.Link) ![]const u8 {
|
||||||
return try parser.linkGetHref(self);
|
return parser.linkGetHref(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_href(self: *parser.Link, href: []const u8, page: *const Page) !void {
|
pub fn set_href(self: *parser.Link, href: []const u8, page: *const Page) !void {
|
||||||
const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
|
const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
|
||||||
return try parser.linkSetHref(self, full);
|
return parser.linkSetHref(self, full);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -879,7 +883,7 @@ pub const HTMLScriptElement = struct {
|
|||||||
// s.src = '...';
|
// s.src = '...';
|
||||||
// This should load the script.
|
// This should load the script.
|
||||||
// addFromElement protects against double execution.
|
// addFromElement protects against double execution.
|
||||||
try page.script_manager.addFromElement(@ptrCast(@alignCast(self)));
|
try page.script_manager.addFromElement(@ptrCast(@alignCast(self)), "dynamic");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -977,22 +981,37 @@ pub const HTMLScriptElement = struct {
|
|||||||
return try parser.elementRemoveAttribute(parser.scriptToElt(self), "nomodule");
|
return try parser.elementRemoveAttribute(parser.scriptToElt(self), "nomodule");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_onload(self: *parser.Script, page: *Page) !?Env.Function {
|
pub fn get_nonce(self: *parser.Script) !?[]const u8 {
|
||||||
|
return try parser.elementGetAttribute(
|
||||||
|
parser.scriptToElt(self),
|
||||||
|
"nonce",
|
||||||
|
) orelse "";
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_nonce(self: *parser.Script, v: []const u8) !void {
|
||||||
|
try parser.elementSetAttribute(
|
||||||
|
parser.scriptToElt(self),
|
||||||
|
"nonce",
|
||||||
|
v,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_onload(self: *parser.Script, page: *Page) !?js.Function {
|
||||||
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
|
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
|
||||||
return state.onload;
|
return state.onload;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_onload(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
|
pub fn set_onload(self: *parser.Script, function: ?js.Function, page: *Page) !void {
|
||||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||||
state.onload = function;
|
state.onload = function;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_onerror(self: *parser.Script, page: *Page) !?Env.Function {
|
pub fn get_onerror(self: *parser.Script, page: *Page) !?js.Function {
|
||||||
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
|
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
|
||||||
return state.onerror;
|
return state.onerror;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_onerror(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
|
pub fn set_onerror(self: *parser.Script, function: ?js.Function, page: *Page) !void {
|
||||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||||
state.onerror = function;
|
state.onerror = function;
|
||||||
}
|
}
|
||||||
@@ -1027,68 +1046,84 @@ pub const HTMLSlotElement = struct {
|
|||||||
flatten: bool = false,
|
flatten: bool = false,
|
||||||
};
|
};
|
||||||
pub fn _assignedNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion {
|
pub fn _assignedNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion {
|
||||||
const opts = opts_ orelse AssignedNodesOpts{ .flatten = false };
|
return findAssignedSlotNodes(self, opts_, false, page);
|
||||||
|
|
||||||
if (try findAssignedSlotNodes(self, opts, page)) |nodes| {
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!opts.flatten) {
|
|
||||||
return &.{};
|
|
||||||
}
|
|
||||||
|
|
||||||
const node: *parser.Node = @ptrCast(@alignCast(self));
|
|
||||||
const nl = try parser.nodeGetChildNodes(node);
|
|
||||||
const len = try parser.nodeListLength(nl);
|
|
||||||
if (len == 0) {
|
|
||||||
return &.{};
|
|
||||||
}
|
|
||||||
|
|
||||||
var assigned = try page.call_arena.alloc(NodeUnion, len);
|
|
||||||
var i: usize = 0;
|
|
||||||
while (true) : (i += 1) {
|
|
||||||
const child = try parser.nodeListItem(nl, @intCast(i)) orelse break;
|
|
||||||
assigned[i] = try Node.toInterface(child);
|
|
||||||
}
|
|
||||||
return assigned[0..i];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn findAssignedSlotNodes(self: *parser.Slot, opts: AssignedNodesOpts, page: *Page) !?[]NodeUnion {
|
// This should return Union, instead of NodeUnion, but we want to re-use
|
||||||
|
// findAssignedSlotNodes. Returning NodeUnion is fine, as long as every element
|
||||||
|
// within is an Element. This could be more efficient
|
||||||
|
pub fn _assignedElements(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion {
|
||||||
|
return findAssignedSlotNodes(self, opts_, true, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findAssignedSlotNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, element_only: bool, page: *Page) ![]NodeUnion {
|
||||||
|
const opts = opts_ orelse AssignedNodesOpts{ .flatten = false };
|
||||||
|
|
||||||
if (opts.flatten) {
|
if (opts.flatten) {
|
||||||
log.warn(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" });
|
log.debug(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name");
|
|
||||||
const node: *parser.Node = @ptrCast(@alignCast(self));
|
const node: *parser.Node = @ptrCast(@alignCast(self));
|
||||||
var root = try parser.nodeGetRootNode(node);
|
|
||||||
if (page.getNodeState(root)) |state| {
|
|
||||||
if (state.shadow_root) |sr| {
|
|
||||||
root = @ptrCast(@alignCast(sr.host));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var arr: std.ArrayList(NodeUnion) = .empty;
|
// First we look for any explicitly assigned nodes (via the slot attribute)
|
||||||
const w = @import("../dom/walker.zig").WalkerChildren{};
|
{
|
||||||
var next: ?*parser.Node = null;
|
const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name");
|
||||||
while (true) {
|
var root = parser.nodeGetRootNode(node);
|
||||||
next = try w.get_next(root, next) orelse break;
|
if (page.getNodeState(root)) |state| {
|
||||||
if (try parser.nodeType(next.?) != .element) {
|
if (state.shadow_root) |sr| {
|
||||||
if (slot_name == null) {
|
root = @ptrCast(@alignCast(sr.host));
|
||||||
// default slot (with no name), takes everything
|
|
||||||
try arr.append(page.call_arena, try Node.toInterface(next.?));
|
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
const el: *parser.Element = @ptrCast(@alignCast(next.?));
|
|
||||||
const element_slot = try parser.elementGetAttribute(el, "slot");
|
|
||||||
|
|
||||||
if (nullableStringsAreEqual(slot_name, element_slot)) {
|
var arr: std.ArrayList(NodeUnion) = .empty;
|
||||||
// either they're the same string or they are both null
|
const w = @import("../dom/walker.zig").WalkerChildren{};
|
||||||
try arr.append(page.call_arena, try Node.toInterface(next.?));
|
var next: ?*parser.Node = null;
|
||||||
continue;
|
while (true) {
|
||||||
|
next = try w.get_next(root, next) orelse break;
|
||||||
|
if (parser.nodeType(next.?) != .element) {
|
||||||
|
if (slot_name == null and !element_only) {
|
||||||
|
// default slot (with no name), takes everything
|
||||||
|
try arr.append(page.call_arena, try Node.toInterface(next.?));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const el: *parser.Element = @ptrCast(@alignCast(next.?));
|
||||||
|
const element_slot = try parser.elementGetAttribute(el, "slot");
|
||||||
|
|
||||||
|
if (nullableStringsAreEqual(slot_name, element_slot)) {
|
||||||
|
// either they're the same string or they are both null
|
||||||
|
try arr.append(page.call_arena, try Node.toInterface(next.?));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (arr.items.len > 0) {
|
||||||
|
return arr.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opts.flatten) {
|
||||||
|
return &.{};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return if (arr.items.len == 0) null else arr.items;
|
|
||||||
|
// Since, we have no explicitly assigned nodes and flatten == false,
|
||||||
|
// we'll collect the children of the slot - the defaults.
|
||||||
|
{
|
||||||
|
const nl = try parser.nodeGetChildNodes(node);
|
||||||
|
const len = parser.nodeListLength(nl);
|
||||||
|
if (len == 0) {
|
||||||
|
return &.{};
|
||||||
|
}
|
||||||
|
|
||||||
|
var assigned = try page.call_arena.alloc(NodeUnion, len);
|
||||||
|
var i: usize = 0;
|
||||||
|
while (true) : (i += 1) {
|
||||||
|
const child = parser.nodeListItem(nl, @intCast(i)) orelse break;
|
||||||
|
if (!element_only or parser.nodeType(child) == .element) {
|
||||||
|
assigned[i] = try Node.toInterface(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return assigned[0..i];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nullableStringsAreEqual(a: ?[]const u8, b: ?[]const u8) bool {
|
fn nullableStringsAreEqual(a: ?[]const u8, b: ?[]const u8) bool {
|
||||||
@@ -1285,336 +1320,38 @@ pub fn toInterfaceFromTag(comptime T: type, e: *parser.Element, tag: parser.Tag)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.HTML.Element" {
|
test "Browser: HTML.Element" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
try testing.htmlRunner("html/element.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let link = document.getElementById('link')", "undefined" },
|
|
||||||
.{ "link.target", "" },
|
|
||||||
.{ "link.target = '_blank'", "_blank" },
|
|
||||||
.{ "link.target", "_blank" },
|
|
||||||
.{ "link.target = ''", "" },
|
|
||||||
|
|
||||||
.{ "link.href", "foo" },
|
|
||||||
.{ "link.href = 'https://lightpanda.io/'", "https://lightpanda.io/" },
|
|
||||||
.{ "link.href", "https://lightpanda.io/" },
|
|
||||||
|
|
||||||
.{ "link.origin", "https://lightpanda.io" },
|
|
||||||
|
|
||||||
.{ "link.host = 'lightpanda.io:443'", "lightpanda.io:443" },
|
|
||||||
.{ "link.host", "lightpanda.io:443" },
|
|
||||||
.{ "link.port", "443" },
|
|
||||||
.{ "link.hostname", "lightpanda.io" },
|
|
||||||
|
|
||||||
.{ "link.host = 'lightpanda.io'", "lightpanda.io" },
|
|
||||||
.{ "link.host", "lightpanda.io" },
|
|
||||||
.{ "link.port", "" },
|
|
||||||
.{ "link.hostname", "lightpanda.io" },
|
|
||||||
|
|
||||||
.{ "link.host", "lightpanda.io" },
|
|
||||||
.{ "link.hostname", "lightpanda.io" },
|
|
||||||
.{ "link.hostname = 'foo.bar'", "foo.bar" },
|
|
||||||
.{ "link.href", "https://foo.bar/" },
|
|
||||||
|
|
||||||
.{ "link.search", "" },
|
|
||||||
.{ "link.search = 'q=bar'", "q=bar" },
|
|
||||||
.{ "link.search", "?q=bar" },
|
|
||||||
.{ "link.href", "https://foo.bar/?q=bar" },
|
|
||||||
|
|
||||||
.{ "link.hash", "" },
|
|
||||||
.{ "link.hash = 'frag'", "frag" },
|
|
||||||
.{ "link.hash", "#frag" },
|
|
||||||
.{ "link.href", "https://foo.bar/?q=bar#frag" },
|
|
||||||
|
|
||||||
.{ "link.port", "" },
|
|
||||||
.{ "link.port = '443'", "443" },
|
|
||||||
.{ "link.host", "foo.bar:443" },
|
|
||||||
.{ "link.hostname", "foo.bar" },
|
|
||||||
.{ "link.href", "https://foo.bar:443/?q=bar#frag" },
|
|
||||||
.{ "link.port = null", "null" },
|
|
||||||
.{ "link.href", "https://foo.bar/?q=bar#frag" },
|
|
||||||
|
|
||||||
.{ "link.href = 'foo'", "foo" },
|
|
||||||
|
|
||||||
.{ "link.type", "" },
|
|
||||||
.{ "link.type = 'text/html'", "text/html" },
|
|
||||||
.{ "link.type", "text/html" },
|
|
||||||
.{ "link.type = ''", "" },
|
|
||||||
|
|
||||||
.{ "link.text", "OK" },
|
|
||||||
.{ "link.text = 'foo'", "foo" },
|
|
||||||
.{ "link.text", "foo" },
|
|
||||||
.{ "link.text = 'OK'", "OK" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let script = document.createElement('script')", "undefined" },
|
|
||||||
.{ "script.src = 'foo.bar'", "foo.bar" },
|
|
||||||
|
|
||||||
.{ "script.async = true", "true" },
|
|
||||||
.{ "script.async", "true" },
|
|
||||||
.{ "script.async = false", "false" },
|
|
||||||
.{ "script.async", "false" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const backup = document.getElementById('content')", "undefined" },
|
|
||||||
.{ "document.getElementById('content').innerText = 'foo';", "foo" },
|
|
||||||
.{ "document.getElementById('content').innerText", "foo" },
|
|
||||||
.{ "document.getElementById('content').innerHTML = backup; true;", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let click_count = 0;", "undefined" },
|
|
||||||
.{ "let clickCbk = function() { click_count++ }", "undefined" },
|
|
||||||
.{ "document.getElementById('content').addEventListener('click', clickCbk);", "undefined" },
|
|
||||||
.{ "document.getElementById('content').click()", "undefined" },
|
|
||||||
.{ "click_count", "1" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let style = document.getElementById('content').style", "undefined" },
|
|
||||||
.{ "style.cssText = 'color: red; font-size: 12px; margin: 5px !important;'", "color: red; font-size: 12px; margin: 5px !important;" },
|
|
||||||
.{ "style.length", "3" },
|
|
||||||
.{ "style.setProperty('background-color', 'blue')", "undefined" },
|
|
||||||
.{ "style.getPropertyValue('background-color')", "blue" },
|
|
||||||
.{ "style.length", "4" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// Image
|
|
||||||
try runner.testCases(&.{
|
|
||||||
// Testing constructors
|
|
||||||
.{ "(new Image).width", "0" },
|
|
||||||
.{ "(new Image).height", "0" },
|
|
||||||
.{ "(new Image(4)).width", "4" },
|
|
||||||
.{ "(new Image(4, 6)).height", "6" },
|
|
||||||
|
|
||||||
// Testing ulong property
|
|
||||||
.{ "let fruit = new Image", null },
|
|
||||||
.{ "fruit.width", "0" },
|
|
||||||
.{ "fruit.width = 5", "5" },
|
|
||||||
.{ "fruit.width", "5" },
|
|
||||||
.{ "fruit.width = '15'", "15" },
|
|
||||||
.{ "fruit.width", "15" },
|
|
||||||
.{ "fruit.width = 'apple'", "apple" },
|
|
||||||
.{ "fruit.width;", "0" },
|
|
||||||
|
|
||||||
// Testing string property
|
|
||||||
.{ "let lyric = new Image", null },
|
|
||||||
.{ "lyric.src", "" },
|
|
||||||
.{ "lyric.src = 'okay'", "okay" },
|
|
||||||
.{ "lyric.src", "okay" },
|
|
||||||
.{ "lyric.src = 15", "15" },
|
|
||||||
.{ "lyric.src", "15" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let a = document.createElement('a');", null },
|
|
||||||
.{ "a.href", "" },
|
|
||||||
.{ "a.host", "" },
|
|
||||||
.{ "a.href = 'about'", null },
|
|
||||||
.{ "a.href", "https://lightpanda.io/opensource-browser/about" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// detached node cannot be focused
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const focused = document.activeElement", null },
|
|
||||||
.{ "document.createElement('a').focus()", null },
|
|
||||||
.{ "document.activeElement === focused", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let l2 = document.createElement('link');", null },
|
|
||||||
.{ "l2.href", "" },
|
|
||||||
.{ "l2.href = 'https://lightpanda.io/opensource-browser/15'", null },
|
|
||||||
.{ "l2.href", "https://lightpanda.io/opensource-browser/15" },
|
|
||||||
|
|
||||||
.{ "l2.href = '/over/9000'", null },
|
|
||||||
.{ "l2.href", "https://lightpanda.io/over/9000" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Browser.HTML.Element.DataSet" {
|
test "Browser: HTML.HtmlLinkElement" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=x data-power='over 9000' data-empty data-some-long-key=ok></div>" });
|
try testing.htmlRunner("html/link.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{ .{ "let div = document.getElementById('x')", null }, .{ "div.dataset.nope", "undefined" }, .{ "div.dataset.power", "over 9000" }, .{ "div.dataset.empty", "" }, .{ "div.dataset.someLongKey", "ok" }, .{ "delete div.dataset.power", "true" }, .{ "div.dataset.power", "undefined" } }, .{});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Browser.HTML.HtmlInputElement.properties" {
|
test "Browser: HTML.HtmlImageElement" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .url = "https://lightpanda.io/noslashattheend" });
|
try testing.htmlRunner("html/image.html");
|
||||||
defer runner.deinit();
|
|
||||||
var alloc = std.heap.ArenaAllocator.init(runner.allocator);
|
|
||||||
defer alloc.deinit();
|
|
||||||
const arena = alloc.allocator();
|
|
||||||
|
|
||||||
try runner.testCases(&.{.{ "let elem_input = document.createElement('input')", null }}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{.{ "elem_input.form", "null" }}, .{}); // Initial value
|
|
||||||
// Valid input.form is tested separately :Browser.HTML.HtmlInputElement.propeties.form
|
|
||||||
try testProperty(arena, &runner, "elem_input.form", "null", &.{.{ .input = "'foo'" }}); // Invalid
|
|
||||||
|
|
||||||
try runner.testCases(&.{.{ "elem_input.accept", "" }}, .{}); // Initial value
|
|
||||||
try testProperty(arena, &runner, "elem_input.accept", null, &str_valids); // Valid
|
|
||||||
|
|
||||||
try runner.testCases(&.{.{ "elem_input.alt", "" }}, .{}); // Initial value
|
|
||||||
try testProperty(arena, &runner, "elem_input.alt", null, &str_valids); // Valid
|
|
||||||
|
|
||||||
try runner.testCases(&.{.{ "elem_input.disabled", "false" }}, .{}); // Initial value
|
|
||||||
try testProperty(arena, &runner, "elem_input.disabled", null, &bool_valids); // Valid
|
|
||||||
|
|
||||||
try runner.testCases(&.{.{ "elem_input.maxLength", "-1" }}, .{}); // Initial value
|
|
||||||
try testProperty(arena, &runner, "elem_input.maxLength", null, &.{.{ .input = "5" }}); // Valid
|
|
||||||
try testProperty(arena, &runner, "elem_input.maxLength", "0", &.{.{ .input = "'banana'" }}); // Invalid
|
|
||||||
try runner.testCases(&.{.{ "try { elem_input.maxLength = -45 } catch(e) {e}", "Error: NegativeValueNotAllowed" }}, .{}); // Error
|
|
||||||
|
|
||||||
try runner.testCases(&.{.{ "elem_input.name", "" }}, .{}); // Initial value
|
|
||||||
try testProperty(arena, &runner, "elem_input.name", null, &str_valids); // Valid
|
|
||||||
|
|
||||||
try runner.testCases(&.{.{ "elem_input.readOnly", "false" }}, .{}); // Initial value
|
|
||||||
try testProperty(arena, &runner, "elem_input.readOnly", null, &bool_valids); // Valid
|
|
||||||
|
|
||||||
try runner.testCases(&.{.{ "elem_input.size", "20" }}, .{}); // Initial value
|
|
||||||
try testProperty(arena, &runner, "elem_input.size", null, &.{.{ .input = "5" }}); // Valid
|
|
||||||
try testProperty(arena, &runner, "elem_input.size", "20", &.{.{ .input = "-26" }}); // Invalid
|
|
||||||
try runner.testCases(&.{.{ "try { elem_input.size = 0 } catch(e) {e}", "Error: ZeroNotAllowed" }}, .{}); // Error
|
|
||||||
try runner.testCases(&.{.{ "try { elem_input.size = 'banana' } catch(e) {e}", "Error: ZeroNotAllowed" }}, .{}); // Error
|
|
||||||
|
|
||||||
try runner.testCases(&.{.{ "elem_input.src", "" }}, .{}); // Initial value
|
|
||||||
try testProperty(arena, &runner, "elem_input.src", null, &.{
|
|
||||||
.{ .input = "'foo'", .expected = "https://lightpanda.io/foo" }, // TODO stitch should work with spaces -> %20
|
|
||||||
.{ .input = "-3", .expected = "https://lightpanda.io/-3" },
|
|
||||||
.{ .input = "''", .expected = "https://lightpanda.io/noslashattheend" },
|
|
||||||
});
|
|
||||||
|
|
||||||
try runner.testCases(&.{.{ "elem_input.type", "text" }}, .{}); // Initial value
|
|
||||||
try testProperty(arena, &runner, "elem_input.type", null, &.{.{ .input = "'checkbox'", .expected = "checkbox" }}); // Valid
|
|
||||||
try testProperty(arena, &runner, "elem_input.type", "text", &.{.{ .input = "'5'" }}); // Invalid
|
|
||||||
|
|
||||||
// Properties that are related
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let input_checked = document.createElement('input')", null },
|
|
||||||
.{ "input_checked.defaultChecked", "false" },
|
|
||||||
.{ "input_checked.checked", "false" },
|
|
||||||
|
|
||||||
.{ "input_checked.defaultChecked = true", "true" },
|
|
||||||
.{ "input_checked.defaultChecked", "true" },
|
|
||||||
.{ "input_checked.checked", "true" }, // Also perceived as true
|
|
||||||
|
|
||||||
.{ "input_checked.checked = false", "false" },
|
|
||||||
.{ "input_checked.defaultChecked", "true" },
|
|
||||||
.{ "input_checked.checked", "false" },
|
|
||||||
|
|
||||||
.{ "input_checked.defaultChecked = true", "true" },
|
|
||||||
.{ "input_checked.checked", "false" }, // Still false
|
|
||||||
}, .{});
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let input_value = document.createElement('input')", null },
|
|
||||||
.{ "input_value.defaultValue", "" },
|
|
||||||
.{ "input_value.value", "" },
|
|
||||||
|
|
||||||
.{ "input_value.defaultValue = 3.1", "3.1" },
|
|
||||||
.{ "input_value.defaultValue", "3.1" },
|
|
||||||
.{ "input_value.value", "3.1" }, // Also perceived as 3.1
|
|
||||||
|
|
||||||
.{ "input_value.value = 'mango'", "mango" },
|
|
||||||
.{ "input_value.defaultValue", "3.1" },
|
|
||||||
.{ "input_value.value", "mango" },
|
|
||||||
|
|
||||||
.{ "input_value.defaultValue = true", "true" },
|
|
||||||
.{ "input_value.value", "mango" }, // Still mango
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Browser.HTML.HtmlInputElement.properties.form" {
|
test "Browser: HTML.HtmlInputElement" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
|
try testing.htmlRunner("html/input.html");
|
||||||
\\ <form action="test.php" target="_blank">
|
|
||||||
\\ <p>
|
|
||||||
\\ <label>First name: <input type="text" name="first-name" /></label>
|
|
||||||
\\ </p>
|
|
||||||
\\ </form>
|
|
||||||
});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let elem_input = document.querySelector('input')", null },
|
|
||||||
.{ "elem_input.form", "[object HTMLFormElement]" }, // Initial value
|
|
||||||
.{ "elem_input.form = 'foo'", null },
|
|
||||||
.{ "elem_input.form", "[object HTMLFormElement]" }, // Invalid
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Browser.HTML.HTMLTemplateElement" {
|
test "Browser: HTML.HtmlTemplateElement" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=c></div>" });
|
try testing.htmlRunner("html/template.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let t = document.createElement('template')", null },
|
|
||||||
.{ "let d = document.createElement('div')", null },
|
|
||||||
.{ "d.id = 'abc'", null },
|
|
||||||
.{ "t.content.append(d)", null },
|
|
||||||
.{ "document.getElementById('abc')", "null" },
|
|
||||||
.{ "document.getElementById('c').appendChild(t.content.cloneNode(true))", null },
|
|
||||||
.{ "document.getElementById('abc').id", "abc" },
|
|
||||||
.{ "t.innerHTML = '<span>over</span><p>9000!</p>';", null },
|
|
||||||
.{ "t.content.childNodes.length", "2" },
|
|
||||||
.{ "t.content.childNodes[0].tagName", "SPAN" },
|
|
||||||
.{ "t.content.childNodes[0].innerHTML", "over" },
|
|
||||||
.{ "t.content.childNodes[1].tagName", "P" },
|
|
||||||
.{ "t.content.childNodes[1].innerHTML", "9000!" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Browser.HTML.HTMLStyleElement" {
|
test "Browser: HTML.HtmlStyleElement" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
|
try testing.htmlRunner("html/style.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let s = document.createElement('style')", null },
|
|
||||||
.{ "s.sheet.type", "text/css" },
|
|
||||||
.{ "s.sheet == s.sheet", "true" },
|
|
||||||
.{ "document.createElement('style').sheet == s.sheet", "false" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Browser: HTML.HTMLScriptElement" {
|
test "Browser: HTML.HtmlScriptElement" {
|
||||||
|
try testing.htmlRunner("html/script/script.html");
|
||||||
try testing.htmlRunner("html/script/inline_defer.html");
|
try testing.htmlRunner("html/script/inline_defer.html");
|
||||||
|
try testing.htmlRunner("html/script/import.html");
|
||||||
|
try testing.htmlRunner("html/script/dynamic_import.html");
|
||||||
|
try testing.htmlRunner("html/script/importmap.html");
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Browser: HTML.HTMLSlotElement" {
|
test "Browser: HTML.HtmlSlotElement" {
|
||||||
try testing.htmlRunner("html/html_slot_element.html");
|
try testing.htmlRunner("html/slot.html");
|
||||||
}
|
|
||||||
|
|
||||||
const Check = struct {
|
|
||||||
input: []const u8,
|
|
||||||
expected: ?[]const u8 = null, // Needed when input != expected
|
|
||||||
};
|
|
||||||
const bool_valids = [_]Check{
|
|
||||||
.{ .input = "true" },
|
|
||||||
.{ .input = "''", .expected = "false" },
|
|
||||||
.{ .input = "13.5", .expected = "true" },
|
|
||||||
};
|
|
||||||
const str_valids = [_]Check{
|
|
||||||
.{ .input = "'foo'", .expected = "foo" },
|
|
||||||
.{ .input = "5", .expected = "5" },
|
|
||||||
.{ .input = "''", .expected = "" },
|
|
||||||
.{ .input = "document", .expected = "[object HTMLDocument]" },
|
|
||||||
};
|
|
||||||
|
|
||||||
// .{ "elem.type = '5'", "5" },
|
|
||||||
// .{ "elem.type", "text" },
|
|
||||||
fn testProperty(
|
|
||||||
arena: std.mem.Allocator,
|
|
||||||
runner: *testing.JsRunner,
|
|
||||||
elem_dot_prop: []const u8,
|
|
||||||
always: ?[]const u8, // Ignores checks' expected if set
|
|
||||||
checks: []const Check,
|
|
||||||
) !void {
|
|
||||||
for (checks) |check| {
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ try std.mem.concat(arena, u8, &.{ elem_dot_prop, " = ", check.input }), null },
|
|
||||||
.{ elem_dot_prop, always orelse check.expected orelse check.input },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
const Env = @import("../env.zig").Env;
|
const js = @import("../js/js.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
|
// https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
|
||||||
@@ -28,21 +28,21 @@ pub const ErrorEvent = struct {
|
|||||||
filename: []const u8,
|
filename: []const u8,
|
||||||
lineno: i32,
|
lineno: i32,
|
||||||
colno: i32,
|
colno: i32,
|
||||||
@"error": ?Env.JsObject,
|
@"error": ?js.Object,
|
||||||
|
|
||||||
const ErrorEventInit = struct {
|
const ErrorEventInit = struct {
|
||||||
message: []const u8 = "",
|
message: []const u8 = "",
|
||||||
filename: []const u8 = "",
|
filename: []const u8 = "",
|
||||||
lineno: i32 = 0,
|
lineno: i32 = 0,
|
||||||
colno: i32 = 0,
|
colno: i32 = 0,
|
||||||
@"error": ?Env.JsObject = null,
|
@"error": ?js.Object = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn constructor(event_type: []const u8, opts: ?ErrorEventInit) !ErrorEvent {
|
pub fn constructor(event_type: []const u8, opts: ?ErrorEventInit) !ErrorEvent {
|
||||||
const event = try parser.eventCreate();
|
const event = try parser.eventCreate();
|
||||||
defer parser.eventDestroy(event);
|
defer parser.eventDestroy(event);
|
||||||
try parser.eventInit(event, event_type, .{});
|
try parser.eventInit(event, event_type, .{});
|
||||||
try parser.eventSetInternalType(event, .event);
|
parser.eventSetInternalType(event, .error_event);
|
||||||
|
|
||||||
const o = opts orelse ErrorEventInit{};
|
const o = opts orelse ErrorEventInit{};
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ pub const ErrorEvent = struct {
|
|||||||
return self.colno;
|
return self.colno;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_error(self: *const ErrorEvent) Env.UndefinedOr(Env.JsObject) {
|
pub fn get_error(self: *const ErrorEvent) js.UndefinedOr(js.Object) {
|
||||||
if (self.@"error") |e| {
|
if (self.@"error") |e| {
|
||||||
return .{ .value = e };
|
return .{ .value = e };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ const Allocator = std.mem.Allocator;
|
|||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
const HTMLElement = @import("elements.zig").HTMLElement;
|
const HTMLElement = @import("elements.zig").HTMLElement;
|
||||||
const FormData = @import("../xhr/form_data.zig").FormData;
|
|
||||||
|
|
||||||
pub const HTMLFormElement = struct {
|
pub const HTMLFormElement = struct {
|
||||||
pub const Self = parser.Form;
|
pub const Self = parser.Form;
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 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");
|
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
|
|
||||||
pub const History = struct {
|
|
||||||
const ScrollRestorationMode = enum {
|
|
||||||
auto,
|
|
||||||
manual,
|
|
||||||
};
|
|
||||||
|
|
||||||
scrollRestoration: ScrollRestorationMode = .auto,
|
|
||||||
state: std.json.Value = .null,
|
|
||||||
|
|
||||||
// count tracks the history length until we implement correctly pushstate.
|
|
||||||
count: u32 = 0,
|
|
||||||
|
|
||||||
pub fn get_length(self: *History) u32 {
|
|
||||||
// TODO return the real history length value.
|
|
||||||
return self.count;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_scrollRestoration(self: *History) []const u8 {
|
|
||||||
return switch (self.scrollRestoration) {
|
|
||||||
.auto => "auto",
|
|
||||||
.manual => "manual",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
|
|
||||||
if (std.mem.eql(u8, "manual", mode)) self.scrollRestoration = .manual;
|
|
||||||
if (std.mem.eql(u8, "auto", mode)) self.scrollRestoration = .auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_state(self: *History) std.json.Value {
|
|
||||||
return self.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement the function
|
|
||||||
// data must handle any argument. We could expect a std.json.Value but
|
|
||||||
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
|
|
||||||
pub fn _pushState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
|
|
||||||
self.count += 1;
|
|
||||||
_ = url;
|
|
||||||
_ = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement the function
|
|
||||||
// data must handle any argument. We could expect a std.json.Value but
|
|
||||||
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
|
|
||||||
pub fn _replaceState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
|
|
||||||
_ = self;
|
|
||||||
_ = url;
|
|
||||||
_ = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement the function
|
|
||||||
pub fn _go(self: *History, delta: ?i32) void {
|
|
||||||
_ = self;
|
|
||||||
_ = delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement the function
|
|
||||||
pub fn _back(self: *History) void {
|
|
||||||
_ = self;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement the function
|
|
||||||
pub fn _forward(self: *History) void {
|
|
||||||
_ = self;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.HTML.History" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "history.scrollRestoration", "auto" },
|
|
||||||
.{ "history.scrollRestoration = 'manual'", "manual" },
|
|
||||||
.{ "history.scrollRestoration = 'foo'", "foo" },
|
|
||||||
.{ "history.scrollRestoration", "manual" },
|
|
||||||
.{ "history.scrollRestoration = 'auto'", "auto" },
|
|
||||||
.{ "history.scrollRestoration", "auto" },
|
|
||||||
|
|
||||||
.{ "history.state", "null" },
|
|
||||||
|
|
||||||
.{ "history.pushState({}, null, '')", "undefined" },
|
|
||||||
|
|
||||||
.{ "history.replaceState({}, null, '')", "undefined" },
|
|
||||||
|
|
||||||
.{ "history.go()", "undefined" },
|
|
||||||
.{ "history.go(1)", "undefined" },
|
|
||||||
.{ "history.go(-1)", "undefined" },
|
|
||||||
|
|
||||||
.{ "history.forward()", "undefined" },
|
|
||||||
|
|
||||||
.{ "history.back()", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,7 @@ const HTMLElem = @import("elements.zig");
|
|||||||
const SVGElem = @import("svg_elements.zig");
|
const SVGElem = @import("svg_elements.zig");
|
||||||
const Window = @import("window.zig").Window;
|
const Window = @import("window.zig").Window;
|
||||||
const Navigator = @import("navigator.zig").Navigator;
|
const Navigator = @import("navigator.zig").Navigator;
|
||||||
const History = @import("history.zig").History;
|
const History = @import("History.zig");
|
||||||
const Location = @import("location.zig").Location;
|
const Location = @import("location.zig").Location;
|
||||||
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
|
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,8 @@
|
|||||||
// 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 std = @import("std");
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const HTMLElement = @import("elements.zig").HTMLElement;
|
const HTMLElement = @import("elements.zig").HTMLElement;
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#htmliframeelement
|
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#htmliframeelement
|
||||||
|
|||||||
@@ -16,91 +16,82 @@
|
|||||||
// 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 Page = @import("../page.zig").Page;
|
const Uri = @import("std").Uri;
|
||||||
|
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
const URL = @import("../url/url.zig").URL;
|
const URL = @import("../url/url.zig").URL;
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface
|
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface
|
||||||
pub const Location = struct {
|
pub const Location = struct {
|
||||||
url: ?URL = null,
|
url: URL,
|
||||||
|
|
||||||
|
/// Initializes the `Location` to be used in `Window`.
|
||||||
|
/// Browsers give such initial values when user not navigated yet:
|
||||||
|
/// Chrome -> chrome://new-tab-page/
|
||||||
|
/// Firefox -> about:newtab
|
||||||
|
/// Safari -> favorites://
|
||||||
|
pub fn init(url: []const u8) !Location {
|
||||||
|
return .{ .url = try .initForLocation(url) };
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_href(self: *Location, page: *Page) ![]const u8 {
|
pub fn get_href(self: *Location, page: *Page) ![]const u8 {
|
||||||
if (self.url) |*u| return u.get_href(page);
|
return self.url.get_href(page);
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_protocol(self: *Location, page: *Page) ![]const u8 {
|
pub fn set_href(_: *const Location, href: []const u8, page: *Page) !void {
|
||||||
if (self.url) |*u| return u.get_protocol(page);
|
return page.navigateFromWebAPI(href, .{ .reason = .script }, .{ .push = null });
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_host(self: *Location, page: *Page) ![]const u8 {
|
pub fn get_protocol(self: *Location) []const u8 {
|
||||||
if (self.url) |*u| return u.get_host(page);
|
return self.url.get_protocol();
|
||||||
return "";
|
}
|
||||||
|
|
||||||
|
pub fn get_host(self: *Location) []const u8 {
|
||||||
|
return self.url.get_host();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_hostname(self: *Location) []const u8 {
|
pub fn get_hostname(self: *Location) []const u8 {
|
||||||
if (self.url) |*u| return u.get_hostname();
|
return self.url.get_hostname();
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_port(self: *Location, page: *Page) ![]const u8 {
|
pub fn get_port(self: *Location) []const u8 {
|
||||||
if (self.url) |*u| return u.get_port(page);
|
return self.url.get_port();
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_pathname(self: *Location) []const u8 {
|
pub fn get_pathname(self: *Location) []const u8 {
|
||||||
if (self.url) |*u| return u.get_pathname();
|
return self.url.get_pathname();
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_search(self: *Location, page: *Page) ![]const u8 {
|
pub fn get_search(self: *Location, page: *Page) ![]const u8 {
|
||||||
if (self.url) |*u| return u.get_search(page);
|
return self.url.get_search(page);
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_hash(self: *Location, page: *Page) ![]const u8 {
|
pub fn get_hash(self: *Location) []const u8 {
|
||||||
if (self.url) |*u| return u.get_hash(page);
|
return self.url.get_hash();
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_origin(self: *Location, page: *Page) ![]const u8 {
|
pub fn get_origin(self: *Location, page: *Page) ![]const u8 {
|
||||||
if (self.url) |*u| return u.get_origin(page);
|
return self.url.get_origin(page);
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void {
|
pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void {
|
||||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void {
|
pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void {
|
||||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
return page.navigateFromWebAPI(url, .{ .reason = .script }, .replace);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _reload(_: *const Location, page: *Page) !void {
|
pub fn _reload(_: *const Location, page: *Page) !void {
|
||||||
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script });
|
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script }, .reload);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _toString(self: *Location, page: *Page) ![]const u8 {
|
pub fn _toString(self: *Location, page: *Page) ![]const u8 {
|
||||||
return try self.get_href(page);
|
return self.get_href(page);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.HTML.Location" {
|
test "Browser: HTML.Location" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
try testing.htmlRunner("html/location.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "location.href", "https://lightpanda.io/opensource-browser/" },
|
|
||||||
.{ "document.location.href", "https://lightpanda.io/opensource-browser/" },
|
|
||||||
|
|
||||||
.{ "location.host", "lightpanda.io" },
|
|
||||||
.{ "location.hostname", "lightpanda.io" },
|
|
||||||
.{ "location.origin", "https://lightpanda.io" },
|
|
||||||
.{ "location.pathname", "/opensource-browser/" },
|
|
||||||
.{ "location.hash", "" },
|
|
||||||
.{ "location.port", "" },
|
|
||||||
.{ "location.search", "" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
// 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 js = @import("../js/js.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Function = @import("../env.zig").Function;
|
|
||||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||||
|
|
||||||
// https://drafts.csswg.org/cssom-view/#the-mediaquerylist-interface
|
// https://drafts.csswg.org/cssom-view/#the-mediaquerylist-interface
|
||||||
@@ -39,7 +39,7 @@ pub const MediaQueryList = struct {
|
|||||||
return self.media;
|
return self.media;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _addListener(_: *const MediaQueryList, _: Function) void {}
|
pub fn _addListener(_: *const MediaQueryList, _: js.Function) void {}
|
||||||
|
|
||||||
pub fn _removeListener(_: *const MediaQueryList, _: Function) void {}
|
pub fn _removeListener(_: *const MediaQueryList, _: js.Function) void {}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -80,17 +80,7 @@ pub const Navigator = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.HTML.Navigator" {
|
test "Browser: HTML.Navigator" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
try testing.htmlRunner("html/navigator.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "navigator.userAgent", "Lightpanda/1.0" },
|
|
||||||
.{ "navigator.appVersion", "1.0" },
|
|
||||||
.{ "navigator.language", "en-US" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,80 +199,6 @@ pub const HTMLOptionsCollection = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.HTML.Select" {
|
test "Browser: HTML.Select" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
|
try testing.htmlRunner("html/select.html");
|
||||||
\\ <form id=f1>
|
|
||||||
\\ <select id=s1 name=s1><option>o1<option>o2</select>
|
|
||||||
\\ </form>
|
|
||||||
\\ <select id=s2></select>
|
|
||||||
});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const s = document.getElementById('s1');", null },
|
|
||||||
.{ "s.form", "[object HTMLFormElement]" },
|
|
||||||
|
|
||||||
.{ "document.getElementById('s2').form", "null" },
|
|
||||||
|
|
||||||
.{ "s.disabled", "false" },
|
|
||||||
.{ "s.disabled = true", null },
|
|
||||||
.{ "s.disabled", "true" },
|
|
||||||
.{ "s.disabled = false", null },
|
|
||||||
.{ "s.disabled", "false" },
|
|
||||||
|
|
||||||
.{ "s.multiple", "false" },
|
|
||||||
.{ "s.multiple = true", null },
|
|
||||||
.{ "s.multiple", "true" },
|
|
||||||
.{ "s.multiple = false", null },
|
|
||||||
.{ "s.multiple", "false" },
|
|
||||||
|
|
||||||
.{ "s.name;", "s1" },
|
|
||||||
.{ "s.name = 'sel1';", null },
|
|
||||||
.{ "s.name", "sel1" },
|
|
||||||
|
|
||||||
.{ "s.length;", "2" },
|
|
||||||
|
|
||||||
.{ "s.selectedIndex", "0" },
|
|
||||||
.{ "s.selectedIndex = 2", null }, // out of range
|
|
||||||
.{ "s.selectedIndex", "-1" },
|
|
||||||
|
|
||||||
.{ "s.selectedIndex = -1", null },
|
|
||||||
.{ "s.selectedIndex", "-1" },
|
|
||||||
|
|
||||||
.{ "s.selectedIndex = 0", null },
|
|
||||||
.{ "s.selectedIndex", "0" },
|
|
||||||
|
|
||||||
.{ "s.selectedIndex = 1", null },
|
|
||||||
.{ "s.selectedIndex", "1" },
|
|
||||||
|
|
||||||
.{ "s.selectedIndex = -323", null },
|
|
||||||
.{ "s.selectedIndex", "-1" },
|
|
||||||
|
|
||||||
.{ "let options = s.options", null },
|
|
||||||
.{ "options.length", "2" },
|
|
||||||
.{ "options.item(1).value", "o2" },
|
|
||||||
.{ "options.selectedIndex", "-1" },
|
|
||||||
|
|
||||||
.{ "let o3 = document.createElement('option');", null },
|
|
||||||
.{ "o3.value = 'o3';", null },
|
|
||||||
.{ "options.add(o3)", null },
|
|
||||||
.{ "options.length", "3" },
|
|
||||||
.{ "options.item(2).value", "o3" },
|
|
||||||
|
|
||||||
.{ "let o4 = document.createElement('option');", null },
|
|
||||||
.{ "o4.value = 'o4';", null },
|
|
||||||
.{ "options.add(o4, 1)", null },
|
|
||||||
.{ "options.length", "4" },
|
|
||||||
.{ "options.item(1).value", "o4" },
|
|
||||||
|
|
||||||
.{ "let o5 = document.createElement('option');", null },
|
|
||||||
.{ "o5.value = 'o5';", null },
|
|
||||||
.{ "options.add(o5, o3)", null },
|
|
||||||
.{ "options.length", "5" },
|
|
||||||
.{ "options.item(3).value", "o5" },
|
|
||||||
|
|
||||||
.{ "options.remove(3)", null },
|
|
||||||
.{ "options.length", "4" },
|
|
||||||
.{ "options.item(3).value", "o3" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,17 +25,12 @@ pub const SVGElement = struct {
|
|||||||
// Currently the prototype chain is not implemented (will not be returned by toInterface())
|
// Currently the prototype chain is not implemented (will not be returned by toInterface())
|
||||||
// For that we need parser.SvgElement and the derived types with tags in the v-table.
|
// For that we need parser.SvgElement and the derived types with tags in the v-table.
|
||||||
pub const prototype = *Element;
|
pub const prototype = *Element;
|
||||||
// While this is a Node, could consider not exposing the subtype untill we have
|
// While this is a Node, could consider not exposing the subtype until we have
|
||||||
// a Self type to cast to.
|
// a Self type to cast to.
|
||||||
pub const subtype = .node;
|
pub const subtype = .node;
|
||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.HTML.SVGElement" {
|
test "Browser: HTML.SVGElement" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
try testing.htmlRunner("html/svg.html");
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "'AString' instanceof SVGElement", "false" },
|
|
||||||
}, .{});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,13 +18,14 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
const Navigator = @import("navigator.zig").Navigator;
|
const Navigator = @import("navigator.zig").Navigator;
|
||||||
const History = @import("history.zig").History;
|
const History = @import("History.zig");
|
||||||
|
const Navigation = @import("../navigation/Navigation.zig");
|
||||||
const Location = @import("location.zig").Location;
|
const Location = @import("location.zig").Location;
|
||||||
const Crypto = @import("../crypto/crypto.zig").Crypto;
|
const Crypto = @import("../crypto/crypto.zig").Crypto;
|
||||||
const Console = @import("../console/console.zig").Console;
|
const Console = @import("../console/console.zig").Console;
|
||||||
@@ -35,11 +36,15 @@ const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig");
|
|||||||
const Screen = @import("screen.zig").Screen;
|
const Screen = @import("screen.zig").Screen;
|
||||||
const domcss = @import("../dom/css.zig");
|
const domcss = @import("../dom/css.zig");
|
||||||
const Css = @import("../css/css.zig").Css;
|
const Css = @import("../css/css.zig").Css;
|
||||||
|
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||||
|
|
||||||
const Function = Env.Function;
|
const Request = @import("../fetch/Request.zig");
|
||||||
const JsObject = Env.JsObject;
|
const fetchFn = @import("../fetch/fetch.zig").fetch;
|
||||||
|
|
||||||
const storage = @import("../storage/storage.zig");
|
const storage = @import("../storage/storage.zig");
|
||||||
|
const ErrorEvent = @import("error_event.zig").ErrorEvent;
|
||||||
|
|
||||||
|
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#interface-window-extensions
|
// https://dom.spec.whatwg.org/#interface-window-extensions
|
||||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
|
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
|
||||||
@@ -51,8 +56,7 @@ pub const Window = struct {
|
|||||||
|
|
||||||
document: *parser.DocumentHTML,
|
document: *parser.DocumentHTML,
|
||||||
target: []const u8 = "",
|
target: []const u8 = "",
|
||||||
history: History = .{},
|
location: Location,
|
||||||
location: Location = .{},
|
|
||||||
storage_shelf: ?*storage.Shelf = null,
|
storage_shelf: ?*storage.Shelf = null,
|
||||||
|
|
||||||
// counter for having unique timer ids
|
// counter for having unique timer ids
|
||||||
@@ -65,6 +69,10 @@ pub const Window = struct {
|
|||||||
performance: Performance,
|
performance: Performance,
|
||||||
screen: Screen = .{},
|
screen: Screen = .{},
|
||||||
css: Css = .{},
|
css: Css = .{},
|
||||||
|
scroll_x: u32 = 0,
|
||||||
|
scroll_y: u32 = 0,
|
||||||
|
onload_callback: ?js.Function = null,
|
||||||
|
onpopstate_callback: ?js.Function = null,
|
||||||
|
|
||||||
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
|
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
|
||||||
var fbs = std.io.fixedBufferStream("");
|
var fbs = std.io.fixedBufferStream("");
|
||||||
@@ -75,6 +83,7 @@ pub const Window = struct {
|
|||||||
return .{
|
return .{
|
||||||
.document = html_doc,
|
.document = html_doc,
|
||||||
.target = target orelse "",
|
.target = target orelse "",
|
||||||
|
.location = try .init("about:blank"),
|
||||||
.navigator = navigator orelse .{},
|
.navigator = navigator orelse .{},
|
||||||
.performance = Performance.init(),
|
.performance = Performance.init(),
|
||||||
};
|
};
|
||||||
@@ -85,6 +94,10 @@ pub const Window = struct {
|
|||||||
try parser.documentHTMLSetLocation(Location, self.document, &self.location);
|
try parser.documentHTMLSetLocation(Location, self.document, &self.location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn changeLocation(self: *Window, new_url: []const u8, page: *Page) !void {
|
||||||
|
return self.location.url.reinit(new_url, page);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
|
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
|
||||||
self.performance.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
|
self.performance.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
|
||||||
self.document = doc;
|
self.document = doc;
|
||||||
@@ -95,12 +108,28 @@ pub const Window = struct {
|
|||||||
self.storage_shelf = shelf;
|
self.storage_shelf = shelf;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_window(self: *Window) *Window {
|
pub fn _fetch(_: *Window, input: Request.RequestInput, options: ?Request.RequestInit, page: *Page) !js.Promise {
|
||||||
return self;
|
return fetchFn(input, options, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_navigator(self: *Window) *Navigator {
|
/// Returns `onload_callback`.
|
||||||
return &self.navigator;
|
pub fn get_onload(self: *const Window) ?js.Function {
|
||||||
|
return self.onload_callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets `onload_callback`.
|
||||||
|
pub fn set_onload(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
|
||||||
|
try DirectEventHandler(Window, self, "load", maybe_listener, &self.onload_callback, page.arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `onpopstate_callback`.
|
||||||
|
pub fn get_onpopstate(self: *const Window) ?js.Function {
|
||||||
|
return self.onpopstate_callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets `onpopstate_callback`.
|
||||||
|
pub fn set_onpopstate(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
|
||||||
|
try DirectEventHandler(Window, self, "popstate", maybe_listener, &self.onpopstate_callback, page.arena);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_location(self: *Window) *Location {
|
pub fn get_location(self: *Window) *Location {
|
||||||
@@ -108,23 +137,7 @@ pub const Window = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void {
|
pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void {
|
||||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_console(self: *Window) *Console {
|
|
||||||
return &self.console;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_crypto(self: *Window) *Crypto {
|
|
||||||
return &self.crypto;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_self(self: *Window) *Window {
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_parent(self: *Window) *Window {
|
|
||||||
return self;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// frames return the window itself, but accessing it via a pseudo
|
// frames return the window itself, but accessing it via a pseudo
|
||||||
@@ -164,16 +177,16 @@ pub const Window = struct {
|
|||||||
return frames.get_length();
|
return frames.get_length();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_top(self: *Window) *Window {
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_document(self: *Window) ?*parser.DocumentHTML {
|
pub fn get_document(self: *Window) ?*parser.DocumentHTML {
|
||||||
return self.document;
|
return self.document;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_history(self: *Window) *History {
|
pub fn get_history(_: *Window, page: *Page) *History {
|
||||||
return &self.history;
|
return &page.session.history;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_navigation(_: *Window, page: *Page) *Navigation {
|
||||||
|
return &page.session.navigation;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
|
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
|
||||||
@@ -202,19 +215,11 @@ pub const Window = struct {
|
|||||||
return &self.storage_shelf.?.bucket.session;
|
return &self.storage_shelf.?.bucket.session;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_performance(self: *Window) *Performance {
|
|
||||||
return &self.performance;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_screen(self: *Window) *Screen {
|
|
||||||
return &self.screen;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_CSS(self: *Window) *Css {
|
pub fn get_CSS(self: *Window) *Css {
|
||||||
return &self.css;
|
return &self.css;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 {
|
pub fn _requestAnimationFrame(self: *Window, cbk: js.Function, page: *Page) !u32 {
|
||||||
return self.createTimeout(cbk, 5, page, .{
|
return self.createTimeout(cbk, 5, page, .{
|
||||||
.animation_frame = true,
|
.animation_frame = true,
|
||||||
.name = "animationFrame",
|
.name = "animationFrame",
|
||||||
@@ -226,11 +231,11 @@ pub const Window = struct {
|
|||||||
_ = self.timers.remove(id);
|
_ = self.timers.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
|
pub fn _setTimeout(self: *Window, cbk: js.Function, delay: ?u32, params: []js.Object, page: *Page) !u32 {
|
||||||
return self.createTimeout(cbk, delay, page, .{ .args = params, .name = "setTimeout" });
|
return self.createTimeout(cbk, delay, page, .{ .args = params, .name = "setTimeout" });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
|
pub fn _setInterval(self: *Window, cbk: js.Function, delay: ?u32, params: []js.Object, page: *Page) !u32 {
|
||||||
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params, .name = "setInterval" });
|
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params, .name = "setInterval" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,14 +247,22 @@ pub const Window = struct {
|
|||||||
_ = self.timers.remove(id);
|
_ = self.timers.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _queueMicrotask(self: *Window, cbk: Function, page: *Page) !u32 {
|
pub fn _queueMicrotask(self: *Window, cbk: js.Function, page: *Page) !u32 {
|
||||||
return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
|
return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _matchMedia(_: *const Window, media: []const u8, page: *Page) !MediaQueryList {
|
pub fn _setImmediate(self: *Window, cbk: js.Function, page: *Page) !u32 {
|
||||||
|
return self.createTimeout(cbk, 0, page, .{ .name = "setImmediate" });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _clearImmediate(self: *Window, id: u32) void {
|
||||||
|
_ = self.timers.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _matchMedia(_: *const Window, media: js.String) !MediaQueryList {
|
||||||
return .{
|
return .{
|
||||||
.matches = false, // TODO?
|
.matches = false, // TODO?
|
||||||
.media = try page.arena.dupe(u8, media),
|
.media = media.string,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,14 +281,33 @@ pub const Window = struct {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn _reportError(self: *Window, err: js.Object, page: *Page) !void {
|
||||||
|
var error_event = try ErrorEvent.constructor("error", .{
|
||||||
|
.@"error" = err,
|
||||||
|
});
|
||||||
|
_ = try parser.eventTargetDispatchEvent(
|
||||||
|
parser.toEventTarget(Window, self),
|
||||||
|
@as(*parser.Event, &error_event.proto),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (parser.eventDefaultPrevented(&error_event.proto) == false) {
|
||||||
|
const err_string = err.toString() catch "Unknown error";
|
||||||
|
log.info(.user_script, "error", .{
|
||||||
|
.err = err_string,
|
||||||
|
.stack = page.stackTrace() catch "???",
|
||||||
|
.source = "window.reportError",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const CreateTimeoutOpts = struct {
|
const CreateTimeoutOpts = struct {
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
args: []Env.JsObject = &.{},
|
args: []js.Object = &.{},
|
||||||
repeat: bool = false,
|
repeat: bool = false,
|
||||||
animation_frame: bool = false,
|
animation_frame: bool = false,
|
||||||
low_priority: bool = false,
|
low_priority: bool = false,
|
||||||
};
|
};
|
||||||
fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 {
|
fn createTimeout(self: *Window, cbk: js.Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 {
|
||||||
const delay = delay_ orelse 0;
|
const delay = delay_ orelse 0;
|
||||||
if (self.timers.count() > 512) {
|
if (self.timers.count() > 512) {
|
||||||
return error.TooManyTimeout;
|
return error.TooManyTimeout;
|
||||||
@@ -295,9 +327,9 @@ pub const Window = struct {
|
|||||||
errdefer _ = self.timers.remove(timer_id);
|
errdefer _ = self.timers.remove(timer_id);
|
||||||
|
|
||||||
const args = opts.args;
|
const args = opts.args;
|
||||||
var persisted_args: []Env.JsObject = &.{};
|
var persisted_args: []js.Object = &.{};
|
||||||
if (args.len > 0) {
|
if (args.len > 0) {
|
||||||
persisted_args = try page.arena.alloc(Env.JsObject, args.len);
|
persisted_args = try page.arena.alloc(js.Object, args.len);
|
||||||
for (args, persisted_args) |a, *ca| {
|
for (args, persisted_args) |a, *ca| {
|
||||||
ca.* = try a.persist();
|
ca.* = try a.persist();
|
||||||
}
|
}
|
||||||
@@ -338,12 +370,20 @@ pub const Window = struct {
|
|||||||
const Opts = struct {
|
const Opts = struct {
|
||||||
top: i32,
|
top: i32,
|
||||||
left: i32,
|
left: i32,
|
||||||
behavior: []const u8,
|
behavior: []const u8 = "",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
pub fn _scrollTo(self: *Window, opts: ScrollToOpts, y: ?u32) !void {
|
pub fn _scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32) !void {
|
||||||
_ = opts;
|
switch (opts) {
|
||||||
_ = y;
|
.x => |x| {
|
||||||
|
self.scroll_x = @intCast(@max(x, 0));
|
||||||
|
self.scroll_y = @intCast(@max(0, y orelse 0));
|
||||||
|
},
|
||||||
|
.opts => |o| {
|
||||||
|
self.scroll_y = @intCast(@max(0, o.top));
|
||||||
|
self.scroll_x = @intCast(@max(0, o.left));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const scroll_event = try parser.eventCreate();
|
const scroll_event = try parser.eventCreate();
|
||||||
@@ -367,6 +407,28 @@ pub const Window = struct {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn _scroll(self: *Window, opts: ScrollToOpts, y: ?i32) !void {
|
||||||
|
// just an alias for scrollTo
|
||||||
|
return self._scrollTo(opts, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_scrollX(self: *const Window) u32 {
|
||||||
|
return self.scroll_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_scrollY(self: *const Window) u32 {
|
||||||
|
return self.scroll_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_pageXOffset(self: *const Window) u32 {
|
||||||
|
// just an alias for scrollX
|
||||||
|
return self.get_scrollX();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_pageYOffset(self: *const Window) u32 {
|
||||||
|
// just an alias for scrollY
|
||||||
|
return self.get_scrollY();
|
||||||
|
}
|
||||||
|
|
||||||
// libdom's document doesn't have a parent, which is correct, but
|
// libdom's document doesn't have a parent, which is correct, but
|
||||||
// breaks the event bubbling that happens for many events from
|
// breaks the event bubbling that happens for many events from
|
||||||
@@ -384,6 +446,18 @@ pub const Window = struct {
|
|||||||
// and thus the target has already been set to the document.
|
// and thus the target has already been set to the document.
|
||||||
return self.base.redispatchEvent(evt);
|
return self.base.redispatchEvent(evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn postAttach(self: *Window, js_this: js.This) !void {
|
||||||
|
try js_this.set("top", self, .{});
|
||||||
|
try js_this.set("self", self, .{});
|
||||||
|
try js_this.set("parent", self, .{});
|
||||||
|
try js_this.set("window", self, .{});
|
||||||
|
try js_this.set("crypto", &self.crypto, .{});
|
||||||
|
try js_this.set("screen", &self.screen, .{});
|
||||||
|
try js_this.set("console", &self.console, .{});
|
||||||
|
try js_this.set("navigator", &self.navigator, .{});
|
||||||
|
try js_this.set("performance", &self.performance, .{});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const TimerCallback = struct {
|
const TimerCallback = struct {
|
||||||
@@ -394,13 +468,13 @@ const TimerCallback = struct {
|
|||||||
repeat: ?u32,
|
repeat: ?u32,
|
||||||
|
|
||||||
// The JavaScript callback to execute
|
// The JavaScript callback to execute
|
||||||
cbk: Function,
|
cbk: js.Function,
|
||||||
|
|
||||||
animation_frame: bool = false,
|
animation_frame: bool = false,
|
||||||
|
|
||||||
window: *Window,
|
window: *Window,
|
||||||
|
|
||||||
args: []Env.JsObject = &.{},
|
args: []js.Object = &.{},
|
||||||
|
|
||||||
fn run(ctx: *anyopaque) ?u32 {
|
fn run(ctx: *anyopaque) ?u32 {
|
||||||
const self: *TimerCallback = @ptrCast(@alignCast(ctx));
|
const self: *TimerCallback = @ptrCast(@alignCast(ctx));
|
||||||
@@ -414,7 +488,7 @@ const TimerCallback = struct {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result: Function.Result = undefined;
|
var result: js.Function.Result = undefined;
|
||||||
|
|
||||||
var call: anyerror!void = undefined;
|
var call: anyerror!void = undefined;
|
||||||
if (self.animation_frame) {
|
if (self.animation_frame) {
|
||||||
|
|||||||
561
src/browser/js/Caller.zig
Normal file
561
src/browser/js/Caller.zig
Normal file
@@ -0,0 +1,561 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
|
const types = @import("types.zig");
|
||||||
|
const Context = @import("Context.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
|
const CALL_ARENA_RETAIN = 1024 * 16;
|
||||||
|
|
||||||
|
// Responsible for calling Zig functions from JS invocations. This could
|
||||||
|
// probably just contained in ExecutionWorld, but having this specific logic, which
|
||||||
|
// is somewhat repetitive between constructors, functions, getters, etc contained
|
||||||
|
// here does feel like it makes it cleaner.
|
||||||
|
const Caller = @This();
|
||||||
|
context: *Context,
|
||||||
|
v8_context: v8.Context,
|
||||||
|
isolate: v8.Isolate,
|
||||||
|
call_arena: Allocator,
|
||||||
|
|
||||||
|
// info is a v8.PropertyCallbackInfo or a v8.FunctionCallback
|
||||||
|
// All we really want from it is the isolate.
|
||||||
|
// executor = Isolate -> getCurrentContext -> getEmbedderData()
|
||||||
|
pub fn init(info: anytype) Caller {
|
||||||
|
const isolate = info.getIsolate();
|
||||||
|
const v8_context = isolate.getCurrentContext();
|
||||||
|
const context: *Context = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
|
||||||
|
|
||||||
|
context.call_depth += 1;
|
||||||
|
return .{
|
||||||
|
.context = context,
|
||||||
|
.isolate = isolate,
|
||||||
|
.v8_context = v8_context,
|
||||||
|
.call_arena = context.call_arena,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Caller) void {
|
||||||
|
const context = self.context;
|
||||||
|
const call_depth = context.call_depth - 1;
|
||||||
|
|
||||||
|
// Because of callbacks, calls can be nested. Because of this, we
|
||||||
|
// can't clear the call_arena after _every_ call. Imagine we have
|
||||||
|
// arr.forEach((i) => { console.log(i); }
|
||||||
|
//
|
||||||
|
// First we call forEach. Inside of our forEach call,
|
||||||
|
// we call console.log. If we reset the call_arena after this call,
|
||||||
|
// it'll reset it for the `forEach` call after, which might still
|
||||||
|
// need the data.
|
||||||
|
//
|
||||||
|
// Therefore, we keep a call_depth, and only reset the call_arena
|
||||||
|
// when a top-level (call_depth == 0) function ends.
|
||||||
|
if (call_depth == 0) {
|
||||||
|
const arena: *ArenaAllocator = @ptrCast(@alignCast(context.call_arena.ptr));
|
||||||
|
_ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set this _after_ we've executed the above code, so that if the
|
||||||
|
// above code executes any callbacks, they aren't being executed
|
||||||
|
// at scope 0, which would be wrong.
|
||||||
|
context.call_depth = call_depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn constructor(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
|
||||||
|
const args = try self.getArgs(Struct, named_function, 0, info);
|
||||||
|
const res = @call(.auto, Struct.constructor, args);
|
||||||
|
|
||||||
|
const ReturnType = @typeInfo(@TypeOf(Struct.constructor)).@"fn".return_type orelse {
|
||||||
|
@compileError(@typeName(Struct) ++ " has a constructor without a return type");
|
||||||
|
};
|
||||||
|
|
||||||
|
const this = info.getThis();
|
||||||
|
if (@typeInfo(ReturnType) == .error_union) {
|
||||||
|
const non_error_res = res catch |err| return err;
|
||||||
|
_ = try self.context.mapZigInstanceToJs(this, non_error_res);
|
||||||
|
} else {
|
||||||
|
_ = try self.context.mapZigInstanceToJs(this, res);
|
||||||
|
}
|
||||||
|
info.getReturnValue().set(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn method(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
|
||||||
|
if (comptime isSelfReceiver(Struct, named_function) == false) {
|
||||||
|
return self.function(Struct, named_function, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = self.context;
|
||||||
|
const func = @field(Struct, named_function.name);
|
||||||
|
var args = try self.getArgs(Struct, named_function, 1, info);
|
||||||
|
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
|
||||||
|
|
||||||
|
// inject 'self' as the first parameter
|
||||||
|
@field(args, "0") = zig_instance;
|
||||||
|
|
||||||
|
const res = @call(.auto, func, args);
|
||||||
|
info.getReturnValue().set(try context.zigValueToJs(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn function(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
|
||||||
|
const context = self.context;
|
||||||
|
const func = @field(Struct, named_function.name);
|
||||||
|
const args = try self.getArgs(Struct, named_function, 0, info);
|
||||||
|
const res = @call(.auto, func, args);
|
||||||
|
info.getReturnValue().set(try context.zigValueToJs(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, idx: u32, info: v8.PropertyCallbackInfo) !u8 {
|
||||||
|
const context = self.context;
|
||||||
|
const func = @field(Struct, named_function.name);
|
||||||
|
const IndexedGet = @TypeOf(func);
|
||||||
|
if (@typeInfo(IndexedGet).@"fn".return_type == null) {
|
||||||
|
@compileError(named_function.full_name ++ " must have a return type");
|
||||||
|
}
|
||||||
|
|
||||||
|
var has_value = true;
|
||||||
|
|
||||||
|
var args: ParamterTypes(IndexedGet) = undefined;
|
||||||
|
const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
|
||||||
|
switch (arg_fields.len) {
|
||||||
|
0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"),
|
||||||
|
3, 4 => {
|
||||||
|
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
|
||||||
|
comptime assertSelfReceiver(Struct, named_function);
|
||||||
|
@field(args, "0") = zig_instance;
|
||||||
|
@field(args, "1") = idx;
|
||||||
|
@field(args, "2") = &has_value;
|
||||||
|
if (comptime arg_fields.len == 4) {
|
||||||
|
comptime assertIsPageArg(Struct, named_function, 3);
|
||||||
|
@field(args, "3") = context.page;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => @compileError(named_function.full_name ++ " has too many parmaters"),
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = @call(.auto, func, args);
|
||||||
|
if (has_value == false) {
|
||||||
|
return v8.Intercepted.No;
|
||||||
|
}
|
||||||
|
info.getReturnValue().set(try context.zigValueToJs(res));
|
||||||
|
return v8.Intercepted.Yes;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
|
||||||
|
const context = self.context;
|
||||||
|
const func = @field(Struct, named_function.name);
|
||||||
|
comptime assertSelfReceiver(Struct, named_function);
|
||||||
|
|
||||||
|
var has_value = true;
|
||||||
|
var args = try self.getArgs(Struct, named_function, 3, info);
|
||||||
|
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
|
||||||
|
@field(args, "0") = zig_instance;
|
||||||
|
@field(args, "1") = try self.nameToString(name);
|
||||||
|
@field(args, "2") = &has_value;
|
||||||
|
|
||||||
|
const res = @call(.auto, func, args);
|
||||||
|
if (has_value == false) {
|
||||||
|
return v8.Intercepted.No;
|
||||||
|
}
|
||||||
|
info.getReturnValue().set(try self.context.zigValueToJs(res));
|
||||||
|
return v8.Intercepted.Yes;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo) !u8 {
|
||||||
|
const context = self.context;
|
||||||
|
const func = @field(Struct, named_function.name);
|
||||||
|
comptime assertSelfReceiver(Struct, named_function);
|
||||||
|
|
||||||
|
var has_value = true;
|
||||||
|
var args = try self.getArgs(Struct, named_function, 4, info);
|
||||||
|
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
|
||||||
|
@field(args, "0") = zig_instance;
|
||||||
|
@field(args, "1") = try self.nameToString(name);
|
||||||
|
@field(args, "2") = try context.jsValueToZig(named_function, @TypeOf(@field(args, "2")), js_value);
|
||||||
|
@field(args, "3") = &has_value;
|
||||||
|
|
||||||
|
const res = @call(.auto, func, args);
|
||||||
|
return namedSetOrDeleteCall(res, has_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deleteNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
|
||||||
|
const context = self.context;
|
||||||
|
const func = @field(Struct, named_function.name);
|
||||||
|
comptime assertSelfReceiver(Struct, named_function);
|
||||||
|
|
||||||
|
var has_value = true;
|
||||||
|
var args = try self.getArgs(Struct, named_function, 3, info);
|
||||||
|
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
|
||||||
|
@field(args, "0") = zig_instance;
|
||||||
|
@field(args, "1") = try self.nameToString(name);
|
||||||
|
@field(args, "2") = &has_value;
|
||||||
|
|
||||||
|
const res = @call(.auto, func, args);
|
||||||
|
return namedSetOrDeleteCall(res, has_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn namedSetOrDeleteCall(res: anytype, has_value: bool) !u8 {
|
||||||
|
if (@typeInfo(@TypeOf(res)) == .error_union) {
|
||||||
|
_ = try res;
|
||||||
|
}
|
||||||
|
if (has_value == false) {
|
||||||
|
return v8.Intercepted.No;
|
||||||
|
}
|
||||||
|
return v8.Intercepted.Yes;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nameToString(self: *Caller, name: v8.Name) ![]const u8 {
|
||||||
|
return self.context.valueToString(.{ .handle = name.handle }, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction) bool {
|
||||||
|
return checkSelfReceiver(Struct, named_function, false);
|
||||||
|
}
|
||||||
|
fn assertSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction) void {
|
||||||
|
_ = checkSelfReceiver(Struct, named_function, true);
|
||||||
|
}
|
||||||
|
fn checkSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction, comptime fail: bool) bool {
|
||||||
|
const func = @field(Struct, named_function.name);
|
||||||
|
const params = @typeInfo(@TypeOf(func)).@"fn".params;
|
||||||
|
if (params.len == 0) {
|
||||||
|
if (fail) {
|
||||||
|
@compileError(named_function.full_name ++ " must have a self parameter");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const R = types.Receiver(Struct);
|
||||||
|
const first_param = params[0].type.?;
|
||||||
|
if (first_param != *R and first_param != *const R) {
|
||||||
|
if (fail) {
|
||||||
|
@compileError(std.fmt.comptimePrint("The first parameter to {s} must be a *{s} or *const {s}. Got: {s}", .{
|
||||||
|
named_function.full_name,
|
||||||
|
@typeName(R),
|
||||||
|
@typeName(R),
|
||||||
|
@typeName(first_param),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assertIsPageArg(comptime Struct: type, comptime named_function: NamedFunction, index: comptime_int) void {
|
||||||
|
const F = @TypeOf(@field(Struct, named_function.name));
|
||||||
|
const param = @typeInfo(F).@"fn".params[index].type.?;
|
||||||
|
if (isPage(param)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
@compileError(std.fmt.comptimePrint("The {d} parameter to {s} must be a *Page or *const Page. Got: {s}", .{ index, named_function.full_name, @typeName(param) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handleError(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, err: anyerror, info: anytype) void {
|
||||||
|
const isolate = self.isolate;
|
||||||
|
|
||||||
|
if (comptime @import("builtin").mode == .Debug and @hasDecl(@TypeOf(info), "length")) {
|
||||||
|
if (log.enabled(.js, .warn)) {
|
||||||
|
self.logFunctionCallError(err, named_function.full_name, info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var js_err: ?v8.Value = switch (err) {
|
||||||
|
error.InvalidArgument => createTypeException(isolate, "invalid argument"),
|
||||||
|
error.OutOfMemory => js._createException(isolate, "out of memory"),
|
||||||
|
error.IllegalConstructor => js._createException(isolate, "Illegal Contructor"),
|
||||||
|
else => blk: {
|
||||||
|
const func = @field(Struct, named_function.name);
|
||||||
|
const return_type = @typeInfo(@TypeOf(func)).@"fn".return_type orelse {
|
||||||
|
// void return type;
|
||||||
|
break :blk null;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (@typeInfo(return_type) != .error_union) {
|
||||||
|
// type defines a custom exception, but this function should
|
||||||
|
// not fail. We failed somewhere inside of js.zig and
|
||||||
|
// should return the error as-is, since it isn't related
|
||||||
|
// to our Struct
|
||||||
|
break :blk null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const function_error_set = @typeInfo(return_type).error_union.error_set;
|
||||||
|
|
||||||
|
const E = comptime getCustomException(Struct) orelse break :blk null;
|
||||||
|
if (function_error_set == E or isErrorSetException(E, err)) {
|
||||||
|
const custom_exception = E.init(self.call_arena, err, named_function.js_name) catch |init_err| {
|
||||||
|
switch (init_err) {
|
||||||
|
// if a custom exceptions' init wants to return a
|
||||||
|
// different error, we need to think about how to
|
||||||
|
// handle that failure.
|
||||||
|
error.OutOfMemory => break :blk js._createException(isolate, "out of memory"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// ughh..how to handle an error here?
|
||||||
|
break :blk self.context.zigValueToJs(custom_exception) catch js._createException(isolate, "internal error");
|
||||||
|
}
|
||||||
|
// this error isn't part of a custom exception
|
||||||
|
break :blk null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (js_err == null) {
|
||||||
|
js_err = js._createException(isolate, @errorName(err));
|
||||||
|
}
|
||||||
|
const js_exception = isolate.throwException(js_err.?);
|
||||||
|
info.getReturnValue().setValueHandle(js_exception.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk the prototype chain to see if a type declares a custom Exception
|
||||||
|
fn getCustomException(comptime Struct: type) ?type {
|
||||||
|
var S = Struct;
|
||||||
|
while (true) {
|
||||||
|
if (@hasDecl(S, "Exception")) {
|
||||||
|
return S.Exception;
|
||||||
|
}
|
||||||
|
if (@hasDecl(S, "prototype") == false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// long ago, we validated that every prototype declaration
|
||||||
|
// is a pointer.
|
||||||
|
S = @typeInfo(S.prototype).pointer.child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does the error we want to return belong to the custom exeception's ErrorSet
|
||||||
|
fn isErrorSetException(comptime E: type, err: anytype) bool {
|
||||||
|
const Entry = std.meta.Tuple(&.{ []const u8, void });
|
||||||
|
|
||||||
|
const error_set = @typeInfo(E.ErrorSet).error_set.?;
|
||||||
|
const entries = comptime blk: {
|
||||||
|
var kv: [error_set.len]Entry = undefined;
|
||||||
|
for (error_set, 0..) |e, i| {
|
||||||
|
kv[i] = .{ e.name, {} };
|
||||||
|
}
|
||||||
|
break :blk kv;
|
||||||
|
};
|
||||||
|
const lookup = std.StaticStringMap(void).initComptime(entries);
|
||||||
|
return lookup.has(@errorName(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we call a method in javascript: cat.lives('nine');
|
||||||
|
//
|
||||||
|
// Then we'd expect a Zig function with 2 parameters: a self and the string.
|
||||||
|
// In this case, offset == 1. Offset is always 1 for setters or methods.
|
||||||
|
//
|
||||||
|
// Offset is always 0 for constructors.
|
||||||
|
//
|
||||||
|
// For constructors, setters and methods, we can further increase offset + 1
|
||||||
|
// if the first parameter is an instance of Page.
|
||||||
|
//
|
||||||
|
// Finally, if the JS function is called with _more_ parameters and
|
||||||
|
// the last parameter in Zig is an array, we'll try to slurp the additional
|
||||||
|
// parameters into the array.
|
||||||
|
fn getArgs(self: *const Caller, comptime Struct: type, comptime named_function: NamedFunction, comptime offset: usize, info: anytype) !ParamterTypes(@TypeOf(@field(Struct, named_function.name))) {
|
||||||
|
const context = self.context;
|
||||||
|
const F = @TypeOf(@field(Struct, named_function.name));
|
||||||
|
var args: ParamterTypes(F) = undefined;
|
||||||
|
|
||||||
|
const params = @typeInfo(F).@"fn".params[offset..];
|
||||||
|
// Except for the constructor, the first parameter is always `self`
|
||||||
|
// This isn't something we'll bind from JS, so skip it.
|
||||||
|
const params_to_map = blk: {
|
||||||
|
if (params.len == 0) {
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the last parameter is the Page, set it, and exclude it
|
||||||
|
// from our params slice, because we don't want to bind it to
|
||||||
|
// a JS argument
|
||||||
|
if (comptime isPage(params[params.len - 1].type.?)) {
|
||||||
|
@field(args, tupleFieldName(params.len - 1 + offset)) = self.context.page;
|
||||||
|
break :blk params[0 .. params.len - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the last parameter is a special JsThis, set it, and exclude it
|
||||||
|
// from our params slice, because we don't want to bind it to
|
||||||
|
// a JS argument
|
||||||
|
if (comptime params[params.len - 1].type.? == js.This) {
|
||||||
|
@field(args, tupleFieldName(params.len - 1 + offset)) = .{ .obj = .{
|
||||||
|
.context = context,
|
||||||
|
.js_obj = info.getThis(),
|
||||||
|
} };
|
||||||
|
|
||||||
|
// AND the 2nd last parameter is state
|
||||||
|
if (params.len > 1 and comptime isPage(params[params.len - 2].type.?)) {
|
||||||
|
@field(args, tupleFieldName(params.len - 2 + offset)) = self.context.page;
|
||||||
|
break :blk params[0 .. params.len - 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk params[0 .. params.len - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have neither a Page nor a JsObject. All params must be
|
||||||
|
// bound to a JavaScript value.
|
||||||
|
break :blk params;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (params_to_map.len == 0) {
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
const js_parameter_count = info.length();
|
||||||
|
const last_js_parameter = params_to_map.len - 1;
|
||||||
|
var is_variadic = false;
|
||||||
|
|
||||||
|
{
|
||||||
|
// This is going to get complicated. If the last Zig parameter
|
||||||
|
// is a slice AND the corresponding javascript parameter is
|
||||||
|
// NOT an an array, then we'll treat it as a variadic.
|
||||||
|
|
||||||
|
const last_parameter_type = params_to_map[params_to_map.len - 1].type.?;
|
||||||
|
const last_parameter_type_info = @typeInfo(last_parameter_type);
|
||||||
|
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
|
||||||
|
const slice_type = last_parameter_type_info.pointer.child;
|
||||||
|
const corresponding_js_value = info.getArg(@as(u32, @intCast(last_js_parameter)));
|
||||||
|
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
|
||||||
|
is_variadic = true;
|
||||||
|
if (js_parameter_count == 0) {
|
||||||
|
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
||||||
|
} else if (js_parameter_count >= params_to_map.len) {
|
||||||
|
const arr = try self.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
|
||||||
|
for (arr, last_js_parameter..) |*a, i| {
|
||||||
|
const js_value = info.getArg(@as(u32, @intCast(i)));
|
||||||
|
a.* = try context.jsValueToZig(named_function, slice_type, js_value);
|
||||||
|
}
|
||||||
|
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
|
||||||
|
} else {
|
||||||
|
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline for (params_to_map, 0..) |param, i| {
|
||||||
|
const field_index = comptime i + offset;
|
||||||
|
if (comptime i == params_to_map.len - 1) {
|
||||||
|
if (is_variadic) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comptime isPage(param.type.?)) {
|
||||||
|
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ named_function.full_name);
|
||||||
|
} else if (comptime param.type.? == js.This) {
|
||||||
|
@compileError("JsThis must be the last parameter: " ++ named_function.full_name);
|
||||||
|
} else if (i >= js_parameter_count) {
|
||||||
|
if (@typeInfo(param.type.?) != .optional) {
|
||||||
|
return error.InvalidArgument;
|
||||||
|
}
|
||||||
|
@field(args, tupleFieldName(field_index)) = null;
|
||||||
|
} else {
|
||||||
|
const js_value = info.getArg(@as(u32, @intCast(i)));
|
||||||
|
@field(args, tupleFieldName(field_index)) = context.jsValueToZig(named_function, param.type.?, js_value) catch {
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is extracted to speed up compilation. When left inlined in handleError,
|
||||||
|
// this can add as much as 10 seconds of compilation time.
|
||||||
|
fn logFunctionCallError(self: *Caller, err: anyerror, function_name: []const u8, info: v8.FunctionCallbackInfo) void {
|
||||||
|
const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args";
|
||||||
|
log.info(.js, "function call error", .{
|
||||||
|
.name = function_name,
|
||||||
|
.err = err,
|
||||||
|
.args = args_dump,
|
||||||
|
.stack = self.context.stackTrace() catch |err1| @errorName(err1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serializeFunctionArgs(self: *Caller, info: v8.FunctionCallbackInfo) ![]const u8 {
|
||||||
|
const separator = log.separator();
|
||||||
|
const js_parameter_count = info.length();
|
||||||
|
|
||||||
|
const context = self.context;
|
||||||
|
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||||
|
for (0..js_parameter_count) |i| {
|
||||||
|
const js_value = info.getArg(@intCast(i));
|
||||||
|
const value_string = try context.valueToDetailString(js_value);
|
||||||
|
const value_type = try context.jsStringToZig(try js_value.typeOf(self.isolate), .{});
|
||||||
|
try std.fmt.format(arr.writer(context.call_arena), "{s}{d}: {s} ({s})", .{
|
||||||
|
separator,
|
||||||
|
i + 1,
|
||||||
|
value_string,
|
||||||
|
value_type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return arr.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want the function name, or more precisely, the "Struct.function" for
|
||||||
|
// displaying helpful @compileError.
|
||||||
|
// However, there's no way to get the name from a std.Builtin.Fn, so we create
|
||||||
|
// a NamedFunction as part of our binding, and pass it around incase we need
|
||||||
|
// to display an error
|
||||||
|
pub const NamedFunction = struct {
|
||||||
|
name: []const u8,
|
||||||
|
js_name: []const u8,
|
||||||
|
full_name: []const u8,
|
||||||
|
|
||||||
|
pub fn init(comptime Struct: type, comptime name: []const u8) NamedFunction {
|
||||||
|
return .{
|
||||||
|
.name = name,
|
||||||
|
.js_name = if (name[0] == '_') name[1..] else name,
|
||||||
|
.full_name = @typeName(Struct) ++ "." ++ name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Takes a function, and returns a tuple for its argument. Used when we
|
||||||
|
// @call a function
|
||||||
|
fn ParamterTypes(comptime F: type) type {
|
||||||
|
const params = @typeInfo(F).@"fn".params;
|
||||||
|
var fields: [params.len]std.builtin.Type.StructField = undefined;
|
||||||
|
|
||||||
|
inline for (params, 0..) |param, i| {
|
||||||
|
fields[i] = .{
|
||||||
|
.name = tupleFieldName(i),
|
||||||
|
.type = param.type.?,
|
||||||
|
.default_value_ptr = null,
|
||||||
|
.is_comptime = false,
|
||||||
|
.alignment = @alignOf(param.type.?),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return @Type(.{ .@"struct" = .{
|
||||||
|
.layout = .auto,
|
||||||
|
.decls = &.{},
|
||||||
|
.fields = &fields,
|
||||||
|
.is_tuple = true,
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tupleFieldName(comptime i: usize) [:0]const u8 {
|
||||||
|
return switch (i) {
|
||||||
|
0 => "0",
|
||||||
|
1 => "1",
|
||||||
|
2 => "2",
|
||||||
|
3 => "3",
|
||||||
|
4 => "4",
|
||||||
|
5 => "5",
|
||||||
|
6 => "6",
|
||||||
|
7 => "7",
|
||||||
|
8 => "8",
|
||||||
|
9 => "9",
|
||||||
|
else => std.fmt.comptimePrint("{d}", .{i}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isPage(comptime T: type) bool {
|
||||||
|
return T == *Page or T == *const Page;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn createTypeException(isolate: v8.Isolate, msg: []const u8) v8.Value {
|
||||||
|
return v8.Exception.initTypeError(v8.String.initUtf8(isolate, msg));
|
||||||
|
}
|
||||||
1901
src/browser/js/Context.zig
Normal file
1901
src/browser/js/Context.zig
Normal file
File diff suppressed because it is too large
Load Diff
539
src/browser/js/Env.zig
Normal file
539
src/browser/js/Env.zig
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
|
const types = @import("types.zig");
|
||||||
|
const Types = types.Types;
|
||||||
|
const Caller = @import("Caller.zig");
|
||||||
|
const Context = @import("Context.zig");
|
||||||
|
const Platform = @import("Platform.zig");
|
||||||
|
const Inspector = @import("Inspector.zig");
|
||||||
|
const ExecutionWorld = @import("ExecutionWorld.zig");
|
||||||
|
const NamedFunction = Caller.NamedFunction;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
|
// The Env maps to a V8 isolate, which represents a isolated sandbox for
|
||||||
|
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
|
||||||
|
// and it's where we'll start ExecutionWorlds, which actually execute JavaScript.
|
||||||
|
// The `S` parameter is arbitrary state. When we start an ExecutionWorld, an instance
|
||||||
|
// of S must be given. This instance is available to any Zig binding.
|
||||||
|
// The `types` parameter is a tuple of Zig structures we want to bind to V8.
|
||||||
|
const Env = @This();
|
||||||
|
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
platform: *const Platform,
|
||||||
|
|
||||||
|
// the global isolate
|
||||||
|
isolate: v8.Isolate,
|
||||||
|
|
||||||
|
// just kept around because we need to free it on deinit
|
||||||
|
isolate_params: *v8.CreateParams,
|
||||||
|
|
||||||
|
// Given a type, we can lookup its index in TYPE_LOOKUP and then have
|
||||||
|
// access to its TunctionTemplate (the thing we need to create an instance
|
||||||
|
// of it)
|
||||||
|
// I.e.:
|
||||||
|
// const index = @field(TYPE_LOOKUP, @typeName(type_name))
|
||||||
|
// const template = templates[index];
|
||||||
|
templates: [Types.len]v8.FunctionTemplate,
|
||||||
|
|
||||||
|
// Given a type index (retrieved via the TYPE_LOOKUP), we can retrieve
|
||||||
|
// the index of its prototype. Types without a prototype have their own
|
||||||
|
// index.
|
||||||
|
prototype_lookup: [Types.len]u16,
|
||||||
|
|
||||||
|
meta_lookup: [Types.len]types.Meta,
|
||||||
|
|
||||||
|
context_id: usize,
|
||||||
|
|
||||||
|
const Opts = struct {};
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, platform: *const Platform, _: Opts) !*Env {
|
||||||
|
// var params = v8.initCreateParams();
|
||||||
|
var params = try allocator.create(v8.CreateParams);
|
||||||
|
errdefer allocator.destroy(params);
|
||||||
|
|
||||||
|
v8.c.v8__Isolate__CreateParams__CONSTRUCT(params);
|
||||||
|
|
||||||
|
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
|
||||||
|
errdefer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
|
||||||
|
|
||||||
|
var isolate = v8.Isolate.init(params);
|
||||||
|
errdefer isolate.deinit();
|
||||||
|
|
||||||
|
// This is the callback that runs whenever a module is dynamically imported.
|
||||||
|
isolate.setHostImportModuleDynamicallyCallback(Context.dynamicModuleCallback);
|
||||||
|
isolate.setPromiseRejectCallback(promiseRejectCallback);
|
||||||
|
isolate.setMicrotasksPolicy(v8.c.kExplicit);
|
||||||
|
|
||||||
|
isolate.enter();
|
||||||
|
errdefer isolate.exit();
|
||||||
|
|
||||||
|
isolate.setHostInitializeImportMetaObjectCallback(Context.metaObjectCallback);
|
||||||
|
|
||||||
|
var temp_scope: v8.HandleScope = undefined;
|
||||||
|
v8.HandleScope.init(&temp_scope, isolate);
|
||||||
|
defer temp_scope.deinit();
|
||||||
|
|
||||||
|
const env = try allocator.create(Env);
|
||||||
|
errdefer allocator.destroy(env);
|
||||||
|
|
||||||
|
env.* = .{
|
||||||
|
.context_id = 0,
|
||||||
|
.platform = platform,
|
||||||
|
.isolate = isolate,
|
||||||
|
.templates = undefined,
|
||||||
|
.allocator = allocator,
|
||||||
|
.isolate_params = params,
|
||||||
|
.meta_lookup = undefined,
|
||||||
|
.prototype_lookup = undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Populate our templates lookup. generateClass creates the
|
||||||
|
// v8.FunctionTemplate, which we store in our env.templates.
|
||||||
|
// The ordering doesn't matter. What matters is that, given a type
|
||||||
|
// we can get its index via: @field(types.LOOKUP, type_name)
|
||||||
|
const templates = &env.templates;
|
||||||
|
inline for (Types, 0..) |s, i| {
|
||||||
|
@setEvalBranchQuota(10_000);
|
||||||
|
templates[i] = v8.Persistent(v8.FunctionTemplate).init(isolate, generateClass(s.defaultValue().?, isolate)).castToFunctionTemplate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Above, we've created all our our FunctionTemplates. Now that we
|
||||||
|
// have them all, we can hook up the prototypes.
|
||||||
|
const meta_lookup = &env.meta_lookup;
|
||||||
|
inline for (Types, 0..) |s, i| {
|
||||||
|
const Struct = s.defaultValue().?;
|
||||||
|
if (@hasDecl(Struct, "prototype")) {
|
||||||
|
const TI = @typeInfo(Struct.prototype);
|
||||||
|
const proto_name = @typeName(types.Receiver(TI.pointer.child));
|
||||||
|
if (@hasField(types.Lookup, proto_name) == false) {
|
||||||
|
@compileError(std.fmt.comptimePrint("Prototype '{s}' for '{s}' is undefined", .{ proto_name, @typeName(Struct) }));
|
||||||
|
}
|
||||||
|
// Hey, look! This is our first real usage of the types.LOOKUP.
|
||||||
|
// Just like we said above, given a type, we can get its
|
||||||
|
// template index.
|
||||||
|
|
||||||
|
const proto_index = @field(types.LOOKUP, proto_name);
|
||||||
|
templates[i].inherit(templates[proto_index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// while we're here, let's populate our meta lookup
|
||||||
|
const subtype: ?types.Sub = if (@hasDecl(Struct, "subtype")) Struct.subtype else null;
|
||||||
|
|
||||||
|
const proto_offset = comptime blk: {
|
||||||
|
if (!@hasField(Struct, "proto")) {
|
||||||
|
break :blk 0;
|
||||||
|
}
|
||||||
|
const proto_info = std.meta.fieldInfo(Struct, .proto);
|
||||||
|
if (@typeInfo(proto_info.type) == .pointer) {
|
||||||
|
// we store the offset as a negative, to so that,
|
||||||
|
// when we reverse this, we know that it's
|
||||||
|
// behind a pointer that we need to resolve.
|
||||||
|
break :blk -@offsetOf(Struct, "proto");
|
||||||
|
}
|
||||||
|
break :blk @offsetOf(Struct, "proto");
|
||||||
|
};
|
||||||
|
|
||||||
|
meta_lookup[i] = .{
|
||||||
|
.index = i,
|
||||||
|
.subtype = subtype,
|
||||||
|
.proto_offset = proto_offset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Env) void {
|
||||||
|
self.isolate.exit();
|
||||||
|
self.isolate.deinit();
|
||||||
|
v8.destroyArrayBufferAllocator(self.isolate_params.array_buffer_allocator.?);
|
||||||
|
self.allocator.destroy(self.isolate_params);
|
||||||
|
self.allocator.destroy(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !Inspector {
|
||||||
|
return Inspector.init(arena, self.isolate, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runMicrotasks(self: *const Env) void {
|
||||||
|
self.isolate.performMicrotasksCheckpoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pumpMessageLoop(self: *const Env) bool {
|
||||||
|
return self.platform.inner.pumpMessageLoop(self.isolate, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runIdleTasks(self: *const Env) void {
|
||||||
|
return self.platform.inner.runIdleTasks(self.isolate, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
|
||||||
|
return .{
|
||||||
|
.env = self,
|
||||||
|
.context = null,
|
||||||
|
.context_arena = ArenaAllocator.init(self.allocator),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// V8 doesn't immediately free memory associated with
|
||||||
|
// a Context, it's managed by the garbage collector. We use the
|
||||||
|
// `lowMemoryNotification` call on the isolate to encourage v8 to free
|
||||||
|
// any contexts which have been freed.
|
||||||
|
pub fn lowMemoryNotification(self: *Env) void {
|
||||||
|
var handle_scope: v8.HandleScope = undefined;
|
||||||
|
v8.HandleScope.init(&handle_scope, self.isolate);
|
||||||
|
defer handle_scope.deinit();
|
||||||
|
self.isolate.lowMemoryNotification();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dumpMemoryStats(self: *Env) void {
|
||||||
|
const stats = self.isolate.getHeapStatistics();
|
||||||
|
std.debug.print(
|
||||||
|
\\ Total Heap Size: {d}
|
||||||
|
\\ Total Heap Size Executable: {d}
|
||||||
|
\\ Total Physical Size: {d}
|
||||||
|
\\ Total Available Size: {d}
|
||||||
|
\\ Used Heap Size: {d}
|
||||||
|
\\ Heap Size Limit: {d}
|
||||||
|
\\ Malloced Memory: {d}
|
||||||
|
\\ External Memory: {d}
|
||||||
|
\\ Peak Malloced Memory: {d}
|
||||||
|
\\ Number Of Native Contexts: {d}
|
||||||
|
\\ Number Of Detached Contexts: {d}
|
||||||
|
\\ Total Global Handles Size: {d}
|
||||||
|
\\ Used Global Handles Size: {d}
|
||||||
|
\\ Zap Garbage: {any}
|
||||||
|
\\
|
||||||
|
, .{ stats.total_heap_size, stats.total_heap_size_executable, stats.total_physical_size, stats.total_available_size, stats.used_heap_size, stats.heap_size_limit, stats.malloced_memory, stats.external_memory, stats.peak_malloced_memory, stats.number_of_native_contexts, stats.number_of_detached_contexts, stats.total_global_handles_size, stats.used_global_handles_size, stats.does_zap_garbage });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn promiseRejectCallback(v8_msg: v8.C_PromiseRejectMessage) callconv(.c) void {
|
||||||
|
const msg = v8.PromiseRejectMessage.initFromC(v8_msg);
|
||||||
|
const isolate = msg.getPromise().toObject().getIsolate();
|
||||||
|
const context = Context.fromIsolate(isolate);
|
||||||
|
|
||||||
|
const value =
|
||||||
|
if (msg.getValue()) |v8_value| context.valueToString(v8_value, .{}) catch |err| @errorName(err) else "no value";
|
||||||
|
|
||||||
|
log.debug(.js, "unhandled rejection", .{ .value = value });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give it a Zig struct, get back a v8.FunctionTemplate.
|
||||||
|
// The FunctionTemplate is a bit like a struct container - it's where
|
||||||
|
// we'll attach functions/getters/setters and where we'll "inherit" a
|
||||||
|
// prototype type (if there is any)
|
||||||
|
fn generateClass(comptime Struct: type, isolate: v8.Isolate) v8.FunctionTemplate {
|
||||||
|
const template = generateConstructor(Struct, isolate);
|
||||||
|
attachClass(Struct, isolate, template);
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normally this is called from generateClass. Where generateClass creates
|
||||||
|
// the constructor (hence, the FunctionTemplate), attachClass adds all
|
||||||
|
// of its functions, getters, setters, ...
|
||||||
|
// But it's extracted from generateClass because we also have 1 global
|
||||||
|
// object (i.e. the Window), which gets attached not only to the Window
|
||||||
|
// constructor/FunctionTemplate as normal, but also through the default
|
||||||
|
// FunctionTemplate of the isolate (in createContext)
|
||||||
|
pub fn attachClass(comptime Struct: type, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
|
||||||
|
const template_proto = template.getPrototypeTemplate();
|
||||||
|
inline for (@typeInfo(Struct).@"struct".decls) |declaration| {
|
||||||
|
const name = declaration.name;
|
||||||
|
if (comptime name[0] == '_') {
|
||||||
|
switch (@typeInfo(@TypeOf(@field(Struct, name)))) {
|
||||||
|
.@"fn" => generateMethod(Struct, name, isolate, template_proto),
|
||||||
|
else => |ti| if (!comptime js.isComplexAttributeType(ti)) {
|
||||||
|
generateAttribute(Struct, name, isolate, template, template_proto);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if (comptime std.mem.startsWith(u8, name, "get_")) {
|
||||||
|
generateProperty(Struct, name[4..], isolate, template_proto);
|
||||||
|
} else if (comptime std.mem.startsWith(u8, name, "static_")) {
|
||||||
|
generateFunction(Struct, name[7..], isolate, template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@hasDecl(Struct, "get_symbol_toStringTag") == false) {
|
||||||
|
// If this WAS defined, then we would have created it in generateProperty.
|
||||||
|
// But if it isn't, we create a default one
|
||||||
|
const string_tag_callback = v8.FunctionTemplate.initCallback(isolate, struct {
|
||||||
|
fn stringTag(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||||
|
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||||
|
const class_name = v8.String.initUtf8(info.getIsolate(), comptime js.classNameForStruct(Struct));
|
||||||
|
info.getReturnValue().set(class_name);
|
||||||
|
}
|
||||||
|
}.stringTag);
|
||||||
|
const key = v8.Symbol.getToStringTag(isolate).toName();
|
||||||
|
template_proto.setAccessorGetter(key, string_tag_callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateIndexer(Struct, template_proto);
|
||||||
|
generateNamedIndexer(Struct, template.getInstanceTemplate());
|
||||||
|
generateUndetectable(Struct, template.getInstanceTemplate());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Even if a struct doesn't have a `constructor` function, we still
|
||||||
|
// `generateConstructor`, because this is how we create our
|
||||||
|
// FunctionTemplate. Such classes exist, but they can't be instantiated
|
||||||
|
// via `new ClassName()` - but they could, for example, be created in
|
||||||
|
// Zig and returned from a function call, which is why we need the
|
||||||
|
// FunctionTemplate.
|
||||||
|
fn generateConstructor(comptime Struct: type, isolate: v8.Isolate) v8.FunctionTemplate {
|
||||||
|
const template = v8.FunctionTemplate.initCallback(isolate, struct {
|
||||||
|
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||||
|
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||||
|
var caller = Caller.init(info);
|
||||||
|
defer caller.deinit();
|
||||||
|
// See comment above. We generateConstructor on all types
|
||||||
|
// in order to create the FunctionTemplate, but there might
|
||||||
|
// not be an actual "constructor" function. So if someone
|
||||||
|
// does `new ClassName()` where ClassName doesn't have
|
||||||
|
// a constructor function, we'll return an error.
|
||||||
|
if (@hasDecl(Struct, "constructor") == false) {
|
||||||
|
const iso = caller.isolate;
|
||||||
|
log.warn(.js, "Illegal constructor call", .{ .name = @typeName(Struct) });
|
||||||
|
const js_exception = iso.throwException(js._createException(iso, "Illegal Constructor"));
|
||||||
|
info.getReturnValue().set(js_exception);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safe to call now, because if Struct.constructor didn't
|
||||||
|
// exist, the above if block would have returned.
|
||||||
|
const named_function = comptime NamedFunction.init(Struct, "constructor");
|
||||||
|
caller.constructor(Struct, named_function, info) catch |err| {
|
||||||
|
caller.handleError(Struct, named_function, err, info);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}.callback);
|
||||||
|
|
||||||
|
if (comptime types.isEmpty(types.Receiver(Struct)) == false) {
|
||||||
|
// If the struct is empty, we won't store a Zig reference inside
|
||||||
|
// the JS object, so we don't need to set the internal field count
|
||||||
|
template.getInstanceTemplate().setInternalFieldCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const class_name = v8.String.initUtf8(isolate, comptime js.classNameForStruct(Struct));
|
||||||
|
template.setClassName(class_name);
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generateMethod(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template_proto: v8.ObjectTemplate) void {
|
||||||
|
var js_name: v8.Name = undefined;
|
||||||
|
if (comptime std.mem.eql(u8, name, "_symbol_iterator")) {
|
||||||
|
js_name = v8.Symbol.getIterator(isolate).toName();
|
||||||
|
} else {
|
||||||
|
js_name = v8.String.initUtf8(isolate, name[1..]).toName();
|
||||||
|
}
|
||||||
|
const function_template = v8.FunctionTemplate.initCallback(isolate, struct {
|
||||||
|
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||||
|
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||||
|
var caller = Caller.init(info);
|
||||||
|
defer caller.deinit();
|
||||||
|
|
||||||
|
const named_function = comptime NamedFunction.init(Struct, name);
|
||||||
|
caller.method(Struct, named_function, info) catch |err| {
|
||||||
|
caller.handleError(Struct, named_function, err, info);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}.callback);
|
||||||
|
template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generateFunction(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
|
||||||
|
const js_name = v8.String.initUtf8(isolate, name).toName();
|
||||||
|
const function_template = v8.FunctionTemplate.initCallback(isolate, struct {
|
||||||
|
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||||
|
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||||
|
var caller = Caller.init(info);
|
||||||
|
defer caller.deinit();
|
||||||
|
|
||||||
|
const named_function = comptime NamedFunction.init(Struct, "static_" ++ name);
|
||||||
|
caller.function(Struct, named_function, info) catch |err| {
|
||||||
|
caller.handleError(Struct, named_function, err, info);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}.callback);
|
||||||
|
template.set(js_name, function_template, v8.PropertyAttribute.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generateAttribute(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template: v8.FunctionTemplate, template_proto: v8.ObjectTemplate) void {
|
||||||
|
const zig_value = @field(Struct, name);
|
||||||
|
const js_value = js.simpleZigValueToJs(isolate, zig_value, true);
|
||||||
|
|
||||||
|
const js_name = v8.String.initUtf8(isolate, name[1..]).toName();
|
||||||
|
|
||||||
|
// apply it both to the type itself
|
||||||
|
template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
|
||||||
|
|
||||||
|
// and to instances of the type
|
||||||
|
template_proto.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generateProperty(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template_proto: v8.ObjectTemplate) void {
|
||||||
|
var js_name: v8.Name = undefined;
|
||||||
|
if (comptime std.mem.eql(u8, name, "symbol_toStringTag")) {
|
||||||
|
js_name = v8.Symbol.getToStringTag(isolate).toName();
|
||||||
|
} else {
|
||||||
|
js_name = v8.String.initUtf8(isolate, name).toName();
|
||||||
|
}
|
||||||
|
|
||||||
|
const getter_callback = v8.FunctionTemplate.initCallback(isolate, struct {
|
||||||
|
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||||
|
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||||
|
var caller = Caller.init(info);
|
||||||
|
defer caller.deinit();
|
||||||
|
|
||||||
|
const named_function = comptime NamedFunction.init(Struct, "get_" ++ name);
|
||||||
|
caller.method(Struct, named_function, info) catch |err| {
|
||||||
|
caller.handleError(Struct, named_function, err, info);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}.callback);
|
||||||
|
|
||||||
|
const setter_name = "set_" ++ name;
|
||||||
|
if (@hasDecl(Struct, setter_name) == false) {
|
||||||
|
template_proto.setAccessorGetter(js_name, getter_callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setter_callback = v8.FunctionTemplate.initCallback(isolate, struct {
|
||||||
|
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||||
|
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||||
|
std.debug.assert(info.length() == 1);
|
||||||
|
|
||||||
|
var caller = Caller.init(info);
|
||||||
|
defer caller.deinit();
|
||||||
|
|
||||||
|
const named_function = comptime NamedFunction.init(Struct, "set_" ++ name);
|
||||||
|
caller.method(Struct, named_function, info) catch |err| {
|
||||||
|
caller.handleError(Struct, named_function, err, info);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}.callback);
|
||||||
|
|
||||||
|
template_proto.setAccessorGetterAndSetter(js_name, getter_callback, setter_callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generateIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void {
|
||||||
|
if (@hasDecl(Struct, "indexed_get") == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const configuration = v8.IndexedPropertyHandlerConfiguration{
|
||||||
|
.getter = struct {
|
||||||
|
fn callback(idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
|
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||||
|
var caller = Caller.init(info);
|
||||||
|
defer caller.deinit();
|
||||||
|
|
||||||
|
const named_function = comptime NamedFunction.init(Struct, "indexed_get");
|
||||||
|
return caller.getIndex(Struct, named_function, idx, info) catch |err| blk: {
|
||||||
|
caller.handleError(Struct, named_function, err, info);
|
||||||
|
break :blk v8.Intercepted.No;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}.callback,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If you're trying to implement setter, read:
|
||||||
|
// https://groups.google.com/g/v8-users/c/8tahYBsHpgY/m/IteS7Wn2AAAJ
|
||||||
|
// The issue I had was
|
||||||
|
// (a) where to attache it: does it go on the instance_template
|
||||||
|
// instead of the prototype?
|
||||||
|
// (b) defining the getter or query to respond with the
|
||||||
|
// PropertyAttribute to indicate if the property can be set
|
||||||
|
template_proto.setIndexedProperty(configuration, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generateNamedIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void {
|
||||||
|
if (@hasDecl(Struct, "named_get") == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var configuration = v8.NamedPropertyHandlerConfiguration{
|
||||||
|
.getter = struct {
|
||||||
|
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
|
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||||
|
var caller = Caller.init(info);
|
||||||
|
defer caller.deinit();
|
||||||
|
|
||||||
|
const named_function = comptime NamedFunction.init(Struct, "named_get");
|
||||||
|
return caller.getNamedIndex(Struct, named_function, .{ .handle = c_name.? }, info) catch |err| blk: {
|
||||||
|
caller.handleError(Struct, named_function, err, info);
|
||||||
|
break :blk v8.Intercepted.No;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}.callback,
|
||||||
|
|
||||||
|
// This is really cool. Without this, we'd intercept _all_ properties
|
||||||
|
// even those explicitly set. So, node.length for example would get routed
|
||||||
|
// to our `named_get`, rather than a `get_length`. This might be
|
||||||
|
// useful if we run into a type that we can't model properly in Zig.
|
||||||
|
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (@hasDecl(Struct, "named_set")) {
|
||||||
|
configuration.setter = struct {
|
||||||
|
fn callback(c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
|
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||||
|
var caller = Caller.init(info);
|
||||||
|
defer caller.deinit();
|
||||||
|
|
||||||
|
const named_function = comptime NamedFunction.init(Struct, "named_set");
|
||||||
|
return caller.setNamedIndex(Struct, named_function, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info) catch |err| blk: {
|
||||||
|
caller.handleError(Struct, named_function, err, info);
|
||||||
|
break :blk v8.Intercepted.No;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}.callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@hasDecl(Struct, "named_delete")) {
|
||||||
|
configuration.deleter = struct {
|
||||||
|
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
|
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||||
|
var caller = Caller.init(info);
|
||||||
|
defer caller.deinit();
|
||||||
|
|
||||||
|
const named_function = comptime NamedFunction.init(Struct, "named_delete");
|
||||||
|
return caller.deleteNamedIndex(Struct, named_function, .{ .handle = c_name.? }, info) catch |err| blk: {
|
||||||
|
caller.handleError(Struct, named_function, err, info);
|
||||||
|
break :blk v8.Intercepted.No;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}.callback;
|
||||||
|
}
|
||||||
|
template_proto.setNamedProperty(configuration, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generateUndetectable(comptime Struct: type, template: v8.ObjectTemplate) void {
|
||||||
|
const has_js_call_as_function = @hasDecl(Struct, "jsCallAsFunction");
|
||||||
|
|
||||||
|
if (has_js_call_as_function) {
|
||||||
|
template.setCallAsFunctionHandler(struct {
|
||||||
|
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||||
|
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||||
|
var caller = Caller.init(info);
|
||||||
|
defer caller.deinit();
|
||||||
|
|
||||||
|
const named_function = comptime NamedFunction.init(Struct, "jsCallAsFunction");
|
||||||
|
caller.method(Struct, named_function, info) catch |err| {
|
||||||
|
caller.handleError(Struct, named_function, err, info);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}.callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@hasDecl(Struct, "mark_as_undetectable") and Struct.mark_as_undetectable) {
|
||||||
|
if (!has_js_call_as_function) {
|
||||||
|
@compileError(@typeName(Struct) ++ ": mark_as_undetectable required jsCallAsFunction to be defined. This is a hard-coded requirement in V8, because mark_as_undetectable only exists for HTMLAllCollection which is also callable.");
|
||||||
|
}
|
||||||
|
template.markAsUndetectable();
|
||||||
|
}
|
||||||
|
}
|
||||||
251
src/browser/js/ExecutionWorld.zig
Normal file
251
src/browser/js/ExecutionWorld.zig
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
const ScriptManager = @import("../ScriptManager.zig");
|
||||||
|
|
||||||
|
const types = @import("types.zig");
|
||||||
|
const Types = types.Types;
|
||||||
|
const Env = @import("Env.zig");
|
||||||
|
const Context = @import("Context.zig");
|
||||||
|
|
||||||
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
|
const CONTEXT_ARENA_RETAIN = 1024 * 64;
|
||||||
|
|
||||||
|
// ExecutionWorld closely models a JS World.
|
||||||
|
// https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#World
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld
|
||||||
|
const ExecutionWorld = @This();
|
||||||
|
env: *Env,
|
||||||
|
|
||||||
|
// Arena whose lifetime is for a single page load. Where
|
||||||
|
// the call_arena lives for a single function call, the context_arena
|
||||||
|
// lives for the lifetime of the entire page. The allocator will be
|
||||||
|
// owned by the Context, but the arena itself is owned by the ExecutionWorld
|
||||||
|
// so that we can re-use it from context to context.
|
||||||
|
context_arena: ArenaAllocator,
|
||||||
|
|
||||||
|
// Currently a context maps to a Browser's Page. Here though, it's only a
|
||||||
|
// mechanism to organization page-specific memory. The ExecutionWorld
|
||||||
|
// does all the work, but having all page-specific data structures
|
||||||
|
// grouped together helps keep things clean.
|
||||||
|
context: ?Context = null,
|
||||||
|
|
||||||
|
// no init, must be initialized via env.newExecutionWorld()
|
||||||
|
|
||||||
|
pub fn deinit(self: *ExecutionWorld) void {
|
||||||
|
if (self.context != null) {
|
||||||
|
self.removeContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.context_arena.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the top Context in the Main ExecutionWorld should hold a handle_scope.
|
||||||
|
// A v8.HandleScope is like an arena. Once created, any "Local" that
|
||||||
|
// v8 creates will be released (or at least, releasable by the v8 GC)
|
||||||
|
// when the handle_scope is freed.
|
||||||
|
// We also maintain our own "context_arena" which allows us to have
|
||||||
|
// all page related memory easily managed.
|
||||||
|
pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_callback: ?js.GlobalMissingCallback) !*Context {
|
||||||
|
std.debug.assert(self.context == null);
|
||||||
|
|
||||||
|
const env = self.env;
|
||||||
|
const isolate = env.isolate;
|
||||||
|
const Global = @TypeOf(page.window);
|
||||||
|
const templates = &self.env.templates;
|
||||||
|
|
||||||
|
var v8_context: v8.Context = blk: {
|
||||||
|
var temp_scope: v8.HandleScope = undefined;
|
||||||
|
v8.HandleScope.init(&temp_scope, isolate);
|
||||||
|
defer temp_scope.deinit();
|
||||||
|
|
||||||
|
const js_global = v8.FunctionTemplate.initDefault(isolate);
|
||||||
|
Env.attachClass(Global, isolate, js_global);
|
||||||
|
|
||||||
|
const global_template = js_global.getInstanceTemplate();
|
||||||
|
global_template.setInternalFieldCount(1);
|
||||||
|
|
||||||
|
// Configure the missing property callback on the global
|
||||||
|
// object.
|
||||||
|
if (global_callback != null) {
|
||||||
|
const configuration = v8.NamedPropertyHandlerConfiguration{
|
||||||
|
.getter = struct {
|
||||||
|
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
|
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||||
|
const context = Context.fromIsolate(info.getIsolate());
|
||||||
|
|
||||||
|
const property = context.valueToString(.{ .handle = c_name.? }, .{}) catch "???";
|
||||||
|
if (context.global_callback.?.missing(property, context)) {
|
||||||
|
return v8.Intercepted.Yes;
|
||||||
|
}
|
||||||
|
return v8.Intercepted.No;
|
||||||
|
}
|
||||||
|
}.callback,
|
||||||
|
.flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings,
|
||||||
|
};
|
||||||
|
global_template.setNamedProperty(configuration, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All the FunctionTemplates that we created and setup in Env.init
|
||||||
|
// are now going to get associated with our global instance.
|
||||||
|
inline for (Types, 0..) |s, i| {
|
||||||
|
const Struct = s.defaultValue().?;
|
||||||
|
const class_name = v8.String.initUtf8(isolate, comptime js.classNameForStruct(Struct));
|
||||||
|
global_template.set(class_name.toName(), templates[i], v8.PropertyAttribute.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The global object (Window) has already been hooked into the v8
|
||||||
|
// engine when the Env was initialized - like every other type.
|
||||||
|
// But the V8 global is its own FunctionTemplate instance so even
|
||||||
|
// though it's also a Window, we need to set the prototype for this
|
||||||
|
// specific instance of the the Window.
|
||||||
|
if (@hasDecl(Global, "prototype")) {
|
||||||
|
const proto_type = types.Receiver(@typeInfo(Global.prototype).pointer.child);
|
||||||
|
const proto_name = @typeName(proto_type);
|
||||||
|
const proto_index = @field(types.LOOKUP, proto_name);
|
||||||
|
js_global.inherit(templates[proto_index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const context_local = v8.Context.init(isolate, global_template, null);
|
||||||
|
const v8_context = v8.Persistent(v8.Context).init(isolate, context_local).castToContext();
|
||||||
|
v8_context.enter();
|
||||||
|
errdefer if (enter) v8_context.exit();
|
||||||
|
defer if (!enter) v8_context.exit();
|
||||||
|
|
||||||
|
// This shouldn't be necessary, but it is:
|
||||||
|
// https://groups.google.com/g/v8-users/c/qAQQBmbi--8
|
||||||
|
// TODO: see if newer V8 engines have a way around this.
|
||||||
|
inline for (Types, 0..) |s, i| {
|
||||||
|
const Struct = s.defaultValue().?;
|
||||||
|
|
||||||
|
if (@hasDecl(Struct, "prototype")) {
|
||||||
|
const proto_type = types.Receiver(@typeInfo(Struct.prototype).pointer.child);
|
||||||
|
const proto_name = @typeName(proto_type);
|
||||||
|
if (@hasField(types.Lookup, proto_name) == false) {
|
||||||
|
@compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ proto_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const proto_index = @field(types.LOOKUP, proto_name);
|
||||||
|
const proto_obj = templates[proto_index].getFunction(v8_context).toObject();
|
||||||
|
|
||||||
|
const self_obj = templates[i].getFunction(v8_context).toObject();
|
||||||
|
_ = self_obj.setPrototype(v8_context, proto_obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break :blk v8_context;
|
||||||
|
};
|
||||||
|
|
||||||
|
// For a Page we only create one HandleScope, it is stored in the main World (enter==true). A page can have multple contexts, 1 for each World.
|
||||||
|
// The main Context that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
|
||||||
|
// like isolated Worlds, will thereby place their objects on the main page's HandleScope. Note: In the furure the number of context will multiply multiple frames support
|
||||||
|
var handle_scope: ?v8.HandleScope = null;
|
||||||
|
if (enter) {
|
||||||
|
handle_scope = @as(v8.HandleScope, undefined);
|
||||||
|
v8.HandleScope.init(&handle_scope.?, isolate);
|
||||||
|
}
|
||||||
|
errdefer if (enter) handle_scope.?.deinit();
|
||||||
|
|
||||||
|
{
|
||||||
|
// If we want to overwrite the built-in console, we have to
|
||||||
|
// delete the built-in one.
|
||||||
|
const js_obj = v8_context.getGlobal();
|
||||||
|
const console_key = v8.String.initUtf8(isolate, "console");
|
||||||
|
if (js_obj.deleteValue(v8_context, console_key) == false) {
|
||||||
|
return error.ConsoleDeleteError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const context_id = env.context_id;
|
||||||
|
env.context_id = context_id + 1;
|
||||||
|
|
||||||
|
self.context = Context{
|
||||||
|
.page = page,
|
||||||
|
.id = context_id,
|
||||||
|
.isolate = isolate,
|
||||||
|
.v8_context = v8_context,
|
||||||
|
.templates = &env.templates,
|
||||||
|
.meta_lookup = &env.meta_lookup,
|
||||||
|
.handle_scope = handle_scope,
|
||||||
|
.script_manager = &page.script_manager,
|
||||||
|
.call_arena = page.call_arena,
|
||||||
|
.arena = self.context_arena.allocator(),
|
||||||
|
.global_callback = global_callback,
|
||||||
|
};
|
||||||
|
|
||||||
|
var context = &self.context.?;
|
||||||
|
{
|
||||||
|
// Store a pointer to our context inside the v8 context so that, given
|
||||||
|
// a v8 context, we can get our context out
|
||||||
|
const data = isolate.initBigIntU64(@intCast(@intFromPtr(context)));
|
||||||
|
v8_context.setEmbedderData(1, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom exception
|
||||||
|
// NOTE: there is no way in v8 to subclass the Error built-in type
|
||||||
|
// TODO: this is an horrible hack
|
||||||
|
inline for (Types) |s| {
|
||||||
|
const Struct = s.defaultValue().?;
|
||||||
|
if (@hasDecl(Struct, "ErrorSet")) {
|
||||||
|
const script = comptime js.classNameForStruct(Struct) ++ ".prototype.__proto__ = Error.prototype";
|
||||||
|
_ = try context.exec(script, "errorSubclass");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primitive attributes are set directly on the FunctionTemplate
|
||||||
|
// when we setup the environment. But we cannot set more complex
|
||||||
|
// types (v8 will crash).
|
||||||
|
//
|
||||||
|
// Plus, just to create more complex types, we always need a
|
||||||
|
// context, i.e. an Array has to have a Context to exist.
|
||||||
|
//
|
||||||
|
// As far as I can tell, getting the FunctionTemplate's object
|
||||||
|
// and setting values directly on it, for each context, is the
|
||||||
|
// way to do this.
|
||||||
|
inline for (Types, 0..) |s, i| {
|
||||||
|
const Struct = s.defaultValue().?;
|
||||||
|
inline for (@typeInfo(Struct).@"struct".decls) |declaration| {
|
||||||
|
const name = declaration.name;
|
||||||
|
if (comptime name[0] == '_') {
|
||||||
|
const value = @field(Struct, name);
|
||||||
|
|
||||||
|
if (comptime js.isComplexAttributeType(@typeInfo(@TypeOf(value)))) {
|
||||||
|
const js_obj = templates[i].getFunction(v8_context).toObject();
|
||||||
|
const js_name = v8.String.initUtf8(isolate, name[1..]).toName();
|
||||||
|
const js_val = try context.zigValueToJs(value);
|
||||||
|
if (!js_obj.setValue(v8_context, js_name, js_val)) {
|
||||||
|
log.fatal(.app, "set class attribute", .{
|
||||||
|
.@"struct" = @typeName(Struct),
|
||||||
|
.name = name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try context.setupGlobal();
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn removeContext(self: *ExecutionWorld) void {
|
||||||
|
// Force running the micro task to drain the queue before reseting the
|
||||||
|
// context arena.
|
||||||
|
// Tasks in the queue are relying to the arena memory could be present in
|
||||||
|
// the queue. Running them later could lead to invalid memory accesses.
|
||||||
|
self.env.runMicrotasks();
|
||||||
|
|
||||||
|
self.context.?.deinit();
|
||||||
|
self.context = null;
|
||||||
|
_ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn terminateExecution(self: *const ExecutionWorld) void {
|
||||||
|
self.env.isolate.terminateExecution();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resumeExecution(self: *const ExecutionWorld) void {
|
||||||
|
self.env.isolate.cancelTerminateExecution();
|
||||||
|
}
|
||||||
144
src/browser/js/Function.zig
Normal file
144
src/browser/js/Function.zig
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Caller = @import("Caller.zig");
|
||||||
|
const Context = @import("Context.zig");
|
||||||
|
const PersistentFunction = v8.Persistent(v8.Function);
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const Function = @This();
|
||||||
|
|
||||||
|
id: usize,
|
||||||
|
context: *js.Context,
|
||||||
|
this: ?v8.Object = null,
|
||||||
|
func: PersistentFunction,
|
||||||
|
|
||||||
|
pub const Result = struct {
|
||||||
|
stack: ?[]const u8,
|
||||||
|
exception: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn getName(self: *const Function, allocator: Allocator) ![]const u8 {
|
||||||
|
const name = self.func.castToFunction().getName();
|
||||||
|
return self.context.valueToString(name, .{ .allocator = allocator });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setName(self: *const Function, name: []const u8) void {
|
||||||
|
const v8_name = v8.String.initUtf8(self.context.isolate, name);
|
||||||
|
self.func.castToFunction().setName(v8_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn withThis(self: *const Function, value: anytype) !Function {
|
||||||
|
const this_obj = if (@TypeOf(value) == js.Object)
|
||||||
|
value.js_obj
|
||||||
|
else
|
||||||
|
(try self.context.zigValueToJs(value)).castTo(v8.Object);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.id = self.id,
|
||||||
|
.this = this_obj,
|
||||||
|
.func = self.func,
|
||||||
|
.context = self.context,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn newInstance(self: *const Function, result: *Result) !js.Object {
|
||||||
|
const context = self.context;
|
||||||
|
|
||||||
|
var try_catch: js.TryCatch = undefined;
|
||||||
|
try_catch.init(context);
|
||||||
|
defer try_catch.deinit();
|
||||||
|
|
||||||
|
// This creates a new instance using this Function as a constructor.
|
||||||
|
// This returns a generic Object
|
||||||
|
const js_obj = self.func.castToFunction().initInstance(context.v8_context, &.{}) orelse {
|
||||||
|
if (try_catch.hasCaught()) {
|
||||||
|
const allocator = context.call_arena;
|
||||||
|
result.stack = try_catch.stack(allocator) catch null;
|
||||||
|
result.exception = (try_catch.exception(allocator) catch "???") orelse "???";
|
||||||
|
} else {
|
||||||
|
result.stack = null;
|
||||||
|
result.exception = "???";
|
||||||
|
}
|
||||||
|
return error.JsConstructorFailed;
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.context = context,
|
||||||
|
.js_obj = js_obj,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
|
||||||
|
return self.callWithThis(T, self.getThis(), args);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, result: *Result) !T {
|
||||||
|
return self.tryCallWithThis(T, self.getThis(), args, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, result: *Result) !T {
|
||||||
|
var try_catch: js.TryCatch = undefined;
|
||||||
|
try_catch.init(self.context);
|
||||||
|
defer try_catch.deinit();
|
||||||
|
|
||||||
|
return self.callWithThis(T, this, args) catch |err| {
|
||||||
|
if (try_catch.hasCaught()) {
|
||||||
|
const allocator = self.context.call_arena;
|
||||||
|
result.stack = try_catch.stack(allocator) catch null;
|
||||||
|
result.exception = (try_catch.exception(allocator) catch @errorName(err)) orelse @errorName(err);
|
||||||
|
} else {
|
||||||
|
result.stack = null;
|
||||||
|
result.exception = @errorName(err);
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
|
||||||
|
const context = self.context;
|
||||||
|
|
||||||
|
const js_this = try context.valueToExistingObject(this);
|
||||||
|
|
||||||
|
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
|
||||||
|
|
||||||
|
const js_args: []const v8.Value = switch (@typeInfo(@TypeOf(aargs))) {
|
||||||
|
.@"struct" => |s| blk: {
|
||||||
|
const fields = s.fields;
|
||||||
|
var js_args: [fields.len]v8.Value = undefined;
|
||||||
|
inline for (fields, 0..) |f, i| {
|
||||||
|
js_args[i] = try context.zigValueToJs(@field(aargs, f.name));
|
||||||
|
}
|
||||||
|
const cargs: [fields.len]v8.Value = js_args;
|
||||||
|
break :blk &cargs;
|
||||||
|
},
|
||||||
|
.pointer => blk: {
|
||||||
|
var values = try context.call_arena.alloc(v8.Value, args.len);
|
||||||
|
for (args, 0..) |a, i| {
|
||||||
|
values[i] = try context.zigValueToJs(a);
|
||||||
|
}
|
||||||
|
break :blk values;
|
||||||
|
},
|
||||||
|
else => @compileError("JS Function called with invalid paremter type"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = self.func.castToFunction().call(context.v8_context, js_this, js_args);
|
||||||
|
if (result == null) {
|
||||||
|
return error.JSExecCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@typeInfo(T) == .void) return {};
|
||||||
|
const named_function = comptime Caller.NamedFunction.init(T, "callResult");
|
||||||
|
return context.jsValueToZig(named_function, T, result.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getThis(self: *const Function) v8.Object {
|
||||||
|
return self.this orelse self.context.v8_context.getGlobal();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn src(self: *const Function) ![]const u8 {
|
||||||
|
const value = self.func.castToFunction().toValue();
|
||||||
|
return self.context.valueToString(value, .{});
|
||||||
|
}
|
||||||
125
src/browser/js/Inspector.zig
Normal file
125
src/browser/js/Inspector.zig
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Context = @import("Context.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const Inspector = @This();
|
||||||
|
|
||||||
|
pub const RemoteObject = v8.RemoteObject;
|
||||||
|
|
||||||
|
isolate: v8.Isolate,
|
||||||
|
inner: *v8.Inspector,
|
||||||
|
session: v8.InspectorSession,
|
||||||
|
|
||||||
|
// We expect allocator to be an arena
|
||||||
|
pub fn init(allocator: Allocator, isolate: v8.Isolate, ctx: anytype) !Inspector {
|
||||||
|
const ContextT = @TypeOf(ctx);
|
||||||
|
|
||||||
|
const InspectorContainer = switch (@typeInfo(ContextT)) {
|
||||||
|
.@"struct" => ContextT,
|
||||||
|
.pointer => |ptr| ptr.child,
|
||||||
|
.void => NoopInspector,
|
||||||
|
else => @compileError("invalid context type"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// If necessary, turn a void context into something we can safely ptrCast
|
||||||
|
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
|
||||||
|
|
||||||
|
const channel = v8.InspectorChannel.init(safe_context, InspectorContainer.onInspectorResponse, InspectorContainer.onInspectorEvent, isolate);
|
||||||
|
|
||||||
|
const client = v8.InspectorClient.init();
|
||||||
|
|
||||||
|
const inner = try allocator.create(v8.Inspector);
|
||||||
|
v8.Inspector.init(inner, client, channel, isolate);
|
||||||
|
return .{ .inner = inner, .isolate = isolate, .session = inner.connect() };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *const Inspector) void {
|
||||||
|
self.session.deinit();
|
||||||
|
self.inner.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(self: *const Inspector, msg: []const u8) void {
|
||||||
|
// Can't assume the main Context exists (with its HandleScope)
|
||||||
|
// available when doing this. Pages (and thus the HandleScope)
|
||||||
|
// comes and goes, but CDP can keep sending messages.
|
||||||
|
const isolate = self.isolate;
|
||||||
|
var temp_scope: v8.HandleScope = undefined;
|
||||||
|
v8.HandleScope.init(&temp_scope, isolate);
|
||||||
|
defer temp_scope.deinit();
|
||||||
|
|
||||||
|
self.session.dispatchProtocolMessage(isolate, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// From CDP docs
|
||||||
|
// https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-ExecutionContextDescription
|
||||||
|
// ----
|
||||||
|
// - name: Human readable name describing given context.
|
||||||
|
// - origin: Execution context origin (ie. URL who initialised the request)
|
||||||
|
// - auxData: Embedder-specific auxiliary data likely matching
|
||||||
|
// {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
|
||||||
|
// - is_default_context: Whether the execution context is default, should match the auxData
|
||||||
|
pub fn contextCreated(
|
||||||
|
self: *const Inspector,
|
||||||
|
context: *const Context,
|
||||||
|
name: []const u8,
|
||||||
|
origin: []const u8,
|
||||||
|
aux_data: ?[]const u8,
|
||||||
|
is_default_context: bool,
|
||||||
|
) void {
|
||||||
|
self.inner.contextCreated(context.v8_context, name, origin, aux_data, is_default_context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieves the RemoteObject for a given value.
|
||||||
|
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
|
||||||
|
// just like a method return value. Therefore, if we've mapped this
|
||||||
|
// value before, we'll get the existing JS PersistedObject and if not
|
||||||
|
// we'll create it and track it for cleanup when the context ends.
|
||||||
|
pub fn getRemoteObject(
|
||||||
|
self: *const Inspector,
|
||||||
|
context: *Context,
|
||||||
|
group: []const u8,
|
||||||
|
value: anytype,
|
||||||
|
) !RemoteObject {
|
||||||
|
const js_value = try context.zigValueToJs(value);
|
||||||
|
|
||||||
|
// We do not want to expose this as a parameter for now
|
||||||
|
const generate_preview = false;
|
||||||
|
return self.session.wrapObject(
|
||||||
|
context.isolate,
|
||||||
|
context.v8_context,
|
||||||
|
js_value,
|
||||||
|
group,
|
||||||
|
generate_preview,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets a value by object ID regardless of which context it is in.
|
||||||
|
pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8) !?*anyopaque {
|
||||||
|
const unwrapped = try self.session.unwrapObject(allocator, object_id);
|
||||||
|
// The values context and groupId are not used here
|
||||||
|
const toa = getTaggedAnyOpaque(unwrapped.value) orelse return null;
|
||||||
|
if (toa.subtype == null or toa.subtype != .node) return error.ObjectIdIsNotANode;
|
||||||
|
return toa.ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NoopInspector = struct {
|
||||||
|
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
|
||||||
|
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque {
|
||||||
|
if (value.isObject() == false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const obj = value.castTo(v8.Object);
|
||||||
|
if (obj.internalFieldCount() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const external_data = obj.getInternalField(0).castTo(v8.External).get().?;
|
||||||
|
return @ptrCast(@alignCast(external_data));
|
||||||
|
}
|
||||||
149
src/browser/js/Object.zig
Normal file
149
src/browser/js/Object.zig
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Caller = @import("Caller.zig");
|
||||||
|
const Context = @import("Context.zig");
|
||||||
|
const PersistentObject = v8.Persistent(v8.Object);
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const Object = @This();
|
||||||
|
js_obj: v8.Object,
|
||||||
|
context: *js.Context,
|
||||||
|
|
||||||
|
pub const SetOpts = packed struct(u32) {
|
||||||
|
READ_ONLY: bool = false,
|
||||||
|
DONT_ENUM: bool = false,
|
||||||
|
DONT_DELETE: bool = false,
|
||||||
|
_: u29 = 0,
|
||||||
|
};
|
||||||
|
pub fn setIndex(self: Object, index: u32, value: anytype, opts: SetOpts) !void {
|
||||||
|
@setEvalBranchQuota(10000);
|
||||||
|
const key = switch (index) {
|
||||||
|
inline 0...20 => |i| std.fmt.comptimePrint("{d}", .{i}),
|
||||||
|
else => try std.fmt.allocPrint(self.context.arena, "{d}", .{index}),
|
||||||
|
};
|
||||||
|
return self.set(key, value, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(self: Object, key: []const u8, value: anytype, opts: SetOpts) error{ FailedToSet, OutOfMemory }!void {
|
||||||
|
const context = self.context;
|
||||||
|
|
||||||
|
const js_key = v8.String.initUtf8(context.isolate, key);
|
||||||
|
const js_value = try context.zigValueToJs(value);
|
||||||
|
|
||||||
|
const res = self.js_obj.defineOwnProperty(context.v8_context, js_key.toName(), js_value, @bitCast(opts)) orelse false;
|
||||||
|
if (!res) {
|
||||||
|
return error.FailedToSet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: Object, key: []const u8) !js.Value {
|
||||||
|
const context = self.context;
|
||||||
|
const js_key = v8.String.initUtf8(context.isolate, key);
|
||||||
|
const js_val = try self.js_obj.getValue(context.v8_context, js_key);
|
||||||
|
return context.createValue(js_val);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isTruthy(self: Object) bool {
|
||||||
|
const js_value = self.js_obj.toValue();
|
||||||
|
return js_value.toBool(self.context.isolate);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toString(self: Object) ![]const u8 {
|
||||||
|
const js_value = self.js_obj.toValue();
|
||||||
|
return self.context.valueToString(js_value, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toDetailString(self: Object) ![]const u8 {
|
||||||
|
const js_value = self.js_obj.toValue();
|
||||||
|
return self.context.valueToDetailString(js_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(self: Object, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
|
||||||
|
return writer.writeAll(try self.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toJson(self: Object, allocator: Allocator) ![]u8 {
|
||||||
|
const json_string = try v8.Json.stringify(self.context.v8_context, self.js_obj.toValue(), null);
|
||||||
|
const str = try self.context.jsStringToZig(json_string, .{ .allocator = allocator });
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn persist(self: Object) !Object {
|
||||||
|
var context = self.context;
|
||||||
|
const js_obj = self.js_obj;
|
||||||
|
|
||||||
|
const persisted = PersistentObject.init(context.isolate, js_obj);
|
||||||
|
try context.js_object_list.append(context.arena, persisted);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.context = context,
|
||||||
|
.js_obj = persisted.castToObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getFunction(self: Object, name: []const u8) !?js.Function {
|
||||||
|
if (self.isNullOrUndefined()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const context = self.context;
|
||||||
|
|
||||||
|
const js_name = v8.String.initUtf8(context.isolate, name);
|
||||||
|
|
||||||
|
const js_value = try self.js_obj.getValue(context.v8_context, js_name.toName());
|
||||||
|
if (!js_value.isFunction()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return try context.createFunction(js_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isNull(self: Object) bool {
|
||||||
|
return self.js_obj.toValue().isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isUndefined(self: Object) bool {
|
||||||
|
return self.js_obj.toValue().isUndefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn triState(self: Object, comptime Struct: type, comptime name: []const u8, comptime T: type) !TriState(T) {
|
||||||
|
if (self.isNull()) {
|
||||||
|
return .{ .null = {} };
|
||||||
|
}
|
||||||
|
if (self.isUndefined()) {
|
||||||
|
return .{ .undefined = {} };
|
||||||
|
}
|
||||||
|
return .{ .value = try self.toZig(Struct, name, T) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isNullOrUndefined(self: Object) bool {
|
||||||
|
return self.js_obj.toValue().isNullOrUndefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nameIterator(self: Object) js.ValueIterator {
|
||||||
|
const context = self.context;
|
||||||
|
const js_obj = self.js_obj;
|
||||||
|
|
||||||
|
const array = js_obj.getPropertyNames(context.v8_context);
|
||||||
|
const count = array.length();
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.count = count,
|
||||||
|
.context = context,
|
||||||
|
.js_obj = array.castTo(v8.Object),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toZig(self: Object, comptime Struct: type, comptime name: []const u8, comptime T: type) !T {
|
||||||
|
const named_function = comptime Caller.NamedFunction.init(Struct, name);
|
||||||
|
return self.context.jsValueToZig(named_function, T, self.js_obj.toValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn TriState(comptime T: type) type {
|
||||||
|
return union(enum) {
|
||||||
|
null: void,
|
||||||
|
undefined: void,
|
||||||
|
value: T,
|
||||||
|
};
|
||||||
|
}
|
||||||
21
src/browser/js/Platform.zig
Normal file
21
src/browser/js/Platform.zig
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Platform = @This();
|
||||||
|
inner: v8.Platform,
|
||||||
|
|
||||||
|
pub fn init() !Platform {
|
||||||
|
if (v8.initV8ICU() == false) {
|
||||||
|
return error.FailedToInitializeICU;
|
||||||
|
}
|
||||||
|
const platform = v8.Platform.initDefault(0, true);
|
||||||
|
v8.initV8Platform(platform);
|
||||||
|
v8.initV8();
|
||||||
|
return .{ .inner = platform };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: Platform) void {
|
||||||
|
_ = v8.deinitV8();
|
||||||
|
v8.deinitV8Platform();
|
||||||
|
self.inner.deinit();
|
||||||
|
}
|
||||||
25
src/browser/js/This.zig
Normal file
25
src/browser/js/This.zig
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
// This only exists so that we know whether a function wants the opaque
|
||||||
|
// JS argument (js.Object), or if it wants the receiver as an opaque
|
||||||
|
// value.
|
||||||
|
// js.Object is normally used when a method wants an opaque JS object
|
||||||
|
// that it'll pass into a callback.
|
||||||
|
// This is used when the function wants to do advanced manipulation
|
||||||
|
// of the v8.Object bound to the instance. For example, postAttach is an
|
||||||
|
// example of using This.
|
||||||
|
|
||||||
|
const This = @This();
|
||||||
|
obj: js.Object,
|
||||||
|
|
||||||
|
pub fn setIndex(self: This, index: u32, value: anytype, opts: js.Object.SetOpts) !void {
|
||||||
|
return self.obj.setIndex(index, value, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(self: This, key: []const u8, value: anytype, opts: js.Object.SetOpts) !void {
|
||||||
|
return self.obj.set(key, value, opts);
|
||||||
|
}
|
||||||
64
src/browser/js/TryCatch.zig
Normal file
64
src/browser/js/TryCatch.zig
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const TryCatch = @This();
|
||||||
|
|
||||||
|
inner: v8.TryCatch,
|
||||||
|
context: *const js.Context,
|
||||||
|
|
||||||
|
pub fn init(self: *TryCatch, context: *const js.Context) void {
|
||||||
|
self.context = context;
|
||||||
|
self.inner.init(context.isolate);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hasCaught(self: TryCatch) bool {
|
||||||
|
return self.inner.hasCaught();
|
||||||
|
}
|
||||||
|
|
||||||
|
// the caller needs to deinit the string returned
|
||||||
|
pub fn exception(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
||||||
|
const msg = self.inner.getException() orelse return null;
|
||||||
|
return try self.context.valueToString(msg, .{ .allocator = allocator });
|
||||||
|
}
|
||||||
|
|
||||||
|
// the caller needs to deinit the string returned
|
||||||
|
pub fn stack(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
||||||
|
const context = self.context;
|
||||||
|
const s = self.inner.getStackTrace(context.v8_context) orelse return null;
|
||||||
|
return try context.valueToString(s, .{ .allocator = allocator });
|
||||||
|
}
|
||||||
|
|
||||||
|
// the caller needs to deinit the string returned
|
||||||
|
pub fn sourceLine(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
||||||
|
const context = self.context;
|
||||||
|
const msg = self.inner.getMessage() orelse return null;
|
||||||
|
const sl = msg.getSourceLine(context.v8_context) orelse return null;
|
||||||
|
return try context.jsStringToZig(sl, .{ .allocator = allocator });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sourceLineNumber(self: TryCatch) ?u32 {
|
||||||
|
const context = self.context;
|
||||||
|
const msg = self.inner.getMessage() orelse return null;
|
||||||
|
return msg.getLineNumber(context.v8_context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// a shorthand method to return either the entire stack message
|
||||||
|
// or just the exception message
|
||||||
|
// - in Debug mode return the stack if available
|
||||||
|
// - otherwise return the exception if available
|
||||||
|
// the caller needs to deinit the string returned
|
||||||
|
pub fn err(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
||||||
|
if (comptime @import("builtin").mode == .Debug) {
|
||||||
|
if (try self.stack(allocator)) |msg| {
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return try self.exception(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *TryCatch) void {
|
||||||
|
self.inner.deinit();
|
||||||
|
}
|
||||||
@@ -190,7 +190,7 @@ test "generate: Union" {
|
|||||||
const value = Union(.{ Astruct, Bstruct, .{Cstruct} });
|
const value = Union(.{ Astruct, Bstruct, .{Cstruct} });
|
||||||
const ti = @typeInfo(value).@"union";
|
const ti = @typeInfo(value).@"union";
|
||||||
try std.testing.expectEqual(3, ti.fields.len);
|
try std.testing.expectEqual(3, ti.fields.len);
|
||||||
try std.testing.expectEqualStrings("*runtime.generate.test.generate: Union.Astruct.Other", @typeName(ti.fields[0].type));
|
try std.testing.expectEqualStrings("*browser.js.generate.test.generate: Union.Astruct.Other", @typeName(ti.fields[0].type));
|
||||||
try std.testing.expectEqualStrings(ti.fields[0].name, "Astruct");
|
try std.testing.expectEqualStrings(ti.fields[0].name, "Astruct");
|
||||||
try std.testing.expectEqual(*Bstruct, ti.fields[1].type);
|
try std.testing.expectEqual(*Bstruct, ti.fields[1].type);
|
||||||
try std.testing.expectEqualStrings(ti.fields[1].name, "Bstruct");
|
try std.testing.expectEqualStrings(ti.fields[1].name, "Bstruct");
|
||||||
504
src/browser/js/js.zig
Normal file
504
src/browser/js/js.zig
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
// Copyright (C) 2023-2024 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");
|
||||||
|
pub const v8 = @import("v8");
|
||||||
|
|
||||||
|
const types = @import("types.zig");
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
pub const Env = @import("Env.zig");
|
||||||
|
pub const ExecutionWorld = @import("ExecutionWorld.zig");
|
||||||
|
pub const Context = @import("Context.zig");
|
||||||
|
pub const Inspector = @import("Inspector.zig");
|
||||||
|
|
||||||
|
// TODO: Is "This" really necessary?
|
||||||
|
pub const This = @import("This.zig");
|
||||||
|
pub const Object = @import("Object.zig");
|
||||||
|
pub const TryCatch = @import("TryCatch.zig");
|
||||||
|
pub const Function = @import("Function.zig");
|
||||||
|
|
||||||
|
const Caller = @import("Caller.zig");
|
||||||
|
const NamedFunction = Context.NamedFunction;
|
||||||
|
|
||||||
|
// If a function returns a []i32, should that map to a plain-old
|
||||||
|
// JavaScript array, or a Int32Array? It's ambiguous. By default, we'll
|
||||||
|
// map arrays/slices to the JavaScript arrays. If you want a TypedArray
|
||||||
|
// wrap it in this.
|
||||||
|
// Also, this type has nothing to do with the Env. But we place it here
|
||||||
|
// for consistency. Want a callback? Env.Callback. Want a JsObject?
|
||||||
|
// Env.JsObject. Want a TypedArray? Env.TypedArray.
|
||||||
|
pub fn TypedArray(comptime T: type) type {
|
||||||
|
return struct {
|
||||||
|
pub const _TYPED_ARRAY_ID_KLUDGE = true;
|
||||||
|
|
||||||
|
values: []const T,
|
||||||
|
|
||||||
|
pub fn dupe(self: TypedArray(T), allocator: Allocator) !TypedArray(T) {
|
||||||
|
return .{ .values = try allocator.dupe(T, self.values) };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const PromiseResolver = struct {
|
||||||
|
context: *Context,
|
||||||
|
resolver: v8.PromiseResolver,
|
||||||
|
|
||||||
|
pub fn promise(self: PromiseResolver) Promise {
|
||||||
|
return self.resolver.getPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve(self: PromiseResolver, value: anytype) !void {
|
||||||
|
const context = self.context;
|
||||||
|
const js_value = try context.zigValueToJs(value);
|
||||||
|
|
||||||
|
// resolver.resolve will return null if the promise isn't pending
|
||||||
|
const ok = self.resolver.resolve(context.v8_context, js_value) orelse return;
|
||||||
|
if (!ok) {
|
||||||
|
return error.FailedToResolvePromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reject(self: PromiseResolver, value: anytype) !void {
|
||||||
|
const context = self.context;
|
||||||
|
const js_value = try context.zigValueToJs(value);
|
||||||
|
|
||||||
|
// resolver.reject will return null if the promise isn't pending
|
||||||
|
const ok = self.resolver.reject(context.v8_context, js_value) orelse return;
|
||||||
|
if (!ok) {
|
||||||
|
return error.FailedToRejectPromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PersistentPromiseResolver = struct {
|
||||||
|
context: *Context,
|
||||||
|
resolver: v8.Persistent(v8.PromiseResolver),
|
||||||
|
|
||||||
|
pub fn deinit(self: *PersistentPromiseResolver) void {
|
||||||
|
self.resolver.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn promise(self: PersistentPromiseResolver) Promise {
|
||||||
|
return self.resolver.castToPromiseResolver().getPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve(self: PersistentPromiseResolver, value: anytype) !void {
|
||||||
|
const context = self.context;
|
||||||
|
const js_value = try context.zigValueToJs(value);
|
||||||
|
|
||||||
|
// resolver.resolve will return null if the promise isn't pending
|
||||||
|
const ok = self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) orelse return;
|
||||||
|
if (!ok) {
|
||||||
|
return error.FailedToResolvePromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reject(self: PersistentPromiseResolver, value: anytype) !void {
|
||||||
|
const context = self.context;
|
||||||
|
const js_value = try context.zigValueToJs(value);
|
||||||
|
|
||||||
|
// resolver.reject will return null if the promise isn't pending
|
||||||
|
const ok = self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) orelse return;
|
||||||
|
if (!ok) {
|
||||||
|
return error.FailedToRejectPromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Promise = v8.Promise;
|
||||||
|
|
||||||
|
// When doing jsValueToZig, string ([]const u8) are managed by the
|
||||||
|
// call_arena. That means that if the API wants to persist the string
|
||||||
|
// (which is relatively common), it needs to dupe it again.
|
||||||
|
// If the parameter is an Env.String rather than a []const u8, then
|
||||||
|
// the page's arena will be used (rather than the call arena).
|
||||||
|
pub const String = struct {
|
||||||
|
string: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Exception = struct {
|
||||||
|
inner: v8.Value,
|
||||||
|
context: *const Context,
|
||||||
|
|
||||||
|
// the caller needs to deinit the string returned
|
||||||
|
pub fn exception(self: Exception, allocator: Allocator) ![]const u8 {
|
||||||
|
return self.context.valueToString(self.inner, .{ .allocator = allocator });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Value = struct {
|
||||||
|
value: v8.Value,
|
||||||
|
context: *const Context,
|
||||||
|
|
||||||
|
// the caller needs to deinit the string returned
|
||||||
|
pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
|
||||||
|
return self.context.valueToString(self.value, .{ .allocator = allocator });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fromJson(ctx: *Context, json: []const u8) !Value {
|
||||||
|
const json_string = v8.String.initUtf8(ctx.isolate, json);
|
||||||
|
const value = try v8.Json.parse(ctx.v8_context, json_string);
|
||||||
|
return Value{ .context = ctx, .value = value };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ValueIterator = struct {
|
||||||
|
count: u32,
|
||||||
|
idx: u32 = 0,
|
||||||
|
js_obj: v8.Object,
|
||||||
|
context: *const Context,
|
||||||
|
|
||||||
|
pub fn next(self: *ValueIterator) !?Value {
|
||||||
|
const idx = self.idx;
|
||||||
|
if (idx == self.count) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
self.idx += 1;
|
||||||
|
|
||||||
|
const context = self.context;
|
||||||
|
const js_val = try self.js_obj.getAtIndex(context.v8_context, idx);
|
||||||
|
return context.createValue(js_val);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn UndefinedOr(comptime T: type) type {
|
||||||
|
return union(enum) {
|
||||||
|
undefined: void,
|
||||||
|
value: T,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// An interface for types that want to have their jsScopeEnd function be
|
||||||
|
// called when the call context ends
|
||||||
|
const CallScopeEndCallback = struct {
|
||||||
|
ptr: *anyopaque,
|
||||||
|
callScopeEndFn: *const fn (ptr: *anyopaque) void,
|
||||||
|
|
||||||
|
fn init(ptr: anytype) CallScopeEndCallback {
|
||||||
|
const T = @TypeOf(ptr);
|
||||||
|
const ptr_info = @typeInfo(T);
|
||||||
|
|
||||||
|
const gen = struct {
|
||||||
|
pub fn callScopeEnd(pointer: *anyopaque) void {
|
||||||
|
const self: T = @ptrCast(@alignCast(pointer));
|
||||||
|
return ptr_info.pointer.child.jsCallScopeEnd(self);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.ptr = ptr,
|
||||||
|
.callScopeEndFn = gen.callScopeEnd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn callScopeEnd(self: CallScopeEndCallback) void {
|
||||||
|
self.callScopeEndFn(self.ptr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Callback called on global's property missing.
|
||||||
|
// Return true to intercept the execution or false to let the call
|
||||||
|
// continue the chain.
|
||||||
|
pub const GlobalMissingCallback = struct {
|
||||||
|
ptr: *anyopaque,
|
||||||
|
missingFn: *const fn (ptr: *anyopaque, name: []const u8, ctx: *Context) bool,
|
||||||
|
|
||||||
|
pub fn init(ptr: anytype) GlobalMissingCallback {
|
||||||
|
const T = @TypeOf(ptr);
|
||||||
|
const ptr_info = @typeInfo(T);
|
||||||
|
|
||||||
|
const gen = struct {
|
||||||
|
pub fn missing(pointer: *anyopaque, name: []const u8, ctx: *Context) bool {
|
||||||
|
const self: T = @ptrCast(@alignCast(pointer));
|
||||||
|
return ptr_info.pointer.child.missing(self, name, ctx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.ptr = ptr,
|
||||||
|
.missingFn = gen.missing,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn missing(self: GlobalMissingCallback, name: []const u8, ctx: *Context) bool {
|
||||||
|
return self.missingFn(self.ptr, name, ctx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attributes that return a primitive type are setup directly on the
|
||||||
|
// FunctionTemplate when the Env is setup. More complex types need a v8.Context
|
||||||
|
// and cannot be set directly on the FunctionTemplate.
|
||||||
|
// We default to saying types are primitives because that's mostly what
|
||||||
|
// we have. If we add a new complex type that isn't explictly handled here,
|
||||||
|
// we'll get a compiler error in simpleZigValueToJs, and can then explicitly
|
||||||
|
// add the type here.
|
||||||
|
pub fn isComplexAttributeType(ti: std.builtin.Type) bool {
|
||||||
|
return switch (ti) {
|
||||||
|
.array => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// These are simple types that we can convert to JS with only an isolate. This
|
||||||
|
// is separated from the Caller's zigValueToJs to make it available when we
|
||||||
|
// don't have a caller (i.e., when setting static attributes on types)
|
||||||
|
pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bool) if (fail) v8.Value else ?v8.Value {
|
||||||
|
switch (@typeInfo(@TypeOf(value))) {
|
||||||
|
.void => return v8.initUndefined(isolate).toValue(),
|
||||||
|
.null => return v8.initNull(isolate).toValue(),
|
||||||
|
.bool => return v8.getValue(if (value) v8.initTrue(isolate) else v8.initFalse(isolate)),
|
||||||
|
.int => |n| switch (n.signedness) {
|
||||||
|
.signed => {
|
||||||
|
if (value >= -2_147_483_648 and value <= 2_147_483_647) {
|
||||||
|
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
|
||||||
|
}
|
||||||
|
if (comptime n.bits <= 64) {
|
||||||
|
return v8.getValue(v8.BigInt.initI64(isolate, @intCast(value)));
|
||||||
|
}
|
||||||
|
@compileError(@typeName(value) ++ " is not supported");
|
||||||
|
},
|
||||||
|
.unsigned => {
|
||||||
|
if (value <= 4_294_967_295) {
|
||||||
|
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
|
||||||
|
}
|
||||||
|
if (comptime n.bits <= 64) {
|
||||||
|
return v8.getValue(v8.BigInt.initU64(isolate, @intCast(value)));
|
||||||
|
}
|
||||||
|
@compileError(@typeName(value) ++ " is not supported");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.comptime_int => {
|
||||||
|
if (value >= 0) {
|
||||||
|
if (value <= 4_294_967_295) {
|
||||||
|
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
|
||||||
|
}
|
||||||
|
return v8.BigInt.initU64(isolate, @intCast(value)).toValue();
|
||||||
|
}
|
||||||
|
if (value >= -2_147_483_648) {
|
||||||
|
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
|
||||||
|
}
|
||||||
|
return v8.BigInt.initI64(isolate, @intCast(value)).toValue();
|
||||||
|
},
|
||||||
|
.comptime_float => return v8.Number.init(isolate, value).toValue(),
|
||||||
|
.float => |f| switch (f.bits) {
|
||||||
|
64 => return v8.Number.init(isolate, value).toValue(),
|
||||||
|
32 => return v8.Number.init(isolate, @floatCast(value)).toValue(),
|
||||||
|
else => @compileError(@typeName(value) ++ " is not supported"),
|
||||||
|
},
|
||||||
|
.pointer => |ptr| {
|
||||||
|
if (ptr.size == .slice and ptr.child == u8) {
|
||||||
|
return v8.String.initUtf8(isolate, value).toValue();
|
||||||
|
}
|
||||||
|
if (ptr.size == .one) {
|
||||||
|
const one_info = @typeInfo(ptr.child);
|
||||||
|
if (one_info == .array and one_info.array.child == u8) {
|
||||||
|
return v8.String.initUtf8(isolate, value).toValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.array => return simpleZigValueToJs(isolate, &value, fail),
|
||||||
|
.optional => {
|
||||||
|
if (value) |v| {
|
||||||
|
return simpleZigValueToJs(isolate, v, fail);
|
||||||
|
}
|
||||||
|
return v8.initNull(isolate).toValue();
|
||||||
|
},
|
||||||
|
.@"struct" => {
|
||||||
|
const T = @TypeOf(value);
|
||||||
|
if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) {
|
||||||
|
const values = value.values;
|
||||||
|
const value_type = @typeInfo(@TypeOf(values)).pointer.child;
|
||||||
|
const len = values.len;
|
||||||
|
const bits = switch (@typeInfo(value_type)) {
|
||||||
|
.int => |n| n.bits,
|
||||||
|
.float => |f| f.bits,
|
||||||
|
else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)),
|
||||||
|
};
|
||||||
|
|
||||||
|
var array_buffer: v8.ArrayBuffer = undefined;
|
||||||
|
if (len == 0) {
|
||||||
|
array_buffer = v8.ArrayBuffer.init(isolate, 0);
|
||||||
|
} else {
|
||||||
|
const buffer_len = len * bits / 8;
|
||||||
|
const backing_store = v8.BackingStore.init(isolate, buffer_len);
|
||||||
|
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
|
||||||
|
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);
|
||||||
|
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (@typeInfo(value_type)) {
|
||||||
|
.int => |n| switch (n.signedness) {
|
||||||
|
.unsigned => switch (n.bits) {
|
||||||
|
8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
else => {},
|
||||||
|
},
|
||||||
|
.signed => switch (n.bits) {
|
||||||
|
8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
else => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.float => |f| switch (f.bits) {
|
||||||
|
32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
else => {},
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
// We normally don't fail in this function unless fail == true
|
||||||
|
// but this can never be valid.
|
||||||
|
@compileError("Invalid TypeArray type: " ++ @typeName(value_type));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.@"union" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail),
|
||||||
|
.@"enum" => {
|
||||||
|
const T = @TypeOf(value);
|
||||||
|
if (@hasDecl(T, "toString")) {
|
||||||
|
// This should be deprecated in favor of the ENUM_JS_USE_TAG.
|
||||||
|
return simpleZigValueToJs(isolate, value.toString(), fail);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@hasDecl(T, "ENUM_JS_USE_TAG")) {
|
||||||
|
return simpleZigValueToJs(isolate, @tagName(value), fail);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
if (fail) {
|
||||||
|
@compileError("Unsupported Zig type " ++ @typeName(@TypeOf(value)));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _createException(isolate: v8.Isolate, msg: []const u8) v8.Value {
|
||||||
|
return v8.Exception.initError(v8.String.initUtf8(isolate, msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn classNameForStruct(comptime Struct: type) []const u8 {
|
||||||
|
if (@hasDecl(Struct, "js_name")) {
|
||||||
|
return Struct.js_name;
|
||||||
|
}
|
||||||
|
@setEvalBranchQuota(10_000);
|
||||||
|
const full_name = @typeName(Struct);
|
||||||
|
const last = std.mem.lastIndexOfScalar(u8, full_name, '.') orelse return full_name;
|
||||||
|
return full_name[last + 1 ..];
|
||||||
|
}
|
||||||
|
|
||||||
|
// When we return a Zig object to V8, we put it on the heap and pass it into
|
||||||
|
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
|
||||||
|
// function parameter, we know what type it _should_ be. Above, in Caller.method
|
||||||
|
// (for example), we know all the parameter types. So if a Zig function takes
|
||||||
|
// a single parameter (its receiver), we know what that type is.
|
||||||
|
//
|
||||||
|
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
|
||||||
|
// to the parameter type:
|
||||||
|
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
|
||||||
|
//
|
||||||
|
// But there are 2 reasons we can't do that.
|
||||||
|
//
|
||||||
|
// == Reason 1 ==
|
||||||
|
// The JS code might pass the wrong type:
|
||||||
|
//
|
||||||
|
// var cat = new Cat();
|
||||||
|
// cat.setOwner(new Cat());
|
||||||
|
//
|
||||||
|
// The zig _setOwner method expects the 2nd parameter to be an *Owner, but
|
||||||
|
// the JS code passed a *Cat.
|
||||||
|
//
|
||||||
|
// To solve this issue, we tag every returned value so that we can check what
|
||||||
|
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
|
||||||
|
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
|
||||||
|
//
|
||||||
|
// == Reason 2 ==
|
||||||
|
// Because of prototype inheritance, even "correct" code can be a challenge. For
|
||||||
|
// example, say the above JavaScript is fixed:
|
||||||
|
//
|
||||||
|
// var cat = new Cat();
|
||||||
|
// cat.setOwner(new Owner("Leto"));
|
||||||
|
//
|
||||||
|
// The issue is that setOwner might not expect an *Owner, but rather a
|
||||||
|
// *Person, which is the prototype for Owner. Now our Zig code is expecting
|
||||||
|
// a *Person, but it was (correctly) given an *Owner.
|
||||||
|
// For this reason, we also store the prototype's type index.
|
||||||
|
//
|
||||||
|
// One of the prototype mechanisms that we support is via composition. Owner
|
||||||
|
// can have a "proto: *Person" field. For this reason, we also store the offset
|
||||||
|
// of the proto field, so that, given an intFromPtr(*Owner) we can access its
|
||||||
|
// proto field.
|
||||||
|
//
|
||||||
|
// The other prototype mechanism that we support is for netsurf, where we just
|
||||||
|
// cast one type to another. In this case, we'll store an offset of -1 (as a
|
||||||
|
// sentinel to indicate that we should just cast directly).
|
||||||
|
pub const TaggedAnyOpaque = struct {
|
||||||
|
// The type of object this is. The type is captured as an index, which
|
||||||
|
// corresponds to both a field in TYPE_LOOKUP and the index of
|
||||||
|
// PROTOTYPE_TABLE
|
||||||
|
index: u16,
|
||||||
|
|
||||||
|
// Ptr to the Zig instance. Between the context where it's called (i.e.
|
||||||
|
// we have the comptime parameter info for all functions), and the index field
|
||||||
|
// we can figure out what type this is.
|
||||||
|
ptr: *anyopaque,
|
||||||
|
|
||||||
|
// When we're asked to describe an object via the Inspector, we _must_ include
|
||||||
|
// the proper subtype (and description) fields in the returned JSON.
|
||||||
|
// V8 will give us a Value and ask us for the subtype. From the v8.Value we
|
||||||
|
// can get a v8.Object, and from the v8.Object, we can get out TaggedAnyOpaque
|
||||||
|
// which is where we store the subtype.
|
||||||
|
subtype: ?types.Sub,
|
||||||
|
};
|
||||||
|
|
||||||
|
// These are here, and not in Inspector.zig, because Inspector.zig isn't always
|
||||||
|
// included (e.g. in the wpt build).
|
||||||
|
|
||||||
|
// This is called from V8. Whenever the v8 inspector has to describe a value
|
||||||
|
// it'll call this function to gets its [optional] subtype - which, from V8's
|
||||||
|
// point of view, is an arbitrary string.
|
||||||
|
pub export fn v8_inspector__Client__IMPL__valueSubtype(
|
||||||
|
_: *v8.c.InspectorClientImpl,
|
||||||
|
c_value: *const v8.C_Value,
|
||||||
|
) callconv(.c) [*c]const u8 {
|
||||||
|
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
|
||||||
|
return if (external_entry.subtype) |st| @tagName(st) else null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as valueSubType above, but for the optional description field.
|
||||||
|
// From what I can tell, some drivers _need_ the description field to be
|
||||||
|
// present, even if it's empty. So if we have a subType for the value, we'll
|
||||||
|
// put an empty description.
|
||||||
|
pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
|
||||||
|
_: *v8.c.InspectorClientImpl,
|
||||||
|
v8_context: *const v8.C_Context,
|
||||||
|
c_value: *const v8.C_Value,
|
||||||
|
) callconv(.c) [*c]const u8 {
|
||||||
|
_ = v8_context;
|
||||||
|
|
||||||
|
// We _must_ include a non-null description in order for the subtype value
|
||||||
|
// to be included. Besides that, I don't know if the value has any meaning
|
||||||
|
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
|
||||||
|
return if (external_entry.subtype == null) null else "";
|
||||||
|
}
|
||||||
184
src/browser/js/types.zig
Normal file
184
src/browser/js/types.zig
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const generate = @import("generate.zig");
|
||||||
|
|
||||||
|
const Interfaces = generate.Tuple(.{
|
||||||
|
@import("../crypto/crypto.zig").Crypto,
|
||||||
|
@import("../console/console.zig").Console,
|
||||||
|
@import("../css/css.zig").Interfaces,
|
||||||
|
@import("../cssom/cssom.zig").Interfaces,
|
||||||
|
@import("../dom/dom.zig").Interfaces,
|
||||||
|
@import("../dom/shadow_root.zig").ShadowRoot,
|
||||||
|
@import("../encoding/encoding.zig").Interfaces,
|
||||||
|
@import("../events/event.zig").Interfaces,
|
||||||
|
@import("../html/html.zig").Interfaces,
|
||||||
|
@import("../iterator/iterator.zig").Interfaces,
|
||||||
|
@import("../storage/storage.zig").Interfaces,
|
||||||
|
@import("../url/url.zig").Interfaces,
|
||||||
|
@import("../xhr/xhr.zig").Interfaces,
|
||||||
|
@import("../navigation/root.zig").Interfaces,
|
||||||
|
@import("../xhr/form_data.zig").Interfaces,
|
||||||
|
@import("../xhr/File.zig"),
|
||||||
|
@import("../xmlserializer/xmlserializer.zig").Interfaces,
|
||||||
|
@import("../fetch/fetch.zig").Interfaces,
|
||||||
|
@import("../streams/streams.zig").Interfaces,
|
||||||
|
});
|
||||||
|
|
||||||
|
pub const Types = @typeInfo(Interfaces).@"struct".fields;
|
||||||
|
|
||||||
|
// Imagine we have a type Cat which has a getter:
|
||||||
|
//
|
||||||
|
// fn get_owner(self: *Cat) *Owner {
|
||||||
|
// return self.owner;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// When we execute caller.getter, we'll end up doing something like:
|
||||||
|
// const res = @call(.auto, Cat.get_owner, .{cat_instance});
|
||||||
|
//
|
||||||
|
// How do we turn `res`, which is an *Owner, into something we can return
|
||||||
|
// to v8? We need the ObjectTemplate associated with Owner. How do we
|
||||||
|
// get that? Well, we store all the ObjectTemplates in an array that's
|
||||||
|
// tied to env. So we do something like:
|
||||||
|
//
|
||||||
|
// env.templates[index_of_owner].initInstance(...);
|
||||||
|
//
|
||||||
|
// But how do we get that `index_of_owner`? `Lookup` is a struct
|
||||||
|
// that looks like:
|
||||||
|
//
|
||||||
|
// const Lookup = struct {
|
||||||
|
// comptime cat: usize = 0,
|
||||||
|
// comptime owner: usize = 1,
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// So to get the template index of `owner`, we can do:
|
||||||
|
//
|
||||||
|
// const index_id = @field(type_lookup, @typeName(@TypeOf(res));
|
||||||
|
//
|
||||||
|
pub const Lookup = blk: {
|
||||||
|
var fields: [Types.len]std.builtin.Type.StructField = undefined;
|
||||||
|
for (Types, 0..) |s, i| {
|
||||||
|
|
||||||
|
// This prototype type check has nothing to do with building our
|
||||||
|
// Lookup. But we put it here, early, so that the rest of the
|
||||||
|
// code doesn't have to worry about checking if Struct.prototype is
|
||||||
|
// a pointer.
|
||||||
|
const Struct = s.defaultValue().?;
|
||||||
|
if (@hasDecl(Struct, "prototype") and @typeInfo(Struct.prototype) != .pointer) {
|
||||||
|
@compileError(std.fmt.comptimePrint("Prototype '{s}' for type '{s} must be a pointer", .{ @typeName(Struct.prototype), @typeName(Struct) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
fields[i] = .{
|
||||||
|
.name = @typeName(Receiver(Struct)),
|
||||||
|
.type = usize,
|
||||||
|
.is_comptime = true,
|
||||||
|
.alignment = @alignOf(usize),
|
||||||
|
.default_value_ptr = &i,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break :blk @Type(.{ .@"struct" = .{
|
||||||
|
.layout = .auto,
|
||||||
|
.decls = &.{},
|
||||||
|
.is_tuple = false,
|
||||||
|
.fields = &fields,
|
||||||
|
} });
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const LOOKUP = Lookup{};
|
||||||
|
|
||||||
|
// Creates a list where the index of a type contains its prototype index
|
||||||
|
// const Animal = struct{};
|
||||||
|
// const Cat = struct{
|
||||||
|
// pub const prototype = *Animal;
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// Would create an array: [0, 0]
|
||||||
|
// Animal, at index, 0, has no prototype, so we set it to itself
|
||||||
|
// Cat, at index 1, has an Animal prototype, so we set it to 0.
|
||||||
|
//
|
||||||
|
// When we're trying to pass an argument to a Zig function, we'll know the
|
||||||
|
// target type (the function parameter type), and we'll have a
|
||||||
|
// TaggedAnyOpaque which will have the index of the type of that parameter.
|
||||||
|
// We'll use the PROTOTYPE_TABLE to see if the TaggedAnyType should be
|
||||||
|
// cast to a prototype.
|
||||||
|
pub const PROTOTYPE_TABLE = blk: {
|
||||||
|
var table: [Types.len]u16 = undefined;
|
||||||
|
for (Types, 0..) |s, i| {
|
||||||
|
var prototype_index = i;
|
||||||
|
const Struct = s.defaultValue().?;
|
||||||
|
if (@hasDecl(Struct, "prototype")) {
|
||||||
|
const TI = @typeInfo(Struct.prototype);
|
||||||
|
const proto_name = @typeName(Receiver(TI.pointer.child));
|
||||||
|
prototype_index = @field(LOOKUP, proto_name);
|
||||||
|
}
|
||||||
|
table[i] = prototype_index;
|
||||||
|
}
|
||||||
|
break :blk table;
|
||||||
|
};
|
||||||
|
|
||||||
|
// This is essentially meta data for each type. Each is stored in env.meta_lookup
|
||||||
|
// The index for a type can be retrieved via:
|
||||||
|
// const index = @field(TYPE_LOOKUP, @typeName(Receiver(Struct)));
|
||||||
|
// const meta = env.meta_lookup[index];
|
||||||
|
pub const Meta = struct {
|
||||||
|
// Every type is given a unique index. That index is used to lookup various
|
||||||
|
// things, i.e. the prototype chain.
|
||||||
|
index: u16,
|
||||||
|
|
||||||
|
// We store the type's subtype here, so that when we create an instance of
|
||||||
|
// the type, and bind it to JavaScript, we can store the subtype along with
|
||||||
|
// the created TaggedAnyOpaque.s
|
||||||
|
subtype: ?Sub,
|
||||||
|
|
||||||
|
// If this type has composition-based prototype, represents the byte-offset
|
||||||
|
// from ptr where the `proto` field is located. A negative offsets is used
|
||||||
|
// to indicate that the prototype field is behind a pointer.
|
||||||
|
proto_offset: i32,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Sub = enum {
|
||||||
|
@"error",
|
||||||
|
array,
|
||||||
|
arraybuffer,
|
||||||
|
dataview,
|
||||||
|
date,
|
||||||
|
generator,
|
||||||
|
iterator,
|
||||||
|
map,
|
||||||
|
node,
|
||||||
|
promise,
|
||||||
|
proxy,
|
||||||
|
regexp,
|
||||||
|
set,
|
||||||
|
typedarray,
|
||||||
|
wasmvalue,
|
||||||
|
weakmap,
|
||||||
|
weakset,
|
||||||
|
webassemblymemory,
|
||||||
|
};
|
||||||
|
|
||||||
|
// When we map a Zig instance into a JsObject, we'll normally store the a
|
||||||
|
// TaggedAnyOpaque (TAO) inside of the JsObject's internal field. This requires
|
||||||
|
// ensuring that the instance template has an InternalFieldCount of 1. However,
|
||||||
|
// for empty objects, we don't need to store the TAO, because we can't just cast
|
||||||
|
// one empty object to another, so for those, as an optimization, we do not set
|
||||||
|
// the InternalFieldCount.
|
||||||
|
pub fn isEmpty(comptime T: type) bool {
|
||||||
|
return @typeInfo(T) != .@"opaque" and @sizeOf(T) == 0 and @hasDecl(T, "js_legacy_factory") == false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a struct:
|
||||||
|
// const Cat = struct {
|
||||||
|
// pub fn meow(self: *Cat) void { ... }
|
||||||
|
// }
|
||||||
|
// Then obviously, the receiver of its methods are going to be a *Cat (or *const Cat)
|
||||||
|
//
|
||||||
|
// However, we can also do:
|
||||||
|
// const Cat = struct {
|
||||||
|
// pub const Self = OtherImpl;
|
||||||
|
// pub fn meow(self: *OtherImpl) void { ... }
|
||||||
|
// }
|
||||||
|
// In which case, as we see above, the receiver is derived from the Self declaration
|
||||||
|
pub fn Receiver(comptime Struct: type) type {
|
||||||
|
return if (@hasDecl(Struct, "Self")) Struct.Self else Struct;
|
||||||
|
}
|
||||||
@@ -26,14 +26,9 @@ const c = @cImport({
|
|||||||
@cInclude("mimalloc.h");
|
@cInclude("mimalloc.h");
|
||||||
});
|
});
|
||||||
|
|
||||||
const Error = error{
|
|
||||||
HeapNotNull,
|
|
||||||
HeapNull,
|
|
||||||
};
|
|
||||||
|
|
||||||
var heap: ?*c.mi_heap_t = null;
|
var heap: ?*c.mi_heap_t = null;
|
||||||
|
|
||||||
pub fn create() Error!void {
|
pub fn create() void {
|
||||||
std.debug.assert(heap == null);
|
std.debug.assert(heap == null);
|
||||||
heap = c.mi_heap_new();
|
heap = c.mi_heap_new();
|
||||||
std.debug.assert(heap != null);
|
std.debug.assert(heap != null);
|
||||||
@@ -45,6 +40,45 @@ pub fn destroy() void {
|
|||||||
heap = null;
|
heap = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getRSS() i64 {
|
||||||
|
if (@import("builtin").mode != .Debug) {
|
||||||
|
// just don't trust my implementation, plus a caller might not know
|
||||||
|
// that this requires parsing some unstructured data
|
||||||
|
@compileError("Only available in debug builds");
|
||||||
|
}
|
||||||
|
var buf: [1024 * 8]u8 = undefined;
|
||||||
|
var fba = std.heap.FixedBufferAllocator.init(&buf);
|
||||||
|
var writer = std.Io.Writer.Allocating.init(fba.allocator());
|
||||||
|
|
||||||
|
c.mi_stats_print_out(struct {
|
||||||
|
fn print(msg: [*c]const u8, data: ?*anyopaque) callconv(.c) void {
|
||||||
|
const w: *std.Io.Writer = @ptrCast(@alignCast(data.?));
|
||||||
|
w.writeAll(std.mem.span(msg)) catch |err| {
|
||||||
|
std.debug.print("Failed to write mimalloc data: {}\n", .{err});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}.print, &writer.writer);
|
||||||
|
|
||||||
|
const data = writer.written();
|
||||||
|
const index = std.mem.indexOf(u8, data, "rss: ") orelse return -1;
|
||||||
|
const sep = std.mem.indexOfScalarPos(u8, data, index + 5, ' ') orelse return -2;
|
||||||
|
const value = std.fmt.parseFloat(f64, data[index + 5 .. sep]) catch return -3;
|
||||||
|
const unit = data[sep + 1 ..];
|
||||||
|
if (std.mem.startsWith(u8, unit, "KiB,")) {
|
||||||
|
return @as(i64, @intFromFloat(value)) * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, unit, "MiB,")) {
|
||||||
|
return @as(i64, @intFromFloat(value)) * 1024 * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, unit, "GiB,")) {
|
||||||
|
return @as(i64, @intFromFloat(value)) * 1024 * 1024 * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -4;
|
||||||
|
}
|
||||||
|
|
||||||
pub export fn m_alloc(size: usize) callconv(.c) ?*anyopaque {
|
pub export fn m_alloc(size: usize) callconv(.c) ?*anyopaque {
|
||||||
std.debug.assert(heap != null);
|
std.debug.assert(heap != null);
|
||||||
return c.mi_heap_malloc(heap.?, size);
|
return c.mi_heap_malloc(heap.?, size);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user