mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-28 15:40:04 +00:00
Compare commits
528 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e25c33eaa6 | ||
|
|
7bddc0a89c | ||
|
|
403f42bf38 | ||
|
|
b2e301418f | ||
|
|
334a2e44a1 | ||
|
|
c9121a03d2 | ||
|
|
cc93180d57 | ||
|
|
4062a425cb | ||
|
|
cce533ebb6 | ||
|
|
48df38cbfe | ||
|
|
f982f073df | ||
|
|
34999f12ca | ||
|
|
c8d5665653 | ||
|
|
ddebaf87d0 | ||
|
|
6b80cd6109 | ||
|
|
7635d8d2a5 | ||
|
|
634e3e35a0 | ||
|
|
da3dc58199 | ||
|
|
4f99df694b | ||
|
|
982b8e2d72 | ||
|
|
6e7c8d7ae2 | ||
|
|
3c858f522b | ||
|
|
f2a30f8cdd | ||
|
|
43785bfab4 | ||
|
|
78edf6d324 | ||
|
|
73565c4493 | ||
|
|
8c37cac957 | ||
|
|
eceab76b6f | ||
|
|
1f81b6ddc4 | ||
|
|
52c3aadd24 | ||
|
|
ad87573d09 | ||
|
|
20fbfc8544 | ||
|
|
7695c8403f | ||
|
|
421983d06e | ||
|
|
328c681a8f | ||
|
|
48d94d0f68 | ||
|
|
10ad5d763e | ||
|
|
2a78c946e4 | ||
|
|
a7872aa054 | ||
|
|
5c228ae0a1 | ||
|
|
ce73f7ac5a | ||
|
|
64107f5957 | ||
|
|
8a1795d56f | ||
|
|
b104c3bfe8 | ||
|
|
82e3f126ff | ||
|
|
175488563e | ||
|
|
da51cdd11d | ||
|
|
a8a47b138f | ||
|
|
b63d4cf675 | ||
|
|
03b999c592 | ||
|
|
a91afab038 | ||
|
|
d4747b5386 | ||
|
|
41b81c8b05 | ||
|
|
552831364d | ||
|
|
42b5e32473 | ||
|
|
e9c36fd6f8 | ||
|
|
952dfbef36 | ||
|
|
254984b600 | ||
|
|
8cbc58d257 | ||
|
|
e6cc3e8c34 | ||
|
|
516335e0ed | ||
|
|
01798ed7f8 | ||
|
|
fcad67a854 | ||
|
|
e359ffead0 | ||
|
|
eb09041859 | ||
|
|
b3d52c966d | ||
|
|
3fb8a14348 | ||
|
|
84a949e7c7 | ||
|
|
eaf1cb26b2 | ||
|
|
f37962d3de | ||
|
|
511e957d4b | ||
|
|
71df03b729 | ||
|
|
839052f4b8 | ||
|
|
7c18d857f0 | ||
|
|
947e672d18 | ||
|
|
96942960a9 | ||
|
|
8b0118e2c8 | ||
|
|
5f9a7a5381 | ||
|
|
6897d72c3e | ||
|
|
aae9a505e0 | ||
|
|
45196e022b | ||
|
|
b9e4c44d63 | ||
|
|
0a9e5b66ee | ||
|
|
8b99e82743 | ||
|
|
059fb85e22 | ||
|
|
8997df861a | ||
|
|
e65667963f | ||
|
|
3d51667fc8 | ||
|
|
7fc6e97cd8 | ||
|
|
1473e58a41 | ||
|
|
2394b2f44f | ||
|
|
516bd98198 | ||
|
|
7d8688a130 | ||
|
|
631ec70058 | ||
|
|
6fd51cfdc0 | ||
|
|
6857b74623 | ||
|
|
5ec4305a9f | ||
|
|
88baff96d0 | ||
|
|
e871f0002b | ||
|
|
7358d48e35 | ||
|
|
178fbf0fca | ||
|
|
a50597ff27 | ||
|
|
e4cb78abee | ||
|
|
732884a3b2 | ||
|
|
80f2c42c69 | ||
|
|
49a5a39659 | ||
|
|
a4a7040b98 | ||
|
|
de5a7d5b99 | ||
|
|
3f92e388be | ||
|
|
25c941b847 | ||
|
|
24b6934d3b | ||
|
|
d286ab406c | ||
|
|
ef6a7a6904 | ||
|
|
c61eda0d24 | ||
|
|
ad226b6fb1 | ||
|
|
24491f0dfe | ||
|
|
870fd1654d | ||
|
|
38bc912e4e | ||
|
|
315c9a2d92 | ||
|
|
a14ad6f700 | ||
|
|
76dcdfb98c | ||
|
|
99c09ba8a1 | ||
|
|
0f18b76813 | ||
|
|
8504e4cd22 | ||
|
|
ebe793e0e7 | ||
|
|
965c6cf4d9 | ||
|
|
2b1ab3184e | ||
|
|
e7d21c2dbe | ||
|
|
11906d9d71 | ||
|
|
ac5a64d77a | ||
|
|
c86c851c60 | ||
|
|
721cf98486 | ||
|
|
84bbb6efd4 | ||
|
|
f897cda6cd | ||
|
|
2da8b25b09 | ||
|
|
3f94fd90dd | ||
|
|
bc6be22cb4 | ||
|
|
e23604e08d | ||
|
|
be858ac9ce | ||
|
|
137ab4a557 | ||
|
|
bad0fc386d | ||
|
|
641c7b2c89 | ||
|
|
21be3db51f | ||
|
|
e978857820 | ||
|
|
3bf596c54c | ||
|
|
aedb823b4d | ||
|
|
7a417435cc | ||
|
|
497d6e80f7 | ||
|
|
ae6ab34e72 | ||
|
|
4c26161728 | ||
|
|
1731dca5dd | ||
|
|
ee2caff46e | ||
|
|
db8fb8b05d | ||
|
|
bec7e141dc | ||
|
|
ab85b4b129 | ||
|
|
b030049b40 | ||
|
|
1338a3d89d | ||
|
|
181178296f | ||
|
|
df7888d6fb | ||
|
|
dd15f5e052 | ||
|
|
f348d85b11 | ||
|
|
8c8a05b8c1 | ||
|
|
34d2fc1503 | ||
|
|
9b3fa809bf | ||
|
|
59535c112e | ||
|
|
04e5a6425a | ||
|
|
424dddf67b | ||
|
|
f0d6ae2a00 | ||
|
|
25298a32fa | ||
|
|
ba28bf01b7 | ||
|
|
d15c29b1a3 | ||
|
|
b083910a51 | ||
|
|
235aad32a6 | ||
|
|
a818560344 | ||
|
|
8f179becf7 | ||
|
|
e1695a0874 | ||
|
|
af7498d283 | ||
|
|
3e2a4d8053 | ||
|
|
29982e2caf | ||
|
|
5fea1df42b | ||
|
|
a041162b32 | ||
|
|
32cd3981d8 | ||
|
|
ca5af87196 | ||
|
|
a8164f612f | ||
|
|
d3bb0b6ff0 | ||
|
|
0ef10c1e13 | ||
|
|
4017911373 | ||
|
|
048034d4b1 | ||
|
|
fcb3f08bcb | ||
|
|
d2a05bb622 | ||
|
|
f7254ee169 | ||
|
|
a0e5c9d570 | ||
|
|
8291e4ba73 | ||
|
|
b324be3b0b | ||
|
|
6ba0ba7126 | ||
|
|
1d8e0629af | ||
|
|
42df54869f | ||
|
|
7b758b85ec | ||
|
|
82987ec401 | ||
|
|
71707b5aa7 | ||
|
|
ca2df83928 | ||
|
|
085771c2f0 | ||
|
|
607a638858 | ||
|
|
5f6d06d05d | ||
|
|
19ecb87b07 | ||
|
|
2a332c0883 | ||
|
|
bb773c6c13 | ||
|
|
238de489c1 | ||
|
|
6b4db330d8 | ||
|
|
ea5d7c0dee | ||
|
|
0f189f1af3 | ||
|
|
0f1b8dd51a | ||
|
|
d7e6946a78 | ||
|
|
255b7b1a54 | ||
|
|
79e1c751a1 | ||
|
|
fc745b9614 | ||
|
|
95b1baebd2 | ||
|
|
56fe1ceb97 | ||
|
|
863a51e556 | ||
|
|
69b3064b45 | ||
|
|
fb3eab1aa8 | ||
|
|
32c7399f26 | ||
|
|
955351b5bd | ||
|
|
75f6c67b6e | ||
|
|
700a3e6ed9 | ||
|
|
00702448c7 | ||
|
|
5074827d51 | ||
|
|
ceb0711e42 | ||
|
|
ddb5824b58 | ||
|
|
39f9209374 | ||
|
|
5fea4cf760 | ||
|
|
0e5ec86ca9 | ||
|
|
8b95211055 | ||
|
|
a27339b954 | ||
|
|
028b728760 | ||
|
|
18e63df01e | ||
|
|
5f459c0901 | ||
|
|
a90bcde38c | ||
|
|
603e7d922e | ||
|
|
861126f810 | ||
|
|
eb9b706ebc | ||
|
|
de9cbae0b2 | ||
|
|
25e890986f | ||
|
|
f66627dd04 | ||
|
|
924eb33b3f | ||
|
|
1b288c541a | ||
|
|
2612b8c86f | ||
|
|
3e2796d456 | ||
|
|
7092913863 | ||
|
|
67625fc347 | ||
|
|
eb55030b06 | ||
|
|
6e1b2d50f2 | ||
|
|
c6f72c44b8 | ||
|
|
d38ded0f26 | ||
|
|
ec20b7bd3a | ||
|
|
0766cf464a | ||
|
|
867f00e091 | ||
|
|
c823b8d7ae | ||
|
|
393d4d336c | ||
|
|
2cb3f2d03d | ||
|
|
279f2dd633 | ||
|
|
dec051a6e0 | ||
|
|
790fdd320c | ||
|
|
feb4a364a7 | ||
|
|
1140149e1e | ||
|
|
2ee9599b6e | ||
|
|
188d45e002 | ||
|
|
7c4c2f7860 | ||
|
|
90b7f2ff3b | ||
|
|
d3f0041e93 | ||
|
|
9d60142828 | ||
|
|
68d5edca60 | ||
|
|
1b369489df | ||
|
|
600ddfbf2d | ||
|
|
415d4dde2a | ||
|
|
1867245ed3 | ||
|
|
71d34592d9 | ||
|
|
db2927eea7 | ||
|
|
bb01a5cb31 | ||
|
|
815319140f | ||
|
|
6e6082119f | ||
|
|
da48ffe05c | ||
|
|
081979be3b | ||
|
|
3673956c1c | ||
|
|
bdd3c274ed | ||
|
|
423034d5c4 | ||
|
|
19fd2b12c0 | ||
|
|
21cd17873f | ||
|
|
9870fa9e34 | ||
|
|
938cd5e136 | ||
|
|
e8025ad4b3 | ||
|
|
07fa141aaa | ||
|
|
18bdf1e8b3 | ||
|
|
5be977005e | ||
|
|
282b64278e | ||
|
|
7263d484de | ||
|
|
bdb059b6c9 | ||
|
|
de3f5011bc | ||
|
|
de9faffa33 | ||
|
|
f67ca69e05 | ||
|
|
dd19e880c5 | ||
|
|
b5e8fa007c | ||
|
|
c3555bfcab | ||
|
|
0383db8788 | ||
|
|
d7af122c18 | ||
|
|
e15b8145b1 | ||
|
|
d75f5f9231 | ||
|
|
9939797792 | ||
|
|
5248b9fc6f | ||
|
|
e15295bdac | ||
|
|
4e1f96e09c | ||
|
|
96cfdebced | ||
|
|
944f34b833 | ||
|
|
1023b2ca9c | ||
|
|
16318bb9f6 | ||
|
|
350586335d | ||
|
|
9d809499a5 | ||
|
|
fdd52c17d7 | ||
|
|
1461d029db | ||
|
|
07cefd71df | ||
|
|
abab10b2cc | ||
|
|
e37d4a6756 | ||
|
|
e2a1ce623c | ||
|
|
0ff243266c | ||
|
|
645da2e307 | ||
|
|
5fd95788f9 | ||
|
|
bd29f168e0 | ||
|
|
dc97e33cd6 | ||
|
|
caf7cb07cd | ||
|
|
ad5df53ee7 | ||
|
|
95920bf207 | ||
|
|
6700166841 | ||
|
|
b8196cd06e | ||
|
|
c28afbf193 | ||
|
|
84ffffb3f3 | ||
|
|
b2c030140c | ||
|
|
90138ed574 | ||
|
|
92f131bbe4 | ||
|
|
338580087e | ||
|
|
deda53a842 | ||
|
|
5391854c82 | ||
|
|
e288bfbec4 | ||
|
|
377fe5bc40 | ||
|
|
d264ff2801 | ||
|
|
a21bb6b02d | ||
|
|
37ecd5cdef | ||
|
|
07a87dfba7 | ||
|
|
9e4db89521 | ||
|
|
536d394e41 | ||
|
|
c0580c7ad0 | ||
|
|
488e72ef4e | ||
|
|
01c224d301 | ||
|
|
eaf95a85a8 | ||
|
|
ba1d084660 | ||
|
|
2e64c461c3 | ||
|
|
ce5dad722f | ||
|
|
7675feca91 | ||
|
|
c66d74e135 | ||
|
|
54d6eed740 | ||
|
|
dc4b75070d | ||
|
|
830eb74725 | ||
|
|
4f21d8d7a8 | ||
|
|
424deb8faf | ||
|
|
b4a40f1257 | ||
|
|
9296c10ca4 | ||
|
|
fbe65cd542 | ||
|
|
ccbb6e4789 | ||
|
|
d70f436304 | ||
|
|
16aaa8201c | ||
|
|
acc1f2f3d7 | ||
|
|
433d254c70 | ||
|
|
ea4eebd2d6 | ||
|
|
3c00a527dd | ||
|
|
f72a354066 | ||
|
|
7c92e0e9ce | ||
|
|
4f6868728d | ||
|
|
0ec4522f9e | ||
|
|
c6e0c6d096 | ||
|
|
dc0fb9ed8a | ||
|
|
66d9eaee78 | ||
|
|
3797272faf | ||
|
|
682b302d04 | ||
|
|
1de10f9b05 | ||
|
|
c4391ff058 | ||
|
|
3822e3f8d9 | ||
|
|
f8f99f3878 | ||
|
|
e77e4acea9 | ||
|
|
c6de444d0b | ||
|
|
89e38c34b8 | ||
|
|
246d17972c | ||
|
|
55a8b37ef8 | ||
|
|
445183001b | ||
|
|
ca9e2200da | ||
|
|
eba3f84c04 | ||
|
|
867e6a8f4b | ||
|
|
df9779ec59 | ||
|
|
1b71d1e46d | ||
|
|
0a58918f47 | ||
|
|
afbd927fc0 | ||
|
|
2aa09ae18d | ||
|
|
09789b0b72 | ||
|
|
2426abd17a | ||
|
|
db4a97743f | ||
|
|
7ca98ed344 | ||
|
|
c9d3d17999 | ||
|
|
628049cfd7 | ||
|
|
ae9a11da53 | ||
|
|
7e097482bc | ||
|
|
df1b151587 | ||
|
|
45eb59a5aa | ||
|
|
687c17bbe2 | ||
|
|
7505aec706 | ||
|
|
c7b414492d | ||
|
|
14b0095822 | ||
|
|
a1256b46c8 | ||
|
|
094270dff7 | ||
|
|
d4e24dabc2 | ||
|
|
842df0d112 | ||
|
|
cfa9427d7c | ||
|
|
3c01e24f02 | ||
|
|
22dbf63ff9 | ||
|
|
814f7394a0 | ||
|
|
9a4cebaa1b | ||
|
|
c30207ac63 | ||
|
|
77afbddb91 | ||
|
|
18feeabe15 | ||
|
|
c3811d3a14 | ||
|
|
f20d6b551d | ||
|
|
311bcadacb | ||
|
|
2189c8cd82 | ||
|
|
6553bb8147 | ||
|
|
dea492fd64 | ||
|
|
00ab7f04fa | ||
|
|
d3ba714aba | ||
|
|
748b37f1d6 | ||
|
|
b83b188aff | ||
|
|
cfefa32603 | ||
|
|
85d8db3ef9 | ||
|
|
3c14dbe382 | ||
|
|
b49b2af11f | ||
|
|
425a36aa51 | ||
|
|
ec0b9de713 | ||
|
|
9f13b14f6d | ||
|
|
01e83b45b5 | ||
|
|
f80566e0cb | ||
|
|
42afacf0af | ||
|
|
2e61e7e682 | ||
|
|
3de9267ea7 | ||
|
|
8c99d4fcd2 | ||
|
|
be4e6e5ba5 | ||
|
|
1b5efea6eb | ||
|
|
6554f80fad | ||
|
|
2e8a9f809e | ||
|
|
dc66032720 | ||
|
|
c9433782d8 | ||
|
|
fef5586ff5 | ||
|
|
1f4a2fd654 | ||
|
|
8243385af6 | ||
|
|
26ce9b2d4a | ||
|
|
119f3169e2 | ||
|
|
16bd22ee01 | ||
|
|
f4a5f73ab2 | ||
|
|
e61a4564ea | ||
|
|
e72edee1f2 | ||
|
|
e8c150fcac | ||
|
|
52418932b1 | ||
|
|
4f81cb9333 | ||
|
|
db46f47b96 | ||
|
|
edfe5594ba | ||
|
|
f25e972594 | ||
|
|
d5488bdd42 | ||
|
|
bbff64bc96 | ||
|
|
635afefdeb | ||
|
|
fd3e67a0b4 | ||
|
|
729a6021ee | ||
|
|
309f254c2c | ||
|
|
5c37f04d64 | ||
|
|
7c3dd8e852 | ||
|
|
66ddedbaf3 | ||
|
|
7981b17897 | ||
|
|
62137d47c8 | ||
|
|
e3b5437f61 | ||
|
|
934693924e | ||
|
|
308fd92a46 | ||
|
|
da1eb71ad0 | ||
|
|
576dbb7ce6 | ||
|
|
d0c381b3df | ||
|
|
55178a81c6 | ||
|
|
249308380b | ||
|
|
d91bec08c3 | ||
|
|
e23ef4b0be | ||
|
|
6037521c49 | ||
|
|
a27fac3677 | ||
|
|
21f2eb664e | ||
|
|
81546ef4b0 | ||
|
|
4b90c8fd45 | ||
|
|
c643fb8aac | ||
|
|
0cae6ceca3 | ||
|
|
5cde59b53c | ||
|
|
7df67630af | ||
|
|
0c89dca261 | ||
|
|
6b953b8793 | ||
|
|
0d1defcf27 | ||
|
|
c1db9c19b3 | ||
|
|
95487755ed | ||
|
|
4813469659 | ||
|
|
4dfd357c0b | ||
|
|
4ca0486518 | ||
|
|
b139c05960 | ||
|
|
3d32759030 | ||
|
|
badfe39a3d | ||
|
|
060e2db351 | ||
|
|
ed802c0404 | ||
|
|
5d8739bfb2 | ||
|
|
086faf44fc | ||
|
|
e5eaa90c61 | ||
|
|
b24807ea29 | ||
|
|
d68bae9bc2 | ||
|
|
b891fb4502 | ||
|
|
ea69b3b4e3 | ||
|
|
23c8616ba5 | ||
|
|
b25c91affd | ||
|
|
151cefe0ec | ||
|
|
3412ff94bc | ||
|
|
14112ed294 | ||
|
|
3e1909b645 | ||
|
|
a4b1fbd6ee | ||
|
|
6d2ef9be5d |
2
.github/actions/install/action.yml
vendored
2
.github/actions/install/action.yml
vendored
@@ -13,7 +13,7 @@ inputs:
|
|||||||
zig-v8:
|
zig-v8:
|
||||||
description: 'zig v8 version to install'
|
description: 'zig v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
default: 'v0.2.8'
|
default: 'v0.3.1'
|
||||||
v8:
|
v8:
|
||||||
description: 'v8 version to install'
|
description: 'v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
2
.github/workflows/e2e-integration-test.yml
vendored
2
.github/workflows/e2e-integration-test.yml
vendored
@@ -63,6 +63,6 @@ jobs:
|
|||||||
|
|
||||||
- name: run end to end integration tests
|
- name: run end to end integration tests
|
||||||
run: |
|
run: |
|
||||||
./lightpanda serve & echo $! > LPD.pid
|
./lightpanda serve --log_level error & echo $! > LPD.pid
|
||||||
go run integration/main.go
|
go run integration/main.go
|
||||||
kill `cat LPD.pid`
|
kill `cat LPD.pid`
|
||||||
|
|||||||
53
.github/workflows/e2e-test.yml
vendored
53
.github/workflows/e2e-test.yml
vendored
@@ -122,10 +122,19 @@ jobs:
|
|||||||
needs: zig-build-release
|
needs: zig-build-release
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MAX_MEMORY: 26000
|
MAX_VmHWM: 28000 # 28MB (KB)
|
||||||
|
MAX_CG_PEAK: 8000 # 8MB (KB)
|
||||||
MAX_AVG_DURATION: 17
|
MAX_AVG_DURATION: 17
|
||||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||||
|
|
||||||
|
# How to give cgroups access to the user actions-runner on the host:
|
||||||
|
# $ sudo apt install cgroup-tools
|
||||||
|
# $ sudo chmod o+w /sys/fs/cgroup/cgroup.procs
|
||||||
|
# $ sudo mkdir -p /sys/fs/cgroup/actions-runner
|
||||||
|
# $ sudo chown -R actions-runner:actions-runner /sys/fs/cgroup/actions-runner
|
||||||
|
CG_ROOT: /sys/fs/cgroup
|
||||||
|
CG: actions-runner/lpd_${{ github.run_id }}_${{ github.run_attempt }}
|
||||||
|
|
||||||
# use a self host runner.
|
# use a self host runner.
|
||||||
runs-on: lpd-bench-hetzner
|
runs-on: lpd-bench-hetzner
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
@@ -150,22 +159,53 @@ jobs:
|
|||||||
go run ws/main.go & echo $! > WS.pid
|
go run ws/main.go & echo $! > WS.pid
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
|
- name: run lightpanda in cgroup
|
||||||
|
run: |
|
||||||
|
if [ ! -f /sys/fs/cgroup/cgroup.controllers ]; then
|
||||||
|
echo "cgroup v2 not available: /sys/fs/cgroup/cgroup.controllers missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p $CG_ROOT/$CG
|
||||||
|
cgexec -g memory:$CG ./lightpanda serve & echo $! > LPD.pid
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
- name: run puppeteer
|
- name: run puppeteer
|
||||||
run: |
|
run: |
|
||||||
./lightpanda serve & echo $! > LPD.pid
|
|
||||||
sleep 2
|
|
||||||
RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1
|
RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1
|
||||||
cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM
|
cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM
|
||||||
kill `cat LPD.pid`
|
kill `cat LPD.pid`
|
||||||
|
|
||||||
|
PID=$(cat LPD.pid)
|
||||||
|
while kill -0 $PID 2>/dev/null; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ ! -f $CG_ROOT/$CG/memory.peak ]; then
|
||||||
|
echo "memory.peak not available in $CG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cat $CG_ROOT/$CG/memory.peak > LPD.cg_mem_peak
|
||||||
|
|
||||||
- name: puppeteer result
|
- name: puppeteer result
|
||||||
run: cat puppeteer.out
|
run: cat puppeteer.out
|
||||||
|
|
||||||
- name: memory regression
|
- name: cgroup memory regression
|
||||||
|
run: |
|
||||||
|
PEAK_BYTES=$(cat LPD.cg_mem_peak)
|
||||||
|
PEAK_KB=$((PEAK_BYTES / 1024))
|
||||||
|
echo "memory.peak_bytes=$PEAK_BYTES"
|
||||||
|
echo "memory.peak_kb=$PEAK_KB"
|
||||||
|
test "$PEAK_KB" -le "$MAX_CG_PEAK"
|
||||||
|
|
||||||
|
- name: virtual memory regression
|
||||||
run: |
|
run: |
|
||||||
export LPD_VmHWM=`cat LPD.VmHWM`
|
export LPD_VmHWM=`cat LPD.VmHWM`
|
||||||
echo "Peak resident set size: $LPD_VmHWM"
|
echo "Peak resident set size: $LPD_VmHWM"
|
||||||
test "$LPD_VmHWM" -le "$MAX_MEMORY"
|
test "$LPD_VmHWM" -le "$MAX_VmHWM"
|
||||||
|
|
||||||
|
- name: cleanup cgroup
|
||||||
|
run: rmdir $CG_ROOT/$CG
|
||||||
|
|
||||||
- name: duration regression
|
- name: duration regression
|
||||||
run: |
|
run: |
|
||||||
@@ -178,7 +218,8 @@ jobs:
|
|||||||
export AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
|
export AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
|
||||||
export TOTAL_DURATION=`cat puppeteer.out|grep 'total duration'|sed 's/total duration (ms) //'`
|
export TOTAL_DURATION=`cat puppeteer.out|grep 'total duration'|sed 's/total duration (ms) //'`
|
||||||
export LPD_VmHWM=`cat LPD.VmHWM`
|
export LPD_VmHWM=`cat LPD.VmHWM`
|
||||||
echo "{\"duration_total\":${TOTAL_DURATION},\"duration_avg\":${AVG_DURATION},\"mem_peak\":${LPD_VmHWM}}" > bench.json
|
export LPD_CG_PEAK_KB=$(( $(cat LPD.cg_mem_peak) / 1024 ))
|
||||||
|
echo "{\"duration_total\":${TOTAL_DURATION},\"duration_avg\":${AVG_DURATION},\"mem_peak\":${LPD_VmHWM},\"cg_mem_peak\":${LPD_CG_PEAK_KB}}" > bench.json
|
||||||
cat bench.json
|
cat bench.json
|
||||||
|
|
||||||
- name: run hyperfine
|
- name: run hyperfine
|
||||||
|
|||||||
88
.github/workflows/wpt.yml
vendored
88
.github/workflows/wpt.yml
vendored
@@ -15,11 +15,11 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
wpt:
|
wpt-build-release:
|
||||||
name: web platform tests json output
|
name: zig build release
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 90
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
@@ -30,11 +30,85 @@ jobs:
|
|||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
- name: build wpt
|
- name: zig build release
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -- version
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||||
|
|
||||||
|
- name: upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: lightpanda-build-release
|
||||||
|
path: |
|
||||||
|
zig-out/bin/lightpanda
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
wpt-build-runner:
|
||||||
|
name: build wpt runner
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
repository: 'lightpanda-io/demo'
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
cd ./wptrunner
|
||||||
|
CGO_ENABLED=0 go build
|
||||||
|
|
||||||
|
- name: upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: wptrunner
|
||||||
|
path: |
|
||||||
|
wptrunner/wptrunner
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
run-wpt:
|
||||||
|
name: web platform tests json output
|
||||||
|
needs:
|
||||||
|
- wpt-build-release
|
||||||
|
- wpt-build-runner
|
||||||
|
|
||||||
|
# use a self host runner.
|
||||||
|
runs-on: lpd-bench-hetzner
|
||||||
|
timeout-minutes: 120
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: fork
|
||||||
|
repository: 'lightpanda-io/wpt'
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# The hosts are configured manually on the self host runner.
|
||||||
|
# - name: create custom hosts
|
||||||
|
# run: ./wpt make-hosts-file | sudo tee -a /etc/hosts
|
||||||
|
|
||||||
|
- name: generate manifest
|
||||||
|
run: ./wpt manifest
|
||||||
|
|
||||||
|
- name: download lightpanda release
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: lightpanda-build-release
|
||||||
|
|
||||||
|
- run: chmod a+x ./lightpanda
|
||||||
|
|
||||||
|
- name: download wptrunner
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: wptrunner
|
||||||
|
|
||||||
|
- run: chmod a+x ./wptrunner
|
||||||
|
|
||||||
- name: run test with json output
|
- name: run test with json output
|
||||||
run: zig-out/bin/lightpanda-wpt --json > wpt.json
|
run: |
|
||||||
|
./wpt serve 2> /dev/null & echo $! > WPT.pid
|
||||||
|
sleep 10s
|
||||||
|
./wptrunner -lpd-path ./lightpanda -json -concurrency 1 > wpt.json
|
||||||
|
kill `cat WPT.pid`
|
||||||
|
|
||||||
- name: write commit
|
- name: write commit
|
||||||
run: |
|
run: |
|
||||||
@@ -51,7 +125,7 @@ jobs:
|
|||||||
|
|
||||||
perf-fmt:
|
perf-fmt:
|
||||||
name: perf-fmt
|
name: perf-fmt
|
||||||
needs: wpt
|
needs: run-wpt
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,11 +1,6 @@
|
|||||||
zig-cache
|
|
||||||
/.zig-cache/
|
/.zig-cache/
|
||||||
/.lp-cache/
|
/.lp-cache/
|
||||||
zig-out
|
zig-out
|
||||||
/vendor/netsurf/out
|
|
||||||
/vendor/libiconv/
|
|
||||||
lightpanda.id
|
lightpanda.id
|
||||||
/v8/
|
|
||||||
/build/
|
|
||||||
/src/html5ever/target/
|
/src/html5ever/target/
|
||||||
src/snapshot.bin
|
src/snapshot.bin
|
||||||
|
|||||||
15
.gitmodules
vendored
15
.gitmodules
vendored
@@ -1,15 +0,0 @@
|
|||||||
[submodule "tests/wpt"]
|
|
||||||
path = tests/wpt
|
|
||||||
url = https://github.com/lightpanda-io/wpt
|
|
||||||
[submodule "vendor/nghttp2"]
|
|
||||||
path = vendor/nghttp2
|
|
||||||
url = https://github.com/nghttp2/nghttp2.git
|
|
||||||
[submodule "vendor/zlib"]
|
|
||||||
path = vendor/zlib
|
|
||||||
url = https://github.com/madler/zlib.git
|
|
||||||
[submodule "vendor/curl"]
|
|
||||||
path = vendor/curl
|
|
||||||
url = https://github.com/curl/curl.git
|
|
||||||
[submodule "vendor/brotli"]
|
|
||||||
path = vendor/brotli
|
|
||||||
url = https://github.com/google/brotli
|
|
||||||
@@ -3,7 +3,7 @@ FROM debian:stable-slim
|
|||||||
ARG MINISIG=0.12
|
ARG MINISIG=0.12
|
||||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||||
ARG V8=14.0.365.4
|
ARG V8=14.0.365.4
|
||||||
ARG ZIG_V8=v0.2.8
|
ARG ZIG_V8=v0.3.1
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
RUN apt-get update -yq && \
|
RUN apt-get update -yq && \
|
||||||
|
|||||||
20
Makefile
20
Makefile
@@ -47,7 +47,7 @@ help:
|
|||||||
|
|
||||||
# $(ZIG) commands
|
# $(ZIG) commands
|
||||||
# ------------
|
# ------------
|
||||||
.PHONY: build build-v8-snapshot build-dev run run-release shell test bench wpt data end2end
|
.PHONY: build build-v8-snapshot build-dev run run-release shell test bench data end2end
|
||||||
|
|
||||||
## Build v8 snapshot
|
## Build v8 snapshot
|
||||||
build-v8-snapshot:
|
build-v8-snapshot:
|
||||||
@@ -57,7 +57,7 @@ build-v8-snapshot:
|
|||||||
|
|
||||||
## Build in release-fast mode
|
## Build in release-fast mode
|
||||||
build: build-v8-snapshot
|
build: build-v8-snapshot
|
||||||
@printf "\033[36mBuilding (release safe)...\033[0m\n"
|
@printf "\033[36mBuilding (release fast)...\033[0m\n"
|
||||||
@$(ZIG) build -Doptimize=ReleaseFast -Dsnapshot_path=../../snapshot.bin -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
@$(ZIG) build -Doptimize=ReleaseFast -Dsnapshot_path=../../snapshot.bin -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||||
@printf "\033[33mBuild OK\033[0m\n"
|
@printf "\033[33mBuild OK\033[0m\n"
|
||||||
|
|
||||||
@@ -82,15 +82,6 @@ shell:
|
|||||||
@printf "\033[36mBuilding shell...\033[0m\n"
|
@printf "\033[36mBuilding shell...\033[0m\n"
|
||||||
@$(ZIG) build shell || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
@$(ZIG) build shell || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||||
|
|
||||||
## Run WPT tests
|
|
||||||
wpt:
|
|
||||||
@printf "\033[36mBuilding wpt...\033[0m\n"
|
|
||||||
@$(ZIG) build wpt -- $(filter-out $@,$(MAKECMDGOALS)) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
|
||||||
|
|
||||||
wpt-summary:
|
|
||||||
@printf "\033[36mBuilding wpt...\033[0m\n"
|
|
||||||
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
|
||||||
|
|
||||||
## Test - `grep` is used to filter out the huge compile command on build
|
## Test - `grep` is used to filter out the huge compile command on build
|
||||||
ifeq ($(OS), macos)
|
ifeq ($(OS), macos)
|
||||||
test:
|
test:
|
||||||
@@ -111,13 +102,8 @@ end2end:
|
|||||||
# ------------
|
# ------------
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
|
|
||||||
## Install and build dependencies for release
|
install: build
|
||||||
install: install-submodule
|
|
||||||
|
|
||||||
data:
|
data:
|
||||||
cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig
|
cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig
|
||||||
|
|
||||||
## Init and update git submodule
|
|
||||||
install-submodule:
|
|
||||||
@git submodule init && \
|
|
||||||
git submodule update
|
|
||||||
|
|||||||
76
README.md
76
README.md
@@ -220,18 +220,6 @@ For **MacOS**, you need cmake and [Rust](https://rust-lang.org/tools/install/).
|
|||||||
brew install cmake
|
brew install cmake
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install Git submodules
|
|
||||||
|
|
||||||
The project uses git submodules for dependencies.
|
|
||||||
|
|
||||||
To init or update the submodules in the `vendor/` directory:
|
|
||||||
|
|
||||||
```
|
|
||||||
make install-submodule
|
|
||||||
```
|
|
||||||
|
|
||||||
This is an alias for `git submodule init && git submodule update`.
|
|
||||||
|
|
||||||
### Build and run
|
### Build and run
|
||||||
|
|
||||||
You an build the entire browser with `make build` or `make build-dev` for debug
|
You an build the entire browser with `make build` or `make build-dev` for debug
|
||||||
@@ -281,35 +269,75 @@ make end2end
|
|||||||
Lightpanda is tested against the standardized [Web Platform
|
Lightpanda is tested against the standardized [Web Platform
|
||||||
Tests](https://web-platform-tests.org/).
|
Tests](https://web-platform-tests.org/).
|
||||||
|
|
||||||
The relevant tests cases are committed in a [dedicated repository](https://github.com/lightpanda-io/wpt) which is fetched by the `make install-submodule` command.
|
We use [a fork](https://github.com/lightpanda-io/wpt/tree/fork) including a custom
|
||||||
|
[`testharnessreport.js`](https://github.com/lightpanda-io/wpt/commit/01a3115c076a3ad0c84849dbbf77a6e3d199c56f).
|
||||||
All the tests cases executed are located in the `tests/wpt` sub-directory.
|
|
||||||
|
|
||||||
For reference, you can easily execute a WPT test case with your browser via
|
For reference, you can easily execute a WPT test case with your browser via
|
||||||
[wpt.live](https://wpt.live).
|
[wpt.live](https://wpt.live).
|
||||||
|
|
||||||
|
#### Configure WPT HTTP server
|
||||||
|
|
||||||
|
To run the test, you must clone the repository, configure the custom hosts and generate the
|
||||||
|
`MANIFEST.json` file.
|
||||||
|
|
||||||
|
Clone the repository with the `fork` branch.
|
||||||
|
```
|
||||||
|
git clone -b fork --depth=1 git@github.com:lightpanda-io/wpt.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Enter into the `wpt/` dir.
|
||||||
|
|
||||||
|
Install custom domains in your `/etc/hosts`
|
||||||
|
```
|
||||||
|
./wpt make-hosts-file | sudo tee -a /etc/hosts
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate `MANIFEST.json`
|
||||||
|
```
|
||||||
|
./wpt manifest
|
||||||
|
```
|
||||||
|
Use the [WPT's setup
|
||||||
|
guide](https://web-platform-tests.org/running-tests/from-local-system.html) for
|
||||||
|
details.
|
||||||
|
|
||||||
#### Run WPT test suite
|
#### Run WPT test suite
|
||||||
|
|
||||||
To run all the tests:
|
An external [Go](https://go.dev) runner is provided by
|
||||||
|
[github.com/lightpanda-io/demo/](https://github.com/lightpanda-io/demo/)
|
||||||
|
repository, located into `wptrunner/` dir.
|
||||||
|
You need to clone the project first.
|
||||||
|
|
||||||
|
First start the WPT's HTTP server from your `wpt/` clone dir.
|
||||||
|
```
|
||||||
|
./wpt serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a Lightpanda browser
|
||||||
|
|
||||||
```
|
```
|
||||||
make wpt
|
zig build run -- --insecure_disable_tls_host_verification
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can start the wptrunner from the Demo's clone dir:
|
||||||
|
```
|
||||||
|
cd wptrunner && go run .
|
||||||
```
|
```
|
||||||
|
|
||||||
Or one specific test:
|
Or one specific test:
|
||||||
|
|
||||||
```
|
```
|
||||||
make wpt Node-childNodes.html
|
cd wptrunner && go run . Node-childNodes.html
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Add a new WPT test case
|
`wptrunner` command accepts `--summary` and `--json` options modifying output.
|
||||||
|
Also `--concurrency` define the concurrency limit.
|
||||||
|
|
||||||
We add new relevant tests cases files when we implemented changes in Lightpanda.
|
:warning: Running the whole test suite will take a long time. In this case,
|
||||||
|
it's useful to build in `releaseFast` mode to make tests faster.
|
||||||
|
|
||||||
To add a new test, copy the file you want from the [WPT
|
```
|
||||||
repo](https://github.com/web-platform-tests/wpt) into the `tests/wpt` directory.
|
zig build -Doptimize=ReleaseFast run
|
||||||
|
```
|
||||||
:warning: Please keep the original directory tree structure of `tests/wpt`.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,36 @@
|
|||||||
.{
|
.{
|
||||||
.name = .browser,
|
.name = .browser,
|
||||||
.paths = .{""},
|
|
||||||
.version = "0.0.0",
|
.version = "0.0.0",
|
||||||
.fingerprint = 0xda130f3af836cea0, // Changing this has security and trust implications.
|
.fingerprint = 0xda130f3af836cea0, // Changing this has security and trust implications.
|
||||||
.minimum_zig_version = "0.15.2",
|
.minimum_zig_version = "0.15.2",
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
.v8 = .{
|
.v8 = .{
|
||||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.2.8.tar.gz",
|
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.1.tar.gz",
|
||||||
.hash = "v8-0.0.0-xddH63lfBADJ7UE2zAJ8nJIBnxoSiimXSaX6Q_M_7DS3",
|
.hash = "v8-0.0.0-xddH64J7BAC81mkf6G9RbEJxS-W3TIRl5iFnShwbqCqy",
|
||||||
|
|
||||||
},
|
},
|
||||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||||
|
.brotli = .{
|
||||||
|
// v1.2.0
|
||||||
|
.url = "https://github.com/google/brotli/archive/028fb5a23661f123017c060daa546b55cf4bde29.tar.gz",
|
||||||
|
.hash = "N-V-__8AAJudKgCQCuIiH6MJjAiIJHfg_tT_Ew-0vZwVkCo_",
|
||||||
|
},
|
||||||
|
.zlib = .{
|
||||||
|
.url = "https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz",
|
||||||
|
.hash = "N-V-__8AAJ2cNgAgfBtAw33Bxfu1IWISDeKKSr3DAqoAysIJ",
|
||||||
|
},
|
||||||
|
.nghttp2 = .{
|
||||||
|
.url = "https://github.com/nghttp2/nghttp2/releases/download/v1.68.0/nghttp2-1.68.0.tar.gz",
|
||||||
|
.hash = "N-V-__8AAL15vQCI63ZL6Zaz5hJg6JTEgYXGbLnMFSnf7FT3",
|
||||||
|
},
|
||||||
.@"boringssl-zig" = .{
|
.@"boringssl-zig" = .{
|
||||||
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
|
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
|
||||||
.hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK",
|
.hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK",
|
||||||
},
|
},
|
||||||
|
.curl = .{
|
||||||
|
.url = "https://github.com/curl/curl/releases/download/curl-8_18_0/curl-8.18.0.tar.gz",
|
||||||
|
.hash = "N-V-__8AALp9QAGn6CCHZ6fK_FfMyGtG824LSHYHHasM3w-y",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
.paths = .{""},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ pub fn init(allocator: Allocator, config: *const Config) !*App {
|
|||||||
app.telemetry = try Telemetry.init(app, config.mode);
|
app.telemetry = try Telemetry.init(app, config.mode);
|
||||||
errdefer app.telemetry.deinit();
|
errdefer app.telemetry.deinit();
|
||||||
|
|
||||||
app.arena_pool = ArenaPool.init(allocator);
|
app.arena_pool = ArenaPool.init(allocator, 512, 1024 * 16);
|
||||||
errdefer app.arena_pool.deinit();
|
errdefer app.arena_pool.deinit();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
|
|||||||
@@ -29,18 +29,19 @@ free_list_len: u16 = 0,
|
|||||||
free_list: ?*Entry = null,
|
free_list: ?*Entry = null,
|
||||||
free_list_max: u16,
|
free_list_max: u16,
|
||||||
entry_pool: std.heap.MemoryPool(Entry),
|
entry_pool: std.heap.MemoryPool(Entry),
|
||||||
|
mutex: std.Thread.Mutex = .{},
|
||||||
|
|
||||||
const Entry = struct {
|
const Entry = struct {
|
||||||
next: ?*Entry,
|
next: ?*Entry,
|
||||||
arena: ArenaAllocator,
|
arena: ArenaAllocator,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(allocator: Allocator) ArenaPool {
|
pub fn init(allocator: Allocator, free_list_max: u16, retain_bytes: usize) ArenaPool {
|
||||||
return .{
|
return .{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.free_list_max = 512, // TODO make configurable
|
.free_list_max = free_list_max,
|
||||||
.retain_bytes = 1024 * 16, // TODO make configurable
|
.retain_bytes = retain_bytes,
|
||||||
.entry_pool = std.heap.MemoryPool(Entry).init(allocator),
|
.entry_pool = .init(allocator),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +55,9 @@ pub fn deinit(self: *ArenaPool) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn acquire(self: *ArenaPool) !Allocator {
|
pub fn acquire(self: *ArenaPool) !Allocator {
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
if (self.free_list) |entry| {
|
if (self.free_list) |entry| {
|
||||||
self.free_list = entry.next;
|
self.free_list = entry.next;
|
||||||
self.free_list_len -= 1;
|
self.free_list_len -= 1;
|
||||||
@@ -73,6 +77,12 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void {
|
|||||||
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
||||||
const entry: *Entry = @fieldParentPtr("arena", arena);
|
const entry: *Entry = @fieldParentPtr("arena", arena);
|
||||||
|
|
||||||
|
// Reset the arena before acquiring the lock to minimize lock hold time
|
||||||
|
_ = arena.reset(.{ .retain_with_limit = self.retain_bytes });
|
||||||
|
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
const free_list_len = self.free_list_len;
|
const free_list_len = self.free_list_len;
|
||||||
if (free_list_len == self.free_list_max) {
|
if (free_list_len == self.free_list_max) {
|
||||||
arena.deinit();
|
arena.deinit();
|
||||||
@@ -80,8 +90,123 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = arena.reset(.{ .retain_with_limit = self.retain_bytes });
|
|
||||||
entry.next = self.free_list;
|
entry.next = self.free_list;
|
||||||
self.free_list_len = free_list_len + 1;
|
self.free_list_len = free_list_len + 1;
|
||||||
self.free_list = entry;
|
self.free_list = entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(_: *const ArenaPool, allocator: Allocator, retain: usize) void {
|
||||||
|
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
||||||
|
_ = arena.reset(.{ .retain_with_limit = retain });
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
test "arena pool - basic acquire and use" {
|
||||||
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
|
defer pool.deinit();
|
||||||
|
|
||||||
|
const alloc = try pool.acquire();
|
||||||
|
const buf = try alloc.alloc(u8, 64);
|
||||||
|
@memset(buf, 0xAB);
|
||||||
|
try testing.expectEqual(@as(u8, 0xAB), buf[0]);
|
||||||
|
|
||||||
|
pool.release(alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "arena pool - reuse entry after release" {
|
||||||
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
|
defer pool.deinit();
|
||||||
|
|
||||||
|
const alloc1 = try pool.acquire();
|
||||||
|
try testing.expectEqual(@as(u16, 0), pool.free_list_len);
|
||||||
|
|
||||||
|
pool.release(alloc1);
|
||||||
|
try testing.expectEqual(@as(u16, 1), pool.free_list_len);
|
||||||
|
|
||||||
|
// The same entry should be returned from the free list.
|
||||||
|
const alloc2 = try pool.acquire();
|
||||||
|
try testing.expectEqual(@as(u16, 0), pool.free_list_len);
|
||||||
|
try testing.expectEqual(alloc1.ptr, alloc2.ptr);
|
||||||
|
|
||||||
|
pool.release(alloc2);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "arena pool - multiple concurrent arenas" {
|
||||||
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
|
defer pool.deinit();
|
||||||
|
|
||||||
|
const a1 = try pool.acquire();
|
||||||
|
const a2 = try pool.acquire();
|
||||||
|
const a3 = try pool.acquire();
|
||||||
|
|
||||||
|
// All three must be distinct arenas.
|
||||||
|
try testing.expect(a1.ptr != a2.ptr);
|
||||||
|
try testing.expect(a2.ptr != a3.ptr);
|
||||||
|
try testing.expect(a1.ptr != a3.ptr);
|
||||||
|
|
||||||
|
_ = try a1.alloc(u8, 16);
|
||||||
|
_ = try a2.alloc(u8, 32);
|
||||||
|
_ = try a3.alloc(u8, 48);
|
||||||
|
|
||||||
|
pool.release(a1);
|
||||||
|
pool.release(a2);
|
||||||
|
pool.release(a3);
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(u16, 3), pool.free_list_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "arena pool - free list respects max limit" {
|
||||||
|
// Cap the free list at 1 so the second release discards its arena.
|
||||||
|
var pool = ArenaPool.init(testing.allocator, 1, 1024 * 16);
|
||||||
|
defer pool.deinit();
|
||||||
|
|
||||||
|
const a1 = try pool.acquire();
|
||||||
|
const a2 = try pool.acquire();
|
||||||
|
|
||||||
|
pool.release(a1);
|
||||||
|
try testing.expectEqual(@as(u16, 1), pool.free_list_len);
|
||||||
|
|
||||||
|
// The free list is full; a2's arena should be destroyed, not queued.
|
||||||
|
pool.release(a2);
|
||||||
|
try testing.expectEqual(@as(u16, 1), pool.free_list_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "arena pool - reset clears memory without releasing" {
|
||||||
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
|
defer pool.deinit();
|
||||||
|
|
||||||
|
const alloc = try pool.acquire();
|
||||||
|
|
||||||
|
const buf = try alloc.alloc(u8, 128);
|
||||||
|
@memset(buf, 0xFF);
|
||||||
|
|
||||||
|
// reset() frees arena memory but keeps the allocator in-flight.
|
||||||
|
pool.reset(alloc, 0);
|
||||||
|
|
||||||
|
// The free list must stay empty; the allocator was not released.
|
||||||
|
try testing.expectEqual(@as(u16, 0), pool.free_list_len);
|
||||||
|
|
||||||
|
// Allocating again through the same arena must still work.
|
||||||
|
const buf2 = try alloc.alloc(u8, 64);
|
||||||
|
@memset(buf2, 0x00);
|
||||||
|
try testing.expectEqual(@as(u8, 0x00), buf2[0]);
|
||||||
|
|
||||||
|
pool.release(alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "arena pool - deinit with entries in free list" {
|
||||||
|
// Verifies that deinit properly cleans up free-listed arenas (no leaks
|
||||||
|
// detected by the test allocator).
|
||||||
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
|
|
||||||
|
const a1 = try pool.acquire();
|
||||||
|
const a2 = try pool.acquire();
|
||||||
|
_ = try a1.alloc(u8, 256);
|
||||||
|
_ = try a2.alloc(u8, 512);
|
||||||
|
pool.release(a1);
|
||||||
|
pool.release(a2);
|
||||||
|
try testing.expectEqual(@as(u16, 2), pool.free_list_len);
|
||||||
|
|
||||||
|
pool.deinit();
|
||||||
|
}
|
||||||
|
|||||||
193
src/Config.zig
193
src/Config.zig
@@ -28,8 +28,16 @@ pub const RunMode = enum {
|
|||||||
fetch,
|
fetch,
|
||||||
serve,
|
serve,
|
||||||
version,
|
version,
|
||||||
|
mcp,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const CDP_MAX_HTTP_REQUEST_SIZE = 4096;
|
||||||
|
|
||||||
|
// max message size
|
||||||
|
// +14 for max websocket payload overhead
|
||||||
|
// +140 for the max control packet that might be interleaved in a message
|
||||||
|
pub const CDP_MAX_MESSAGE_SIZE = 512 * 1024 + 14 + 140;
|
||||||
|
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
exec_name: []const u8,
|
exec_name: []const u8,
|
||||||
http_headers: HttpHeaders,
|
http_headers: HttpHeaders,
|
||||||
@@ -52,56 +60,56 @@ pub fn deinit(self: *const Config, allocator: Allocator) void {
|
|||||||
|
|
||||||
pub fn tlsVerifyHost(self: *const Config) bool {
|
pub fn tlsVerifyHost(self: *const Config) bool {
|
||||||
return switch (self.mode) {
|
return switch (self.mode) {
|
||||||
inline .serve, .fetch => |opts| opts.common.tls_verify_host,
|
inline .serve, .fetch, .mcp => |opts| opts.common.tls_verify_host,
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn obeyRobots(self: *const Config) bool {
|
pub fn obeyRobots(self: *const Config) bool {
|
||||||
return switch (self.mode) {
|
return switch (self.mode) {
|
||||||
inline .serve, .fetch => |opts| opts.common.obey_robots,
|
inline .serve, .fetch, .mcp => |opts| opts.common.obey_robots,
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn httpProxy(self: *const Config) ?[:0]const u8 {
|
pub fn httpProxy(self: *const Config) ?[:0]const u8 {
|
||||||
return switch (self.mode) {
|
return switch (self.mode) {
|
||||||
inline .serve, .fetch => |opts| opts.common.http_proxy,
|
inline .serve, .fetch, .mcp => |opts| opts.common.http_proxy,
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn proxyBearerToken(self: *const Config) ?[:0]const u8 {
|
pub fn proxyBearerToken(self: *const Config) ?[:0]const u8 {
|
||||||
return switch (self.mode) {
|
return switch (self.mode) {
|
||||||
inline .serve, .fetch => |opts| opts.common.proxy_bearer_token,
|
inline .serve, .fetch, .mcp => |opts| opts.common.proxy_bearer_token,
|
||||||
.help, .version => null,
|
.help, .version => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn httpMaxConcurrent(self: *const Config) u8 {
|
pub fn httpMaxConcurrent(self: *const Config) u8 {
|
||||||
return switch (self.mode) {
|
return switch (self.mode) {
|
||||||
inline .serve, .fetch => |opts| opts.common.http_max_concurrent orelse 10,
|
inline .serve, .fetch, .mcp => |opts| opts.common.http_max_concurrent orelse 10,
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn httpMaxHostOpen(self: *const Config) u8 {
|
pub fn httpMaxHostOpen(self: *const Config) u8 {
|
||||||
return switch (self.mode) {
|
return switch (self.mode) {
|
||||||
inline .serve, .fetch => |opts| opts.common.http_max_host_open orelse 4,
|
inline .serve, .fetch, .mcp => |opts| opts.common.http_max_host_open orelse 4,
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn httpConnectTimeout(self: *const Config) u31 {
|
pub fn httpConnectTimeout(self: *const Config) u31 {
|
||||||
return switch (self.mode) {
|
return switch (self.mode) {
|
||||||
inline .serve, .fetch => |opts| opts.common.http_connect_timeout orelse 0,
|
inline .serve, .fetch, .mcp => |opts| opts.common.http_connect_timeout orelse 0,
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn httpTimeout(self: *const Config) u31 {
|
pub fn httpTimeout(self: *const Config) u31 {
|
||||||
return switch (self.mode) {
|
return switch (self.mode) {
|
||||||
inline .serve, .fetch => |opts| opts.common.http_timeout orelse 5000,
|
inline .serve, .fetch, .mcp => |opts| opts.common.http_timeout orelse 5000,
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -112,62 +120,86 @@ pub fn httpMaxRedirects(_: *const Config) u8 {
|
|||||||
|
|
||||||
pub fn httpMaxResponseSize(self: *const Config) ?usize {
|
pub fn httpMaxResponseSize(self: *const Config) ?usize {
|
||||||
return switch (self.mode) {
|
return switch (self.mode) {
|
||||||
inline .serve, .fetch => |opts| opts.common.http_max_response_size,
|
inline .serve, .fetch, .mcp => |opts| opts.common.http_max_response_size,
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn logLevel(self: *const Config) ?log.Level {
|
pub fn logLevel(self: *const Config) ?log.Level {
|
||||||
return switch (self.mode) {
|
return switch (self.mode) {
|
||||||
inline .serve, .fetch => |opts| opts.common.log_level,
|
inline .serve, .fetch, .mcp => |opts| opts.common.log_level,
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn logFormat(self: *const Config) ?log.Format {
|
pub fn logFormat(self: *const Config) ?log.Format {
|
||||||
return switch (self.mode) {
|
return switch (self.mode) {
|
||||||
inline .serve, .fetch => |opts| opts.common.log_format,
|
inline .serve, .fetch, .mcp => |opts| opts.common.log_format,
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn logFilterScopes(self: *const Config) ?[]const log.Scope {
|
pub fn logFilterScopes(self: *const Config) ?[]const log.Scope {
|
||||||
return switch (self.mode) {
|
return switch (self.mode) {
|
||||||
inline .serve, .fetch => |opts| opts.common.log_filter_scopes,
|
inline .serve, .fetch, .mcp => |opts| opts.common.log_filter_scopes,
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn userAgentSuffix(self: *const Config) ?[]const u8 {
|
pub fn userAgentSuffix(self: *const Config) ?[]const u8 {
|
||||||
return switch (self.mode) {
|
return switch (self.mode) {
|
||||||
inline .serve, .fetch => |opts| opts.common.user_agent_suffix,
|
inline .serve, .fetch, .mcp => |opts| opts.common.user_agent_suffix,
|
||||||
.help, .version => null,
|
.help, .version => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn maxConnections(self: *const Config) u16 {
|
||||||
|
return switch (self.mode) {
|
||||||
|
.serve => |opts| opts.cdp_max_connections,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn maxPendingConnections(self: *const Config) u31 {
|
||||||
|
return switch (self.mode) {
|
||||||
|
.serve => |opts| opts.cdp_max_pending_connections,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub const Mode = union(RunMode) {
|
pub const Mode = union(RunMode) {
|
||||||
help: bool, // false when being printed because of an error
|
help: bool, // false when being printed because of an error
|
||||||
fetch: Fetch,
|
fetch: Fetch,
|
||||||
serve: Serve,
|
serve: Serve,
|
||||||
version: void,
|
version: void,
|
||||||
|
mcp: Mcp,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Serve = struct {
|
pub const Serve = struct {
|
||||||
host: []const u8 = "127.0.0.1",
|
host: []const u8 = "127.0.0.1",
|
||||||
port: u16 = 9222,
|
port: u16 = 9222,
|
||||||
timeout: u31 = 10,
|
timeout: u31 = 10,
|
||||||
max_connections: u16 = 16,
|
cdp_max_connections: u16 = 16,
|
||||||
max_tabs_per_connection: u16 = 8,
|
cdp_max_pending_connections: u16 = 128,
|
||||||
max_memory_per_tab: u64 = 512 * 1024 * 1024,
|
|
||||||
max_pending_connections: u16 = 128,
|
|
||||||
common: Common = .{},
|
common: Common = .{},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const Mcp = struct {
|
||||||
|
common: Common = .{},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const DumpFormat = enum {
|
||||||
|
html,
|
||||||
|
markdown,
|
||||||
|
wpt,
|
||||||
|
};
|
||||||
|
|
||||||
pub const Fetch = struct {
|
pub const Fetch = struct {
|
||||||
url: [:0]const u8,
|
url: [:0]const u8,
|
||||||
dump: bool = false,
|
dump_mode: ?DumpFormat = null,
|
||||||
common: Common = .{},
|
common: Common = .{},
|
||||||
withbase: bool = false,
|
with_base: bool = false,
|
||||||
|
with_frames: bool = false,
|
||||||
strip: dump.Opts.Strip = .{},
|
strip: dump.Opts.Strip = .{},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -298,15 +330,16 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
|||||||
const usage =
|
const usage =
|
||||||
\\usage: {s} command [options] [URL]
|
\\usage: {s} command [options] [URL]
|
||||||
\\
|
\\
|
||||||
\\Command can be either 'fetch', 'serve' or 'help'
|
\\Command can be either 'fetch', 'serve', 'mcp' or 'help'
|
||||||
\\
|
\\
|
||||||
\\fetch command
|
\\fetch command
|
||||||
\\Fetches the specified URL
|
\\Fetches the specified URL
|
||||||
\\Example: {s} fetch --dump https://lightpanda.io/
|
\\Example: {s} fetch --dump html https://lightpanda.io/
|
||||||
\\
|
\\
|
||||||
\\Options:
|
\\Options:
|
||||||
\\--dump Dumps document to stdout.
|
\\--dump Dumps document to stdout.
|
||||||
\\ Defaults to false.
|
\\ Argument must be 'html' or 'markdown'.
|
||||||
|
\\ Defaults to no dump.
|
||||||
\\
|
\\
|
||||||
\\--strip_mode Comma separated list of tag groups to remove from dump
|
\\--strip_mode Comma separated list of tag groups to remove from dump
|
||||||
\\ the dump. e.g. --strip_mode js,css
|
\\ the dump. e.g. --strip_mode js,css
|
||||||
@@ -317,6 +350,8 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
|||||||
\\
|
\\
|
||||||
\\--with_base Add a <base> tag in dump. Defaults to false.
|
\\--with_base Add a <base> tag in dump. Defaults to false.
|
||||||
\\
|
\\
|
||||||
|
\\--with_frames Includes the contents of iframes. Defaults to false.
|
||||||
|
\\
|
||||||
++ common_options ++
|
++ common_options ++
|
||||||
\\
|
\\
|
||||||
\\serve command
|
\\serve command
|
||||||
@@ -333,21 +368,20 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
|||||||
\\--timeout Inactivity timeout in seconds before disconnecting clients
|
\\--timeout Inactivity timeout in seconds before disconnecting clients
|
||||||
\\ Defaults to 10 (seconds). Limited to 604800 (1 week).
|
\\ Defaults to 10 (seconds). Limited to 604800 (1 week).
|
||||||
\\
|
\\
|
||||||
\\--max_connections
|
\\--cdp_max_connections
|
||||||
\\ Maximum number of simultaneous CDP connections.
|
\\ Maximum number of simultaneous CDP connections.
|
||||||
\\ Defaults to 16.
|
\\ Defaults to 16.
|
||||||
\\
|
\\
|
||||||
\\--max_tabs Maximum number of tabs per CDP connection.
|
\\--cdp_max_pending_connections
|
||||||
\\ Defaults to 8.
|
|
||||||
\\
|
|
||||||
\\--max_tab_memory
|
|
||||||
\\ Maximum memory per tab in bytes.
|
|
||||||
\\ Defaults to 536870912 (512 MB).
|
|
||||||
\\
|
|
||||||
\\--max_pending_connections
|
|
||||||
\\ Maximum pending connections in the accept queue.
|
\\ Maximum pending connections in the accept queue.
|
||||||
\\ Defaults to 128.
|
\\ Defaults to 128.
|
||||||
\\
|
\\
|
||||||
|
++ common_options ++
|
||||||
|
\\
|
||||||
|
\\mcp command
|
||||||
|
\\Starts an MCP (Model Context Protocol) server over stdio
|
||||||
|
\\Example: {s} mcp
|
||||||
|
\\
|
||||||
++ common_options ++
|
++ common_options ++
|
||||||
\\
|
\\
|
||||||
\\version command
|
\\version command
|
||||||
@@ -357,7 +391,7 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
|||||||
\\Displays this message
|
\\Displays this message
|
||||||
\\
|
\\
|
||||||
;
|
;
|
||||||
std.debug.print(usage, .{ self.exec_name, self.exec_name, self.exec_name, self.exec_name });
|
std.debug.print(usage, .{ self.exec_name, self.exec_name, self.exec_name, self.exec_name, self.exec_name });
|
||||||
if (success) {
|
if (success) {
|
||||||
return std.process.cleanExit();
|
return std.process.cleanExit();
|
||||||
}
|
}
|
||||||
@@ -392,6 +426,8 @@ pub fn parseArgs(allocator: Allocator) !Config {
|
|||||||
return init(allocator, exec_name, .{ .help = false }) },
|
return init(allocator, exec_name, .{ .help = false }) },
|
||||||
.fetch => .{ .fetch = parseFetchArgs(allocator, &args) catch
|
.fetch => .{ .fetch = parseFetchArgs(allocator, &args) catch
|
||||||
return init(allocator, exec_name, .{ .help = false }) },
|
return init(allocator, exec_name, .{ .help = false }) },
|
||||||
|
.mcp => .{ .mcp = parseMcpArgs(allocator, &args) catch
|
||||||
|
return init(allocator, exec_name, .{ .help = false }) },
|
||||||
.version => .{ .version = {} },
|
.version => .{ .version = {} },
|
||||||
};
|
};
|
||||||
return init(allocator, exec_name, mode);
|
return init(allocator, exec_name, mode);
|
||||||
@@ -422,6 +458,10 @@ fn inferMode(opt: []const u8) ?RunMode {
|
|||||||
return .fetch;
|
return .fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, opt, "--with_frames")) {
|
||||||
|
return .fetch;
|
||||||
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, opt, "--host")) {
|
if (std.mem.eql(u8, opt, "--host")) {
|
||||||
return .serve;
|
return .serve;
|
||||||
}
|
}
|
||||||
@@ -479,53 +519,27 @@ fn parseServeArgs(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--max_connections", opt)) {
|
if (std.mem.eql(u8, "--cdp_max_connections", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--max_connections" });
|
log.fatal(.app, "missing argument value", .{ .arg = "--cdp_max_connections" });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
|
|
||||||
serve.max_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
serve.cdp_max_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||||
log.fatal(.app, "invalid argument value", .{ .arg = "--max_connections", .err = err });
|
log.fatal(.app, "invalid argument value", .{ .arg = "--cdp_max_connections", .err = err });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--max_tabs", opt)) {
|
if (std.mem.eql(u8, "--cdp_max_pending_connections", opt)) {
|
||||||
const str = args.next() orelse {
|
const str = args.next() orelse {
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--max_tabs" });
|
log.fatal(.app, "missing argument value", .{ .arg = "--cdp_max_pending_connections" });
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
|
|
||||||
serve.max_tabs_per_connection = std.fmt.parseInt(u16, str, 10) catch |err| {
|
serve.cdp_max_pending_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||||
log.fatal(.app, "invalid argument value", .{ .arg = "--max_tabs", .err = err });
|
log.fatal(.app, "invalid argument value", .{ .arg = "--cdp_max_pending_connections", .err = err });
|
||||||
return error.InvalidArgument;
|
|
||||||
};
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--max_tab_memory", opt)) {
|
|
||||||
const str = args.next() orelse {
|
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--max_tab_memory" });
|
|
||||||
return error.InvalidArgument;
|
|
||||||
};
|
|
||||||
|
|
||||||
serve.max_memory_per_tab = std.fmt.parseInt(u64, str, 10) catch |err| {
|
|
||||||
log.fatal(.app, "invalid argument value", .{ .arg = "--max_tab_memory", .err = err });
|
|
||||||
return error.InvalidArgument;
|
|
||||||
};
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--max_pending_connections", opt)) {
|
|
||||||
const str = args.next() orelse {
|
|
||||||
log.fatal(.app, "missing argument value", .{ .arg = "--max_pending_connections" });
|
|
||||||
return error.InvalidArgument;
|
|
||||||
};
|
|
||||||
|
|
||||||
serve.max_pending_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
|
||||||
log.fatal(.app, "invalid argument value", .{ .arg = "--max_pending_connections", .err = err });
|
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
};
|
};
|
||||||
continue;
|
continue;
|
||||||
@@ -542,19 +556,48 @@ fn parseServeArgs(
|
|||||||
return serve;
|
return serve;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parseMcpArgs(
|
||||||
|
allocator: Allocator,
|
||||||
|
args: *std.process.ArgIterator,
|
||||||
|
) !Mcp {
|
||||||
|
var mcp: Mcp = .{};
|
||||||
|
|
||||||
|
while (args.next()) |opt| {
|
||||||
|
if (try parseCommonArg(allocator, opt, args, &mcp.common)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.fatal(.mcp, "unknown argument", .{ .mode = "mcp", .arg = opt });
|
||||||
|
return error.UnkownOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp;
|
||||||
|
}
|
||||||
|
|
||||||
fn parseFetchArgs(
|
fn parseFetchArgs(
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
args: *std.process.ArgIterator,
|
args: *std.process.ArgIterator,
|
||||||
) !Fetch {
|
) !Fetch {
|
||||||
var fetch_dump: bool = false;
|
var dump_mode: ?DumpFormat = null;
|
||||||
var withbase: bool = false;
|
var with_base: bool = false;
|
||||||
|
var with_frames: bool = false;
|
||||||
var url: ?[:0]const u8 = null;
|
var url: ?[:0]const u8 = null;
|
||||||
var common: Common = .{};
|
var common: Common = .{};
|
||||||
var strip: dump.Opts.Strip = .{};
|
var strip: dump.Opts.Strip = .{};
|
||||||
|
|
||||||
while (args.next()) |opt| {
|
while (args.next()) |opt| {
|
||||||
if (std.mem.eql(u8, "--dump", opt)) {
|
if (std.mem.eql(u8, "--dump", opt)) {
|
||||||
fetch_dump = true;
|
var peek_args = args.*;
|
||||||
|
if (peek_args.next()) |next_arg| {
|
||||||
|
if (std.meta.stringToEnum(DumpFormat, next_arg)) |mode| {
|
||||||
|
dump_mode = mode;
|
||||||
|
_ = args.next();
|
||||||
|
} else {
|
||||||
|
dump_mode = .html;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dump_mode = .html;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,7 +611,12 @@ fn parseFetchArgs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--with_base", opt)) {
|
if (std.mem.eql(u8, "--with_base", opt)) {
|
||||||
withbase = true;
|
with_base = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--with_frames", opt)) {
|
||||||
|
with_frames = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,10 +669,11 @@ fn parseFetchArgs(
|
|||||||
|
|
||||||
return .{
|
return .{
|
||||||
.url = url.?,
|
.url = url.?,
|
||||||
.dump = fetch_dump,
|
.dump_mode = dump_mode,
|
||||||
.strip = strip,
|
.strip = strip,
|
||||||
.common = common,
|
.common = common,
|
||||||
.withbase = withbase,
|
.with_base = with_base,
|
||||||
|
.with_frames = with_frames,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1375
src/Net.zig
Normal file
1375
src/Net.zig
Normal file
File diff suppressed because it is too large
Load Diff
@@ -73,6 +73,7 @@ const EventListeners = struct {
|
|||||||
page_navigated: List = .{},
|
page_navigated: List = .{},
|
||||||
page_network_idle: List = .{},
|
page_network_idle: List = .{},
|
||||||
page_network_almost_idle: List = .{},
|
page_network_almost_idle: List = .{},
|
||||||
|
page_frame_created: List = .{},
|
||||||
http_request_fail: List = .{},
|
http_request_fail: List = .{},
|
||||||
http_request_start: List = .{},
|
http_request_start: List = .{},
|
||||||
http_request_intercept: List = .{},
|
http_request_intercept: List = .{},
|
||||||
@@ -89,6 +90,7 @@ const Events = union(enum) {
|
|||||||
page_navigated: *const PageNavigated,
|
page_navigated: *const PageNavigated,
|
||||||
page_network_idle: *const PageNetworkIdle,
|
page_network_idle: *const PageNetworkIdle,
|
||||||
page_network_almost_idle: *const PageNetworkAlmostIdle,
|
page_network_almost_idle: *const PageNetworkAlmostIdle,
|
||||||
|
page_frame_created: *const PageFrameCreated,
|
||||||
http_request_fail: *const RequestFail,
|
http_request_fail: *const RequestFail,
|
||||||
http_request_start: *const RequestStart,
|
http_request_start: *const RequestStart,
|
||||||
http_request_intercept: *const RequestIntercept,
|
http_request_intercept: *const RequestIntercept,
|
||||||
@@ -102,24 +104,36 @@ const EventType = std.meta.FieldEnum(Events);
|
|||||||
pub const PageRemove = struct {};
|
pub const PageRemove = struct {};
|
||||||
|
|
||||||
pub const PageNavigate = struct {
|
pub const PageNavigate = struct {
|
||||||
req_id: usize,
|
req_id: u32,
|
||||||
|
frame_id: u32,
|
||||||
timestamp: u64,
|
timestamp: u64,
|
||||||
url: [:0]const u8,
|
url: [:0]const u8,
|
||||||
opts: Page.NavigateOpts,
|
opts: Page.NavigateOpts,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const PageNavigated = struct {
|
pub const PageNavigated = struct {
|
||||||
req_id: usize,
|
req_id: u32,
|
||||||
|
frame_id: u32,
|
||||||
timestamp: u64,
|
timestamp: u64,
|
||||||
url: [:0]const u8,
|
url: [:0]const u8,
|
||||||
opts: Page.NavigatedOpts,
|
opts: Page.NavigatedOpts,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const PageNetworkIdle = struct {
|
pub const PageNetworkIdle = struct {
|
||||||
|
req_id: u32,
|
||||||
|
frame_id: u32,
|
||||||
timestamp: u64,
|
timestamp: u64,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const PageNetworkAlmostIdle = struct {
|
pub const PageNetworkAlmostIdle = struct {
|
||||||
|
req_id: u32,
|
||||||
|
frame_id: u32,
|
||||||
|
timestamp: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PageFrameCreated = struct {
|
||||||
|
frame_id: u32,
|
||||||
|
parent_id: u32,
|
||||||
timestamp: u64,
|
timestamp: u64,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -305,6 +319,7 @@ test "Notification" {
|
|||||||
|
|
||||||
// noop
|
// noop
|
||||||
notifier.dispatch(.page_navigate, &.{
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.frame_id = 0,
|
||||||
.req_id = 1,
|
.req_id = 1,
|
||||||
.timestamp = 4,
|
.timestamp = 4,
|
||||||
.url = undefined,
|
.url = undefined,
|
||||||
@@ -315,6 +330,7 @@ test "Notification" {
|
|||||||
|
|
||||||
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
||||||
notifier.dispatch(.page_navigate, &.{
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.frame_id = 0,
|
||||||
.req_id = 1,
|
.req_id = 1,
|
||||||
.timestamp = 4,
|
.timestamp = 4,
|
||||||
.url = undefined,
|
.url = undefined,
|
||||||
@@ -324,6 +340,7 @@ test "Notification" {
|
|||||||
|
|
||||||
notifier.unregisterAll(&tc);
|
notifier.unregisterAll(&tc);
|
||||||
notifier.dispatch(.page_navigate, &.{
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.frame_id = 0,
|
||||||
.req_id = 1,
|
.req_id = 1,
|
||||||
.timestamp = 10,
|
.timestamp = 10,
|
||||||
.url = undefined,
|
.url = undefined,
|
||||||
@@ -334,23 +351,25 @@ test "Notification" {
|
|||||||
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
||||||
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
|
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
|
||||||
notifier.dispatch(.page_navigate, &.{
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.frame_id = 0,
|
||||||
.req_id = 1,
|
.req_id = 1,
|
||||||
.timestamp = 10,
|
.timestamp = 10,
|
||||||
.url = undefined,
|
.url = undefined,
|
||||||
.opts = .{},
|
.opts = .{},
|
||||||
});
|
});
|
||||||
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 6, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 6, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(14, tc.page_navigate);
|
try testing.expectEqual(14, tc.page_navigate);
|
||||||
try testing.expectEqual(6, tc.page_navigated);
|
try testing.expectEqual(6, tc.page_navigated);
|
||||||
|
|
||||||
notifier.unregisterAll(&tc);
|
notifier.unregisterAll(&tc);
|
||||||
notifier.dispatch(.page_navigate, &.{
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.frame_id = 0,
|
||||||
.req_id = 1,
|
.req_id = 1,
|
||||||
.timestamp = 100,
|
.timestamp = 100,
|
||||||
.url = undefined,
|
.url = undefined,
|
||||||
.opts = .{},
|
.opts = .{},
|
||||||
});
|
});
|
||||||
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(14, tc.page_navigate);
|
try testing.expectEqual(14, tc.page_navigate);
|
||||||
try testing.expectEqual(6, tc.page_navigated);
|
try testing.expectEqual(6, tc.page_navigated);
|
||||||
|
|
||||||
@@ -358,27 +377,27 @@ test "Notification" {
|
|||||||
// unregister
|
// unregister
|
||||||
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
||||||
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
|
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
|
||||||
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigate, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(114, tc.page_navigate);
|
try testing.expectEqual(114, tc.page_navigate);
|
||||||
try testing.expectEqual(1006, tc.page_navigated);
|
try testing.expectEqual(1006, tc.page_navigated);
|
||||||
|
|
||||||
notifier.unregister(.page_navigate, &tc);
|
notifier.unregister(.page_navigate, &tc);
|
||||||
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigate, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(114, tc.page_navigate);
|
try testing.expectEqual(114, tc.page_navigate);
|
||||||
try testing.expectEqual(2006, tc.page_navigated);
|
try testing.expectEqual(2006, tc.page_navigated);
|
||||||
|
|
||||||
notifier.unregister(.page_navigated, &tc);
|
notifier.unregister(.page_navigated, &tc);
|
||||||
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigate, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(114, tc.page_navigate);
|
try testing.expectEqual(114, tc.page_navigate);
|
||||||
try testing.expectEqual(2006, tc.page_navigated);
|
try testing.expectEqual(2006, tc.page_navigated);
|
||||||
|
|
||||||
// already unregistered, try anyways
|
// already unregistered, try anyways
|
||||||
notifier.unregister(.page_navigated, &tc);
|
notifier.unregister(.page_navigated, &tc);
|
||||||
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigate, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(114, tc.page_navigate);
|
try testing.expectEqual(114, tc.page_navigate);
|
||||||
try testing.expectEqual(2006, tc.page_navigated);
|
try testing.expectEqual(2006, tc.page_navigated);
|
||||||
}
|
}
|
||||||
|
|||||||
1000
src/Server.zig
1000
src/Server.zig
File diff suppressed because it is too large
Load Diff
@@ -24,9 +24,9 @@ const ArenaAllocator = std.heap.ArenaAllocator;
|
|||||||
const js = @import("js/js.zig");
|
const js = @import("js/js.zig");
|
||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
const App = @import("../App.zig");
|
const App = @import("../App.zig");
|
||||||
|
const HttpClient = @import("../http/Client.zig");
|
||||||
|
|
||||||
const ArenaPool = App.ArenaPool;
|
const ArenaPool = App.ArenaPool;
|
||||||
const HttpClient = App.Http.Client;
|
|
||||||
|
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
@@ -44,13 +44,10 @@ session: ?Session,
|
|||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
arena_pool: *ArenaPool,
|
arena_pool: *ArenaPool,
|
||||||
http_client: *HttpClient,
|
http_client: *HttpClient,
|
||||||
call_arena: ArenaAllocator,
|
|
||||||
page_arena: ArenaAllocator,
|
|
||||||
session_arena: ArenaAllocator,
|
|
||||||
transfer_arena: ArenaAllocator,
|
|
||||||
|
|
||||||
const InitOpts = struct {
|
const InitOpts = struct {
|
||||||
env: js.Env.InitOpts = .{},
|
env: js.Env.InitOpts = .{},
|
||||||
|
http_client: *HttpClient,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(app: *App, opts: InitOpts) !Browser {
|
pub fn init(app: *App, opts: InitOpts) !Browser {
|
||||||
@@ -65,21 +62,13 @@ pub fn init(app: *App, opts: InitOpts) !Browser {
|
|||||||
.session = null,
|
.session = null,
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.arena_pool = &app.arena_pool,
|
.arena_pool = &app.arena_pool,
|
||||||
.http_client = app.http.client,
|
.http_client = opts.http_client,
|
||||||
.call_arena = ArenaAllocator.init(allocator),
|
|
||||||
.page_arena = ArenaAllocator.init(allocator),
|
|
||||||
.session_arena = ArenaAllocator.init(allocator),
|
|
||||||
.transfer_arena = ArenaAllocator.init(allocator),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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.session_arena.deinit();
|
|
||||||
self.transfer_arena.deinit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn newSession(self: *Browser, notification: *Notification) !*Session {
|
pub fn newSession(self: *Browser, notification: *Notification) !*Session {
|
||||||
@@ -94,24 +83,33 @@ pub fn closeSession(self: *Browser) void {
|
|||||||
if (self.session) |*session| {
|
if (self.session) |*session| {
|
||||||
session.deinit();
|
session.deinit();
|
||||||
self.session = null;
|
self.session = null;
|
||||||
_ = self.session_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
|
||||||
self.env.memoryPressureNotification(.critical);
|
self.env.memoryPressureNotification(.critical);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMicrotasks(self: *const Browser) void {
|
pub fn runMicrotasks(self: *Browser) void {
|
||||||
self.env.runMicrotasks();
|
self.env.runMicrotasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMacrotasks(self: *Browser) !?u64 {
|
pub fn runMacrotasks(self: *Browser) !?u64 {
|
||||||
return try self.env.runMacrotasks();
|
const env = &self.env;
|
||||||
|
|
||||||
|
const time_to_next = try self.env.runMacrotasks();
|
||||||
|
env.pumpMessageLoop();
|
||||||
|
|
||||||
|
// either of the above could have queued more microtasks
|
||||||
|
env.runMicrotasks();
|
||||||
|
|
||||||
|
return time_to_next;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMessageLoop(self: *const Browser) void {
|
pub fn hasBackgroundTasks(self: *Browser) bool {
|
||||||
while (self.env.pumpMessageLoop()) {
|
return self.env.hasBackgroundTasks();
|
||||||
if (comptime IS_DEBUG) {
|
}
|
||||||
log.debug(.browser, "pumpMessageLoop", .{});
|
pub fn waitForBackgroundTasks(self: *Browser) void {
|
||||||
}
|
self.env.waitForBackgroundTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn runIdleTasks(self: *const Browser) void {
|
||||||
self.env.runIdleTasks();
|
self.env.runIdleTasks();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const Page = @import("Page.zig");
|
|||||||
const Node = @import("webapi/Node.zig");
|
const Node = @import("webapi/Node.zig");
|
||||||
const Event = @import("webapi/Event.zig");
|
const Event = @import("webapi/Event.zig");
|
||||||
const EventTarget = @import("webapi/EventTarget.zig");
|
const EventTarget = @import("webapi/EventTarget.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
@@ -55,7 +56,12 @@ pub const EventManager = @This();
|
|||||||
|
|
||||||
page: *Page,
|
page: *Page,
|
||||||
arena: Allocator,
|
arena: Allocator,
|
||||||
|
// Used as an optimization in Page._documentIsComplete. If we know there are no
|
||||||
|
// 'load' listeners in the document, we can skip dispatching the per-resource
|
||||||
|
// 'load' event (e.g. amazon product page has no listener and ~350 resources)
|
||||||
|
has_dom_load_listener: bool,
|
||||||
listener_pool: std.heap.MemoryPool(Listener),
|
listener_pool: std.heap.MemoryPool(Listener),
|
||||||
|
ignore_list: std.ArrayList(*Listener),
|
||||||
list_pool: std.heap.MemoryPool(std.DoublyLinkedList),
|
list_pool: std.heap.MemoryPool(std.DoublyLinkedList),
|
||||||
lookup: std.HashMapUnmanaged(
|
lookup: std.HashMapUnmanaged(
|
||||||
EventKey,
|
EventKey,
|
||||||
@@ -66,15 +72,17 @@ lookup: std.HashMapUnmanaged(
|
|||||||
dispatch_depth: usize,
|
dispatch_depth: usize,
|
||||||
deferred_removals: std.ArrayList(struct { list: *std.DoublyLinkedList, listener: *Listener }),
|
deferred_removals: std.ArrayList(struct { list: *std.DoublyLinkedList, listener: *Listener }),
|
||||||
|
|
||||||
pub fn init(page: *Page) EventManager {
|
pub fn init(arena: Allocator, page: *Page) EventManager {
|
||||||
return .{
|
return .{
|
||||||
.page = page,
|
.page = page,
|
||||||
.lookup = .{},
|
.lookup = .{},
|
||||||
.arena = page.arena,
|
.arena = arena,
|
||||||
.list_pool = std.heap.MemoryPool(std.DoublyLinkedList).init(page.arena),
|
.ignore_list = .{},
|
||||||
.listener_pool = std.heap.MemoryPool(Listener).init(page.arena),
|
.list_pool = .init(arena),
|
||||||
|
.listener_pool = .init(arena),
|
||||||
.dispatch_depth = 0,
|
.dispatch_depth = 0,
|
||||||
.deferred_removals = .{},
|
.deferred_removals = .{},
|
||||||
|
.has_dom_load_listener = false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +113,10 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
|||||||
// Allocate the type string we'll use in both listener and key
|
// Allocate the type string we'll use in both listener and key
|
||||||
const type_string = try String.init(self.arena, typ, .{});
|
const type_string = try String.init(self.arena, typ, .{});
|
||||||
|
|
||||||
|
if (type_string.eql(comptime .wrap("load")) and target._type == .node) {
|
||||||
|
self.has_dom_load_listener = true;
|
||||||
|
}
|
||||||
|
|
||||||
const gop = try self.lookup.getOrPut(self.arena, .{
|
const gop = try self.lookup.getOrPut(self.arena, .{
|
||||||
.type_string = type_string,
|
.type_string = type_string,
|
||||||
.event_target = @intFromPtr(target),
|
.event_target = @intFromPtr(target),
|
||||||
@@ -145,6 +157,11 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
|||||||
};
|
};
|
||||||
// append the listener to the list of listeners for this target
|
// append the listener to the list of listeners for this target
|
||||||
gop.value_ptr.*.append(&listener.node);
|
gop.value_ptr.*.append(&listener.node);
|
||||||
|
|
||||||
|
// Track load listeners for script execution ignore list
|
||||||
|
if (type_string.eql(comptime .wrap("load"))) {
|
||||||
|
try self.ignore_list.append(self.arena, listener);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void {
|
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void {
|
||||||
@@ -157,6 +174,10 @@ pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callba
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clearIgnoreList(self: *EventManager) void {
|
||||||
|
self.ignore_list.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
// Dispatching can be recursive from the compiler's point of view, so we need to
|
// Dispatching can be recursive from the compiler's point of view, so we need to
|
||||||
// give it an explicit error set so that other parts of the code can use and
|
// give it an explicit error set so that other parts of the code can use and
|
||||||
// inferred error.
|
// inferred error.
|
||||||
@@ -168,42 +189,31 @@ const DispatchError = error{
|
|||||||
ExecutionError,
|
ExecutionError,
|
||||||
JsException,
|
JsException,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const DispatchOpts = struct {
|
||||||
|
// A "load" event triggered by a script (in ScriptManager) should not trigger
|
||||||
|
// a "load" listener added within that script. Therefore, any "load" listener
|
||||||
|
// that we add go into an ignore list until after the script finishes executing.
|
||||||
|
// The ignore list is only checked when apply_ignore == true, which is only
|
||||||
|
// set by the ScriptManager when raising the script's "load" event.
|
||||||
|
apply_ignore: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) DispatchError!void {
|
pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) DispatchError!void {
|
||||||
|
return self.dispatchOpts(target, event, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, comptime opts: DispatchOpts) DispatchError!void {
|
||||||
|
event.acquireRef();
|
||||||
|
defer event.deinit(false, self.page);
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
|
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
|
||||||
}
|
}
|
||||||
|
|
||||||
event._target = target;
|
|
||||||
event._dispatch_target = target; // Store original target for composedPath()
|
|
||||||
var was_handled = false;
|
|
||||||
|
|
||||||
defer if (was_handled) {
|
|
||||||
var ls: js.Local.Scope = undefined;
|
|
||||||
self.page.js.localScope(&ls);
|
|
||||||
defer ls.deinit();
|
|
||||||
ls.local.runMicrotasks();
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (target._type) {
|
switch (target._type) {
|
||||||
.node => |node| try self.dispatchNode(node, event, &was_handled),
|
.node => |node| try self.dispatchNode(node, event, opts),
|
||||||
.xhr,
|
else => try self.dispatchDirect(target, event, null, .{ .context = "dispatch" }),
|
||||||
.window,
|
|
||||||
.abort_signal,
|
|
||||||
.media_query_list,
|
|
||||||
.message_port,
|
|
||||||
.text_track_cue,
|
|
||||||
.navigation,
|
|
||||||
.screen,
|
|
||||||
.screen_orientation,
|
|
||||||
.visual_viewport,
|
|
||||||
.generic,
|
|
||||||
=> {
|
|
||||||
const list = self.lookup.get(.{
|
|
||||||
.event_target = @intFromPtr(target),
|
|
||||||
.type_string = event._type_string,
|
|
||||||
}) orelse return;
|
|
||||||
try self.dispatchAll(list, target, event, &was_handled);
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,13 +222,22 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat
|
|||||||
// property is just a shortcut for calling addEventListener, but they are distinct.
|
// property is just a shortcut for calling addEventListener, but they are distinct.
|
||||||
// An event set via property cannot be removed by removeEventListener. If you
|
// An event set via property cannot be removed by removeEventListener. If you
|
||||||
// set both the property and add a listener, they both execute.
|
// set both the property and add a listener, they both execute.
|
||||||
const DispatchWithFunctionOptions = struct {
|
const DispatchDirectOptions = struct {
|
||||||
context: []const u8,
|
context: []const u8,
|
||||||
inject_target: bool = true,
|
inject_target: bool = true,
|
||||||
};
|
};
|
||||||
pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *Event, function_: ?js.Function, comptime opts: DispatchWithFunctionOptions) !void {
|
|
||||||
|
// Direct dispatch for non-DOM targets (Window, XHR, AbortSignal) or DOM nodes with
|
||||||
|
// property handlers. No propagation - just calls the handler and registered listeners.
|
||||||
|
// Handler can be: null, ?js.Function.Global, ?js.Function.Temp, or js.Function
|
||||||
|
pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event, handler: anytype, comptime opts: DispatchDirectOptions) !void {
|
||||||
|
const page = self.page;
|
||||||
|
|
||||||
|
event.acquireRef();
|
||||||
|
defer event.deinit(false, page);
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.event, "dispatchWithFunction", .{ .type = event._type_string.str(), .context = opts.context, .has_function = function_ != null });
|
log.debug(.event, "dispatchDirect", .{ .type = event._type_string, .context = opts.context });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comptime opts.inject_target) {
|
if (comptime opts.inject_target) {
|
||||||
@@ -227,14 +246,15 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
|
|||||||
}
|
}
|
||||||
|
|
||||||
var was_dispatched = false;
|
var was_dispatched = false;
|
||||||
defer if (was_dispatched) {
|
|
||||||
var ls: js.Local.Scope = undefined;
|
|
||||||
self.page.js.localScope(&ls);
|
|
||||||
defer ls.deinit();
|
|
||||||
ls.local.runMicrotasks();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (function_) |func| {
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer {
|
||||||
|
ls.local.runMicrotasks();
|
||||||
|
ls.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getFunction(handler, &ls.local)) |func| {
|
||||||
event._current_target = target;
|
event._current_target = target;
|
||||||
if (func.callWithThis(void, target, .{event})) {
|
if (func.callWithThis(void, target, .{event})) {
|
||||||
was_dispatched = true;
|
was_dispatched = true;
|
||||||
@@ -244,121 +264,15 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// listeners reigstered via addEventListener
|
||||||
const list = self.lookup.get(.{
|
const list = self.lookup.get(.{
|
||||||
.event_target = @intFromPtr(target),
|
.event_target = @intFromPtr(target),
|
||||||
.type_string = event._type_string,
|
.type_string = event._type_string,
|
||||||
}) orelse return;
|
}) orelse return;
|
||||||
try self.dispatchAll(list, target, event, &was_dispatched);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool) !void {
|
// This is a slightly simplified version of what you'll find in dispatchPhase
|
||||||
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
// It is simpler because, for direct dispatching, we know there's no ancestors
|
||||||
|
// and only the single target phase.
|
||||||
// Defer runs even on early return - ensures event phase is reset
|
|
||||||
// and default actions execute (unless prevented)
|
|
||||||
defer {
|
|
||||||
event._event_phase = .none;
|
|
||||||
|
|
||||||
// Execute default action if not prevented
|
|
||||||
if (event._prevent_default) {
|
|
||||||
// can't return in a defer (╯°□°)╯︵ ┻━┻
|
|
||||||
} else if (event._type_string.eqlSlice("click")) {
|
|
||||||
self.page.handleClick(target) catch |err| {
|
|
||||||
log.warn(.event, "page.click", .{ .err = err });
|
|
||||||
};
|
|
||||||
} else if (event._type_string.eqlSlice("keydown")) {
|
|
||||||
self.page.handleKeydown(target, event) catch |err| {
|
|
||||||
log.warn(.event, "page.keydown", .{ .err = err });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var path_len: usize = 0;
|
|
||||||
var path_buffer: [128]*EventTarget = undefined;
|
|
||||||
|
|
||||||
var node: ?*Node = target;
|
|
||||||
while (node) |n| {
|
|
||||||
if (path_len >= path_buffer.len) break;
|
|
||||||
path_buffer[path_len] = n.asEventTarget();
|
|
||||||
path_len += 1;
|
|
||||||
|
|
||||||
// Check if this node is a shadow root
|
|
||||||
if (n.is(ShadowRoot)) |shadow| {
|
|
||||||
event._needs_retargeting = true;
|
|
||||||
|
|
||||||
// If event is not composed, stop at shadow boundary
|
|
||||||
if (!event._composed) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, jump to the shadow host and continue
|
|
||||||
node = shadow._host.asNode();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
node = n._parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Even though the window isn't part of the DOM, events always propagate
|
|
||||||
// through it in the capture phase (unless we stopped at a shadow boundary)
|
|
||||||
if (path_len < path_buffer.len) {
|
|
||||||
path_buffer[path_len] = self.page.window.asEventTarget();
|
|
||||||
path_len += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = path_buffer[0..path_len];
|
|
||||||
|
|
||||||
// Phase 1: Capturing phase (root → target, excluding target)
|
|
||||||
// This happens for all events, regardless of bubbling
|
|
||||||
event._event_phase = .capturing_phase;
|
|
||||||
var i: usize = path_len;
|
|
||||||
while (i > 1) {
|
|
||||||
i -= 1;
|
|
||||||
const current_target = path[i];
|
|
||||||
if (self.lookup.get(.{
|
|
||||||
.event_target = @intFromPtr(current_target),
|
|
||||||
.type_string = event._type_string,
|
|
||||||
})) |list| {
|
|
||||||
try self.dispatchPhase(list, current_target, event, was_handled, true);
|
|
||||||
if (event._stop_propagation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2: At target
|
|
||||||
event._event_phase = .at_target;
|
|
||||||
const target_et = target.asEventTarget();
|
|
||||||
if (self.lookup.get(.{
|
|
||||||
.type_string = event._type_string,
|
|
||||||
.event_target = @intFromPtr(target_et),
|
|
||||||
})) |list| {
|
|
||||||
try self.dispatchPhase(list, target_et, event, was_handled, null);
|
|
||||||
if (event._stop_propagation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 3: Bubbling phase (target → root, excluding target)
|
|
||||||
// This only happens if the event bubbles
|
|
||||||
if (event._bubbles) {
|
|
||||||
event._event_phase = .bubbling_phase;
|
|
||||||
for (path[1..]) |current_target| {
|
|
||||||
if (self.lookup.get(.{
|
|
||||||
.type_string = event._type_string,
|
|
||||||
.event_target = @intFromPtr(current_target),
|
|
||||||
})) |list| {
|
|
||||||
try self.dispatchPhase(list, current_target, event, was_handled, false);
|
|
||||||
if (event._stop_propagation) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime capture_only: ?bool) !void {
|
|
||||||
const page = self.page;
|
|
||||||
|
|
||||||
// Track dispatch depth for deferred removal
|
// Track dispatch depth for deferred removal
|
||||||
self.dispatch_depth += 1;
|
self.dispatch_depth += 1;
|
||||||
@@ -392,8 +306,267 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
is_done = (listener == last_listener);
|
is_done = (listener == last_listener);
|
||||||
node = n.next;
|
node = n.next;
|
||||||
|
|
||||||
|
// Skip removed listeners
|
||||||
|
if (listener.removed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the listener has an aborted signal, remove it and skip
|
||||||
|
if (listener.signal) |signal| {
|
||||||
|
if (signal.getAborted()) {
|
||||||
|
self.removeListener(list, listener);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove "once" listeners BEFORE calling them so nested dispatches don't see them
|
||||||
|
if (listener.once) {
|
||||||
|
self.removeListener(list, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
was_dispatched = true;
|
||||||
|
event._current_target = target;
|
||||||
|
|
||||||
|
switch (listener.function) {
|
||||||
|
.value => |value| try ls.toLocal(value).callWithThis(void, target, .{event}),
|
||||||
|
.string => |string| {
|
||||||
|
const str = try page.call_arena.dupeZ(u8, string.str());
|
||||||
|
try ls.local.eval(str, null);
|
||||||
|
},
|
||||||
|
.object => |obj_global| {
|
||||||
|
const obj = ls.toLocal(obj_global);
|
||||||
|
if (try obj.getFunction("handleEvent")) |handleEvent| {
|
||||||
|
try handleEvent.callWithThis(void, obj, .{event});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event._stop_immediate_propagation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getFunction(handler: anytype, local: *const js.Local) ?js.Function {
|
||||||
|
const T = @TypeOf(handler);
|
||||||
|
const ti = @typeInfo(T);
|
||||||
|
|
||||||
|
if (ti == .null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (ti == .optional) {
|
||||||
|
return getFunction(handler orelse return null, local);
|
||||||
|
}
|
||||||
|
return switch (T) {
|
||||||
|
js.Function => handler,
|
||||||
|
js.Function.Temp => local.toLocal(handler),
|
||||||
|
js.Function.Global => local.toLocal(handler),
|
||||||
|
else => @compileError("handler must be null or \\??js.Function(\\.(Temp|Global))?"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts: DispatchOpts) !void {
|
||||||
|
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
||||||
|
|
||||||
|
{
|
||||||
|
const et = target.asEventTarget();
|
||||||
|
event._target = et;
|
||||||
|
event._dispatch_target = et; // Store original target for composedPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = self.page;
|
||||||
|
var was_handled = false;
|
||||||
|
|
||||||
|
defer if (was_handled) {
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
ls.local.runMicrotasks();
|
||||||
|
};
|
||||||
|
|
||||||
|
const activation_state = ActivationState.create(event, target, page);
|
||||||
|
|
||||||
|
// Defer runs even on early return - ensures event phase is reset
|
||||||
|
// and default actions execute (unless prevented)
|
||||||
|
defer {
|
||||||
|
event._event_phase = .none;
|
||||||
|
event._stop_propagation = false;
|
||||||
|
event._stop_immediate_propagation = false;
|
||||||
|
// Handle checkbox/radio activation rollback or commit
|
||||||
|
if (activation_state) |state| {
|
||||||
|
state.restore(event, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute default action if not prevented
|
||||||
|
if (event._prevent_default) {
|
||||||
|
// can't return in a defer (╯°□°)╯︵ ┻━┻
|
||||||
|
} else if (event._type_string.eql(comptime .wrap("click"))) {
|
||||||
|
page.handleClick(target) catch |err| {
|
||||||
|
log.warn(.event, "page.click", .{ .err = err });
|
||||||
|
};
|
||||||
|
} else if (event._type_string.eql(comptime .wrap("keydown"))) {
|
||||||
|
page.handleKeydown(target, event) catch |err| {
|
||||||
|
log.warn(.event, "page.keydown", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var path_len: usize = 0;
|
||||||
|
var path_buffer: [128]*EventTarget = undefined;
|
||||||
|
|
||||||
|
var node: ?*Node = target;
|
||||||
|
while (node) |n| {
|
||||||
|
if (path_len >= path_buffer.len) break;
|
||||||
|
path_buffer[path_len] = n.asEventTarget();
|
||||||
|
path_len += 1;
|
||||||
|
|
||||||
|
// Check if this node is a shadow root
|
||||||
|
if (n.is(ShadowRoot)) |shadow| {
|
||||||
|
event._needs_retargeting = true;
|
||||||
|
|
||||||
|
// If event is not composed, stop at shadow boundary
|
||||||
|
if (!event._composed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, jump to the shadow host and continue
|
||||||
|
node = shadow._host.asNode();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
node = n._parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Even though the window isn't part of the DOM, most events propagate
|
||||||
|
// through it in the capture phase (unless we stopped at a shadow boundary)
|
||||||
|
// The only explicit exception is "load"
|
||||||
|
if (event._type_string.eql(comptime .wrap("load")) == false) {
|
||||||
|
if (path_len < path_buffer.len) {
|
||||||
|
path_buffer[path_len] = page.window.asEventTarget();
|
||||||
|
path_len += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = path_buffer[0..path_len];
|
||||||
|
|
||||||
|
// Phase 1: Capturing phase (root → target, excluding target)
|
||||||
|
// This happens for all events, regardless of bubbling
|
||||||
|
event._event_phase = .capturing_phase;
|
||||||
|
var i: usize = path_len;
|
||||||
|
while (i > 1) {
|
||||||
|
i -= 1;
|
||||||
|
if (event._stop_propagation) return;
|
||||||
|
const current_target = path[i];
|
||||||
|
if (self.lookup.get(.{
|
||||||
|
.event_target = @intFromPtr(current_target),
|
||||||
|
.type_string = event._type_string,
|
||||||
|
})) |list| {
|
||||||
|
try self.dispatchPhase(list, current_target, event, &was_handled, comptime .init(true, opts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: At target
|
||||||
|
if (event._stop_propagation) return;
|
||||||
|
event._event_phase = .at_target;
|
||||||
|
const target_et = target.asEventTarget();
|
||||||
|
|
||||||
|
blk: {
|
||||||
|
// Get inline handler (e.g., onclick property) for this target
|
||||||
|
if (self.getInlineHandler(target_et, event)) |inline_handler| {
|
||||||
|
was_handled = true;
|
||||||
|
event._current_target = target_et;
|
||||||
|
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
self.page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
|
try ls.toLocal(inline_handler).callWithThis(void, target_et, .{event});
|
||||||
|
|
||||||
|
if (event._stop_propagation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event._stop_immediate_propagation) {
|
||||||
|
break :blk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.lookup.get(.{
|
||||||
|
.type_string = event._type_string,
|
||||||
|
.event_target = @intFromPtr(target_et),
|
||||||
|
})) |list| {
|
||||||
|
try self.dispatchPhase(list, target_et, event, &was_handled, comptime .init(null, opts));
|
||||||
|
if (event._stop_propagation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: Bubbling phase (target → root, excluding target)
|
||||||
|
// This only happens if the event bubbles
|
||||||
|
if (event._bubbles) {
|
||||||
|
event._event_phase = .bubbling_phase;
|
||||||
|
for (path[1..]) |current_target| {
|
||||||
|
if (event._stop_propagation) break;
|
||||||
|
if (self.lookup.get(.{
|
||||||
|
.type_string = event._type_string,
|
||||||
|
.event_target = @intFromPtr(current_target),
|
||||||
|
})) |list| {
|
||||||
|
try self.dispatchPhase(list, current_target, event, &was_handled, comptime .init(false, opts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DispatchPhaseOpts = struct {
|
||||||
|
capture_only: ?bool = null,
|
||||||
|
apply_ignore: bool = false,
|
||||||
|
|
||||||
|
fn init(capture_only: ?bool, opts: DispatchOpts) DispatchPhaseOpts {
|
||||||
|
return .{
|
||||||
|
.capture_only = capture_only,
|
||||||
|
.apply_ignore = opts.apply_ignore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime opts: DispatchPhaseOpts) !void {
|
||||||
|
const page = self.page;
|
||||||
|
|
||||||
|
// Track dispatch depth for deferred removal
|
||||||
|
self.dispatch_depth += 1;
|
||||||
|
defer {
|
||||||
|
const dispatch_depth = self.dispatch_depth;
|
||||||
|
// Only destroy deferred listeners when we exit the outermost dispatch
|
||||||
|
if (dispatch_depth == 1) {
|
||||||
|
for (self.deferred_removals.items) |removal| {
|
||||||
|
removal.list.remove(&removal.listener.node);
|
||||||
|
self.listener_pool.destroy(removal.listener);
|
||||||
|
}
|
||||||
|
self.deferred_removals.clearRetainingCapacity();
|
||||||
|
} else {
|
||||||
|
self.dispatch_depth = dispatch_depth - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the last listener in the list as sentinel - listeners added during dispatch will be after it
|
||||||
|
const last_node = list.last orelse return;
|
||||||
|
const last_listener: *Listener = @alignCast(@fieldParentPtr("node", last_node));
|
||||||
|
|
||||||
|
// Iterate through the list, stopping after we've encountered the last_listener
|
||||||
|
var node = list.first;
|
||||||
|
var is_done = false;
|
||||||
|
node_loop: while (node) |n| {
|
||||||
|
if (is_done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||||
|
is_done = (listener == last_listener);
|
||||||
|
node = n.next;
|
||||||
|
|
||||||
// Skip non-matching listeners
|
// Skip non-matching listeners
|
||||||
if (comptime capture_only) |capture| {
|
if (comptime opts.capture_only) |capture| {
|
||||||
if (listener.capture != capture) {
|
if (listener.capture != capture) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -412,6 +585,14 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (comptime opts.apply_ignore) {
|
||||||
|
for (self.ignore_list.items) |ignored| {
|
||||||
|
if (ignored == listener) {
|
||||||
|
continue :node_loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove "once" listeners BEFORE calling them so nested dispatches don't see them
|
// Remove "once" listeners BEFORE calling them so nested dispatches don't see them
|
||||||
if (listener.once) {
|
if (listener.once) {
|
||||||
self.removeListener(list, listener);
|
self.removeListener(list, listener);
|
||||||
@@ -455,9 +636,20 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-Node dispatching (XHR, Window without propagation)
|
fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?js.Function.Global {
|
||||||
fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool) !void {
|
const global_event_handlers = @import("webapi/global_event_handlers.zig");
|
||||||
return self.dispatchPhase(list, current_target, event, was_handled, null);
|
const handler_type = global_event_handlers.fromEventType(event._type_string.str()) orelse return null;
|
||||||
|
|
||||||
|
// Look up the inline handler for this target
|
||||||
|
const html_element = switch (target._type) {
|
||||||
|
.node => |n| n.is(Element.Html) orelse return null,
|
||||||
|
else => return null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return html_element.getAttributeFunction(handler_type, self.page) catch |err| {
|
||||||
|
log.warn(.event, "inline html callback", .{ .type = handler_type, .err = err });
|
||||||
|
return null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void {
|
fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void {
|
||||||
@@ -575,3 +767,144 @@ fn isAncestorOrSelf(ancestor: *Node, node: *Node) bool {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handles the default action for clicking on input checked/radio. Maybe this
|
||||||
|
// could be generalized if needed, but I'm not sure. This wasn't obvious to me
|
||||||
|
// but when an input is clicked, it's important to think about both the intent
|
||||||
|
// and the actual result. Imagine you have an unchecked checkbox. When clicked,
|
||||||
|
// the checkbox immediately becomes checked, and event handlers see this "checked"
|
||||||
|
// intent. But a listener can preventDefault() in which case the check we did at
|
||||||
|
// the start will be undone.
|
||||||
|
// This is a bit more complicated for radio buttons, as the checking/unchecking
|
||||||
|
// and the rollback can impact a different radio input. So if you "check" a radio
|
||||||
|
// the intent is that it becomes checked and whatever was checked before becomes
|
||||||
|
// unchecked, so that if you have to rollback (because of a preventDefault())
|
||||||
|
// then both inputs have to revert to their original values.
|
||||||
|
const ActivationState = struct {
|
||||||
|
old_checked: bool,
|
||||||
|
input: *Element.Html.Input,
|
||||||
|
previously_checked_radio: ?*Input,
|
||||||
|
|
||||||
|
const Input = Element.Html.Input;
|
||||||
|
|
||||||
|
fn create(event: *const Event, target: *Node, page: *Page) ?ActivationState {
|
||||||
|
if (event._type_string.eql(comptime .wrap("click")) == false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = target.is(Element.Html.Input) orelse return null;
|
||||||
|
if (input._input_type != .checkbox and input._input_type != .radio) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const old_checked = input._checked;
|
||||||
|
var previously_checked_radio: ?*Element.Html.Input = null;
|
||||||
|
|
||||||
|
// For radio buttons, find the currently checked radio in the group
|
||||||
|
if (input._input_type == .radio and !old_checked) {
|
||||||
|
previously_checked_radio = try findCheckedRadioInGroup(input, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle checkbox or check radio (which unchecks others in group)
|
||||||
|
const new_checked = if (input._input_type == .checkbox) !old_checked else true;
|
||||||
|
try input.setChecked(new_checked, page);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.input = input,
|
||||||
|
.old_checked = old_checked,
|
||||||
|
.previously_checked_radio = previously_checked_radio,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore(self: *const ActivationState, event: *const Event, page: *Page) void {
|
||||||
|
const input = self.input;
|
||||||
|
if (event._prevent_default) {
|
||||||
|
// Rollback: restore previous state
|
||||||
|
input._checked = self.old_checked;
|
||||||
|
input._checked_dirty = true;
|
||||||
|
if (self.previously_checked_radio) |prev_radio| {
|
||||||
|
prev_radio._checked = true;
|
||||||
|
prev_radio._checked_dirty = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit: fire input and change events only if state actually changed
|
||||||
|
// and the element is connected to a document (detached elements don't fire).
|
||||||
|
// For checkboxes, state always changes. For radios, only if was unchecked.
|
||||||
|
const state_changed = (input._input_type == .checkbox) or !self.old_checked;
|
||||||
|
if (state_changed and input.asElement().asNode().isConnected()) {
|
||||||
|
fireEvent(page, input, "input") catch |err| {
|
||||||
|
log.warn(.event, "input event", .{ .err = err });
|
||||||
|
};
|
||||||
|
fireEvent(page, input, "change") catch |err| {
|
||||||
|
log.warn(.event, "change event", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findCheckedRadioInGroup(input: *Input, page: *Page) !?*Input {
|
||||||
|
const elem = input.asElement();
|
||||||
|
|
||||||
|
const name = elem.getAttributeSafe(comptime .wrap("name")) orelse return null;
|
||||||
|
if (name.len == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = input.getForm(page);
|
||||||
|
|
||||||
|
// Walk from the root of the tree containing this element
|
||||||
|
// This handles both document-attached and orphaned elements
|
||||||
|
const root = elem.asNode().getRootNode(null);
|
||||||
|
|
||||||
|
const TreeWalker = @import("webapi/TreeWalker.zig");
|
||||||
|
var walker = TreeWalker.Full.init(root, .{});
|
||||||
|
|
||||||
|
while (walker.next()) |node| {
|
||||||
|
const other_element = node.is(Element) orelse continue;
|
||||||
|
const other_input = other_element.is(Input) orelse continue;
|
||||||
|
|
||||||
|
if (other_input._input_type != .radio) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip the input we're checking from
|
||||||
|
if (other_input == input) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const other_name = other_element.getAttributeSafe(comptime .wrap("name")) orelse continue;
|
||||||
|
if (!std.mem.eql(u8, name, other_name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if same form context
|
||||||
|
const other_form = other_input.getForm(page);
|
||||||
|
if (form) |f| {
|
||||||
|
const of = other_form orelse continue;
|
||||||
|
if (f != of) {
|
||||||
|
continue; // Different forms
|
||||||
|
}
|
||||||
|
} else if (other_form != null) {
|
||||||
|
continue; // form is null but other has a form
|
||||||
|
}
|
||||||
|
|
||||||
|
if (other_input._checked) {
|
||||||
|
return other_input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire input or change event
|
||||||
|
fn fireEvent(page: *Page, input: *Input, comptime typ: []const u8) !void {
|
||||||
|
const event = try Event.initTrusted(comptime .wrap(typ), .{
|
||||||
|
.bubbles = true,
|
||||||
|
.cancelable = false,
|
||||||
|
}, page);
|
||||||
|
|
||||||
|
const target = input.asElement().asEventTarget();
|
||||||
|
try page._event_manager.dispatch(target, event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -42,10 +42,96 @@ const Allocator = std.mem.Allocator;
|
|||||||
const IS_DEBUG = builtin.mode == .Debug;
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
// Shared across all frames of a Page.
|
||||||
const Factory = @This();
|
const Factory = @This();
|
||||||
_page: *Page,
|
|
||||||
|
_arena: Allocator,
|
||||||
_slab: SlabAllocator,
|
_slab: SlabAllocator,
|
||||||
|
|
||||||
|
pub fn init(arena: Allocator) !*Factory {
|
||||||
|
const self = try arena.create(Factory);
|
||||||
|
self.* = .{
|
||||||
|
._arena = arena,
|
||||||
|
._slab = SlabAllocator.init(arena, 128),
|
||||||
|
};
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a root object
|
||||||
|
pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
|
return self.eventTargetWithAllocator(self._slab.allocator(), child);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eventTargetWithAllocator(_: *const Factory, allocator: Allocator, child: anytype) !*@TypeOf(child) {
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ EventTarget, @TypeOf(child) },
|
||||||
|
).allocate(allocator);
|
||||||
|
|
||||||
|
const event_ptr = chain.get(0);
|
||||||
|
event_ptr.* = .{
|
||||||
|
._type = unionInit(EventTarget.Type, chain.get(1)),
|
||||||
|
};
|
||||||
|
chain.setLeaf(1, child);
|
||||||
|
|
||||||
|
return chain.get(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn standaloneEventTarget(self: *Factory, child: anytype) !*EventTarget {
|
||||||
|
const allocator = self._slab.allocator();
|
||||||
|
const et = try allocator.create(EventTarget);
|
||||||
|
et.* = .{ ._type = unionInit(EventTarget.Type, child) };
|
||||||
|
return et;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a root object
|
||||||
|
pub fn event(_: *const Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ Event, @TypeOf(child) },
|
||||||
|
).allocate(arena);
|
||||||
|
|
||||||
|
// Special case: Event has a _type_string field, so we need manual setup
|
||||||
|
const event_ptr = chain.get(0);
|
||||||
|
event_ptr.* = try eventInit(arena, typ, chain.get(1));
|
||||||
|
chain.setLeaf(1, child);
|
||||||
|
|
||||||
|
return chain.get(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uiEvent(_: *const Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ Event, UIEvent, @TypeOf(child) },
|
||||||
|
).allocate(arena);
|
||||||
|
|
||||||
|
// Special case: Event has a _type_string field, so we need manual setup
|
||||||
|
const event_ptr = chain.get(0);
|
||||||
|
event_ptr.* = try eventInit(arena, typ, chain.get(1));
|
||||||
|
chain.setMiddle(1, UIEvent.Type);
|
||||||
|
chain.setLeaf(2, child);
|
||||||
|
|
||||||
|
return chain.get(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mouseEvent(_: *const Factory, arena: Allocator, typ: String, mouse: MouseEvent, child: anytype) !*@TypeOf(child) {
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ Event, UIEvent, MouseEvent, @TypeOf(child) },
|
||||||
|
).allocate(arena);
|
||||||
|
|
||||||
|
// Special case: Event has a _type_string field, so we need manual setup
|
||||||
|
const event_ptr = chain.get(0);
|
||||||
|
event_ptr.* = try eventInit(arena, typ, chain.get(1));
|
||||||
|
chain.setMiddle(1, UIEvent.Type);
|
||||||
|
|
||||||
|
// Set MouseEvent with all its fields
|
||||||
|
const mouse_ptr = chain.get(2);
|
||||||
|
mouse_ptr.* = mouse;
|
||||||
|
mouse_ptr._proto = chain.get(1);
|
||||||
|
mouse_ptr._type = unionInit(MouseEvent.Type, chain.get(3));
|
||||||
|
|
||||||
|
chain.setLeaf(3, child);
|
||||||
|
|
||||||
|
return chain.get(3);
|
||||||
|
}
|
||||||
|
|
||||||
fn PrototypeChain(comptime types: []const type) type {
|
fn PrototypeChain(comptime types: []const type) type {
|
||||||
return struct {
|
return struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
@@ -149,86 +235,14 @@ fn AutoPrototypeChain(comptime types: []const type) type {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(page: *Page) Factory {
|
fn eventInit(arena: Allocator, typ: String, value: anytype) !Event {
|
||||||
return .{
|
|
||||||
._page = page,
|
|
||||||
._slab = SlabAllocator.init(page.arena, 128),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is a root object
|
|
||||||
pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
|
||||||
const allocator = self._slab.allocator();
|
|
||||||
const chain = try PrototypeChain(
|
|
||||||
&.{ EventTarget, @TypeOf(child) },
|
|
||||||
).allocate(allocator);
|
|
||||||
|
|
||||||
const event_ptr = chain.get(0);
|
|
||||||
event_ptr.* = .{
|
|
||||||
._type = unionInit(EventTarget.Type, chain.get(1)),
|
|
||||||
};
|
|
||||||
chain.setLeaf(1, child);
|
|
||||||
|
|
||||||
return chain.get(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is a root object
|
|
||||||
pub fn event(self: *Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
|
||||||
const chain = try PrototypeChain(
|
|
||||||
&.{ Event, @TypeOf(child) },
|
|
||||||
).allocate(arena);
|
|
||||||
|
|
||||||
// Special case: Event has a _type_string field, so we need manual setup
|
|
||||||
const event_ptr = chain.get(0);
|
|
||||||
event_ptr.* = try self.eventInit(arena, typ, chain.get(1));
|
|
||||||
chain.setLeaf(1, child);
|
|
||||||
|
|
||||||
return chain.get(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn uiEvent(self: *Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
|
||||||
const chain = try PrototypeChain(
|
|
||||||
&.{ Event, UIEvent, @TypeOf(child) },
|
|
||||||
).allocate(arena);
|
|
||||||
|
|
||||||
// Special case: Event has a _type_string field, so we need manual setup
|
|
||||||
const event_ptr = chain.get(0);
|
|
||||||
event_ptr.* = try self.eventInit(arena, typ, chain.get(1));
|
|
||||||
chain.setMiddle(1, UIEvent.Type);
|
|
||||||
chain.setLeaf(2, child);
|
|
||||||
|
|
||||||
return chain.get(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mouseEvent(self: *Factory, arena: Allocator, typ: String, mouse: MouseEvent, child: anytype) !*@TypeOf(child) {
|
|
||||||
const chain = try PrototypeChain(
|
|
||||||
&.{ Event, UIEvent, MouseEvent, @TypeOf(child) },
|
|
||||||
).allocate(arena);
|
|
||||||
|
|
||||||
// Special case: Event has a _type_string field, so we need manual setup
|
|
||||||
const event_ptr = chain.get(0);
|
|
||||||
event_ptr.* = try self.eventInit(arena, typ, chain.get(1));
|
|
||||||
chain.setMiddle(1, UIEvent.Type);
|
|
||||||
|
|
||||||
// Set MouseEvent with all its fields
|
|
||||||
const mouse_ptr = chain.get(2);
|
|
||||||
mouse_ptr.* = mouse;
|
|
||||||
mouse_ptr._proto = chain.get(1);
|
|
||||||
mouse_ptr._type = unionInit(MouseEvent.Type, chain.get(3));
|
|
||||||
|
|
||||||
chain.setLeaf(3, child);
|
|
||||||
|
|
||||||
return chain.get(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn eventInit(self: *const Factory, arena: Allocator, typ: String, value: anytype) !Event {
|
|
||||||
// Round to 2ms for privacy (browsers do this)
|
// Round to 2ms for privacy (browsers do this)
|
||||||
const raw_timestamp = @import("../datetime.zig").milliTimestamp(.monotonic);
|
const raw_timestamp = @import("../datetime.zig").milliTimestamp(.monotonic);
|
||||||
const time_stamp = (raw_timestamp / 2) * 2;
|
const time_stamp = (raw_timestamp / 2) * 2;
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
|
._rc = 0,
|
||||||
._arena = arena,
|
._arena = arena,
|
||||||
._page = self._page,
|
|
||||||
._type = unionInit(Event.Type, value),
|
._type = unionInit(Event.Type, value),
|
||||||
._type_string = typ,
|
._type_string = typ,
|
||||||
._time_stamp = time_stamp,
|
._time_stamp = time_stamp,
|
||||||
@@ -329,7 +343,7 @@ pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeO
|
|||||||
chain.setMiddle(2, Element.Type);
|
chain.setMiddle(2, Element.Type);
|
||||||
|
|
||||||
// will never allocate, can't fail
|
// will never allocate, can't fail
|
||||||
const tag_name_str = String.init(self._page.arena, tag_name, .{}) catch unreachable;
|
const tag_name_str = String.init(self._arena, tag_name, .{}) catch unreachable;
|
||||||
|
|
||||||
// Manually set Element.Svg with the tag_name
|
// Manually set Element.Svg with the tag_name
|
||||||
chain.set(3, .{
|
chain.set(3, .{
|
||||||
@@ -374,7 +388,7 @@ pub fn destroy(self: *Factory, value: anytype) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (comptime @hasField(S, "_proto")) {
|
if (comptime @hasField(S, "_proto")) {
|
||||||
self.destroyChain(value, true, 0, std.mem.Alignment.@"1");
|
self.destroyChain(value, 0, std.mem.Alignment.@"1");
|
||||||
} else {
|
} else {
|
||||||
self.destroyStandalone(value);
|
self.destroyStandalone(value);
|
||||||
}
|
}
|
||||||
@@ -388,7 +402,6 @@ pub fn destroyStandalone(self: *Factory, value: anytype) void {
|
|||||||
fn destroyChain(
|
fn destroyChain(
|
||||||
self: *Factory,
|
self: *Factory,
|
||||||
value: anytype,
|
value: anytype,
|
||||||
comptime first: bool,
|
|
||||||
old_size: usize,
|
old_size: usize,
|
||||||
old_align: std.mem.Alignment,
|
old_align: std.mem.Alignment,
|
||||||
) void {
|
) void {
|
||||||
@@ -400,23 +413,8 @@ fn destroyChain(
|
|||||||
const new_size = current_size + @sizeOf(S);
|
const new_size = current_size + @sizeOf(S);
|
||||||
const new_align = std.mem.Alignment.max(old_align, std.mem.Alignment.of(S));
|
const new_align = std.mem.Alignment.max(old_align, std.mem.Alignment.of(S));
|
||||||
|
|
||||||
// This is initially called from a deinit. We don't want to call that
|
|
||||||
// same deinit. So when this is the first time destroyChain is called
|
|
||||||
// we don't call deinit (because we're in that deinit)
|
|
||||||
if (!comptime first) {
|
|
||||||
// But if it isn't the first time
|
|
||||||
if (@hasDecl(S, "deinit")) {
|
|
||||||
// And it has a deinit, we'll call it
|
|
||||||
switch (@typeInfo(@TypeOf(S.deinit)).@"fn".params.len) {
|
|
||||||
1 => value.deinit(),
|
|
||||||
2 => value.deinit(self._page),
|
|
||||||
else => @compileLog(@typeName(S) ++ " has an invalid deinit function"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (@hasField(S, "_proto")) {
|
if (@hasField(S, "_proto")) {
|
||||||
self.destroyChain(value._proto, false, new_size, new_align);
|
self.destroyChain(value._proto, new_size, new_align);
|
||||||
} else {
|
} else {
|
||||||
// no proto so this is the head of the chain.
|
// no proto so this is the head of the chain.
|
||||||
// we use this as the ptr to the start of the chain.
|
// we use this as the ptr to the start of the chain.
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ params: []const u8 = "",
|
|||||||
// IANA defines max. charset value length as 40.
|
// IANA defines max. charset value length as 40.
|
||||||
// We keep 41 for null-termination since HTML parser expects in this format.
|
// We keep 41 for null-termination since HTML parser expects in this format.
|
||||||
charset: [41]u8 = default_charset,
|
charset: [41]u8 = default_charset,
|
||||||
charset_len: usize = 5,
|
charset_len: usize = default_charset_len,
|
||||||
|
|
||||||
/// String "UTF-8" continued by null characters.
|
/// String "UTF-8" continued by null characters.
|
||||||
pub const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
|
const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
|
||||||
|
const default_charset_len = 5;
|
||||||
|
|
||||||
/// Mime with unknown Content-Type, empty params and empty charset.
|
/// Mime with unknown Content-Type, empty params and empty charset.
|
||||||
pub const unknown = Mime{ .content_type = .{ .unknown = {} } };
|
pub const unknown = Mime{ .content_type = .{ .unknown = {} } };
|
||||||
@@ -38,6 +39,10 @@ pub const ContentTypeEnum = enum {
|
|||||||
text_javascript,
|
text_javascript,
|
||||||
text_plain,
|
text_plain,
|
||||||
text_css,
|
text_css,
|
||||||
|
image_jpeg,
|
||||||
|
image_gif,
|
||||||
|
image_png,
|
||||||
|
image_webp,
|
||||||
application_json,
|
application_json,
|
||||||
unknown,
|
unknown,
|
||||||
other,
|
other,
|
||||||
@@ -49,6 +54,10 @@ pub const ContentType = union(ContentTypeEnum) {
|
|||||||
text_javascript: void,
|
text_javascript: void,
|
||||||
text_plain: void,
|
text_plain: void,
|
||||||
text_css: void,
|
text_css: void,
|
||||||
|
image_jpeg: void,
|
||||||
|
image_gif: void,
|
||||||
|
image_png: void,
|
||||||
|
image_webp: void,
|
||||||
application_json: void,
|
application_json: void,
|
||||||
unknown: void,
|
unknown: void,
|
||||||
other: struct { type: []const u8, sub_type: []const u8 },
|
other: struct { type: []const u8, sub_type: []const u8 },
|
||||||
@@ -61,6 +70,10 @@ pub fn contentTypeString(mime: *const Mime) []const u8 {
|
|||||||
.text_javascript => "application/javascript",
|
.text_javascript => "application/javascript",
|
||||||
.text_plain => "text/plain",
|
.text_plain => "text/plain",
|
||||||
.text_css => "text/css",
|
.text_css => "text/css",
|
||||||
|
.image_jpeg => "image/jpeg",
|
||||||
|
.image_png => "image/png",
|
||||||
|
.image_gif => "image/gif",
|
||||||
|
.image_webp => "image/webp",
|
||||||
.application_json => "application/json",
|
.application_json => "application/json",
|
||||||
else => "",
|
else => "",
|
||||||
};
|
};
|
||||||
@@ -115,17 +128,17 @@ pub fn parse(input: []u8) !Mime {
|
|||||||
|
|
||||||
const params = trimLeft(normalized[type_len..]);
|
const params = trimLeft(normalized[type_len..]);
|
||||||
|
|
||||||
var charset: [41]u8 = undefined;
|
var charset: [41]u8 = default_charset;
|
||||||
var charset_len: usize = undefined;
|
var charset_len: usize = default_charset_len;
|
||||||
|
|
||||||
var it = std.mem.splitScalar(u8, params, ';');
|
var it = std.mem.splitScalar(u8, params, ';');
|
||||||
while (it.next()) |attr| {
|
while (it.next()) |attr| {
|
||||||
const i = std.mem.indexOfScalarPos(u8, attr, 0, '=') orelse return error.Invalid;
|
const i = std.mem.indexOfScalarPos(u8, attr, 0, '=') orelse continue;
|
||||||
const name = trimLeft(attr[0..i]);
|
const name = trimLeft(attr[0..i]);
|
||||||
|
|
||||||
const value = trimRight(attr[i + 1 ..]);
|
const value = trimRight(attr[i + 1 ..]);
|
||||||
if (value.len == 0) {
|
if (value.len == 0) {
|
||||||
return error.Invalid;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attribute_name = std.meta.stringToEnum(enum {
|
const attribute_name = std.meta.stringToEnum(enum {
|
||||||
@@ -138,7 +151,7 @@ pub fn parse(input: []u8) !Mime {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attribute_value = try parseCharset(value);
|
const attribute_value = parseCharset(value) catch continue;
|
||||||
@memcpy(charset[0..attribute_value.len], attribute_value);
|
@memcpy(charset[0..attribute_value.len], attribute_value);
|
||||||
// Null-terminate right after attribute value.
|
// Null-terminate right after attribute value.
|
||||||
charset[attribute_value.len] = 0;
|
charset[attribute_value.len] = 0;
|
||||||
@@ -243,6 +256,11 @@ fn parseContentType(value: []const u8) !struct { ContentType, usize } {
|
|||||||
@"application/javascript",
|
@"application/javascript",
|
||||||
@"application/x-javascript",
|
@"application/x-javascript",
|
||||||
|
|
||||||
|
@"image/jpeg",
|
||||||
|
@"image/png",
|
||||||
|
@"image/gif",
|
||||||
|
@"image/webp",
|
||||||
|
|
||||||
@"application/json",
|
@"application/json",
|
||||||
}, type_name)) |known_type| {
|
}, type_name)) |known_type| {
|
||||||
const ct: ContentType = switch (known_type) {
|
const ct: ContentType = switch (known_type) {
|
||||||
@@ -251,6 +269,10 @@ fn parseContentType(value: []const u8) !struct { ContentType, usize } {
|
|||||||
.@"text/javascript", .@"application/javascript", .@"application/x-javascript" => .{ .text_javascript = {} },
|
.@"text/javascript", .@"application/javascript", .@"application/x-javascript" => .{ .text_javascript = {} },
|
||||||
.@"text/plain" => .{ .text_plain = {} },
|
.@"text/plain" => .{ .text_plain = {} },
|
||||||
.@"text/css" => .{ .text_css = {} },
|
.@"text/css" => .{ .text_css = {} },
|
||||||
|
.@"image/jpeg" => .{ .image_jpeg = {} },
|
||||||
|
.@"image/png" => .{ .image_png = {} },
|
||||||
|
.@"image/gif" => .{ .image_gif = {} },
|
||||||
|
.@"image/webp" => .{ .image_webp = {} },
|
||||||
.@"application/json" => .{ .application_json = {} },
|
.@"application/json" => .{ .application_json = {} },
|
||||||
};
|
};
|
||||||
return .{ ct, attribute_start };
|
return .{ ct, attribute_start };
|
||||||
@@ -313,6 +335,19 @@ test "Mime: invalid" {
|
|||||||
"text/ html",
|
"text/ html",
|
||||||
"text / html",
|
"text / html",
|
||||||
"text/html other",
|
"text/html other",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (invalids) |invalid| {
|
||||||
|
const mutable_input = try testing.arena_allocator.dupe(u8, invalid);
|
||||||
|
try testing.expectError(error.Invalid, Mime.parse(mutable_input));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Mime: malformed parameters are ignored" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
// These should all parse successfully as text/html with malformed params ignored
|
||||||
|
const valid_with_malformed_params = [_][]const u8{
|
||||||
"text/html; x",
|
"text/html; x",
|
||||||
"text/html; x=",
|
"text/html; x=",
|
||||||
"text/html; x= ",
|
"text/html; x= ",
|
||||||
@@ -321,11 +356,13 @@ test "Mime: invalid" {
|
|||||||
"text/html; charset=\"\"",
|
"text/html; charset=\"\"",
|
||||||
"text/html; charset=\"",
|
"text/html; charset=\"",
|
||||||
"text/html; charset=\"\\",
|
"text/html; charset=\"\\",
|
||||||
|
"text/html;\"",
|
||||||
};
|
};
|
||||||
|
|
||||||
for (invalids) |invalid| {
|
for (valid_with_malformed_params) |input| {
|
||||||
const mutable_input = try testing.arena_allocator.dupe(u8, invalid);
|
const mutable_input = try testing.arena_allocator.dupe(u8, input);
|
||||||
try testing.expectError(error.Invalid, Mime.parse(mutable_input));
|
const mime = try Mime.parse(mutable_input);
|
||||||
|
try testing.expectEqual(.text_html, std.meta.activeTag(mime.content_type));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +395,11 @@ test "Mime: parse common" {
|
|||||||
|
|
||||||
try expect(.{ .content_type = .{ .application_json = {} } }, "application/json");
|
try expect(.{ .content_type = .{ .application_json = {} } }, "application/json");
|
||||||
try expect(.{ .content_type = .{ .text_css = {} } }, "text/css");
|
try expect(.{ .content_type = .{ .text_css = {} } }, "text/css");
|
||||||
|
|
||||||
|
try expect(.{ .content_type = .{ .image_jpeg = {} } }, "image/jpeg");
|
||||||
|
try expect(.{ .content_type = .{ .image_png = {} } }, "image/png");
|
||||||
|
try expect(.{ .content_type = .{ .image_gif = {} } }, "image/gif");
|
||||||
|
try expect(.{ .content_type = .{ .image_webp = {} } }, "image/webp");
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Mime: parse uncommon" {
|
test "Mime: parse uncommon" {
|
||||||
@@ -409,6 +451,12 @@ test "Mime: parse charset" {
|
|||||||
.charset = "custom-non-standard-charset-value",
|
.charset = "custom-non-standard-charset-value",
|
||||||
.params = "charset=\"custom-non-standard-charset-value\"",
|
.params = "charset=\"custom-non-standard-charset-value\"",
|
||||||
}, "text/xml;charset=\"custom-non-standard-charset-value\"");
|
}, "text/xml;charset=\"custom-non-standard-charset-value\"");
|
||||||
|
|
||||||
|
try expect(.{
|
||||||
|
.content_type = .{ .text_html = {} },
|
||||||
|
.charset = "UTF-8",
|
||||||
|
.params = "x=\"",
|
||||||
|
}, "text/html;x=\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Mime: isHTML" {
|
test "Mime: isHTML" {
|
||||||
|
|||||||
1125
src/browser/Page.zig
1125
src/browser/Page.zig
File diff suppressed because it is too large
Load Diff
@@ -16,12 +16,54 @@
|
|||||||
// 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 builtin = @import("builtin");
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
|
|
||||||
|
pub const CompiledPattern = struct {
|
||||||
|
pattern: []const u8,
|
||||||
|
ty: enum {
|
||||||
|
prefix, // "/admin/" - prefix match
|
||||||
|
exact, // "/admin$" - exact match
|
||||||
|
wildcard, // any pattern that contains *
|
||||||
|
},
|
||||||
|
|
||||||
|
fn compile(pattern: []const u8) CompiledPattern {
|
||||||
|
if (pattern.len == 0) {
|
||||||
|
return .{
|
||||||
|
.pattern = pattern,
|
||||||
|
.ty = .prefix,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_wildcard = std.mem.indexOfScalar(u8, pattern, '*') != null;
|
||||||
|
|
||||||
|
if (is_wildcard) {
|
||||||
|
return .{
|
||||||
|
.pattern = pattern,
|
||||||
|
.ty = .wildcard,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const has_end_anchor = pattern[pattern.len - 1] == '$';
|
||||||
|
return .{
|
||||||
|
.pattern = pattern,
|
||||||
|
.ty = if (has_end_anchor) .exact else .prefix,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub const Rule = union(enum) {
|
pub const Rule = union(enum) {
|
||||||
allow: []const u8,
|
allow: CompiledPattern,
|
||||||
disallow: []const u8,
|
disallow: CompiledPattern,
|
||||||
|
|
||||||
|
fn allowRule(pattern: []const u8) Rule {
|
||||||
|
return .{ .allow = CompiledPattern.compile(pattern) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disallowRule(pattern: []const u8) Rule {
|
||||||
|
return .{ .disallow = CompiledPattern.compile(pattern) };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Key = enum {
|
pub const Key = enum {
|
||||||
@@ -44,11 +86,22 @@ pub const RobotStore = struct {
|
|||||||
const Context = @This();
|
const Context = @This();
|
||||||
|
|
||||||
pub fn hash(_: Context, value: []const u8) u32 {
|
pub fn hash(_: Context, value: []const u8) u32 {
|
||||||
var hasher = std.hash.Wyhash.init(value.len);
|
var key = value;
|
||||||
for (value) |c| {
|
var buf: [128]u8 = undefined;
|
||||||
std.hash.autoHash(&hasher, std.ascii.toLower(c));
|
var h = std.hash.Wyhash.init(value.len);
|
||||||
|
|
||||||
|
while (key.len >= 128) {
|
||||||
|
const lower = std.ascii.lowerString(buf[0..], key[0..128]);
|
||||||
|
h.update(lower);
|
||||||
|
key = key[128..];
|
||||||
}
|
}
|
||||||
return @truncate(hasher.final());
|
|
||||||
|
if (key.len > 0) {
|
||||||
|
const lower = std.ascii.lowerString(buf[0..key.len], key);
|
||||||
|
h.update(lower);
|
||||||
|
}
|
||||||
|
|
||||||
|
return @truncate(h.final());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn eql(_: Context, a: []const u8, b: []const u8) bool {
|
pub fn eql(_: Context, a: []const u8, b: []const u8) bool {
|
||||||
@@ -58,12 +111,16 @@ pub const RobotStore = struct {
|
|||||||
|
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
map: RobotsMap,
|
map: RobotsMap,
|
||||||
|
mutex: std.Thread.Mutex = .{},
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator) RobotStore {
|
pub fn init(allocator: std.mem.Allocator) RobotStore {
|
||||||
return .{ .allocator = allocator, .map = .empty };
|
return .{ .allocator = allocator, .map = .empty };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *RobotStore) void {
|
pub fn deinit(self: *RobotStore) void {
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
var iter = self.map.iterator();
|
var iter = self.map.iterator();
|
||||||
|
|
||||||
while (iter.next()) |entry| {
|
while (iter.next()) |entry| {
|
||||||
@@ -79,6 +136,9 @@ pub const RobotStore = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(self: *RobotStore, url: []const u8) ?RobotsEntry {
|
pub fn get(self: *RobotStore, url: []const u8) ?RobotsEntry {
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
return self.map.get(url);
|
return self.map.get(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,11 +147,17 @@ pub const RobotStore = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn put(self: *RobotStore, url: []const u8, robots: Robots) !void {
|
pub fn put(self: *RobotStore, url: []const u8, robots: Robots) !void {
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
const duped = try self.allocator.dupe(u8, url);
|
const duped = try self.allocator.dupe(u8, url);
|
||||||
try self.map.put(self.allocator, duped, .{ .present = robots });
|
try self.map.put(self.allocator, duped, .{ .present = robots });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn putAbsent(self: *RobotStore, url: []const u8) !void {
|
pub fn putAbsent(self: *RobotStore, url: []const u8) !void {
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
const duped = try self.allocator.dupe(u8, url);
|
const duped = try self.allocator.dupe(u8, url);
|
||||||
try self.map.put(self.allocator, duped, .absent);
|
try self.map.put(self.allocator, duped, .absent);
|
||||||
}
|
}
|
||||||
@@ -112,8 +178,8 @@ const State = struct {
|
|||||||
fn freeRulesInList(allocator: std.mem.Allocator, rules: []const Rule) void {
|
fn freeRulesInList(allocator: std.mem.Allocator, rules: []const Rule) void {
|
||||||
for (rules) |rule| {
|
for (rules) |rule| {
|
||||||
switch (rule) {
|
switch (rule) {
|
||||||
.allow => |value| allocator.free(value),
|
.allow => |compiled| allocator.free(compiled.pattern),
|
||||||
.disallow => |value| allocator.free(value),
|
.disallow => |compiled| allocator.free(compiled.pattern),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +188,7 @@ fn parseRulesWithUserAgent(
|
|||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
user_agent: []const u8,
|
user_agent: []const u8,
|
||||||
raw_bytes: []const u8,
|
raw_bytes: []const u8,
|
||||||
) ![]const Rule {
|
) ![]Rule {
|
||||||
var rules: std.ArrayList(Rule) = .empty;
|
var rules: std.ArrayList(Rule) = .empty;
|
||||||
defer rules.deinit(allocator);
|
defer rules.deinit(allocator);
|
||||||
|
|
||||||
@@ -201,13 +267,13 @@ fn parseRulesWithUserAgent(
|
|||||||
.in_our_entry => {
|
.in_our_entry => {
|
||||||
const duped_value = try allocator.dupe(u8, value);
|
const duped_value = try allocator.dupe(u8, value);
|
||||||
errdefer allocator.free(duped_value);
|
errdefer allocator.free(duped_value);
|
||||||
try rules.append(allocator, .{ .allow = duped_value });
|
try rules.append(allocator, Rule.allowRule(duped_value));
|
||||||
},
|
},
|
||||||
.in_other_entry => {},
|
.in_other_entry => {},
|
||||||
.in_wildcard_entry => {
|
.in_wildcard_entry => {
|
||||||
const duped_value = try allocator.dupe(u8, value);
|
const duped_value = try allocator.dupe(u8, value);
|
||||||
errdefer allocator.free(duped_value);
|
errdefer allocator.free(duped_value);
|
||||||
try wildcard_rules.append(allocator, .{ .allow = duped_value });
|
try wildcard_rules.append(allocator, Rule.allowRule(duped_value));
|
||||||
},
|
},
|
||||||
.not_in_entry => {
|
.not_in_entry => {
|
||||||
log.warn(.browser, "robots unexpected rule", .{ .rule = "allow" });
|
log.warn(.browser, "robots unexpected rule", .{ .rule = "allow" });
|
||||||
@@ -220,15 +286,19 @@ fn parseRulesWithUserAgent(
|
|||||||
|
|
||||||
switch (state.entry) {
|
switch (state.entry) {
|
||||||
.in_our_entry => {
|
.in_our_entry => {
|
||||||
|
if (value.len == 0) continue;
|
||||||
|
|
||||||
const duped_value = try allocator.dupe(u8, value);
|
const duped_value = try allocator.dupe(u8, value);
|
||||||
errdefer allocator.free(duped_value);
|
errdefer allocator.free(duped_value);
|
||||||
try rules.append(allocator, .{ .disallow = duped_value });
|
try rules.append(allocator, Rule.disallowRule(duped_value));
|
||||||
},
|
},
|
||||||
.in_other_entry => {},
|
.in_other_entry => {},
|
||||||
.in_wildcard_entry => {
|
.in_wildcard_entry => {
|
||||||
|
if (value.len == 0) continue;
|
||||||
|
|
||||||
const duped_value = try allocator.dupe(u8, value);
|
const duped_value = try allocator.dupe(u8, value);
|
||||||
errdefer allocator.free(duped_value);
|
errdefer allocator.free(duped_value);
|
||||||
try wildcard_rules.append(allocator, .{ .disallow = duped_value });
|
try wildcard_rules.append(allocator, Rule.disallowRule(duped_value));
|
||||||
},
|
},
|
||||||
.not_in_entry => {
|
.not_in_entry => {
|
||||||
log.warn(.browser, "robots unexpected rule", .{ .rule = "disallow" });
|
log.warn(.browser, "robots unexpected rule", .{ .rule = "disallow" });
|
||||||
@@ -252,6 +322,39 @@ fn parseRulesWithUserAgent(
|
|||||||
|
|
||||||
pub fn fromBytes(allocator: std.mem.Allocator, user_agent: []const u8, bytes: []const u8) !Robots {
|
pub fn fromBytes(allocator: std.mem.Allocator, user_agent: []const u8, bytes: []const u8) !Robots {
|
||||||
const rules = try parseRulesWithUserAgent(allocator, user_agent, bytes);
|
const rules = try parseRulesWithUserAgent(allocator, user_agent, bytes);
|
||||||
|
|
||||||
|
// sort by order once.
|
||||||
|
std.mem.sort(Rule, rules, {}, struct {
|
||||||
|
fn lessThan(_: void, a: Rule, b: Rule) bool {
|
||||||
|
const a_len = switch (a) {
|
||||||
|
.allow => |p| p.pattern.len,
|
||||||
|
.disallow => |p| p.pattern.len,
|
||||||
|
};
|
||||||
|
|
||||||
|
const b_len = switch (b) {
|
||||||
|
.allow => |p| p.pattern.len,
|
||||||
|
.disallow => |p| p.pattern.len,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sort by length first.
|
||||||
|
if (a_len != b_len) {
|
||||||
|
return a_len > b_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, allow should beat disallow.
|
||||||
|
const a_is_allow = switch (a) {
|
||||||
|
.allow => true,
|
||||||
|
.disallow => false,
|
||||||
|
};
|
||||||
|
const b_is_allow = switch (b) {
|
||||||
|
.allow => true,
|
||||||
|
.disallow => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return a_is_allow and !b_is_allow;
|
||||||
|
}
|
||||||
|
}.lessThan);
|
||||||
|
|
||||||
return .{ .rules = rules };
|
return .{ .rules = rules };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,86 +363,102 @@ pub fn deinit(self: *Robots, allocator: std.mem.Allocator) void {
|
|||||||
allocator.free(self.rules);
|
allocator.free(self.rules);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn matchPatternRecursive(pattern: []const u8, path: []const u8, exact_match: bool) bool {
|
|
||||||
if (pattern.len == 0) return true;
|
|
||||||
|
|
||||||
const star_pos = std.mem.indexOfScalar(u8, pattern, '*') orelse {
|
|
||||||
if (exact_match) {
|
|
||||||
// If we end in '$', we must be exactly equal.
|
|
||||||
return std.mem.eql(u8, path, pattern);
|
|
||||||
} else {
|
|
||||||
// Otherwise, we are just a prefix.
|
|
||||||
return std.mem.startsWith(u8, path, pattern);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ensure the prefix before the '*' matches.
|
|
||||||
if (!std.mem.startsWith(u8, path, pattern[0..star_pos])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const suffix_pattern = pattern[star_pos + 1 ..];
|
|
||||||
if (suffix_pattern.len == 0) return true;
|
|
||||||
|
|
||||||
var i: usize = star_pos;
|
|
||||||
while (i <= path.len) : (i += 1) {
|
|
||||||
if (matchPatternRecursive(suffix_pattern, path[i..], exact_match)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// There are rules for how the pattern in robots.txt should be matched.
|
/// There are rules for how the pattern in robots.txt should be matched.
|
||||||
///
|
///
|
||||||
/// * should match 0 or more of any character.
|
/// * should match 0 or more of any character.
|
||||||
/// $ should signify the end of a path, making it exact.
|
/// $ should signify the end of a path, making it exact.
|
||||||
/// otherwise, it is a prefix path.
|
/// otherwise, it is a prefix path.
|
||||||
fn matchPattern(pattern: []const u8, path: []const u8) ?usize {
|
fn matchPattern(compiled: CompiledPattern, path: []const u8) bool {
|
||||||
if (pattern.len == 0) return 0;
|
switch (compiled.ty) {
|
||||||
|
.prefix => return std.mem.startsWith(u8, path, compiled.pattern),
|
||||||
|
.exact => {
|
||||||
|
const pattern = compiled.pattern;
|
||||||
|
return std.mem.eql(u8, path, pattern[0 .. pattern.len - 1]);
|
||||||
|
},
|
||||||
|
.wildcard => {
|
||||||
|
const pattern = compiled.pattern;
|
||||||
const exact_match = pattern[pattern.len - 1] == '$';
|
const exact_match = pattern[pattern.len - 1] == '$';
|
||||||
const inner_pattern = if (exact_match) pattern[0 .. pattern.len - 1] else pattern;
|
const inner_pattern = if (exact_match) pattern[0 .. pattern.len - 1] else pattern;
|
||||||
|
return matchInnerPattern(inner_pattern, path, exact_match);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (matchPatternRecursive(
|
fn matchInnerPattern(pattern: []const u8, path: []const u8, exact_match: bool) bool {
|
||||||
inner_pattern,
|
var pattern_idx: usize = 0;
|
||||||
path,
|
var path_idx: usize = 0;
|
||||||
exact_match,
|
|
||||||
)) return pattern.len else return null;
|
var star_pattern_idx: ?usize = null;
|
||||||
|
var star_path_idx: ?usize = null;
|
||||||
|
|
||||||
|
while (pattern_idx < pattern.len or path_idx < path.len) {
|
||||||
|
// 1: If pattern is consumed and we are doing prefix match, we matched.
|
||||||
|
if (pattern_idx >= pattern.len and !exact_match) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2: Current character is a wildcard
|
||||||
|
if (pattern_idx < pattern.len and pattern[pattern_idx] == '*') {
|
||||||
|
star_pattern_idx = pattern_idx;
|
||||||
|
star_path_idx = path_idx;
|
||||||
|
pattern_idx += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3: Characters match, advance both heads.
|
||||||
|
if (pattern_idx < pattern.len and path_idx < path.len and pattern[pattern_idx] == path[path_idx]) {
|
||||||
|
pattern_idx += 1;
|
||||||
|
path_idx += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4: we have a previous wildcard, backtrack and try matching more.
|
||||||
|
if (star_pattern_idx) |star_p_idx| {
|
||||||
|
// if we have exhausted the path,
|
||||||
|
// we know we haven't matched.
|
||||||
|
if (star_path_idx.? > path.len) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern_idx = star_p_idx + 1;
|
||||||
|
path_idx = star_path_idx.?;
|
||||||
|
star_path_idx.? += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallthrough: No match and no backtracking.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle trailing widlcards that can match 0 characters.
|
||||||
|
while (pattern_idx < pattern.len and pattern[pattern_idx] == '*') {
|
||||||
|
pattern_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exact_match) {
|
||||||
|
// Both must be fully consumed.
|
||||||
|
return pattern_idx == pattern.len and path_idx == path.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For prefix match, pattern must be completed.
|
||||||
|
return pattern_idx == pattern.len;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isAllowed(self: *const Robots, path: []const u8) bool {
|
pub fn isAllowed(self: *const Robots, path: []const u8) bool {
|
||||||
const rules = self.rules;
|
for (self.rules) |rule| {
|
||||||
|
|
||||||
var longest_match_len: usize = 0;
|
|
||||||
var is_allowed_result = true;
|
|
||||||
|
|
||||||
for (rules) |rule| {
|
|
||||||
switch (rule) {
|
switch (rule) {
|
||||||
.allow => |pattern| {
|
.allow => |compiled| if (matchPattern(compiled, path)) return true,
|
||||||
if (matchPattern(pattern, path)) |len| {
|
.disallow => |compiled| if (matchPattern(compiled, path)) return false,
|
||||||
// Longest or Last Wins.
|
|
||||||
if (len >= longest_match_len) {
|
|
||||||
longest_match_len = len;
|
|
||||||
is_allowed_result = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.disallow => |pattern| {
|
|
||||||
if (pattern.len == 0) continue;
|
|
||||||
|
|
||||||
if (matchPattern(pattern, path)) |len| {
|
|
||||||
// Longest or Last Wins.
|
|
||||||
if (len >= longest_match_len) {
|
|
||||||
longest_match_len = len;
|
|
||||||
is_allowed_result = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return is_allowed_result;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn testMatch(pattern: []const u8, path: []const u8) bool {
|
||||||
|
comptime if (!builtin.is_test) unreachable;
|
||||||
|
|
||||||
|
return matchPattern(CompiledPattern.compile(pattern), path);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Robots: simple robots.txt" {
|
test "Robots: simple robots.txt" {
|
||||||
@@ -362,77 +481,77 @@ test "Robots: simple robots.txt" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try std.testing.expectEqual(1, rules.len);
|
try std.testing.expectEqual(1, rules.len);
|
||||||
try std.testing.expectEqualStrings("/admin/", rules[0].disallow);
|
try std.testing.expectEqualStrings("/admin/", rules[0].disallow.pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Robots: matchPattern - simple prefix" {
|
test "Robots: matchPattern - simple prefix" {
|
||||||
try std.testing.expect(matchPattern("/admin", "/admin/page") != null);
|
try std.testing.expect(testMatch("/admin", "/admin/page"));
|
||||||
try std.testing.expect(matchPattern("/admin", "/admin") != null);
|
try std.testing.expect(testMatch("/admin", "/admin"));
|
||||||
try std.testing.expect(matchPattern("/admin", "/other") == null);
|
try std.testing.expect(!testMatch("/admin", "/other"));
|
||||||
try std.testing.expect(matchPattern("/admin/page", "/admin") == null);
|
try std.testing.expect(!testMatch("/admin/page", "/admin"));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Robots: matchPattern - single wildcard" {
|
test "Robots: matchPattern - single wildcard" {
|
||||||
try std.testing.expect(matchPattern("/admin/*", "/admin/") != null);
|
try std.testing.expect(testMatch("/admin/*", "/admin/"));
|
||||||
try std.testing.expect(matchPattern("/admin/*", "/admin/page") != null);
|
try std.testing.expect(testMatch("/admin/*", "/admin/page"));
|
||||||
try std.testing.expect(matchPattern("/admin/*", "/admin/page/subpage") != null);
|
try std.testing.expect(testMatch("/admin/*", "/admin/page/subpage"));
|
||||||
try std.testing.expect(matchPattern("/admin/*", "/other/page") == null);
|
try std.testing.expect(!testMatch("/admin/*", "/other/page"));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Robots: matchPattern - wildcard in middle" {
|
test "Robots: matchPattern - wildcard in middle" {
|
||||||
try std.testing.expect(matchPattern("/abc/*/xyz", "/abc/def/xyz") != null);
|
try std.testing.expect(testMatch("/abc/*/xyz", "/abc/def/xyz"));
|
||||||
try std.testing.expect(matchPattern("/abc/*/xyz", "/abc/def/ghi/xyz") != null);
|
try std.testing.expect(testMatch("/abc/*/xyz", "/abc/def/ghi/xyz"));
|
||||||
try std.testing.expect(matchPattern("/abc/*/xyz", "/abc/def") == null);
|
try std.testing.expect(!testMatch("/abc/*/xyz", "/abc/def"));
|
||||||
try std.testing.expect(matchPattern("/abc/*/xyz", "/other/def/xyz") == null);
|
try std.testing.expect(!testMatch("/abc/*/xyz", "/other/def/xyz"));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Robots: matchPattern - complex wildcard case" {
|
test "Robots: matchPattern - complex wildcard case" {
|
||||||
try std.testing.expect(matchPattern("/abc/*/def/xyz", "/abc/def/def/xyz") != null);
|
try std.testing.expect(testMatch("/abc/*/def/xyz", "/abc/def/def/xyz"));
|
||||||
try std.testing.expect(matchPattern("/abc/*/def/xyz", "/abc/ANYTHING/def/xyz") != null);
|
try std.testing.expect(testMatch("/abc/*/def/xyz", "/abc/ANYTHING/def/xyz"));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Robots: matchPattern - multiple wildcards" {
|
test "Robots: matchPattern - multiple wildcards" {
|
||||||
try std.testing.expect(matchPattern("/a/*/b/*/c", "/a/x/b/y/c") != null);
|
try std.testing.expect(testMatch("/a/*/b/*/c", "/a/x/b/y/c"));
|
||||||
try std.testing.expect(matchPattern("/a/*/b/*/c", "/a/x/y/b/z/w/c") != null);
|
try std.testing.expect(testMatch("/a/*/b/*/c", "/a/x/y/b/z/w/c"));
|
||||||
try std.testing.expect(matchPattern("/*.php", "/index.php") != null);
|
try std.testing.expect(testMatch("/*.php", "/index.php"));
|
||||||
try std.testing.expect(matchPattern("/*.php", "/admin/index.php") != null);
|
try std.testing.expect(testMatch("/*.php", "/admin/index.php"));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Robots: matchPattern - end anchor" {
|
test "Robots: matchPattern - end anchor" {
|
||||||
try std.testing.expect(matchPattern("/*.php$", "/index.php") != null);
|
try std.testing.expect(testMatch("/*.php$", "/index.php"));
|
||||||
try std.testing.expect(matchPattern("/*.php$", "/index.php?param=value") == null);
|
try std.testing.expect(!testMatch("/*.php$", "/index.php?param=value"));
|
||||||
try std.testing.expect(matchPattern("/admin$", "/admin") != null);
|
try std.testing.expect(testMatch("/admin$", "/admin"));
|
||||||
try std.testing.expect(matchPattern("/admin$", "/admin/") == null);
|
try std.testing.expect(!testMatch("/admin$", "/admin/"));
|
||||||
try std.testing.expect(matchPattern("/fish$", "/fish") != null);
|
try std.testing.expect(testMatch("/fish$", "/fish"));
|
||||||
try std.testing.expect(matchPattern("/fish$", "/fishheads") == null);
|
try std.testing.expect(!testMatch("/fish$", "/fishheads"));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Robots: matchPattern - wildcard with extension" {
|
test "Robots: matchPattern - wildcard with extension" {
|
||||||
try std.testing.expect(matchPattern("/fish*.php", "/fish.php") != null);
|
try std.testing.expect(testMatch("/fish*.php", "/fish.php"));
|
||||||
try std.testing.expect(matchPattern("/fish*.php", "/fishheads.php") != null);
|
try std.testing.expect(testMatch("/fish*.php", "/fishheads.php"));
|
||||||
try std.testing.expect(matchPattern("/fish*.php", "/fish/salmon.php") != null);
|
try std.testing.expect(testMatch("/fish*.php", "/fish/salmon.php"));
|
||||||
try std.testing.expect(matchPattern("/fish*.php", "/fish.asp") == null);
|
try std.testing.expect(!testMatch("/fish*.php", "/fish.asp"));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Robots: matchPattern - empty and edge cases" {
|
test "Robots: matchPattern - empty and edge cases" {
|
||||||
try std.testing.expect(matchPattern("", "/anything") != null);
|
try std.testing.expect(testMatch("", "/anything"));
|
||||||
try std.testing.expect(matchPattern("/", "/") != null);
|
try std.testing.expect(testMatch("/", "/"));
|
||||||
try std.testing.expect(matchPattern("*", "/anything") != null);
|
try std.testing.expect(testMatch("*", "/anything"));
|
||||||
try std.testing.expect(matchPattern("/*", "/anything") != null);
|
try std.testing.expect(testMatch("/*", "/anything"));
|
||||||
try std.testing.expect(matchPattern("$", "") != null);
|
try std.testing.expect(testMatch("$", ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Robots: matchPattern - real world examples" {
|
test "Robots: matchPattern - real world examples" {
|
||||||
try std.testing.expect(matchPattern("/", "/anything") != null);
|
try std.testing.expect(testMatch("/", "/anything"));
|
||||||
|
|
||||||
try std.testing.expect(matchPattern("/admin/", "/admin/page") != null);
|
try std.testing.expect(testMatch("/admin/", "/admin/page"));
|
||||||
try std.testing.expect(matchPattern("/admin/", "/public/page") == null);
|
try std.testing.expect(!testMatch("/admin/", "/public/page"));
|
||||||
|
|
||||||
try std.testing.expect(matchPattern("/*.pdf$", "/document.pdf") != null);
|
try std.testing.expect(testMatch("/*.pdf$", "/document.pdf"));
|
||||||
try std.testing.expect(matchPattern("/*.pdf$", "/document.pdf.bak") == null);
|
try std.testing.expect(!testMatch("/*.pdf$", "/document.pdf.bak"));
|
||||||
|
|
||||||
try std.testing.expect(matchPattern("/*?", "/page?param=value") != null);
|
try std.testing.expect(testMatch("/*?", "/page?param=value"));
|
||||||
try std.testing.expect(matchPattern("/*?", "/page") == null);
|
try std.testing.expect(!testMatch("/*?", "/page"));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Robots: isAllowed - basic allow/disallow" {
|
test "Robots: isAllowed - basic allow/disallow" {
|
||||||
@@ -675,7 +794,7 @@ test "Robots: isAllowed - complex real-world example" {
|
|||||||
try std.testing.expect(robots.isAllowed("/cgi-bin/script.sh") == true);
|
try std.testing.expect(robots.isAllowed("/cgi-bin/script.sh") == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Robots: isAllowed - order doesn't matter for same length" {
|
test "Robots: isAllowed - order doesn't matter + allow wins" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
var robots = try Robots.fromBytes(allocator, "Bot",
|
var robots = try Robots.fromBytes(allocator, "Bot",
|
||||||
@@ -687,7 +806,7 @@ test "Robots: isAllowed - order doesn't matter for same length" {
|
|||||||
);
|
);
|
||||||
defer robots.deinit(allocator);
|
defer robots.deinit(allocator);
|
||||||
|
|
||||||
try std.testing.expect(robots.isAllowed("/page") == false);
|
try std.testing.expect(robots.isAllowed("/page") == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Robots: isAllowed - empty file uses wildcard defaults" {
|
test "Robots: isAllowed - empty file uses wildcard defaults" {
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ const std = @import("std");
|
|||||||
const lp = @import("lightpanda");
|
const lp = @import("lightpanda");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const js = @import("js/js.zig");
|
|
||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
|
const Http = @import("../http/Http.zig");
|
||||||
|
const String = @import("../string.zig").String;
|
||||||
|
|
||||||
|
const js = @import("js/js.zig");
|
||||||
const URL = @import("URL.zig");
|
const URL = @import("URL.zig");
|
||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
const Browser = @import("Browser.zig");
|
const Browser = @import("Browser.zig");
|
||||||
const Http = @import("../http/Http.zig");
|
|
||||||
|
|
||||||
const Element = @import("webapi/Element.zig");
|
const Element = @import("webapi/Element.zig");
|
||||||
|
|
||||||
@@ -83,10 +84,11 @@ imported_modules: std.StringHashMapUnmanaged(ImportedModule),
|
|||||||
// importmap contains resolved urls.
|
// importmap contains resolved urls.
|
||||||
importmap: std.StringHashMapUnmanaged([:0]const u8),
|
importmap: std.StringHashMapUnmanaged([:0]const u8),
|
||||||
|
|
||||||
pub fn init(page: *Page) ScriptManager {
|
// have we notified the page that all scripts are loaded (used to fire the "load"
|
||||||
// page isn't fully initialized, we can setup our reference, but that's it.
|
// event).
|
||||||
const browser = page._session.browser;
|
page_notified_of_completion: bool,
|
||||||
const allocator = browser.allocator;
|
|
||||||
|
pub fn init(allocator: Allocator, http_client: *Http.Client, page: *Page) ScriptManager {
|
||||||
return .{
|
return .{
|
||||||
.page = page,
|
.page = page,
|
||||||
.async_scripts = .{},
|
.async_scripts = .{},
|
||||||
@@ -96,9 +98,10 @@ pub fn init(page: *Page) ScriptManager {
|
|||||||
.is_evaluating = false,
|
.is_evaluating = false,
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.imported_modules = .empty,
|
.imported_modules = .empty,
|
||||||
.client = browser.http_client,
|
.client = http_client,
|
||||||
.static_scripts_done = false,
|
.static_scripts_done = false,
|
||||||
.buffer_pool = BufferPool.init(allocator, 5),
|
.buffer_pool = BufferPool.init(allocator, 5),
|
||||||
|
.page_notified_of_completion = false,
|
||||||
.script_pool = std.heap.MemoryPool(Script).init(allocator),
|
.script_pool = std.heap.MemoryPool(Script).init(allocator),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -262,6 +265,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
.url = url,
|
.url = url,
|
||||||
.ctx = script,
|
.ctx = script,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
|
.frame_id = page._frame_id,
|
||||||
.headers = try self.getHeaders(url),
|
.headers = try self.getHeaders(url),
|
||||||
.blocking = is_blocking,
|
.blocking = is_blocking,
|
||||||
.cookie_jar = &page._session.cookie_jar,
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
@@ -361,9 +365,11 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
.manager = self,
|
.manager = self,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const page = self.page;
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
var ls: js.Local.Scope = undefined;
|
var ls: js.Local.Scope = undefined;
|
||||||
self.page.js.localScope(&ls);
|
page.js.localScope(&ls);
|
||||||
defer ls.deinit();
|
defer ls.deinit();
|
||||||
|
|
||||||
log.debug(.http, "script queue", .{
|
log.debug(.http, "script queue", .{
|
||||||
@@ -378,10 +384,11 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
.url = url,
|
.url = url,
|
||||||
.ctx = script,
|
.ctx = script,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
|
.frame_id = page._frame_id,
|
||||||
.headers = try self.getHeaders(url),
|
.headers = try self.getHeaders(url),
|
||||||
.cookie_jar = &self.page._session.cookie_jar,
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
.notification = self.page._session.notification,
|
.notification = page._session.notification,
|
||||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||||
.header_callback = Script.headerCallback,
|
.header_callback = Script.headerCallback,
|
||||||
.data_callback = Script.dataCallback,
|
.data_callback = Script.dataCallback,
|
||||||
@@ -454,9 +461,10 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
|||||||
} },
|
} },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const page = self.page;
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
var ls: js.Local.Scope = undefined;
|
var ls: js.Local.Scope = undefined;
|
||||||
self.page.js.localScope(&ls);
|
page.js.localScope(&ls);
|
||||||
defer ls.deinit();
|
defer ls.deinit();
|
||||||
|
|
||||||
log.debug(.http, "script queue", .{
|
log.debug(.http, "script queue", .{
|
||||||
@@ -479,11 +487,12 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
|||||||
try self.client.request(.{
|
try self.client.request(.{
|
||||||
.url = url,
|
.url = url,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
|
.frame_id = page._frame_id,
|
||||||
.headers = try self.getHeaders(url),
|
.headers = try self.getHeaders(url),
|
||||||
.ctx = script,
|
.ctx = script,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
.cookie_jar = &self.page._session.cookie_jar,
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
.notification = self.page._session.notification,
|
.notification = page._session.notification,
|
||||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||||
.header_callback = Script.headerCallback,
|
.header_callback = Script.headerCallback,
|
||||||
.data_callback = Script.dataCallback,
|
.data_callback = Script.dataCallback,
|
||||||
@@ -567,19 +576,12 @@ fn evaluate(self: *ScriptManager) void {
|
|||||||
// Page makes this safe to call multiple times.
|
// Page makes this safe to call multiple times.
|
||||||
page.documentIsLoaded();
|
page.documentIsLoaded();
|
||||||
|
|
||||||
if (self.async_scripts.first == null) {
|
if (self.async_scripts.first == null and self.page_notified_of_completion == false) {
|
||||||
// Looks like all async scripts are done too!
|
self.page_notified_of_completion = true;
|
||||||
// Page makes this safe to call multiple times.
|
page.scriptsCompletedLoading();
|
||||||
page.documentIsComplete();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isDone(self: *const ScriptManager) bool {
|
|
||||||
return self.static_scripts_done and // page is done processing initial html
|
|
||||||
self.defer_scripts.first == null and // no deferred scripts
|
|
||||||
self.async_scripts.first == null; // no async scripts
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
|
fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
|
||||||
const content = script.source.content();
|
const content = script.source.content();
|
||||||
|
|
||||||
@@ -621,6 +623,18 @@ pub const Script = struct {
|
|||||||
script_element: ?*Element.Html.Script,
|
script_element: ?*Element.Html.Script,
|
||||||
manager: *ScriptManager,
|
manager: *ScriptManager,
|
||||||
|
|
||||||
|
// for debugging a rare production issue
|
||||||
|
header_callback_called: bool = false,
|
||||||
|
|
||||||
|
// for debugging a rare production issue
|
||||||
|
debug_transfer_id: u32 = 0,
|
||||||
|
debug_transfer_tries: u8 = 0,
|
||||||
|
debug_transfer_aborted: bool = false,
|
||||||
|
debug_transfer_bytes_received: usize = 0,
|
||||||
|
debug_transfer_notified_fail: bool = false,
|
||||||
|
debug_transfer_redirecting: bool = false,
|
||||||
|
debug_transfer_intercept_state: u8 = 0,
|
||||||
|
|
||||||
const Kind = enum {
|
const Kind = enum {
|
||||||
module,
|
module,
|
||||||
javascript,
|
javascript,
|
||||||
@@ -684,7 +698,38 @@ pub const Script = struct {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
lp.assert(self.source.remote.capacity == 0, "ScriptManager.HeaderCallback", .{ .capacity = self.source.remote.capacity });
|
{
|
||||||
|
// temp debug, trying to figure out why the next assert sometimes
|
||||||
|
// fails. Is the buffer just corrupt or is headerCallback really
|
||||||
|
// being called twice?
|
||||||
|
lp.assert(self.header_callback_called == false, "ScriptManager.Header recall", .{
|
||||||
|
.m = @tagName(std.meta.activeTag(self.mode)),
|
||||||
|
.a1 = self.debug_transfer_id,
|
||||||
|
.a2 = self.debug_transfer_tries,
|
||||||
|
.a3 = self.debug_transfer_aborted,
|
||||||
|
.a4 = self.debug_transfer_bytes_received,
|
||||||
|
.a5 = self.debug_transfer_notified_fail,
|
||||||
|
.a6 = self.debug_transfer_redirecting,
|
||||||
|
.a7 = self.debug_transfer_intercept_state,
|
||||||
|
.b1 = transfer.id,
|
||||||
|
.b2 = transfer._tries,
|
||||||
|
.b3 = transfer.aborted,
|
||||||
|
.b4 = transfer.bytes_received,
|
||||||
|
.b5 = transfer._notified_fail,
|
||||||
|
.b6 = transfer._redirecting,
|
||||||
|
.b7 = @intFromEnum(transfer._intercept_state),
|
||||||
|
});
|
||||||
|
self.header_callback_called = true;
|
||||||
|
self.debug_transfer_id = transfer.id;
|
||||||
|
self.debug_transfer_tries = transfer._tries;
|
||||||
|
self.debug_transfer_aborted = transfer.aborted;
|
||||||
|
self.debug_transfer_bytes_received = transfer.bytes_received;
|
||||||
|
self.debug_transfer_notified_fail = transfer._notified_fail;
|
||||||
|
self.debug_transfer_redirecting = transfer._redirecting;
|
||||||
|
self.debug_transfer_intercept_state = @intFromEnum(transfer._intercept_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity });
|
||||||
var buffer = self.manager.buffer_pool.get();
|
var buffer = self.manager.buffer_pool.get();
|
||||||
if (transfer.getContentLength()) |cl| {
|
if (transfer.getContentLength()) |cl| {
|
||||||
try buffer.ensureTotalCapacity(self.manager.allocator, cl);
|
try buffer.ensureTotalCapacity(self.manager.allocator, cl);
|
||||||
@@ -814,13 +859,15 @@ pub const Script = struct {
|
|||||||
.kind = self.kind,
|
.kind = self.kind,
|
||||||
.cacheable = cacheable,
|
.cacheable = cacheable,
|
||||||
});
|
});
|
||||||
self.executeCallback("error", local.toLocal(script_element._on_error), page);
|
self.executeCallback(comptime .wrap("error"), page);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.executeCallback("load", local.toLocal(script_element._on_load), page);
|
self.executeCallback(comptime .wrap("load"), page);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer page._event_manager.clearIgnoreList();
|
||||||
|
|
||||||
var try_catch: js.TryCatch = undefined;
|
var try_catch: js.TryCatch = undefined;
|
||||||
try_catch.init(local);
|
try_catch.init(local);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
@@ -839,19 +886,18 @@ pub const Script = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.browser, "executed script", .{ .src = url, .success = success, .on_load = script_element._on_load != null });
|
log.debug(.browser, "executed script", .{ .src = url, .success = success });
|
||||||
}
|
}
|
||||||
|
|
||||||
defer {
|
defer {
|
||||||
// We should run microtasks even if script execution fails.
|
local.runMacrotasks(); // also runs microtasks
|
||||||
local.runMicrotasks();
|
|
||||||
_ = page.js.scheduler.run() catch |err| {
|
_ = page.js.scheduler.run() catch |err| {
|
||||||
log.err(.page, "scheduler", .{ .err = err });
|
log.err(.page, "scheduler", .{ .err = err });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
self.executeCallback("load", local.toLocal(script_element._on_load), page);
|
self.executeCallback(comptime .wrap("load"), page);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -862,14 +908,12 @@ pub const Script = struct {
|
|||||||
.cacheable = cacheable,
|
.cacheable = cacheable,
|
||||||
});
|
});
|
||||||
|
|
||||||
self.executeCallback("error", local.toLocal(script_element._on_error), page);
|
self.executeCallback(comptime .wrap("error"), page);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function, page: *Page) void {
|
fn executeCallback(self: *const Script, typ: String, page: *Page) void {
|
||||||
const cb = cb_ orelse return;
|
|
||||||
|
|
||||||
const Event = @import("webapi/Event.zig");
|
const Event = @import("webapi/Event.zig");
|
||||||
const event = Event.initTrusted(comptime .wrap(typ), .{}, page) catch |err| {
|
const event = Event.initTrusted(typ, .{}, page) catch |err| {
|
||||||
log.warn(.js, "script internal callback", .{
|
log.warn(.js, "script internal callback", .{
|
||||||
.url = self.url,
|
.url = self.url,
|
||||||
.type = typ,
|
.type = typ,
|
||||||
@@ -877,14 +921,11 @@ pub const Script = struct {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
page._event_manager.dispatchOpts(self.script_element.?.asNode().asEventTarget(), event, .{ .apply_ignore = true }) catch |err| {
|
||||||
|
|
||||||
var caught: js.TryCatch.Caught = undefined;
|
|
||||||
cb.tryCall(void, .{event}, &caught) catch {
|
|
||||||
log.warn(.js, "script callback", .{
|
log.warn(.js, "script callback", .{
|
||||||
.url = self.url,
|
.url = self.url,
|
||||||
.type = typ,
|
.type = typ,
|
||||||
.caught = caught,
|
.err = err,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -897,7 +938,7 @@ const BufferPool = struct {
|
|||||||
max_concurrent_transfers: u8,
|
max_concurrent_transfers: u8,
|
||||||
mem_pool: std.heap.MemoryPool(Container),
|
mem_pool: std.heap.MemoryPool(Container),
|
||||||
|
|
||||||
const List = std.DoublyLinkedList;
|
const List = std.SinglyLinkedList;
|
||||||
|
|
||||||
const Container = struct {
|
const Container = struct {
|
||||||
node: List.Node,
|
node: List.Node,
|
||||||
@@ -956,7 +997,7 @@ const BufferPool = struct {
|
|||||||
b.clearRetainingCapacity();
|
b.clearRetainingCapacity();
|
||||||
container.* = .{ .buf = b, .node = .{} };
|
container.* = .{ .buf = b, .node = .{} };
|
||||||
self.count += 1;
|
self.count += 1;
|
||||||
self.available.append(&container.node);
|
self.available.prepend(&container.node);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1004,23 +1045,35 @@ fn parseDataURI(allocator: Allocator, src: []const u8) !?[]const u8 {
|
|||||||
|
|
||||||
const uri = src[5..];
|
const uri = src[5..];
|
||||||
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
|
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
|
||||||
|
const data = uri[data_starts + 1 ..];
|
||||||
|
|
||||||
var data = uri[data_starts + 1 ..];
|
const unescaped = try URL.unescape(allocator, data);
|
||||||
|
|
||||||
// Extract the encoding.
|
|
||||||
const metadata = uri[0..data_starts];
|
const metadata = uri[0..data_starts];
|
||||||
if (std.mem.endsWith(u8, metadata, ";base64")) {
|
if (std.mem.endsWith(u8, metadata, ";base64") == false) {
|
||||||
const decoder = std.base64.standard.Decoder;
|
return unescaped;
|
||||||
const decoded_size = try decoder.calcSizeForSlice(data);
|
|
||||||
|
|
||||||
const buffer = try allocator.alloc(u8, decoded_size);
|
|
||||||
errdefer allocator.free(buffer);
|
|
||||||
|
|
||||||
try decoder.decode(buffer, data);
|
|
||||||
data = buffer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
// Forgiving base64 decode per WHATWG spec:
|
||||||
|
// https://infra.spec.whatwg.org/#forgiving-base64-decode
|
||||||
|
// Step 1: Remove all ASCII whitespace
|
||||||
|
var stripped = try std.ArrayList(u8).initCapacity(allocator, unescaped.len);
|
||||||
|
for (unescaped) |c| {
|
||||||
|
if (!std.ascii.isWhitespace(c)) {
|
||||||
|
stripped.appendAssumeCapacity(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const trimmed = std.mem.trimRight(u8, stripped.items, "=");
|
||||||
|
|
||||||
|
// Length % 4 == 1 is invalid
|
||||||
|
if (trimmed.len % 4 == 1) {
|
||||||
|
return error.InvalidCharacterError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded_size = std.base64.standard_no_pad.Decoder.calcSizeForSlice(trimmed) catch return error.InvalidCharacterError;
|
||||||
|
const buffer = try allocator.alloc(u8, decoded_size);
|
||||||
|
std.base64.standard_no_pad.Decoder.decode(buffer, trimmed) catch return error.InvalidCharacterError;
|
||||||
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const testing = @import("../testing.zig");
|
const testing = @import("../testing.zig");
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const lp = @import("lightpanda");
|
const lp = @import("lightpanda");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ const Browser = @import("Browser.zig");
|
|||||||
const Notification = @import("../Notification.zig");
|
const Notification = @import("../Notification.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
// Session is like a browser's tab.
|
// Session is like a browser's tab.
|
||||||
// It owns the js env and the loader for all the pages of the session.
|
// It owns the js env and the loader for all the pages of the session.
|
||||||
@@ -45,16 +46,6 @@ notification: *Notification,
|
|||||||
// Used to create our Inspector and in the BrowserContext.
|
// Used to create our Inspector and in the BrowserContext.
|
||||||
arena: Allocator,
|
arena: Allocator,
|
||||||
|
|
||||||
// The page's arena is unsuitable for data that has to existing while
|
|
||||||
// navigating from one page to another. For example, if we're clicking
|
|
||||||
// on an HREF, the URL exists in the original page (where the click
|
|
||||||
// originated) but also has to exist in the new page.
|
|
||||||
// While we could use the Session's arena, this could accumulate a lot of
|
|
||||||
// memory if we do many navigation events. The `transfer_arena` is meant to
|
|
||||||
// bridge the gap: existing long enough to store any data needed to end one
|
|
||||||
// page and start another.
|
|
||||||
transfer_arena: Allocator,
|
|
||||||
|
|
||||||
cookie_jar: storage.Cookie.Jar,
|
cookie_jar: storage.Cookie.Jar,
|
||||||
storage_shed: storage.Shed,
|
storage_shed: storage.Shed,
|
||||||
|
|
||||||
@@ -63,20 +54,24 @@ navigation: Navigation,
|
|||||||
|
|
||||||
page: ?Page,
|
page: ?Page,
|
||||||
|
|
||||||
|
frame_id_gen: u32,
|
||||||
|
|
||||||
pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void {
|
pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void {
|
||||||
const allocator = browser.app.allocator;
|
const allocator = browser.app.allocator;
|
||||||
const session_allocator = browser.session_arena.allocator();
|
const arena = try browser.arena_pool.acquire();
|
||||||
|
errdefer browser.arena_pool.release(arena);
|
||||||
|
|
||||||
self.* = .{
|
self.* = .{
|
||||||
.page = null,
|
.page = null,
|
||||||
|
.arena = arena,
|
||||||
.history = .{},
|
.history = .{},
|
||||||
.navigation = .{},
|
.frame_id_gen = 0,
|
||||||
|
// The prototype (EventTarget) for Navigation is created when a Page is created.
|
||||||
|
.navigation = .{ ._proto = undefined },
|
||||||
.storage_shed = .{},
|
.storage_shed = .{},
|
||||||
.browser = browser,
|
.browser = browser,
|
||||||
.notification = notification,
|
.notification = notification,
|
||||||
.arena = session_allocator,
|
|
||||||
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
||||||
.transfer_arena = browser.transfer_arena.allocator(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,8 +79,11 @@ pub fn deinit(self: *Session) void {
|
|||||||
if (self.page != null) {
|
if (self.page != null) {
|
||||||
self.removePage();
|
self.removePage();
|
||||||
}
|
}
|
||||||
|
const browser = self.browser;
|
||||||
|
|
||||||
self.cookie_jar.deinit();
|
self.cookie_jar.deinit();
|
||||||
self.storage_shed.deinit(self.browser.app.allocator);
|
self.storage_shed.deinit(browser.app.allocator);
|
||||||
|
browser.arena_pool.release(self.arena);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: the caller is not the owner of the returned value,
|
// NOTE: the caller is not the owner of the returned value,
|
||||||
@@ -93,11 +91,9 @@ pub fn deinit(self: *Session) void {
|
|||||||
pub fn createPage(self: *Session) !*Page {
|
pub fn createPage(self: *Session) !*Page {
|
||||||
lp.assert(self.page == null, "Session.createPage - page not null", .{});
|
lp.assert(self.page == null, "Session.createPage - page not null", .{});
|
||||||
|
|
||||||
_ = self.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
|
||||||
|
|
||||||
self.page = @as(Page, undefined);
|
self.page = @as(Page, undefined);
|
||||||
const page = &self.page.?;
|
const page = &self.page.?;
|
||||||
try Page.init(page, self);
|
try Page.init(page, self.nextFrameId(), self, null);
|
||||||
|
|
||||||
// Creates a new NavigationEventTarget for this page.
|
// Creates a new NavigationEventTarget for this page.
|
||||||
try self.navigation.onNewPage(page);
|
try self.navigation.onNewPage(page);
|
||||||
@@ -127,6 +123,26 @@ pub fn removePage(self: *Session) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn replacePage(self: *Session) !*Page {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
log.debug(.browser, "replace page", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.assert(self.page != null, "Session.replacePage null page", .{});
|
||||||
|
|
||||||
|
var current = self.page.?;
|
||||||
|
const frame_id = current._frame_id;
|
||||||
|
const parent = current.parent;
|
||||||
|
current.deinit();
|
||||||
|
|
||||||
|
self.browser.env.memoryPressureNotification(.moderate);
|
||||||
|
|
||||||
|
self.page = @as(Page, undefined);
|
||||||
|
const page = &self.page.?;
|
||||||
|
try Page.init(page, frame_id, self, parent);
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn currentPage(self: *Session) ?*Page {
|
pub fn currentPage(self: *Session) ?*Page {
|
||||||
return &(self.page orelse return null);
|
return &(self.page orelse return null);
|
||||||
}
|
}
|
||||||
@@ -137,53 +153,232 @@ pub const WaitResult = enum {
|
|||||||
cdp_socket,
|
cdp_socket,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub fn findPage(self: *Session, frame_id: u32) ?*Page {
|
||||||
|
const page = self.currentPage() orelse return null;
|
||||||
|
return if (page._frame_id == frame_id) page else null;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
||||||
|
var page = &(self.page orelse return .no_page);
|
||||||
while (true) {
|
while (true) {
|
||||||
if (self.page) |*page| {
|
const wait_result = self._wait(page, wait_ms) catch |err| {
|
||||||
switch (page.wait(wait_ms)) {
|
switch (err) {
|
||||||
|
error.JsError => {}, // already logged (with hopefully more context)
|
||||||
|
else => log.err(.browser, "session wait", .{
|
||||||
|
.err = err,
|
||||||
|
.url = page.url,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
return .done;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (wait_result) {
|
||||||
.done => {
|
.done => {
|
||||||
if (page._queued_navigation == null) {
|
if (page._queued_navigation == null) {
|
||||||
return .done;
|
return .done;
|
||||||
}
|
}
|
||||||
self.processScheduledNavigation() catch return .done;
|
page = self.processScheduledNavigation(page) catch return .done;
|
||||||
},
|
},
|
||||||
else => |result| return result,
|
else => |result| return result,
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return .no_page;
|
|
||||||
}
|
|
||||||
// if we've successfull navigated, we'll give the new page another
|
|
||||||
// page.wait(wait_ms)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn processScheduledNavigation(self: *Session) !void {
|
fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
||||||
defer _ = self.browser.transfer_arena.reset(.{ .retain_with_limit = 8 * 1024 });
|
var timer = try std.time.Timer.start();
|
||||||
const url, const opts = blk: {
|
var ms_remaining = wait_ms;
|
||||||
const qn = self.page.?._queued_navigation.?;
|
|
||||||
// qn might not be safe to use after self.removePage is called, hence
|
|
||||||
// this block;
|
|
||||||
const url = qn.url;
|
|
||||||
const opts = qn.opts;
|
|
||||||
|
|
||||||
// This was already aborted on the page, but it would be pretty
|
const browser = self.browser;
|
||||||
// bad if old requests went to the new page, so let's make double sure
|
var http_client = browser.http_client;
|
||||||
self.browser.http_client.abort();
|
|
||||||
|
// I'd like the page to know NOTHING about cdp_socket / CDP, but the
|
||||||
|
// fact is that the behavior of wait changes depending on whether or
|
||||||
|
// not we're using CDP.
|
||||||
|
// If we aren't using CDP, as soon as we think there's nothing left
|
||||||
|
// to do, we can exit - we'de done.
|
||||||
|
// But if we are using CDP, we should wait for the whole `wait_ms`
|
||||||
|
// because the http_click.tick() also monitors the CDP socket. And while
|
||||||
|
// we could let CDP poll http (like it does for HTTP requests), the fact
|
||||||
|
// is that we know more about the timing of stuff (e.g. how long to
|
||||||
|
// poll/sleep) in the page.
|
||||||
|
const exit_when_done = http_client.cdp_client == null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
switch (page._parse_state) {
|
||||||
|
.pre, .raw, .text, .image => {
|
||||||
|
// The main page hasn't started/finished navigating.
|
||||||
|
// There's no JS to run, and no reason to run the scheduler.
|
||||||
|
if (http_client.active == 0 and exit_when_done) {
|
||||||
|
// haven't started navigating, I guess.
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
// Either we have active http connections, or we're in CDP
|
||||||
|
// mode with an extra socket. Either way, we're waiting
|
||||||
|
// for http traffic
|
||||||
|
if (try http_client.tick(@intCast(ms_remaining)) == .cdp_socket) {
|
||||||
|
// exit_when_done is explicitly set when there isn't
|
||||||
|
// an extra socket, so it should not be possibl to
|
||||||
|
// get an cdp_socket message when exit_when_done
|
||||||
|
// is true.
|
||||||
|
if (IS_DEBUG) {
|
||||||
|
std.debug.assert(exit_when_done == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// data on a socket we aren't handling, return to caller
|
||||||
|
return .cdp_socket;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.html, .complete => {
|
||||||
|
if (page._queued_navigation != null) {
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The HTML page was parsed. We now either have JS scripts to
|
||||||
|
// download, or scheduled tasks to execute, or both.
|
||||||
|
|
||||||
|
// scheduler.run could trigger new http transfers, so do not
|
||||||
|
// store http_client.active BEFORE this call and then use
|
||||||
|
// it AFTER.
|
||||||
|
const ms_to_next_task = try browser.runMacrotasks();
|
||||||
|
|
||||||
|
// Each call to this runs scheduled load events.
|
||||||
|
try page.dispatchLoad();
|
||||||
|
|
||||||
|
const http_active = http_client.active;
|
||||||
|
const total_network_activity = http_active + http_client.intercepted;
|
||||||
|
if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
|
||||||
|
page.notifyNetworkAlmostIdle();
|
||||||
|
}
|
||||||
|
if (page._notified_network_idle.check(total_network_activity == 0)) {
|
||||||
|
page.notifyNetworkIdle();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (http_active == 0 and exit_when_done) {
|
||||||
|
// we don't need to consider http_client.intercepted here
|
||||||
|
// because exit_when_done is true, and that can only be
|
||||||
|
// the case when interception isn't possible.
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(http_client.intercepted == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ms: u64 = ms_to_next_task orelse blk: {
|
||||||
|
if (wait_ms - ms_remaining < 100) {
|
||||||
|
if (comptime builtin.is_test) {
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
// Look, we want to exit ASAP, but we don't want
|
||||||
|
// to exit so fast that we've run none of the
|
||||||
|
// background jobs.
|
||||||
|
break :blk 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (browser.hasBackgroundTasks()) {
|
||||||
|
// _we_ have nothing to run, but v8 is working on
|
||||||
|
// background tasks. We'll wait for them.
|
||||||
|
browser.waitForBackgroundTasks();
|
||||||
|
break :blk 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No http transfers, no cdp extra socket, no
|
||||||
|
// scheduled tasks, we're done.
|
||||||
|
return .done;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ms > ms_remaining) {
|
||||||
|
// Same as above, except we have a scheduled task,
|
||||||
|
// it just happens to be too far into the future
|
||||||
|
// compared to how long we were told to wait.
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a task to run in the not-so-distant future.
|
||||||
|
// You might think we can just sleep until that task is
|
||||||
|
// ready, but we should continue to run lowPriority tasks
|
||||||
|
// in the meantime, and that could unblock things. So
|
||||||
|
// we'll just sleep for a bit, and then restart our wait
|
||||||
|
// loop to see if anything new can be processed.
|
||||||
|
std.Thread.sleep(std.time.ns_per_ms * @as(u64, @intCast(@min(ms, 20))));
|
||||||
|
} else {
|
||||||
|
// We're here because we either have active HTTP
|
||||||
|
// connections, or exit_when_done == false (aka, there's
|
||||||
|
// an cdp_socket registered with the http client).
|
||||||
|
// We should continue to run lowPriority tasks, so we
|
||||||
|
// minimize how long we'll poll for network I/O.
|
||||||
|
var ms_to_wait = @min(200, ms_to_next_task orelse 200);
|
||||||
|
if (ms_to_wait > 10 and browser.hasBackgroundTasks()) {
|
||||||
|
// if we have background tasks, we don't want to wait too
|
||||||
|
// long for a message from the client. We want to go back
|
||||||
|
// to the top of the loop and run macrotasks.
|
||||||
|
ms_to_wait = 10;
|
||||||
|
}
|
||||||
|
if (try http_client.tick(@min(ms_remaining, ms_to_wait)) == .cdp_socket) {
|
||||||
|
// data on a socket we aren't handling, return to caller
|
||||||
|
return .cdp_socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.err => |err| {
|
||||||
|
page._parse_state = .{ .raw_done = @errorName(err) };
|
||||||
|
return err;
|
||||||
|
},
|
||||||
|
.raw_done => {
|
||||||
|
if (exit_when_done) {
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
// we _could_ http_client.tick(ms_to_wait), but this has
|
||||||
|
// the same result, and I feel is more correct.
|
||||||
|
return .no_page;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const ms_elapsed = timer.lap() / 1_000_000;
|
||||||
|
if (ms_elapsed >= ms_remaining) {
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
ms_remaining -= @intCast(ms_elapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processScheduledNavigation(self: *Session, current_page: *Page) !*Page {
|
||||||
|
const browser = self.browser;
|
||||||
|
|
||||||
|
const qn = current_page._queued_navigation.?;
|
||||||
|
// take ownership of the page's queued navigation
|
||||||
|
current_page._queued_navigation = null;
|
||||||
|
defer browser.arena_pool.release(qn.arena);
|
||||||
|
|
||||||
|
const frame_id, const parent = blk: {
|
||||||
|
const page = &self.page.?;
|
||||||
|
const frame_id = page._frame_id;
|
||||||
|
const parent = page.parent;
|
||||||
|
|
||||||
|
browser.http_client.abort();
|
||||||
self.removePage();
|
self.removePage();
|
||||||
|
|
||||||
break :blk .{ url, opts };
|
break :blk .{ frame_id, parent };
|
||||||
};
|
};
|
||||||
|
|
||||||
const page = self.createPage() catch |err| {
|
self.page = @as(Page, undefined);
|
||||||
log.err(.browser, "queued navigation page error", .{
|
const page = &self.page.?;
|
||||||
.err = err,
|
try Page.init(page, frame_id, self, parent);
|
||||||
.url = url,
|
|
||||||
});
|
// Creates a new NavigationEventTarget for this page.
|
||||||
|
try self.navigation.onNewPage(page);
|
||||||
|
|
||||||
|
// start JS env
|
||||||
|
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
||||||
|
self.notification.dispatch(.page_created, page);
|
||||||
|
|
||||||
|
page.navigate(qn.url, qn.opts) catch |err| {
|
||||||
|
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
|
||||||
page.navigate(url, opts) catch |err| {
|
return page;
|
||||||
log.err(.browser, "queued navigation error", .{ .err = err, .url = url });
|
}
|
||||||
return err;
|
|
||||||
};
|
pub fn nextFrameId(self: *Session) u32 {
|
||||||
|
const id = self.frame_id_gen +% 1;
|
||||||
|
self.frame_id_gen = id;
|
||||||
|
return id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,44 +20,61 @@ const std = @import("std");
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const ResolveOpts = struct {
|
const ResolveOpts = struct {
|
||||||
|
encode: bool = false,
|
||||||
always_dupe: bool = false,
|
always_dupe: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// path is anytype, so that it can be used with both []const u8 and [:0]const u8
|
// path is anytype, so that it can be used with both []const u8 and [:0]const u8
|
||||||
pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime opts: ResolveOpts) ![:0]const u8 {
|
pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime opts: ResolveOpts) ![:0]const u8 {
|
||||||
const PT = @TypeOf(path);
|
const PT = @TypeOf(path);
|
||||||
if (base.len == 0 or isCompleteHTTPUrl(path)) {
|
if (base.len == 0 or isCompleteHTTPUrl(path)) {
|
||||||
if (comptime opts.always_dupe or !isNullTerminated(PT)) {
|
if (comptime opts.always_dupe or !isNullTerminated(PT)) {
|
||||||
return allocator.dupeZ(u8, path);
|
const duped = try allocator.dupeZ(u8, path);
|
||||||
|
return processResolved(allocator, duped, opts);
|
||||||
|
}
|
||||||
|
if (comptime opts.encode) {
|
||||||
|
return processResolved(allocator, path, opts);
|
||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.len == 0) {
|
if (path.len == 0) {
|
||||||
if (comptime opts.always_dupe) {
|
if (comptime opts.always_dupe) {
|
||||||
return allocator.dupeZ(u8, base);
|
const duped = try allocator.dupeZ(u8, base);
|
||||||
|
return processResolved(allocator, duped, opts);
|
||||||
|
}
|
||||||
|
if (comptime opts.encode) {
|
||||||
|
return processResolved(allocator, base, opts);
|
||||||
}
|
}
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path[0] == '?') {
|
if (path[0] == '?') {
|
||||||
const base_path_end = std.mem.indexOfAny(u8, base, "?#") orelse base.len;
|
const base_path_end = std.mem.indexOfAny(u8, base, "?#") orelse base.len;
|
||||||
return std.mem.joinZ(allocator, "", &.{ base[0..base_path_end], path });
|
const result = try std.mem.joinZ(allocator, "", &.{ base[0..base_path_end], path });
|
||||||
|
return processResolved(allocator, result, opts);
|
||||||
}
|
}
|
||||||
if (path[0] == '#') {
|
if (path[0] == '#') {
|
||||||
const base_fragment_start = std.mem.indexOfScalar(u8, base, '#') orelse base.len;
|
const base_fragment_start = std.mem.indexOfScalar(u8, base, '#') orelse base.len;
|
||||||
return std.mem.joinZ(allocator, "", &.{ base[0..base_fragment_start], path });
|
const result = try std.mem.joinZ(allocator, "", &.{ base[0..base_fragment_start], path });
|
||||||
|
return processResolved(allocator, result, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.startsWith(u8, path, "//")) {
|
if (std.mem.startsWith(u8, path, "//")) {
|
||||||
// network-path reference
|
// network-path reference
|
||||||
const index = std.mem.indexOfScalar(u8, base, ':') orelse {
|
const index = std.mem.indexOfScalar(u8, base, ':') orelse {
|
||||||
if (comptime isNullTerminated(PT)) {
|
if (comptime isNullTerminated(PT)) {
|
||||||
|
if (comptime opts.encode) {
|
||||||
|
return processResolved(allocator, path, opts);
|
||||||
|
}
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
return allocator.dupeZ(u8, path);
|
const duped = try allocator.dupeZ(u8, path);
|
||||||
|
return processResolved(allocator, duped, opts);
|
||||||
};
|
};
|
||||||
const protocol = base[0 .. index + 1];
|
const protocol = base[0 .. index + 1];
|
||||||
return std.mem.joinZ(allocator, "", &.{ protocol, path });
|
const result = try std.mem.joinZ(allocator, "", &.{ protocol, path });
|
||||||
|
return processResolved(allocator, result, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scheme_end = std.mem.indexOf(u8, base, "://");
|
const scheme_end = std.mem.indexOf(u8, base, "://");
|
||||||
@@ -65,7 +82,8 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
|
|||||||
const path_start = std.mem.indexOfScalarPos(u8, base, authority_start, '/') orelse base.len;
|
const path_start = std.mem.indexOfScalarPos(u8, base, authority_start, '/') orelse base.len;
|
||||||
|
|
||||||
if (path[0] == '/') {
|
if (path[0] == '/') {
|
||||||
return std.mem.joinZ(allocator, "", &.{ base[0..path_start], path });
|
const result = try std.mem.joinZ(allocator, "", &.{ base[0..path_start], path });
|
||||||
|
return processResolved(allocator, result, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalized_base: []const u8 = base[0..path_start];
|
var normalized_base: []const u8 = base[0..path_start];
|
||||||
@@ -127,7 +145,119 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
|
|||||||
|
|
||||||
// we always have an extra space
|
// we always have an extra space
|
||||||
out[out_i] = 0;
|
out[out_i] = 0;
|
||||||
return out[0..out_i :0];
|
return processResolved(allocator, out[0..out_i :0], opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processResolved(allocator: Allocator, url: [:0]const u8, comptime opts: ResolveOpts) ![:0]const u8 {
|
||||||
|
if (!comptime opts.encode) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
return ensureEncoded(allocator, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensureEncoded(allocator: Allocator, url: [:0]const u8) ![:0]const u8 {
|
||||||
|
const scheme_end = std.mem.indexOf(u8, url, "://");
|
||||||
|
const authority_start = if (scheme_end) |end| end + 3 else 0;
|
||||||
|
const path_start = std.mem.indexOfScalarPos(u8, url, authority_start, '/') orelse return url;
|
||||||
|
|
||||||
|
const query_start = std.mem.indexOfScalarPos(u8, url, path_start, '?');
|
||||||
|
const fragment_start = std.mem.indexOfScalarPos(u8, url, query_start orelse path_start, '#');
|
||||||
|
|
||||||
|
const path_end = query_start orelse fragment_start orelse url.len;
|
||||||
|
const query_end = if (query_start) |_| (fragment_start orelse url.len) else path_end;
|
||||||
|
|
||||||
|
const path_to_encode = url[path_start..path_end];
|
||||||
|
const encoded_path = try percentEncodeSegment(allocator, path_to_encode, true);
|
||||||
|
|
||||||
|
const encoded_query = if (query_start) |qs| blk: {
|
||||||
|
const query_to_encode = url[qs + 1 .. query_end];
|
||||||
|
const encoded = try percentEncodeSegment(allocator, query_to_encode, false);
|
||||||
|
break :blk encoded;
|
||||||
|
} else null;
|
||||||
|
|
||||||
|
const encoded_fragment = if (fragment_start) |fs| blk: {
|
||||||
|
const fragment_to_encode = url[fs + 1 ..];
|
||||||
|
const encoded = try percentEncodeSegment(allocator, fragment_to_encode, false);
|
||||||
|
break :blk encoded;
|
||||||
|
} else null;
|
||||||
|
|
||||||
|
if (encoded_path.ptr == path_to_encode.ptr and
|
||||||
|
(encoded_query == null or encoded_query.?.ptr == url[query_start.? + 1 .. query_end].ptr) and
|
||||||
|
(encoded_fragment == null or encoded_fragment.?.ptr == url[fragment_start.? + 1 ..].ptr))
|
||||||
|
{
|
||||||
|
// nothing has changed
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf = try std.ArrayList(u8).initCapacity(allocator, url.len + 20);
|
||||||
|
try buf.appendSlice(allocator, url[0..path_start]);
|
||||||
|
try buf.appendSlice(allocator, encoded_path);
|
||||||
|
if (encoded_query) |eq| {
|
||||||
|
try buf.append(allocator, '?');
|
||||||
|
try buf.appendSlice(allocator, eq);
|
||||||
|
}
|
||||||
|
if (encoded_fragment) |ef| {
|
||||||
|
try buf.append(allocator, '#');
|
||||||
|
try buf.appendSlice(allocator, ef);
|
||||||
|
}
|
||||||
|
try buf.append(allocator, 0);
|
||||||
|
return buf.items[0 .. buf.items.len - 1 :0];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime is_path: bool) ![]const u8 {
|
||||||
|
// Check if encoding is needed
|
||||||
|
var needs_encoding = false;
|
||||||
|
for (segment) |c| {
|
||||||
|
if (shouldPercentEncode(c, is_path)) {
|
||||||
|
needs_encoding = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!needs_encoding) {
|
||||||
|
return segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf = try std.ArrayList(u8).initCapacity(allocator, segment.len + 10);
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < segment.len) : (i += 1) {
|
||||||
|
const c = segment[i];
|
||||||
|
|
||||||
|
// Check if this is an already-encoded sequence (%XX)
|
||||||
|
if (c == '%' and i + 2 < segment.len) {
|
||||||
|
const end = i + 2;
|
||||||
|
const h1 = segment[i + 1];
|
||||||
|
const h2 = segment[end];
|
||||||
|
if (std.ascii.isHex(h1) and std.ascii.isHex(h2)) {
|
||||||
|
try buf.appendSlice(allocator, segment[i .. end + 1]);
|
||||||
|
i = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldPercentEncode(c, is_path)) {
|
||||||
|
try buf.writer(allocator).print("%{X:0>2}", .{c});
|
||||||
|
} else {
|
||||||
|
try buf.append(allocator, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shouldPercentEncode(c: u8, comptime is_path: bool) bool {
|
||||||
|
return switch (c) {
|
||||||
|
// Unreserved characters (RFC 3986)
|
||||||
|
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => false,
|
||||||
|
// sub-delims allowed in both path and query
|
||||||
|
'!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => false,
|
||||||
|
// Separators allowed in both path and query
|
||||||
|
'/', ':', '@' => false,
|
||||||
|
// Query-specific: '?' is allowed in queries but not in paths
|
||||||
|
'?' => comptime is_path,
|
||||||
|
// Everything else needs encoding (including space)
|
||||||
|
else => true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isNullTerminated(comptime value: type) bool {
|
fn isNullTerminated(comptime value: type) bool {
|
||||||
@@ -512,6 +642,33 @@ pub fn getRobotsUrl(arena: Allocator, url: [:0]const u8) ![:0]const u8 {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn unescape(arena: Allocator, input: []const u8) ![]const u8 {
|
||||||
|
if (std.mem.indexOfScalar(u8, input, '%') == null) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = try std.ArrayList(u8).initCapacity(arena, input.len);
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < input.len) {
|
||||||
|
if (input[i] == '%' and i + 2 < input.len) {
|
||||||
|
const hex = input[i + 1 .. i + 3];
|
||||||
|
const byte = std.fmt.parseInt(u8, hex, 16) catch {
|
||||||
|
result.appendAssumeCapacity(input[i]);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
result.appendAssumeCapacity(byte);
|
||||||
|
i += 3;
|
||||||
|
} else {
|
||||||
|
result.appendAssumeCapacity(input[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.items;
|
||||||
|
}
|
||||||
|
|
||||||
const testing = @import("../testing.zig");
|
const testing = @import("../testing.zig");
|
||||||
test "URL: isCompleteHTTPUrl" {
|
test "URL: isCompleteHTTPUrl" {
|
||||||
try testing.expectEqual(true, isCompleteHTTPUrl("http://example.com/about"));
|
try testing.expectEqual(true, isCompleteHTTPUrl("http://example.com/about"));
|
||||||
@@ -691,6 +848,293 @@ test "URL: resolve" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "URL: ensureEncoded" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
const Case = struct {
|
||||||
|
url: [:0]const u8,
|
||||||
|
expected: [:0]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cases = [_]Case{
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/over 9000!",
|
||||||
|
.expected = "https://example.com/over%209000!",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "http://example.com/hello world.html",
|
||||||
|
.expected = "http://example.com/hello%20world.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/file[1].html",
|
||||||
|
.expected = "https://example.com/file%5B1%5D.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/file{name}.html",
|
||||||
|
.expected = "https://example.com/file%7Bname%7D.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/page?query=hello world",
|
||||||
|
.expected = "https://example.com/page?query=hello%20world",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/page?a=1&b=value with spaces",
|
||||||
|
.expected = "https://example.com/page?a=1&b=value%20with%20spaces",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/page#section one",
|
||||||
|
.expected = "https://example.com/page#section%20one",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/my path?query=my value#my anchor",
|
||||||
|
.expected = "https://example.com/my%20path?query=my%20value#my%20anchor",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/already%20encoded",
|
||||||
|
.expected = "https://example.com/already%20encoded",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/file%5B1%5D.html",
|
||||||
|
.expected = "https://example.com/file%5B1%5D.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/caf%C3%A9",
|
||||||
|
.expected = "https://example.com/caf%C3%A9",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/page?query=already%20encoded",
|
||||||
|
.expected = "https://example.com/page?query=already%20encoded",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/page?a=1&b=value%20here",
|
||||||
|
.expected = "https://example.com/page?a=1&b=value%20here",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/page#section%20one",
|
||||||
|
.expected = "https://example.com/page#section%20one",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/part%20encoded and not",
|
||||||
|
.expected = "https://example.com/part%20encoded%20and%20not",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/page?a=encoded%20value&b=not encoded",
|
||||||
|
.expected = "https://example.com/page?a=encoded%20value&b=not%20encoded",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/my%20path?query=not encoded#encoded%20anchor",
|
||||||
|
.expected = "https://example.com/my%20path?query=not%20encoded#encoded%20anchor",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/fully%20encoded?query=also%20encoded#and%20this",
|
||||||
|
.expected = "https://example.com/fully%20encoded?query=also%20encoded#and%20this",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/path-with_under~tilde",
|
||||||
|
.expected = "https://example.com/path-with_under~tilde",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/sub-delims!$&'()*+,;=",
|
||||||
|
.expected = "https://example.com/sub-delims!$&'()*+,;=",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com",
|
||||||
|
.expected = "https://example.com",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com?query=value",
|
||||||
|
.expected = "https://example.com?query=value",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/clean/path",
|
||||||
|
.expected = "https://example.com/clean/path",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/path?clean=query#clean-fragment",
|
||||||
|
.expected = "https://example.com/path?clean=query#clean-fragment",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/100% complete",
|
||||||
|
.expected = "https://example.com/100%25%20complete",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/path?value=100% done",
|
||||||
|
.expected = "https://example.com/path?value=100%25%20done",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (cases) |case| {
|
||||||
|
const result = try ensureEncoded(testing.arena_allocator, case.url);
|
||||||
|
try testing.expectString(case.expected, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "URL: resolve with encoding" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
const Case = struct {
|
||||||
|
base: [:0]const u8,
|
||||||
|
path: [:0]const u8,
|
||||||
|
expected: [:0]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cases = [_]Case{
|
||||||
|
// Spaces should be encoded as %20, but ! is allowed
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/dir/",
|
||||||
|
.path = "over 9000!",
|
||||||
|
.expected = "https://example.com/dir/over%209000!",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "hello world.html",
|
||||||
|
.expected = "https://example.com/hello%20world.html",
|
||||||
|
},
|
||||||
|
// Multiple spaces
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "path with multiple spaces",
|
||||||
|
.expected = "https://example.com/path%20with%20%20multiple%20%20%20spaces",
|
||||||
|
},
|
||||||
|
// Special characters that need encoding
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file[1].html",
|
||||||
|
.expected = "https://example.com/file%5B1%5D.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file{name}.html",
|
||||||
|
.expected = "https://example.com/file%7Bname%7D.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file<test>.html",
|
||||||
|
.expected = "https://example.com/file%3Ctest%3E.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file\"quote\".html",
|
||||||
|
.expected = "https://example.com/file%22quote%22.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file|pipe.html",
|
||||||
|
.expected = "https://example.com/file%7Cpipe.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file\\backslash.html",
|
||||||
|
.expected = "https://example.com/file%5Cbackslash.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file^caret.html",
|
||||||
|
.expected = "https://example.com/file%5Ecaret.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file`backtick`.html",
|
||||||
|
.expected = "https://example.com/file%60backtick%60.html",
|
||||||
|
},
|
||||||
|
// Characters that should NOT be encoded
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "path-with_under~tilde.html",
|
||||||
|
.expected = "https://example.com/path-with_under~tilde.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "path/with/slashes",
|
||||||
|
.expected = "https://example.com/path/with/slashes",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "sub-delims!$&'()*+,;=.html",
|
||||||
|
.expected = "https://example.com/sub-delims!$&'()*+,;=.html",
|
||||||
|
},
|
||||||
|
// Already encoded characters should not be double-encoded
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "already%20encoded",
|
||||||
|
.expected = "https://example.com/already%20encoded",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file%5B1%5D.html",
|
||||||
|
.expected = "https://example.com/file%5B1%5D.html",
|
||||||
|
},
|
||||||
|
// Mix of encoded and unencoded
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "part%20encoded and not",
|
||||||
|
.expected = "https://example.com/part%20encoded%20and%20not",
|
||||||
|
},
|
||||||
|
// Query strings and fragments ARE encoded
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file name.html?query=value with spaces",
|
||||||
|
.expected = "https://example.com/file%20name.html?query=value%20with%20spaces",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file name.html#anchor with spaces",
|
||||||
|
.expected = "https://example.com/file%20name.html#anchor%20with%20spaces",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file.html?hello=world !",
|
||||||
|
.expected = "https://example.com/file.html?hello=world%20!",
|
||||||
|
},
|
||||||
|
// Query structural characters should NOT be encoded
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file.html?a=1&b=2",
|
||||||
|
.expected = "https://example.com/file.html?a=1&b=2",
|
||||||
|
},
|
||||||
|
// Relative paths with encoding
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/dir/page.html",
|
||||||
|
.path = "../other dir/file.html",
|
||||||
|
.expected = "https://example.com/other%20dir/file.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/dir/",
|
||||||
|
.path = "./sub dir/file.html",
|
||||||
|
.expected = "https://example.com/dir/sub%20dir/file.html",
|
||||||
|
},
|
||||||
|
// Absolute paths with encoding
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/some/path",
|
||||||
|
.path = "/absolute path/file.html",
|
||||||
|
.expected = "https://example.com/absolute%20path/file.html",
|
||||||
|
},
|
||||||
|
// Unicode/high bytes (though ideally these should be UTF-8 encoded first)
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "café",
|
||||||
|
.expected = "https://example.com/caf%C3%A9",
|
||||||
|
},
|
||||||
|
// Empty path
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "",
|
||||||
|
.expected = "https://example.com/",
|
||||||
|
},
|
||||||
|
// Complete URL as path (should not be encoded)
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "https://other.com/path with spaces",
|
||||||
|
.expected = "https://other.com/path%20with%20spaces",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (cases) |case| {
|
||||||
|
const result = try resolve(testing.arena_allocator, case.base, case.path, .{ .encode = true });
|
||||||
|
try testing.expectString(case.expected, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "URL: eqlDocument" {
|
test "URL: eqlDocument" {
|
||||||
defer testing.reset();
|
defer testing.reset();
|
||||||
{
|
{
|
||||||
@@ -816,3 +1260,68 @@ test "URL: getRobotsUrl" {
|
|||||||
try testing.expectString("https://example.com/robots.txt", url);
|
try testing.expectString("https://example.com/robots.txt", url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "URL: unescape" {
|
||||||
|
defer testing.reset();
|
||||||
|
const arena = testing.arena_allocator;
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "hello world");
|
||||||
|
try testing.expectEqual("hello world", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "hello%20world");
|
||||||
|
try testing.expectEqual("hello world", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "%48%65%6c%6c%6f");
|
||||||
|
try testing.expectEqual("Hello", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "%48%65%6C%6C%6F");
|
||||||
|
try testing.expectEqual("Hello", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "a%3Db");
|
||||||
|
try testing.expectEqual("a=b", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "a%3DB");
|
||||||
|
try testing.expectEqual("a=B", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "ZDIgPSAndHdvJzs%3D");
|
||||||
|
try testing.expectEqual("ZDIgPSAndHdvJzs=", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "%5a%44%4d%67%50%53%41%6e%64%47%68%79%5a%57%55%6e%4f%77%3D%3D");
|
||||||
|
try testing.expectEqual("ZDMgPSAndGhyZWUnOw==", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "hello%2world");
|
||||||
|
try testing.expectEqual("hello%2world", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "hello%ZZworld");
|
||||||
|
try testing.expectEqual("hello%ZZworld", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "hello%");
|
||||||
|
try testing.expectEqual("hello%", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "hello%2");
|
||||||
|
try testing.expectEqual("hello%2", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -583,7 +583,7 @@ fn consumeNumeric(self: *Tokenizer) Token {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.advance(2);
|
self.advance(2);
|
||||||
} else if (self.hasAtLeast(1) and std.ascii.isDigit(self.byteAt(2))) {
|
} else if (self.hasAtLeast(2) and std.ascii.isDigit(self.byteAt(2))) {
|
||||||
self.advance(1);
|
self.advance(1);
|
||||||
} else {
|
} else {
|
||||||
break :blk;
|
break :blk;
|
||||||
|
|||||||
@@ -20,16 +20,15 @@ const std = @import("std");
|
|||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
const Node = @import("webapi/Node.zig");
|
const Node = @import("webapi/Node.zig");
|
||||||
const Slot = @import("webapi/element/html/Slot.zig");
|
const Slot = @import("webapi/element/html/Slot.zig");
|
||||||
|
const IFrame = @import("webapi/element/html/IFrame.zig");
|
||||||
|
|
||||||
pub const RootOpts = struct {
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
with_base: bool = false,
|
|
||||||
strip: Opts.Strip = .{},
|
|
||||||
shadow: Opts.Shadow = .rendered,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Opts = struct {
|
pub const Opts = struct {
|
||||||
strip: Strip = .{},
|
with_base: bool = false,
|
||||||
shadow: Shadow = .rendered,
|
with_frames: bool = false,
|
||||||
|
strip: Opts.Strip = .{},
|
||||||
|
shadow: Opts.Shadow = .rendered,
|
||||||
|
|
||||||
pub const Strip = struct {
|
pub const Strip = struct {
|
||||||
js: bool = false,
|
js: bool = false,
|
||||||
@@ -49,7 +48,7 @@ pub const Opts = struct {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn root(doc: *Node.Document, opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void {
|
pub fn root(doc: *Node.Document, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
if (doc.is(Node.Document.HTMLDocument)) |html_doc| {
|
if (doc.is(Node.Document.HTMLDocument)) |html_doc| {
|
||||||
blk: {
|
blk: {
|
||||||
// Ideally we just render the doctype which is part of the document
|
// Ideally we just render the doctype which is part of the document
|
||||||
@@ -71,7 +70,7 @@ pub fn root(doc: *Node.Document, opts: RootOpts, writer: *std.Io.Writer, page: *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return deep(doc.asNode(), .{ .strip = opts.strip, .shadow = opts.shadow }, writer, page);
|
return deep(doc.asNode(), opts, writer, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {
|
pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {
|
||||||
@@ -83,19 +82,19 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
|
|||||||
.cdata => |cd| {
|
.cdata => |cd| {
|
||||||
if (node.is(Node.CData.Comment)) |_| {
|
if (node.is(Node.CData.Comment)) |_| {
|
||||||
try writer.writeAll("<!--");
|
try writer.writeAll("<!--");
|
||||||
try writer.writeAll(cd.getData());
|
try writer.writeAll(cd.getData().str());
|
||||||
try writer.writeAll("-->");
|
try writer.writeAll("-->");
|
||||||
} else if (node.is(Node.CData.ProcessingInstruction)) |pi| {
|
} else if (node.is(Node.CData.ProcessingInstruction)) |pi| {
|
||||||
try writer.writeAll("<?");
|
try writer.writeAll("<?");
|
||||||
try writer.writeAll(pi._target);
|
try writer.writeAll(pi._target);
|
||||||
try writer.writeAll(" ");
|
try writer.writeAll(" ");
|
||||||
try writer.writeAll(cd.getData());
|
try writer.writeAll(cd.getData().str());
|
||||||
try writer.writeAll("?>");
|
try writer.writeAll("?>");
|
||||||
} else {
|
} else {
|
||||||
if (shouldEscapeText(node._parent)) {
|
if (shouldEscapeText(node._parent)) {
|
||||||
try writeEscapedText(cd.getData(), writer);
|
try writeEscapedText(cd.getData().str(), writer);
|
||||||
} else {
|
} else {
|
||||||
try writer.writeAll(cd.getData());
|
try writer.writeAll(cd.getData().str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -140,7 +139,24 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.with_frames and el.is(IFrame) != null) {
|
||||||
|
const frame = el.as(IFrame);
|
||||||
|
if (frame.getContentDocument()) |doc| {
|
||||||
|
// A frame's document should always ahave a page, but
|
||||||
|
// I'm not willing to crash a release build on that assertion.
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(doc._page != null);
|
||||||
|
}
|
||||||
|
if (doc._page) |frame_page| {
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
root(doc, opts, writer, frame_page) catch return error.WriteFailed;
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
try children(node, opts, writer, page);
|
try children(node, opts, writer, page);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isVoidElement(el)) {
|
if (!isVoidElement(el)) {
|
||||||
try writer.writeAll("</");
|
try writer.writeAll("</");
|
||||||
try writer.writeAll(el.getTagNameDump());
|
try writer.writeAll(el.getTagNameDump());
|
||||||
@@ -172,7 +188,11 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
|
|||||||
try writer.writeAll(">\n");
|
try writer.writeAll(">\n");
|
||||||
},
|
},
|
||||||
.document_fragment => try children(node, opts, writer, page),
|
.document_fragment => try children(node, opts, writer, page),
|
||||||
.attribute => unreachable,
|
.attribute => {
|
||||||
|
// Not called normally, but can be called via XMLSerializer.serializeToString
|
||||||
|
// in which case it should return an empty string
|
||||||
|
try writer.writeAll("");
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,6 +314,12 @@ fn shouldEscapeText(node_: ?*Node) bool {
|
|||||||
if (node.is(Node.Element.Html.Script) != null) {
|
if (node.is(Node.Element.Html.Script) != null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// When scripting is enabled, <noscript> is a raw text element per the HTML spec
|
||||||
|
// (https://html.spec.whatwg.org/multipage/parsing.html#serialising-html-fragments).
|
||||||
|
// Its text content must not be HTML-escaped during serialization.
|
||||||
|
if (node.is(Node.Element.Html.Generic)) |generic| {
|
||||||
|
if (generic._tag == .noscript) return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
fn writeEscapedText(text: []const u8, writer: *std.Io.Writer) !void {
|
fn writeEscapedText(text: []const u8, writer: *std.Io.Writer) !void {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const string = @import("../../string.zig");
|
|||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
|
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
|
const Local = @import("Local.zig");
|
||||||
const Context = @import("Context.zig");
|
const Context = @import("Context.zig");
|
||||||
const TaggedOpaque = @import("TaggedOpaque.zig");
|
const TaggedOpaque = @import("TaggedOpaque.zig");
|
||||||
|
|
||||||
@@ -33,25 +34,24 @@ const CALL_ARENA_RETAIN = 1024 * 16;
|
|||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
const Caller = @This();
|
const Caller = @This();
|
||||||
local: js.Local,
|
local: Local,
|
||||||
prev_local: ?*const js.Local,
|
prev_local: ?*const js.Local,
|
||||||
prev_context: *Context,
|
prev_context: *Context,
|
||||||
|
|
||||||
// Takes the raw v8 isolate and extracts the context from it.
|
// Takes the raw v8 isolate and extracts the context from it.
|
||||||
pub fn init(self: *Caller, v8_isolate: *v8.Isolate) void {
|
pub fn init(self: *Caller, v8_isolate: *v8.Isolate) void {
|
||||||
const v8_context_handle = v8.v8__Isolate__GetCurrentContext(v8_isolate);
|
const v8_context = v8.v8__Isolate__GetCurrentContext(v8_isolate).?;
|
||||||
|
initWithContext(self, Context.fromC(v8_context), v8_context);
|
||||||
const embedder_data = v8.v8__Context__GetEmbedderData(v8_context_handle, 1);
|
}
|
||||||
var lossless: bool = undefined;
|
|
||||||
const ctx: *Context = @ptrFromInt(v8.v8__BigInt__Uint64Value(embedder_data, &lossless));
|
|
||||||
|
|
||||||
|
fn initWithContext(self: *Caller, ctx: *Context, v8_context: *const v8.Context) void {
|
||||||
ctx.call_depth += 1;
|
ctx.call_depth += 1;
|
||||||
self.* = Caller{
|
self.* = Caller{
|
||||||
.local = .{
|
.local = .{
|
||||||
.ctx = ctx,
|
.ctx = ctx,
|
||||||
.handle = v8_context_handle.?,
|
.handle = v8_context,
|
||||||
.call_arena = ctx.call_arena,
|
.call_arena = ctx.call_arena,
|
||||||
.isolate = .{ .handle = v8_isolate },
|
.isolate = ctx.isolate,
|
||||||
},
|
},
|
||||||
.prev_local = ctx.local,
|
.prev_local = ctx.local,
|
||||||
.prev_context = ctx.page.js,
|
.prev_context = ctx.page.js,
|
||||||
@@ -92,25 +92,28 @@ pub const CallOpts = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn constructor(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
|
pub fn constructor(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
|
||||||
|
const local = &self.local;
|
||||||
|
|
||||||
var hs: js.HandleScope = undefined;
|
var hs: js.HandleScope = undefined;
|
||||||
hs.init(self.local.isolate);
|
hs.init(local.isolate);
|
||||||
defer hs.deinit();
|
defer hs.deinit();
|
||||||
|
|
||||||
const info = FunctionCallbackInfo{ .handle = handle };
|
const info = FunctionCallbackInfo{ .handle = handle };
|
||||||
|
|
||||||
if (!info.isConstructCall()) {
|
if (!info.isConstructCall()) {
|
||||||
self.handleError(T, @TypeOf(func), error.InvalidArgument, info, opts);
|
handleError(T, @TypeOf(func), local, error.InvalidArgument, info, opts);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self._constructor(func, info) catch |err| {
|
self._constructor(func, info) catch |err| {
|
||||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void {
|
fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void {
|
||||||
const F = @TypeOf(func);
|
const F = @TypeOf(func);
|
||||||
const args = try self.getArgs(F, 0, info);
|
const local = &self.local;
|
||||||
|
const args = try getArgs(F, 0, local, info);
|
||||||
const res = @call(.auto, func, args);
|
const res = @call(.auto, func, args);
|
||||||
|
|
||||||
const ReturnType = @typeInfo(F).@"fn".return_type orelse {
|
const ReturnType = @typeInfo(F).@"fn".return_type orelse {
|
||||||
@@ -118,12 +121,12 @@ fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void
|
|||||||
};
|
};
|
||||||
|
|
||||||
const new_this_handle = info.getThis();
|
const new_this_handle = info.getThis();
|
||||||
var this = js.Object{ .local = &self.local, .handle = new_this_handle };
|
var this = js.Object{ .local = local, .handle = new_this_handle };
|
||||||
if (@typeInfo(ReturnType) == .error_union) {
|
if (@typeInfo(ReturnType) == .error_union) {
|
||||||
const non_error_res = res catch |err| return err;
|
const non_error_res = res catch |err| return err;
|
||||||
this = try self.local.mapZigInstanceToJs(new_this_handle, non_error_res);
|
this = try local.mapZigInstanceToJs(new_this_handle, non_error_res);
|
||||||
} else {
|
} else {
|
||||||
this = try self.local.mapZigInstanceToJs(new_this_handle, res);
|
this = try local.mapZigInstanceToJs(new_this_handle, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we got back a different object (existing wrapper), copy the prototype
|
// If we got back a different object (existing wrapper), copy the prototype
|
||||||
@@ -140,144 +143,141 @@ fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void
|
|||||||
info.getReturnValue().set(this.handle);
|
info.getReturnValue().set(this.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn method(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
|
|
||||||
var hs: js.HandleScope = undefined;
|
|
||||||
hs.init(self.local.isolate);
|
|
||||||
defer hs.deinit();
|
|
||||||
|
|
||||||
const info = FunctionCallbackInfo{ .handle = handle };
|
|
||||||
self._method(T, func, info, opts) catch |err| {
|
|
||||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _method(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void {
|
|
||||||
const F = @TypeOf(func);
|
|
||||||
var args = try self.getArgs(F, 1, info);
|
|
||||||
|
|
||||||
const js_this = info.getThis();
|
|
||||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, js_this);
|
|
||||||
|
|
||||||
const res = @call(.auto, func, args);
|
|
||||||
|
|
||||||
const mapped = try self.local.zigValueToJs(res, opts);
|
|
||||||
const return_value = info.getReturnValue();
|
|
||||||
return_value.set(mapped);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn function(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
|
|
||||||
var hs: js.HandleScope = undefined;
|
|
||||||
hs.init(self.local.isolate);
|
|
||||||
defer hs.deinit();
|
|
||||||
|
|
||||||
const info = FunctionCallbackInfo{ .handle = handle };
|
|
||||||
self._function(func, info, opts) catch |err| {
|
|
||||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _function(self: *Caller, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void {
|
|
||||||
const F = @TypeOf(func);
|
|
||||||
const args = try self.getArgs(F, 0, info);
|
|
||||||
const res = @call(.auto, func, args);
|
|
||||||
info.getReturnValue().set(try self.local.zigValueToJs(res, opts));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||||
|
const local = &self.local;
|
||||||
|
|
||||||
var hs: js.HandleScope = undefined;
|
var hs: js.HandleScope = undefined;
|
||||||
hs.init(self.local.isolate);
|
hs.init(local.isolate);
|
||||||
defer hs.deinit();
|
defer hs.deinit();
|
||||||
|
|
||||||
const info = PropertyCallbackInfo{ .handle = handle };
|
const info = PropertyCallbackInfo{ .handle = handle };
|
||||||
return self._getIndex(T, func, idx, info, opts) catch |err| {
|
return _getIndex(T, local, func, idx, info, opts) catch |err| {
|
||||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||||
// not intercepted
|
// not intercepted
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
fn _getIndex(comptime T: type, local: *const Local, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||||
const F = @TypeOf(func);
|
const F = @TypeOf(func);
|
||||||
var args = try self.getArgs(F, 2, info);
|
var args: ParameterTypes(F) = undefined;
|
||||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||||
@field(args, "1") = idx;
|
@field(args, "1") = idx;
|
||||||
|
if (@typeInfo(F).@"fn".params.len == 3) {
|
||||||
|
@field(args, "2") = local.ctx.page;
|
||||||
|
}
|
||||||
const ret = @call(.auto, func, args);
|
const ret = @call(.auto, func, args);
|
||||||
return self.handleIndexedReturn(T, F, true, ret, info, opts);
|
return handleIndexedReturn(T, F, true, local, ret, info, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||||
|
const local = &self.local;
|
||||||
|
|
||||||
var hs: js.HandleScope = undefined;
|
var hs: js.HandleScope = undefined;
|
||||||
hs.init(self.local.isolate);
|
hs.init(local.isolate);
|
||||||
defer hs.deinit();
|
defer hs.deinit();
|
||||||
|
|
||||||
const info = PropertyCallbackInfo{ .handle = handle };
|
const info = PropertyCallbackInfo{ .handle = handle };
|
||||||
return self._getNamedIndex(T, func, name, info, opts) catch |err| {
|
return _getNamedIndex(T, local, func, name, info, opts) catch |err| {
|
||||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||||
// not intercepted
|
// not intercepted
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
fn _getNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||||
const F = @TypeOf(func);
|
const F = @TypeOf(func);
|
||||||
var args = try self.getArgs(F, 2, info);
|
var args: ParameterTypes(F) = undefined;
|
||||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||||
@field(args, "1") = try self.nameToString(@TypeOf(args.@"1"), name);
|
@field(args, "1") = try nameToString(local, @TypeOf(args.@"1"), name);
|
||||||
|
if (@typeInfo(F).@"fn".params.len == 3) {
|
||||||
|
@field(args, "2") = local.ctx.page;
|
||||||
|
}
|
||||||
const ret = @call(.auto, func, args);
|
const ret = @call(.auto, func, args);
|
||||||
return self.handleIndexedReturn(T, F, true, ret, info, opts);
|
return handleIndexedReturn(T, F, true, local, ret, info, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, js_value: *const v8.Value, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, js_value: *const v8.Value, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||||
|
const local = &self.local;
|
||||||
|
|
||||||
var hs: js.HandleScope = undefined;
|
var hs: js.HandleScope = undefined;
|
||||||
hs.init(self.local.isolate);
|
hs.init(local.isolate);
|
||||||
defer hs.deinit();
|
defer hs.deinit();
|
||||||
|
|
||||||
const info = PropertyCallbackInfo{ .handle = handle };
|
const info = PropertyCallbackInfo{ .handle = handle };
|
||||||
return self._setNamedIndex(T, func, name, .{ .local = &self.local, .handle = js_value }, info, opts) catch |err| {
|
return _setNamedIndex(T, local, func, name, .{ .local = &self.local, .handle = js_value }, info, opts) catch |err| {
|
||||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||||
// not intercepted
|
// not intercepted
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, js_value: js.Value, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
fn _setNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *const v8.Name, js_value: js.Value, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||||
const F = @TypeOf(func);
|
const F = @TypeOf(func);
|
||||||
var args: ParameterTypes(F) = undefined;
|
var args: ParameterTypes(F) = undefined;
|
||||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||||
@field(args, "1") = try self.nameToString(@TypeOf(args.@"1"), name);
|
@field(args, "1") = try nameToString(local, @TypeOf(args.@"1"), name);
|
||||||
@field(args, "2") = try self.local.jsValueToZig(@TypeOf(@field(args, "2")), js_value);
|
@field(args, "2") = try local.jsValueToZig(@TypeOf(@field(args, "2")), js_value);
|
||||||
if (@typeInfo(F).@"fn".params.len == 4) {
|
if (@typeInfo(F).@"fn".params.len == 4) {
|
||||||
@field(args, "3") = self.local.ctx.page;
|
@field(args, "3") = local.ctx.page;
|
||||||
}
|
}
|
||||||
const ret = @call(.auto, func, args);
|
const ret = @call(.auto, func, args);
|
||||||
return self.handleIndexedReturn(T, F, false, ret, info, opts);
|
return handleIndexedReturn(T, F, false, local, ret, info, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||||
|
const local = &self.local;
|
||||||
|
|
||||||
var hs: js.HandleScope = undefined;
|
var hs: js.HandleScope = undefined;
|
||||||
hs.init(self.local.isolate);
|
hs.init(local.isolate);
|
||||||
defer hs.deinit();
|
defer hs.deinit();
|
||||||
|
|
||||||
const info = PropertyCallbackInfo{ .handle = handle };
|
const info = PropertyCallbackInfo{ .handle = handle };
|
||||||
return self._deleteNamedIndex(T, func, name, info, opts) catch |err| {
|
return _deleteNamedIndex(T, local, func, name, info, opts) catch |err| {
|
||||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
fn _deleteNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||||
const F = @TypeOf(func);
|
const F = @TypeOf(func);
|
||||||
var args: ParameterTypes(F) = undefined;
|
var args: ParameterTypes(F) = undefined;
|
||||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||||
@field(args, "1") = try self.nameToString(@TypeOf(args.@"1"), name);
|
@field(args, "1") = try nameToString(local, @TypeOf(args.@"1"), name);
|
||||||
if (@typeInfo(F).@"fn".params.len == 3) {
|
if (@typeInfo(F).@"fn".params.len == 3) {
|
||||||
@field(args, "2") = self.local.ctx.page;
|
@field(args, "2") = local.ctx.page;
|
||||||
}
|
}
|
||||||
const ret = @call(.auto, func, args);
|
const ret = @call(.auto, func, args);
|
||||||
return self.handleIndexedReturn(T, F, false, ret, info, opts);
|
return handleIndexedReturn(T, F, false, local, ret, info, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, comptime getter: bool, ret: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
pub fn getEnumerator(self: *Caller, comptime T: type, func: anytype, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||||
|
const local = &self.local;
|
||||||
|
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(local.isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
|
||||||
|
const info = PropertyCallbackInfo{ .handle = handle };
|
||||||
|
return _getEnumerator(T, local, func, info, opts) catch |err| {
|
||||||
|
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||||
|
// not intercepted
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _getEnumerator(comptime T: type, local: *const Local, func: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||||
|
const F = @TypeOf(func);
|
||||||
|
var args: ParameterTypes(F) = undefined;
|
||||||
|
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||||
|
if (@typeInfo(F).@"fn".params.len == 2) {
|
||||||
|
@field(args, "1") = local.ctx.page;
|
||||||
|
}
|
||||||
|
const ret = @call(.auto, func, args);
|
||||||
|
return handleIndexedReturn(T, F, true, local, ret, info, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleIndexedReturn(comptime T: type, comptime F: type, comptime with_value: bool, local: *const Local, ret: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||||
// need to unwrap this error immediately for when opts.null_as_undefined == true
|
// need to unwrap this error immediately for when opts.null_as_undefined == true
|
||||||
// and we need to compare it to null;
|
// and we need to compare it to null;
|
||||||
const non_error_ret = switch (@typeInfo(@TypeOf(ret))) {
|
const non_error_ret = switch (@typeInfo(@TypeOf(ret))) {
|
||||||
@@ -292,7 +292,7 @@ fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, compti
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.handleError(T, F, err, info, opts);
|
handleError(T, F, local, err, info, opts);
|
||||||
// not intercepted
|
// not intercepted
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
@@ -300,8 +300,8 @@ fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, compti
|
|||||||
else => ret,
|
else => ret,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (comptime getter) {
|
if (comptime with_value) {
|
||||||
info.getReturnValue().set(try self.local.zigValueToJs(non_error_ret, opts));
|
info.getReturnValue().set(try local.zigValueToJs(non_error_ret, opts));
|
||||||
}
|
}
|
||||||
// intercepted
|
// intercepted
|
||||||
return 1;
|
return 1;
|
||||||
@@ -314,36 +314,41 @@ fn isInErrorSet(err: anyerror, comptime T: type) bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nameToString(self: *const Caller, comptime T: type, name: *const v8.Name) !T {
|
fn nameToString(local: *const Local, comptime T: type, name: *const v8.Name) !T {
|
||||||
const handle = @as(*const v8.String, @ptrCast(name));
|
const handle = @as(*const v8.String, @ptrCast(name));
|
||||||
if (T == string.String) {
|
if (T == string.String) {
|
||||||
return js.String.toSSO(.{ .local = &self.local, .handle = handle }, false);
|
return js.String.toSSO(.{ .local = local, .handle = handle }, false);
|
||||||
}
|
}
|
||||||
if (T == string.Global) {
|
if (T == string.Global) {
|
||||||
return js.String.toSSO(.{ .local = &self.local, .handle = handle }, true);
|
return js.String.toSSO(.{ .local = local, .handle = handle }, true);
|
||||||
}
|
}
|
||||||
return try js.String.toSlice(.{ .local = &self.local, .handle = handle });
|
return try js.String.toSlice(.{ .local = local, .handle = handle });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror, info: anytype, comptime opts: CallOpts) void {
|
fn handleError(comptime T: type, comptime F: type, local: *const Local, err: anyerror, info: anytype, comptime opts: CallOpts) void {
|
||||||
const isolate = self.local.isolate;
|
const isolate = local.isolate;
|
||||||
|
|
||||||
if (comptime @import("builtin").mode == .Debug and @TypeOf(info) == FunctionCallbackInfo) {
|
if (comptime IS_DEBUG and @TypeOf(info) == FunctionCallbackInfo) {
|
||||||
if (log.enabled(.js, .warn)) {
|
if (log.enabled(.js, .debug)) {
|
||||||
self.logFunctionCallError(@typeName(T), @typeName(F), err, info);
|
const DOMException = @import("../webapi/DOMException.zig");
|
||||||
|
if (DOMException.fromError(err) == null) {
|
||||||
|
// This isn't a DOMException, let's log it
|
||||||
|
logFunctionCallError(local, @typeName(T), @typeName(F), err, info);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const js_err: *const v8.Value = switch (err) {
|
const js_err: *const v8.Value = switch (err) {
|
||||||
error.TryCatchRethrow => return,
|
error.TryCatchRethrow => return,
|
||||||
error.InvalidArgument => isolate.createTypeError("invalid argument"),
|
error.InvalidArgument => isolate.createTypeError("invalid argument"),
|
||||||
|
error.TypeError => isolate.createTypeError(""),
|
||||||
error.OutOfMemory => isolate.createError("out of memory"),
|
error.OutOfMemory => isolate.createError("out of memory"),
|
||||||
error.IllegalConstructor => isolate.createError("Illegal Contructor"),
|
error.IllegalConstructor => isolate.createError("Illegal Contructor"),
|
||||||
else => blk: {
|
else => blk: {
|
||||||
if (comptime opts.dom_exception) {
|
if (comptime opts.dom_exception) {
|
||||||
const DOMException = @import("../webapi/DOMException.zig");
|
const DOMException = @import("../webapi/DOMException.zig");
|
||||||
if (DOMException.fromError(err)) |ex| {
|
if (DOMException.fromError(err)) |ex| {
|
||||||
const value = self.local.zigValueToJs(ex, .{}) catch break :blk isolate.createError("internal error");
|
const value = local.zigValueToJs(ex, .{}) catch break :blk isolate.createError("internal error");
|
||||||
break :blk value.handle;
|
break :blk value.handle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,120 +360,20 @@ fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror,
|
|||||||
info.getReturnValue().setValueHandle(js_exception);
|
info.getReturnValue().setValueHandle(js_exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we call a method in javascript: cat.lives('nine');
|
|
||||||
//
|
|
||||||
// Then we'd expect a Zig function with 2 parameters: a self and the string.
|
|
||||||
// In this case, offset == 1. Offset is always 1 for setters or methods.
|
|
||||||
//
|
|
||||||
// Offset is always 0 for constructors.
|
|
||||||
//
|
|
||||||
// For constructors, setters and methods, we can further increase offset + 1
|
|
||||||
// if the first parameter is an instance of Page.
|
|
||||||
//
|
|
||||||
// Finally, if the JS function is called with _more_ parameters and
|
|
||||||
// the last parameter in Zig is an array, we'll try to slurp the additional
|
|
||||||
// parameters into the array.
|
|
||||||
fn getArgs(self: *const Caller, comptime F: type, comptime offset: usize, info: anytype) !ParameterTypes(F) {
|
|
||||||
const local = &self.local;
|
|
||||||
var args: ParameterTypes(F) = undefined;
|
|
||||||
|
|
||||||
const params = @typeInfo(F).@"fn".params[offset..];
|
|
||||||
// Except for the constructor, the first parameter is always `self`
|
|
||||||
// This isn't something we'll bind from JS, so skip it.
|
|
||||||
const params_to_map = blk: {
|
|
||||||
if (params.len == 0) {
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the last parameter is the Page, set it, and exclude it
|
|
||||||
// from our params slice, because we don't want to bind it to
|
|
||||||
// a JS argument
|
|
||||||
if (comptime isPage(params[params.len - 1].type.?)) {
|
|
||||||
@field(args, tupleFieldName(params.len - 1 + offset)) = local.ctx.page;
|
|
||||||
break :blk params[0 .. params.len - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// we have neither a Page nor a JsObject. All params must be
|
|
||||||
// bound to a JavaScript value.
|
|
||||||
break :blk params;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (params_to_map.len == 0) {
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
const js_parameter_count = info.length();
|
|
||||||
const last_js_parameter = params_to_map.len - 1;
|
|
||||||
var is_variadic = false;
|
|
||||||
|
|
||||||
{
|
|
||||||
// This is going to get complicated. If the last Zig parameter
|
|
||||||
// is a slice AND the corresponding javascript parameter is
|
|
||||||
// NOT an an array, then we'll treat it as a variadic.
|
|
||||||
|
|
||||||
const last_parameter_type = params_to_map[params_to_map.len - 1].type.?;
|
|
||||||
const last_parameter_type_info = @typeInfo(last_parameter_type);
|
|
||||||
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
|
|
||||||
const slice_type = last_parameter_type_info.pointer.child;
|
|
||||||
const corresponding_js_value = info.getArg(@intCast(last_js_parameter), local);
|
|
||||||
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
|
|
||||||
is_variadic = true;
|
|
||||||
if (js_parameter_count == 0) {
|
|
||||||
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
|
||||||
} else if (js_parameter_count >= params_to_map.len) {
|
|
||||||
const arr = try local.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
|
|
||||||
for (arr, last_js_parameter..) |*a, i| {
|
|
||||||
a.* = try local.jsValueToZig(slice_type, info.getArg(@intCast(i), local));
|
|
||||||
}
|
|
||||||
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
|
|
||||||
} else {
|
|
||||||
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline for (params_to_map, 0..) |param, i| {
|
|
||||||
const field_index = comptime i + offset;
|
|
||||||
if (comptime i == params_to_map.len - 1) {
|
|
||||||
if (is_variadic) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (comptime isPage(param.type.?)) {
|
|
||||||
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ @typeName(F));
|
|
||||||
} else if (i >= js_parameter_count) {
|
|
||||||
if (@typeInfo(param.type.?) != .optional) {
|
|
||||||
return error.InvalidArgument;
|
|
||||||
}
|
|
||||||
@field(args, tupleFieldName(field_index)) = null;
|
|
||||||
} else {
|
|
||||||
const js_val = info.getArg(@intCast(i), local);
|
|
||||||
@field(args, tupleFieldName(field_index)) = local.jsValueToZig(param.type.?, js_val) catch {
|
|
||||||
return error.InvalidArgument;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is extracted to speed up compilation. When left inlined in handleError,
|
// This is extracted to speed up compilation. When left inlined in handleError,
|
||||||
// this can add as much as 10 seconds of compilation time.
|
// this can add as much as 10 seconds of compilation time.
|
||||||
fn logFunctionCallError(self: *Caller, type_name: []const u8, func: []const u8, err: anyerror, info: FunctionCallbackInfo) void {
|
fn logFunctionCallError(local: *const Local, type_name: []const u8, func: []const u8, err: anyerror, info: FunctionCallbackInfo) void {
|
||||||
const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args";
|
const args_dump = serializeFunctionArgs(local, info) catch "failed to serialize args";
|
||||||
log.info(.js, "function call error", .{
|
log.debug(.js, "function call error", .{
|
||||||
.type = type_name,
|
.type = type_name,
|
||||||
.func = func,
|
.func = func,
|
||||||
.err = err,
|
.err = err,
|
||||||
.args = args_dump,
|
.args = args_dump,
|
||||||
.stack = self.local.stackTrace() catch |err1| @errorName(err1),
|
.stack = local.stackTrace() catch |err1| @errorName(err1),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serializeFunctionArgs(self: *Caller, info: FunctionCallbackInfo) ![]const u8 {
|
fn serializeFunctionArgs(local: *const Local, info: FunctionCallbackInfo) ![]const u8 {
|
||||||
const local = &self.local;
|
|
||||||
var buf = std.Io.Writer.Allocating.init(local.call_arena);
|
var buf = std.Io.Writer.Allocating.init(local.call_arena);
|
||||||
|
|
||||||
const separator = log.separator();
|
const separator = log.separator();
|
||||||
@@ -585,3 +490,275 @@ const ReturnValue = struct {
|
|||||||
v8.v8__ReturnValue__Set(self.handle, handle);
|
v8.v8__ReturnValue__Set(self.handle, handle);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const Function = struct {
|
||||||
|
pub const Opts = struct {
|
||||||
|
noop: bool = false,
|
||||||
|
static: bool = false,
|
||||||
|
dom_exception: bool = false,
|
||||||
|
as_typed_array: bool = false,
|
||||||
|
null_as_undefined: bool = false,
|
||||||
|
cache: ?Caching = null,
|
||||||
|
|
||||||
|
// We support two ways to cache a value directly into a v8::Object. The
|
||||||
|
// difference between the two is like the difference between a Map
|
||||||
|
// and a Struct.
|
||||||
|
// 1 - Using the object's internal fields. Think of this as
|
||||||
|
// adding a field to the struct. It's fast, but the space is reserved
|
||||||
|
// upfront for _every_ instance, whether we use it or not.
|
||||||
|
//
|
||||||
|
// 2 - Using the object's private state with a v8::Private key. Think of
|
||||||
|
// this as a HashMap. It takes no memory if the cache isn't used
|
||||||
|
// but has overhead when used.
|
||||||
|
//
|
||||||
|
// Consider `window.document`, (1) we have relatively few Window objects,
|
||||||
|
// (2) They all have a document and (3) The document is accessed _a lot_.
|
||||||
|
// An internal field makes sense.
|
||||||
|
//
|
||||||
|
// Consider `node.childNodes`, (1) we can have 20K+ node objects, (2)
|
||||||
|
// 95% of nodes will never have their .childNodes access by JavaScript.
|
||||||
|
// Private map lookup makes sense.
|
||||||
|
pub const Caching = union(enum) {
|
||||||
|
internal: u8,
|
||||||
|
private: []const u8,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn call(comptime T: type, info_handle: *const v8.FunctionCallbackInfo, func: anytype, comptime opts: Opts) void {
|
||||||
|
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(info_handle).?;
|
||||||
|
const v8_context = v8.v8__Isolate__GetCurrentContext(v8_isolate).?;
|
||||||
|
|
||||||
|
const ctx = Context.fromC(v8_context);
|
||||||
|
const info = FunctionCallbackInfo{ .handle = info_handle };
|
||||||
|
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.initWithIsolateHandle(v8_isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
|
||||||
|
var cache_state: CacheState = undefined;
|
||||||
|
if (comptime opts.cache) |cache| {
|
||||||
|
// This API is a bit weird. On
|
||||||
|
if (respondFromCache(cache, ctx, v8_context, info, &cache_state)) {
|
||||||
|
// Value was fetched from the cache and returned already
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Cache miss: cache_state will have been populated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var caller: Caller = undefined;
|
||||||
|
caller.initWithContext(ctx, v8_context);
|
||||||
|
defer caller.deinit();
|
||||||
|
|
||||||
|
const js_value = _call(T, &caller.local, info, func, opts) catch |err| {
|
||||||
|
handleError(T, @TypeOf(func), &caller.local, err, info, .{
|
||||||
|
.dom_exception = opts.dom_exception,
|
||||||
|
.as_typed_array = opts.as_typed_array,
|
||||||
|
.null_as_undefined = opts.null_as_undefined,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (comptime opts.cache) |cache| {
|
||||||
|
cache_state.save(cache, js_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _call(comptime T: type, local: *const Local, info: FunctionCallbackInfo, func: anytype, comptime opts: Opts) !js.Value {
|
||||||
|
const F = @TypeOf(func);
|
||||||
|
var args: ParameterTypes(F) = undefined;
|
||||||
|
if (comptime opts.static) {
|
||||||
|
args = try getArgs(F, 0, local, info);
|
||||||
|
} else {
|
||||||
|
args = try getArgs(F, 1, local, info);
|
||||||
|
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||||
|
}
|
||||||
|
const res = @call(.auto, func, args);
|
||||||
|
const js_value = try local.zigValueToJs(res, .{
|
||||||
|
.dom_exception = opts.dom_exception,
|
||||||
|
.as_typed_array = opts.as_typed_array,
|
||||||
|
.null_as_undefined = opts.null_as_undefined,
|
||||||
|
});
|
||||||
|
info.getReturnValue().set(js_value);
|
||||||
|
return js_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can cache a value directly into the v8::Object so that our callback to fetch a property
|
||||||
|
// can be fast. Generally, think of it like this:
|
||||||
|
// fn callback(handle: *const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
|
// const js_obj = info.getThis();
|
||||||
|
// const cached_value = js_obj.getFromCache("Nodes.childNodes");
|
||||||
|
// info.returnValue().set(cached_value);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// That above pseudocode snippet is largely what this respondFromCache is doing.
|
||||||
|
// But on miss, it's also setting the `cache_state` with all of the data it
|
||||||
|
// got checking the cache, so that, once we get the value from our Zig code,
|
||||||
|
// it's quick to store in the v8::Object for subsequent calls.
|
||||||
|
fn respondFromCache(comptime cache: Opts.Caching, ctx: *Context, v8_context: *const v8.Context, info: FunctionCallbackInfo, cache_state: *CacheState) bool {
|
||||||
|
const js_this = info.getThis();
|
||||||
|
const return_value = info.getReturnValue();
|
||||||
|
|
||||||
|
switch (cache) {
|
||||||
|
.internal => |idx| {
|
||||||
|
if (v8.v8__Object__GetInternalField(js_this, idx)) |cached| {
|
||||||
|
// means we can't cache undefined, since we can't tell the
|
||||||
|
// difference between "it isn't in the cache" and "it's
|
||||||
|
// in the cache with a valud of undefined"
|
||||||
|
if (!v8.v8__Value__IsUndefined(cached)) {
|
||||||
|
return_value.set(cached);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// store this so that we can quickly save the result into the cache
|
||||||
|
cache_state.* = .{
|
||||||
|
.js_this = js_this,
|
||||||
|
.v8_context = v8_context,
|
||||||
|
.mode = .{ .internal = idx },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
.private => |private_symbol| {
|
||||||
|
const global_handle = &@field(ctx.env.private_symbols, private_symbol).handle;
|
||||||
|
const private_key: *const v8.Private = v8.v8__Global__Get(global_handle, ctx.isolate.handle).?;
|
||||||
|
if (v8.v8__Object__GetPrivate(js_this, v8_context, private_key)) |cached| {
|
||||||
|
// This means we can't cache "undefined", since we can't tell
|
||||||
|
// the difference between a (a) undefined == not in the cache
|
||||||
|
// and (b) undefined == the cache value. If this becomes
|
||||||
|
// important, we can check HasPrivate first. But that requires
|
||||||
|
// calling HasPrivate then GetPrivate.
|
||||||
|
if (!v8.v8__Value__IsUndefined(cached)) {
|
||||||
|
return_value.set(cached);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// store this so that we can quickly save the result into the cache
|
||||||
|
cache_state.* = .{
|
||||||
|
.js_this = js_this,
|
||||||
|
.v8_context = v8_context,
|
||||||
|
.mode = .{ .private = private_key },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache miss
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CacheState = struct {
|
||||||
|
js_this: *const v8.Object,
|
||||||
|
v8_context: *const v8.Context,
|
||||||
|
mode: union(enum) {
|
||||||
|
internal: u8,
|
||||||
|
private: *const v8.Private,
|
||||||
|
},
|
||||||
|
|
||||||
|
pub fn save(self: *const CacheState, comptime cache: Opts.Caching, js_value: js.Value) void {
|
||||||
|
if (comptime cache == .internal) {
|
||||||
|
v8.v8__Object__SetInternalField(self.js_this, self.mode.internal, js_value.handle);
|
||||||
|
} else {
|
||||||
|
var out: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Object__SetPrivate(self.js_this, self.v8_context, self.mode.private, js_value.handle, &out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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(comptime F: type, comptime offset: usize, local: *const Local, info: FunctionCallbackInfo) !ParameterTypes(F) {
|
||||||
|
var args: ParameterTypes(F) = undefined;
|
||||||
|
|
||||||
|
const params = @typeInfo(F).@"fn".params[offset..];
|
||||||
|
// Except for the constructor, the first parameter is always `self`
|
||||||
|
// This isn't something we'll bind from JS, so skip it.
|
||||||
|
const params_to_map = blk: {
|
||||||
|
if (params.len == 0) {
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the last parameter is the Page, set it, and exclude it
|
||||||
|
// from our params slice, because we don't want to bind it to
|
||||||
|
// a JS argument
|
||||||
|
if (comptime isPage(params[params.len - 1].type.?)) {
|
||||||
|
@field(args, tupleFieldName(params.len - 1 + offset)) = local.ctx.page;
|
||||||
|
break :blk params[0 .. params.len - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have neither a Page nor a JsObject. All params must be
|
||||||
|
// bound to a JavaScript value.
|
||||||
|
break :blk params;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (params_to_map.len == 0) {
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
const js_parameter_count = info.length();
|
||||||
|
const last_js_parameter = params_to_map.len - 1;
|
||||||
|
var is_variadic = false;
|
||||||
|
|
||||||
|
{
|
||||||
|
// This is going to get complicated. If the last Zig parameter
|
||||||
|
// is a slice AND the corresponding javascript parameter is
|
||||||
|
// NOT an an array, then we'll treat it as a variadic.
|
||||||
|
|
||||||
|
const last_parameter_type = params_to_map[params_to_map.len - 1].type.?;
|
||||||
|
const last_parameter_type_info = @typeInfo(last_parameter_type);
|
||||||
|
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
|
||||||
|
const slice_type = last_parameter_type_info.pointer.child;
|
||||||
|
const corresponding_js_value = info.getArg(@intCast(last_js_parameter), local);
|
||||||
|
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
|
||||||
|
is_variadic = true;
|
||||||
|
if (js_parameter_count == 0) {
|
||||||
|
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
||||||
|
} else if (js_parameter_count >= params_to_map.len) {
|
||||||
|
const arr = try local.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
|
||||||
|
for (arr, last_js_parameter..) |*a, i| {
|
||||||
|
a.* = try local.jsValueToZig(slice_type, info.getArg(@intCast(i), local));
|
||||||
|
}
|
||||||
|
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
|
||||||
|
} else {
|
||||||
|
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline for (params_to_map, 0..) |param, i| {
|
||||||
|
const field_index = comptime i + offset;
|
||||||
|
if (comptime i == params_to_map.len - 1) {
|
||||||
|
if (is_variadic) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comptime isPage(param.type.?)) {
|
||||||
|
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ @typeName(F));
|
||||||
|
} else if (i >= js_parameter_count) {
|
||||||
|
if (@typeInfo(param.type.?) != .optional) {
|
||||||
|
return error.InvalidArgument;
|
||||||
|
}
|
||||||
|
@field(args, tupleFieldName(field_index)) = null;
|
||||||
|
} else {
|
||||||
|
const js_val = info.getArg(@intCast(i), local);
|
||||||
|
@field(args, tupleFieldName(field_index)) = local.jsValueToZig(param.type.?, js_val) catch {
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,13 +43,13 @@ env: *Env,
|
|||||||
page: *Page,
|
page: *Page,
|
||||||
isolate: js.Isolate,
|
isolate: js.Isolate,
|
||||||
|
|
||||||
|
// Per-context microtask queue for isolation between contexts
|
||||||
|
microtask_queue: *v8.MicrotaskQueue,
|
||||||
|
|
||||||
// The v8::Global<v8::Context>. When necessary, we can create a v8::Local<<v8::Context>>
|
// The v8::Global<v8::Context>. When necessary, we can create a v8::Local<<v8::Context>>
|
||||||
// from this, and we can free it when the context is done.
|
// from this, and we can free it when the context is done.
|
||||||
handle: v8.Global,
|
handle: v8.Global,
|
||||||
|
|
||||||
// True if the context is auto-entered,
|
|
||||||
entered: bool,
|
|
||||||
|
|
||||||
cpu_profiler: ?*v8.CpuProfiler = null,
|
cpu_profiler: ?*v8.CpuProfiler = null,
|
||||||
|
|
||||||
heap_profiler: ?*v8.HeapProfiler = null,
|
heap_profiler: ?*v8.HeapProfiler = null,
|
||||||
@@ -124,10 +124,6 @@ script_manager: ?*ScriptManager,
|
|||||||
// Our macrotasks
|
// Our macrotasks
|
||||||
scheduler: Scheduler,
|
scheduler: Scheduler,
|
||||||
|
|
||||||
// Prevents us from enqueuing a microtask for this context while we're shutting
|
|
||||||
// down.
|
|
||||||
shutting_down: bool = false,
|
|
||||||
|
|
||||||
unknown_properties: (if (IS_DEBUG) std.StringHashMapUnmanaged(UnknownPropertyStat) else void) = if (IS_DEBUG) .{} else {},
|
unknown_properties: (if (IS_DEBUG) std.StringHashMapUnmanaged(UnknownPropertyStat) else void) = if (IS_DEBUG) .{} else {},
|
||||||
|
|
||||||
const ModuleEntry = struct {
|
const ModuleEntry = struct {
|
||||||
@@ -148,17 +144,12 @@ const ModuleEntry = struct {
|
|||||||
resolver_promise: ?js.Promise.Global = null,
|
resolver_promise: ?js.Promise.Global = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn fromC(c_context: *const v8.Context) *Context {
|
pub fn fromC(c_context: *const v8.Context) *Context {
|
||||||
const data = v8.v8__Context__GetEmbedderData(c_context, 1).?;
|
return @ptrCast(@alignCast(v8.v8__Context__GetAlignedPointerFromEmbedderData(c_context, 1)));
|
||||||
const big_int = js.BigInt{ .handle = @ptrCast(data) };
|
|
||||||
return @ptrFromInt(big_int.getUint64());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fromIsolate(isolate: js.Isolate) *Context {
|
pub fn fromIsolate(isolate: js.Isolate) *Context {
|
||||||
const v8_context = v8.v8__Isolate__GetCurrentContext(isolate.handle).?;
|
return fromC(v8.v8__Isolate__GetCurrentContext(isolate.handle).?);
|
||||||
const data = v8.v8__Context__GetEmbedderData(v8_context, 1).?;
|
|
||||||
const big_int = js.BigInt{ .handle = @ptrCast(data) };
|
|
||||||
return @ptrFromInt(big_int.getUint64());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Context) void {
|
pub fn deinit(self: *Context) void {
|
||||||
@@ -172,21 +163,15 @@ pub fn deinit(self: *Context) void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer self.env.app.arena_pool.release(self.arena);
|
|
||||||
|
const env = self.env;
|
||||||
|
defer env.app.arena_pool.release(self.arena);
|
||||||
|
|
||||||
var hs: js.HandleScope = undefined;
|
var hs: js.HandleScope = undefined;
|
||||||
const entered = self.enter(&hs);
|
const entered = self.enter(&hs);
|
||||||
defer entered.exit();
|
defer entered.exit();
|
||||||
|
|
||||||
// We might have microtasks in the isolate that refence this context. The
|
// this can release objects
|
||||||
// only option we have is to run them. But a microtask could queue another
|
|
||||||
// microtask, so we set the shutting_down flag, so that any such microtask
|
|
||||||
// will be a noop (this isn't automatic, when v8 calls our microtask callback
|
|
||||||
// the first thing we'll check is if self.shutting_down == true).
|
|
||||||
self.shutting_down = true;
|
|
||||||
self.env.runMicrotasks();
|
|
||||||
|
|
||||||
// can release objects
|
|
||||||
self.scheduler.deinit();
|
self.scheduler.deinit();
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -248,11 +233,12 @@ pub fn deinit(self: *Context) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.entered) {
|
|
||||||
v8.v8__Context__Exit(@ptrCast(v8.v8__Global__Get(&self.handle, self.isolate.handle)));
|
|
||||||
}
|
|
||||||
|
|
||||||
v8.v8__Global__Reset(&self.handle);
|
v8.v8__Global__Reset(&self.handle);
|
||||||
|
env.isolate.notifyContextDisposed();
|
||||||
|
// There can be other tasks associated with this context that we need to
|
||||||
|
// purge while the context is still alive.
|
||||||
|
_ = env.pumpMessageLoop();
|
||||||
|
v8.v8__MicrotaskQueue__DELETE(self.microtask_queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn weakRef(self: *Context, obj: anytype) void {
|
pub fn weakRef(self: *Context, obj: anytype) void {
|
||||||
@@ -333,11 +319,14 @@ pub fn localScope(self: *Context, ls: *js.Local.Scope) void {
|
|||||||
const isolate = self.isolate;
|
const isolate = self.isolate;
|
||||||
js.HandleScope.init(&ls.handle_scope, isolate);
|
js.HandleScope.init(&ls.handle_scope, isolate);
|
||||||
|
|
||||||
|
const local_v8_context: *const v8.Context = @ptrCast(v8.v8__Global__Get(&self.handle, isolate.handle));
|
||||||
|
v8.v8__Context__Enter(local_v8_context);
|
||||||
|
|
||||||
// TODO: add and init ls.hs for the handlescope
|
// TODO: add and init ls.hs for the handlescope
|
||||||
ls.local = .{
|
ls.local = .{
|
||||||
.ctx = self,
|
.ctx = self,
|
||||||
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, isolate.handle)),
|
|
||||||
.isolate = isolate,
|
.isolate = isolate,
|
||||||
|
.handle = local_v8_context,
|
||||||
.call_arena = self.call_arena,
|
.call_arena = self.call_arena,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -364,7 +353,6 @@ pub fn stringToPersistedFunction(self: *Context, str: []const u8) !js.Function.G
|
|||||||
extra = "(e)";
|
extra = "(e)";
|
||||||
}
|
}
|
||||||
const full = try std.fmt.allocPrintSentinel(self.call_arena, "(function(e) {{ {s}{s} }})", .{ normalized, extra }, 0);
|
const full = try std.fmt.allocPrintSentinel(self.call_arena, "(function(e) {{ {s}{s} }})", .{ normalized, extra }, 0);
|
||||||
|
|
||||||
const js_val = try ls.local.compileAndRun(full, null);
|
const js_val = try ls.local.compileAndRun(full, null);
|
||||||
if (!js_val.isFunction()) {
|
if (!js_val.isFunction()) {
|
||||||
return error.StringFunctionError;
|
return error.StringFunctionError;
|
||||||
@@ -612,10 +600,19 @@ pub fn dynamicModuleCallback(
|
|||||||
.isolate = self.isolate,
|
.isolate = self.isolate,
|
||||||
};
|
};
|
||||||
|
|
||||||
const resource = js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| {
|
const resource = blk: {
|
||||||
|
const resource_value = js.Value{ .handle = resource_name.?, .local = &local };
|
||||||
|
if (resource_value.isNullOrUndefined()) {
|
||||||
|
// will only be null / undefined in extreme cases (e.g. WPT tests)
|
||||||
|
// where you're
|
||||||
|
break :blk self.page.base();
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| {
|
||||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" });
|
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" });
|
||||||
return @constCast((local.rejectPromise("Out of memory") catch return null).handle);
|
return @constCast((local.rejectPromise("Out of memory") catch return null).handle);
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const specifier = js.String.toSliceZ(.{ .local = &local, .handle = v8_specifier.? }) catch |err| {
|
const specifier = js.String.toSliceZ(.{ .local = &local, .handle = v8_specifier.? }) catch |err| {
|
||||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" });
|
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" });
|
||||||
@@ -998,13 +995,10 @@ pub fn queueSlotchangeDelivery(self: *Context) !void {
|
|||||||
// But for these Context microtasks, we want to (a) make sure the context isn't
|
// But for these Context microtasks, we want to (a) make sure the context isn't
|
||||||
// being shut down and (b) that it's entered.
|
// being shut down and (b) that it's entered.
|
||||||
fn enqueueMicrotask(self: *Context, callback: anytype) void {
|
fn enqueueMicrotask(self: *Context, callback: anytype) void {
|
||||||
self.isolate.enqueueMicrotask(struct {
|
// Use context-specific microtask queue instead of isolate queue
|
||||||
|
v8.v8__MicrotaskQueue__EnqueueMicrotask(self.microtask_queue, self.isolate.handle, struct {
|
||||||
fn run(data: ?*anyopaque) callconv(.c) void {
|
fn run(data: ?*anyopaque) callconv(.c) void {
|
||||||
const ctx: *Context = @ptrCast(@alignCast(data.?));
|
const ctx: *Context = @ptrCast(@alignCast(data.?));
|
||||||
if (ctx.shutting_down) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hs: js.HandleScope = undefined;
|
var hs: js.HandleScope = undefined;
|
||||||
const entered = ctx.enter(&hs);
|
const entered = ctx.enter(&hs);
|
||||||
defer entered.exit();
|
defer entered.exit();
|
||||||
@@ -1014,10 +1008,11 @@ fn enqueueMicrotask(self: *Context, callback: anytype) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn queueMicrotaskFunc(self: *Context, cb: js.Function) void {
|
pub fn queueMicrotaskFunc(self: *Context, cb: js.Function) void {
|
||||||
self.isolate.enqueueMicrotaskFunc(cb);
|
// Use context-specific microtask queue instead of isolate queue
|
||||||
|
v8.v8__MicrotaskQueue__EnqueueMicrotaskFunc(self.microtask_queue, self.isolate.handle, cb.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn createFinalizerCallback(self: *Context, global: v8.Global, ptr: *anyopaque, finalizerFn: *const fn (ptr: *anyopaque) void) !*FinalizerCallback {
|
pub fn createFinalizerCallback(self: *Context, global: v8.Global, ptr: *anyopaque, finalizerFn: *const fn (ptr: *anyopaque, page: *Page) void) !*FinalizerCallback {
|
||||||
const fc = try self.finalizer_callback_pool.create();
|
const fc = try self.finalizer_callback_pool.create();
|
||||||
fc.* = .{
|
fc.* = .{
|
||||||
.ctx = self,
|
.ctx = self,
|
||||||
@@ -1037,10 +1032,10 @@ pub const FinalizerCallback = struct {
|
|||||||
ctx: *Context,
|
ctx: *Context,
|
||||||
ptr: *anyopaque,
|
ptr: *anyopaque,
|
||||||
global: v8.Global,
|
global: v8.Global,
|
||||||
finalizerFn: *const fn (ptr: *anyopaque) void,
|
finalizerFn: *const fn (ptr: *anyopaque, page: *Page) void,
|
||||||
|
|
||||||
pub fn deinit(self: *FinalizerCallback) void {
|
pub fn deinit(self: *FinalizerCallback) void {
|
||||||
self.finalizerFn(self.ptr);
|
self.finalizerFn(self.ptr, self.ctx.page);
|
||||||
self.ctx.finalizer_callback_pool.destroy(self);
|
self.ctx.finalizer_callback_pool.destroy(self);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ const JsApis = bridge.JsApis;
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const IS_DEBUG = builtin.mode == .Debug;
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
|
fn initClassIds() void {
|
||||||
|
inline for (JsApis, 0..) |JsApi, i| {
|
||||||
|
JsApi.Meta.class_id = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var class_id_once = std.once(initClassIds);
|
||||||
|
|
||||||
// The Env maps to a V8 isolate, which represents a isolated sandbox for
|
// The Env maps to a V8 isolate, which represents a isolated sandbox for
|
||||||
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
|
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
|
||||||
// and it's where we'll start ExecutionWorlds, which actually execute JavaScript.
|
// and it's where we'll start ExecutionWorlds, which actually execute JavaScript.
|
||||||
@@ -54,7 +62,8 @@ platform: *const Platform,
|
|||||||
// the global isolate
|
// the global isolate
|
||||||
isolate: js.Isolate,
|
isolate: js.Isolate,
|
||||||
|
|
||||||
contexts: std.ArrayList(*js.Context),
|
contexts: [64]*Context,
|
||||||
|
context_count: usize,
|
||||||
|
|
||||||
// just kept around because we need to free it on deinit
|
// just kept around because we need to free it on deinit
|
||||||
isolate_params: *v8.CreateParams,
|
isolate_params: *v8.CreateParams,
|
||||||
@@ -73,11 +82,28 @@ global_template: v8.Eternal,
|
|||||||
// Inspector associated with the Isolate. Exists when CDP is being used.
|
// Inspector associated with the Isolate. Exists when CDP is being used.
|
||||||
inspector: ?*Inspector,
|
inspector: ?*Inspector,
|
||||||
|
|
||||||
|
// We can store data in a v8::Object's Private data bag. The keys are v8::Private
|
||||||
|
// which an be created once per isolaet.
|
||||||
|
private_symbols: PrivateSymbols,
|
||||||
|
|
||||||
|
microtask_queues_are_running: bool,
|
||||||
|
|
||||||
pub const InitOpts = struct {
|
pub const InitOpts = struct {
|
||||||
with_inspector: bool = false,
|
with_inspector: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(app: *App, opts: InitOpts) !Env {
|
pub fn init(app: *App, opts: InitOpts) !Env {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
comptime {
|
||||||
|
// V8 requirement for any data using SetAlignedPointerInInternalField
|
||||||
|
const a = @alignOf(@import("TaggedOpaque.zig"));
|
||||||
|
std.debug.assert(a >= 2 and a % 2 == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize class IDs once before any V8 work
|
||||||
|
class_id_once.call();
|
||||||
|
|
||||||
const allocator = app.allocator;
|
const allocator = app.allocator;
|
||||||
const snapshot = &app.snapshot;
|
const snapshot = &app.snapshot;
|
||||||
|
|
||||||
@@ -114,13 +140,13 @@ pub fn init(app: *App, opts: InitOpts) !Env {
|
|||||||
errdefer allocator.free(templates);
|
errdefer allocator.free(templates);
|
||||||
|
|
||||||
var global_eternal: v8.Eternal = undefined;
|
var global_eternal: v8.Eternal = undefined;
|
||||||
|
var private_symbols: PrivateSymbols = undefined;
|
||||||
{
|
{
|
||||||
var temp_scope: js.HandleScope = undefined;
|
var temp_scope: js.HandleScope = undefined;
|
||||||
temp_scope.init(isolate);
|
temp_scope.init(isolate);
|
||||||
defer temp_scope.deinit();
|
defer temp_scope.deinit();
|
||||||
|
|
||||||
inline for (JsApis, 0..) |JsApi, i| {
|
inline for (JsApis, 0..) |_, i| {
|
||||||
JsApi.Meta.class_id = i;
|
|
||||||
const data = v8.v8__Isolate__GetDataFromSnapshotOnce(isolate_handle, snapshot.data_start + i);
|
const data = v8.v8__Isolate__GetDataFromSnapshotOnce(isolate_handle, snapshot.data_start + i);
|
||||||
const function_handle: *const v8.FunctionTemplate = @ptrCast(data);
|
const function_handle: *const v8.FunctionTemplate = @ptrCast(data);
|
||||||
// Make function template eternal
|
// Make function template eternal
|
||||||
@@ -152,7 +178,24 @@ pub fn init(app: *App, opts: InitOpts) !Env {
|
|||||||
.data = null,
|
.data = null,
|
||||||
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
||||||
});
|
});
|
||||||
|
// I don't 100% understand this. We actually set this up in the snapshot,
|
||||||
|
// but for the global instance, it doesn't work. SetIndexedHandler and
|
||||||
|
// SetNamedHandler are set on the Instance template, and that's the key
|
||||||
|
// difference. The context has its own global instance, so we need to set
|
||||||
|
// these back up directly on it. There might be a better way to do this.
|
||||||
|
v8.v8__ObjectTemplate__SetIndexedHandler(global_template_local, &.{
|
||||||
|
.getter = Window.JsApi.index.getter,
|
||||||
|
.setter = null,
|
||||||
|
.query = null,
|
||||||
|
.deleter = null,
|
||||||
|
.enumerator = null,
|
||||||
|
.definer = null,
|
||||||
|
.descriptor = null,
|
||||||
|
.data = null,
|
||||||
|
.flags = 0,
|
||||||
|
});
|
||||||
v8.v8__Eternal__New(isolate_handle, @ptrCast(global_template_local), &global_eternal);
|
v8.v8__Eternal__New(isolate_handle, @ptrCast(global_template_local), &global_eternal);
|
||||||
|
private_symbols = PrivateSymbols.init(isolate_handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
var inspector: ?*js.Inspector = null;
|
var inspector: ?*js.Inspector = null;
|
||||||
@@ -163,22 +206,25 @@ pub fn init(app: *App, opts: InitOpts) !Env {
|
|||||||
return .{
|
return .{
|
||||||
.app = app,
|
.app = app,
|
||||||
.context_id = 0,
|
.context_id = 0,
|
||||||
.contexts = .empty,
|
.contexts = undefined,
|
||||||
|
.context_count = 0,
|
||||||
.isolate = isolate,
|
.isolate = isolate,
|
||||||
.platform = &app.platform,
|
.platform = &app.platform,
|
||||||
.templates = templates,
|
.templates = templates,
|
||||||
.isolate_params = params,
|
.isolate_params = params,
|
||||||
.inspector = inspector,
|
.inspector = inspector,
|
||||||
.eternal_function_templates = eternal_function_templates,
|
|
||||||
.global_template = global_eternal,
|
.global_template = global_eternal,
|
||||||
|
.private_symbols = private_symbols,
|
||||||
|
.microtask_queues_are_running = false,
|
||||||
|
.eternal_function_templates = eternal_function_templates,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Env) void {
|
pub fn deinit(self: *Env) void {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
std.debug.assert(self.contexts.items.len == 0);
|
std.debug.assert(self.context_count == 0);
|
||||||
}
|
}
|
||||||
for (self.contexts.items) |ctx| {
|
for (self.contexts[0..self.context_count]) |ctx| {
|
||||||
ctx.deinit();
|
ctx.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,10 +233,9 @@ pub fn deinit(self: *Env) void {
|
|||||||
i.deinit(allocator);
|
i.deinit(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.contexts.deinit(allocator);
|
|
||||||
|
|
||||||
allocator.free(self.templates);
|
allocator.free(self.templates);
|
||||||
allocator.free(self.eternal_function_templates);
|
allocator.free(self.eternal_function_templates);
|
||||||
|
self.private_symbols.deinit();
|
||||||
|
|
||||||
self.isolate.exit();
|
self.isolate.exit();
|
||||||
self.isolate.deinit();
|
self.isolate.deinit();
|
||||||
@@ -198,7 +243,7 @@ pub fn deinit(self: *Env) void {
|
|||||||
allocator.destroy(self.isolate_params);
|
allocator.destroy(self.isolate_params);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn createContext(self: *Env, page: *Page, enter: bool) !*Context {
|
pub fn createContext(self: *Env, page: *Page) !*Context {
|
||||||
const context_arena = try self.app.arena_pool.acquire();
|
const context_arena = try self.app.arena_pool.acquire();
|
||||||
errdefer self.app.arena_pool.release(context_arena);
|
errdefer self.app.arena_pool.release(context_arena);
|
||||||
|
|
||||||
@@ -207,26 +252,45 @@ pub fn createContext(self: *Env, page: *Page, enter: bool) !*Context {
|
|||||||
hs.init(isolate);
|
hs.init(isolate);
|
||||||
defer hs.deinit();
|
defer hs.deinit();
|
||||||
|
|
||||||
|
// Create a per-context microtask queue for isolation
|
||||||
|
const microtask_queue = v8.v8__MicrotaskQueue__New(isolate.handle, v8.kExplicit).?;
|
||||||
|
errdefer v8.v8__MicrotaskQueue__DELETE(microtask_queue);
|
||||||
|
|
||||||
// Get the global template that was created once per isolate
|
// Get the global template that was created once per isolate
|
||||||
const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&self.global_template, isolate.handle).?));
|
const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&self.global_template, isolate.handle).?));
|
||||||
const v8_context = v8.v8__Context__New(isolate.handle, global_template, null).?;
|
v8.v8__ObjectTemplate__SetInternalFieldCount(global_template, comptime Snapshot.countInternalFields(Window.JsApi));
|
||||||
|
|
||||||
|
const v8_context = v8.v8__Context__New__Config(isolate.handle, &.{
|
||||||
|
.global_template = global_template,
|
||||||
|
.global_object = null,
|
||||||
|
.microtask_queue = microtask_queue,
|
||||||
|
}).?;
|
||||||
|
|
||||||
// Create the v8::Context and wrap it in a v8::Global
|
// Create the v8::Context and wrap it in a v8::Global
|
||||||
var context_global: v8.Global = undefined;
|
var context_global: v8.Global = undefined;
|
||||||
v8.v8__Global__New(isolate.handle, v8_context, &context_global);
|
v8.v8__Global__New(isolate.handle, v8_context, &context_global);
|
||||||
|
|
||||||
// our window wrapped in a v8::Global
|
// get the global object for the context, this maps to our Window
|
||||||
const global_obj = v8.v8__Context__Global(v8_context).?;
|
const global_obj = v8.v8__Context__Global(v8_context).?;
|
||||||
|
{
|
||||||
|
// Store our TAO inside the internal field of the global object. This
|
||||||
|
// maps the v8::Object -> Zig instance. Almost all objects have this, and
|
||||||
|
// it gets setup automatically as objects are created, but the Window
|
||||||
|
// object already exists in v8 (it's the global) so we manually create
|
||||||
|
// the mapping here.
|
||||||
|
const tao = try context_arena.create(@import("TaggedOpaque.zig"));
|
||||||
|
tao.* = .{
|
||||||
|
.value = @ptrCast(page.window),
|
||||||
|
.prototype_chain = (&Window.JsApi.Meta.prototype_chain).ptr,
|
||||||
|
.prototype_len = @intCast(Window.JsApi.Meta.prototype_chain.len),
|
||||||
|
.subtype = .node, // this probably isn't right, but it's what we've been doing all along
|
||||||
|
};
|
||||||
|
v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao);
|
||||||
|
}
|
||||||
|
// our window wrapped in a v8::Global
|
||||||
var global_global: v8.Global = undefined;
|
var global_global: v8.Global = undefined;
|
||||||
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
|
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
|
||||||
|
|
||||||
if (enter) {
|
|
||||||
v8.v8__Context__Enter(v8_context);
|
|
||||||
}
|
|
||||||
errdefer if (enter) {
|
|
||||||
v8.v8__Context__Exit(v8_context);
|
|
||||||
};
|
|
||||||
|
|
||||||
const context_id = self.context_id;
|
const context_id = self.context_id;
|
||||||
self.context_id = context_id + 1;
|
self.context_id = context_id + 1;
|
||||||
|
|
||||||
@@ -235,12 +299,12 @@ pub fn createContext(self: *Env, page: *Page, enter: bool) !*Context {
|
|||||||
.env = self,
|
.env = self,
|
||||||
.page = page,
|
.page = page,
|
||||||
.id = context_id,
|
.id = context_id,
|
||||||
.entered = enter,
|
|
||||||
.isolate = isolate,
|
.isolate = isolate,
|
||||||
.arena = context_arena,
|
.arena = context_arena,
|
||||||
.handle = context_global,
|
.handle = context_global,
|
||||||
.templates = self.templates,
|
.templates = self.templates,
|
||||||
.call_arena = page.call_arena,
|
.call_arena = page.call_arena,
|
||||||
|
.microtask_queue = microtask_queue,
|
||||||
.script_manager = &page._script_manager,
|
.script_manager = &page._script_manager,
|
||||||
.scheduler = .init(context_arena),
|
.scheduler = .init(context_arena),
|
||||||
.finalizer_callback_pool = std.heap.MemoryPool(Context.FinalizerCallback).init(self.app.allocator),
|
.finalizer_callback_pool = std.heap.MemoryPool(Context.FinalizerCallback).init(self.app.allocator),
|
||||||
@@ -249,17 +313,24 @@ pub fn createContext(self: *Env, page: *Page, enter: bool) !*Context {
|
|||||||
|
|
||||||
// Store a pointer to our context inside the v8 context so that, given
|
// Store a pointer to our context inside the v8 context so that, given
|
||||||
// a v8 context, we can get our context out
|
// a v8 context, we can get our context out
|
||||||
const data = isolate.initBigInt(@intFromPtr(context));
|
v8.v8__Context__SetAlignedPointerInEmbedderData(v8_context, 1, @ptrCast(context));
|
||||||
v8.v8__Context__SetEmbedderData(v8_context, 1, @ptrCast(data.handle));
|
|
||||||
|
const count = self.context_count;
|
||||||
|
if (count >= self.contexts.len) {
|
||||||
|
return error.TooManyContexts;
|
||||||
|
}
|
||||||
|
self.contexts[count] = context;
|
||||||
|
self.context_count = count + 1;
|
||||||
|
|
||||||
try self.contexts.append(self.app.allocator, context);
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn destroyContext(self: *Env, context: *Context) void {
|
pub fn destroyContext(self: *Env, context: *Context) void {
|
||||||
for (self.contexts.items, 0..) |ctx, i| {
|
for (self.contexts[0..self.context_count], 0..) |ctx, i| {
|
||||||
if (ctx == context) {
|
if (ctx == context) {
|
||||||
_ = self.contexts.swapRemove(i);
|
// Swap with last element and decrement count
|
||||||
|
self.context_count -= 1;
|
||||||
|
self.contexts[i] = self.contexts[self.context_count];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -277,16 +348,26 @@ pub fn destroyContext(self: *Env, context: *Context) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.deinit();
|
context.deinit();
|
||||||
isolate.notifyContextDisposed();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMicrotasks(self: *const Env) void {
|
pub fn runMicrotasks(self: *Env) void {
|
||||||
self.isolate.performMicrotasksCheckpoint();
|
if (self.microtask_queues_are_running == false) {
|
||||||
|
const v8_isolate = self.isolate.handle;
|
||||||
|
|
||||||
|
self.microtask_queues_are_running = true;
|
||||||
|
defer self.microtask_queues_are_running = false;
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < self.context_count) : (i += 1) {
|
||||||
|
const ctx = self.contexts[i];
|
||||||
|
v8.v8__MicrotaskQueue__PerformCheckpoint(ctx.microtask_queue, v8_isolate);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMacrotasks(self: *Env) !?u64 {
|
pub fn runMacrotasks(self: *Env) !?u64 {
|
||||||
var ms_to_next_task: ?u64 = null;
|
var ms_to_next_task: ?u64 = null;
|
||||||
for (self.contexts.items) |ctx| {
|
for (self.contexts[0..self.context_count]) |ctx| {
|
||||||
if (comptime builtin.is_test == false) {
|
if (comptime builtin.is_test == false) {
|
||||||
// I hate this comptime check as much as you do. But we have tests
|
// I hate this comptime check as much as you do. But we have tests
|
||||||
// which rely on short execution before shutdown. In real world, it's
|
// which rely on short execution before shutdown. In real world, it's
|
||||||
@@ -309,12 +390,31 @@ pub fn runMacrotasks(self: *Env) !?u64 {
|
|||||||
return ms_to_next_task;
|
return ms_to_next_task;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pumpMessageLoop(self: *const Env) bool {
|
pub fn pumpMessageLoop(self: *const Env) void {
|
||||||
var hs: v8.HandleScope = undefined;
|
var hs: v8.HandleScope = undefined;
|
||||||
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate.handle);
|
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate.handle);
|
||||||
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||||
|
|
||||||
return v8.v8__Platform__PumpMessageLoop(self.platform.handle, self.isolate.handle, false);
|
const isolate = self.isolate.handle;
|
||||||
|
const platform = self.platform.handle;
|
||||||
|
while (v8.v8__Platform__PumpMessageLoop(platform, isolate, false)) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hasBackgroundTasks(self: *const Env) bool {
|
||||||
|
return v8.v8__Isolate__HasPendingBackgroundTasks(self.isolate.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn waitForBackgroundTasks(self: *Env) void {
|
||||||
|
var hs: v8.HandleScope = undefined;
|
||||||
|
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate.handle);
|
||||||
|
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||||
|
|
||||||
|
const isolate = self.isolate.handle;
|
||||||
|
const platform = self.platform.handle;
|
||||||
|
while (v8.v8__Isolate__HasPendingBackgroundTasks(isolate)) {
|
||||||
|
_ = v8.v8__Platform__PumpMessageLoop(platform, isolate, true);
|
||||||
|
self.runMicrotasks();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runIdleTasks(self: *const Env) void {
|
pub fn runIdleTasks(self: *const Env) void {
|
||||||
@@ -405,3 +505,19 @@ fn oomCallback(c_location: [*c]const u8, details: ?*const v8.OOMDetails) callcon
|
|||||||
log.fatal(.app, "V8 OOM", .{ .location = location, .detail = detail });
|
log.fatal(.app, "V8 OOM", .{ .location = location, .detail = detail });
|
||||||
@import("../../crash_handler.zig").crash("V8 OOM", .{ .location = location, .detail = detail }, @returnAddress());
|
@import("../../crash_handler.zig").crash("V8 OOM", .{ .location = location, .detail = detail }, @returnAddress());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PrivateSymbols = struct {
|
||||||
|
const Private = @import("Private.zig");
|
||||||
|
|
||||||
|
child_nodes: Private,
|
||||||
|
|
||||||
|
fn init(isolate: *v8.Isolate) PrivateSymbols {
|
||||||
|
return .{
|
||||||
|
.child_nodes = Private.init(isolate, "child_nodes"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(self: *PrivateSymbols) void {
|
||||||
|
self.child_nodes.deinit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ handle: v8.HandleScope,
|
|||||||
// value, as v8 will then have taken the address of the function-scopped (and no
|
// value, as v8 will then have taken the address of the function-scopped (and no
|
||||||
// longer valid) local.
|
// longer valid) local.
|
||||||
pub fn init(self: *HandleScope, isolate: js.Isolate) void {
|
pub fn init(self: *HandleScope, isolate: js.Isolate) void {
|
||||||
v8.v8__HandleScope__CONSTRUCT(&self.handle, isolate.handle);
|
self.initWithIsolateHandle(isolate.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initWithIsolateHandle(self: *HandleScope, isolate: *v8.Isolate) void {
|
||||||
|
v8.v8__HandleScope__CONSTRUCT(&self.handle, isolate);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *HandleScope) void {
|
pub fn deinit(self: *HandleScope) void {
|
||||||
|
|||||||
@@ -241,8 +241,6 @@ pub const Session = struct {
|
|||||||
msg.ptr,
|
msg.ptr,
|
||||||
msg.len,
|
msg.len,
|
||||||
);
|
);
|
||||||
|
|
||||||
v8.v8__Isolate__PerformMicrotaskCheckpoint(isolate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets a value by object ID regardless of which context it is in.
|
// Gets a value by object ID regardless of which context it is in.
|
||||||
@@ -364,9 +362,8 @@ pub fn getTaggedOpaque(value: *const v8.Value) ?*TaggedOpaque {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const external_value = v8.v8__Object__GetInternalField(value, 0).?;
|
const tao_ptr = v8.v8__Object__GetAlignedPointerFromInternalField(value, 0).?;
|
||||||
const external_data = v8.v8__External__Value(external_value).?;
|
return @ptrCast(@alignCast(tao_ptr));
|
||||||
return @ptrCast(@alignCast(external_data));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cZigStringToString(s: v8.CZigString) ?[]const u8 {
|
fn cZigStringToString(s: v8.CZigString) ?[]const u8 {
|
||||||
|
|||||||
@@ -41,18 +41,6 @@ pub fn exit(self: Isolate) void {
|
|||||||
v8.v8__Isolate__Exit(self.handle);
|
v8.v8__Isolate__Exit(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn performMicrotasksCheckpoint(self: Isolate) void {
|
|
||||||
v8.v8__Isolate__PerformMicrotaskCheckpoint(self.handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enqueueMicrotask(self: Isolate, callback: anytype, data: anytype) void {
|
|
||||||
v8.v8__Isolate__EnqueueMicrotask(self.handle, callback, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enqueueMicrotaskFunc(self: Isolate, function: js.Function) void {
|
|
||||||
v8.v8__Isolate__EnqueueMicrotaskFunc(self.handle, function.handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lowMemoryNotification(self: Isolate) void {
|
pub fn lowMemoryNotification(self: Isolate) void {
|
||||||
v8.v8__Isolate__LowMemoryNotification(self.handle);
|
v8.v8__Isolate__LowMemoryNotification(self.handle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +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 Page = @import("../Page.zig");
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
const string = @import("../../string.zig");
|
const string = @import("../../string.zig");
|
||||||
|
|
||||||
@@ -75,8 +76,20 @@ pub fn newArray(self: *const Local, len: u32) js.Array {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new typed array. Memory is owned by JS context.
|
||||||
|
/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Typed_arrays
|
||||||
|
pub fn createTypedArray(self: *const Local, comptime array_type: js.ArrayType, size: usize) js.ArrayBufferRef(array_type) {
|
||||||
|
return .init(self, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runMacrotasks(self: *const Local) void {
|
||||||
|
const env = self.ctx.env;
|
||||||
|
env.pumpMessageLoop();
|
||||||
|
env.runMicrotasks(); // macrotasks can cause microtasks to queue
|
||||||
|
}
|
||||||
|
|
||||||
pub fn runMicrotasks(self: *const Local) void {
|
pub fn runMicrotasks(self: *const Local) void {
|
||||||
self.isolate.performMicrotasksCheckpoint();
|
self.ctx.env.runMicrotasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
// == Executors ==
|
// == Executors ==
|
||||||
@@ -181,11 +194,7 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
|
|||||||
.subtype = if (@hasDecl(JsApi.Meta, "subtype")) JsApi.Meta.subype else .node,
|
.subtype = if (@hasDecl(JsApi.Meta, "subtype")) JsApi.Meta.subype else .node,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Skip setting internal field for the global object (Window)
|
v8.v8__Object__SetAlignedPointerInInternalField(js_obj.handle, 0, tao);
|
||||||
// Window accessors get the instance from context.page.window instead
|
|
||||||
// if (resolved.class_id != @import("../webapi/Window.zig").JsApi.Meta.class_id) {
|
|
||||||
v8.v8__Object__SetInternalField(js_obj.handle, 0, isolate.createExternal(tao));
|
|
||||||
// }
|
|
||||||
} else {
|
} else {
|
||||||
// If the struct is empty, we don't need to do all
|
// If the struct is empty, we don't need to do all
|
||||||
// the TOA stuff and setting the internal data.
|
// the TOA stuff and setting the internal data.
|
||||||
@@ -214,7 +223,7 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
|
|||||||
try ctx.finalizer_callbacks.put(ctx.arena, @intFromPtr(resolved.ptr), fc);
|
try ctx.finalizer_callbacks.put(ctx.arena, @intFromPtr(resolved.ptr), fc);
|
||||||
}
|
}
|
||||||
|
|
||||||
conditionallyFlagHandoff(value);
|
conditionallyReference(value);
|
||||||
if (@hasDecl(JsApi.Meta, "weak")) {
|
if (@hasDecl(JsApi.Meta, "weak")) {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
std.debug.assert(JsApi.Meta.weak == true);
|
std.debug.assert(JsApi.Meta.weak == true);
|
||||||
@@ -310,7 +319,17 @@ pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts)
|
|||||||
js.Value => return value,
|
js.Value => return value,
|
||||||
js.Exception => return .{ .local = self, .handle = isolate.throwException(value.handle) },
|
js.Exception => return .{ .local = self, .handle = isolate.throwException(value.handle) },
|
||||||
|
|
||||||
|
js.ArrayBufferRef(.int8).Global, js.ArrayBufferRef(.uint8).Global,
|
||||||
|
js.ArrayBufferRef(.uint8_clamped).Global, js.ArrayBufferRef(.int16).Global,
|
||||||
|
js.ArrayBufferRef(.uint16).Global, js.ArrayBufferRef(.int32).Global,
|
||||||
|
js.ArrayBufferRef(.uint32).Global, js.ArrayBufferRef(.float16).Global,
|
||||||
|
js.ArrayBufferRef(.float32).Global, js.ArrayBufferRef(.float64).Global,
|
||||||
|
=> {
|
||||||
|
return .{ .local = self, .handle = value.local(self).handle };
|
||||||
|
},
|
||||||
|
|
||||||
inline
|
inline
|
||||||
|
js.Array,
|
||||||
js.Function,
|
js.Function,
|
||||||
js.Object,
|
js.Object,
|
||||||
js.Promise,
|
js.Promise,
|
||||||
@@ -442,6 +461,13 @@ pub fn jsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !T {
|
|||||||
return js_val;
|
return js_val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (comptime o.child == js.NullableString) {
|
||||||
|
if (js_val.isUndefined()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return .{ .value = try js_val.toStringSlice() };
|
||||||
|
}
|
||||||
|
|
||||||
if (comptime o.child == js.Object) {
|
if (comptime o.child == js.Object) {
|
||||||
return js.Object{
|
return js.Object{
|
||||||
.local = self,
|
.local = self,
|
||||||
@@ -1043,7 +1069,7 @@ const Resolved = struct {
|
|||||||
class_id: u16,
|
class_id: u16,
|
||||||
prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry,
|
prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry,
|
||||||
finalizer_from_v8: ?*const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void = null,
|
finalizer_from_v8: ?*const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void = null,
|
||||||
finalizer_from_zig: ?*const fn (ptr: *anyopaque) void = null,
|
finalizer_from_zig: ?*const fn (ptr: *anyopaque, page: *Page) void = null,
|
||||||
};
|
};
|
||||||
pub fn resolveValue(value: anytype) Resolved {
|
pub fn resolveValue(value: anytype) Resolved {
|
||||||
const T = bridge.Struct(@TypeOf(value));
|
const T = bridge.Struct(@TypeOf(value));
|
||||||
@@ -1082,14 +1108,14 @@ fn resolveT(comptime T: type, value: *anyopaque) Resolved {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn conditionallyFlagHandoff(value: anytype) void {
|
fn conditionallyReference(value: anytype) void {
|
||||||
const T = bridge.Struct(@TypeOf(value));
|
const T = bridge.Struct(@TypeOf(value));
|
||||||
if (@hasField(T, "_v8_handoff")) {
|
if (@hasDecl(T, "acquireRef")) {
|
||||||
value._v8_handoff = true;
|
value.acquireRef();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (@hasField(T, "_proto")) {
|
if (@hasField(T, "_proto")) {
|
||||||
conditionallyFlagHandoff(value._proto);
|
conditionallyReference(value._proto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1164,6 +1190,9 @@ fn _debugValue(self: *const Local, js_val: js.Value, seen: *std.AutoHashMapUnman
|
|||||||
|
|
||||||
if (js_val.isSymbol()) {
|
if (js_val.isSymbol()) {
|
||||||
const symbol_handle = v8.v8__Symbol__Description(@ptrCast(js_val.handle), self.isolate.handle).?;
|
const symbol_handle = v8.v8__Symbol__Description(@ptrCast(js_val.handle), self.isolate.handle).?;
|
||||||
|
if (v8.v8__Value__IsUndefined(symbol_handle)) {
|
||||||
|
return writer.writeAll("undefined (symbol)");
|
||||||
|
}
|
||||||
return writer.print("{f} (symbol)", .{js.String{ .local = self, .handle = @ptrCast(symbol_handle) }});
|
return writer.print("{f} (symbol)", .{js.String{ .local = self, .handle = @ptrCast(symbol_handle) }});
|
||||||
}
|
}
|
||||||
const js_val_str = try js_val.toStringSlice();
|
const js_val_str = try js_val.toStringSlice();
|
||||||
@@ -1187,13 +1216,20 @@ fn _debugValue(self: *const Local, js_val: js.Value, seen: *std.AutoHashMapUnman
|
|||||||
gop.value_ptr.* = {};
|
gop.value_ptr.* = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const names_arr = js_obj.getOwnPropertyNames();
|
|
||||||
const len = names_arr.len();
|
|
||||||
|
|
||||||
if (depth > 20) {
|
if (depth > 20) {
|
||||||
return writer.writeAll("...deeply nested object...");
|
return writer.writeAll("...deeply nested object...");
|
||||||
}
|
}
|
||||||
const own_len = js_obj.getOwnPropertyNames().len();
|
|
||||||
|
const names_arr = js_obj.getOwnPropertyNames() catch {
|
||||||
|
return writer.writeAll("...invalid object...");
|
||||||
|
};
|
||||||
|
const len = names_arr.len();
|
||||||
|
|
||||||
|
const own_len = blk: {
|
||||||
|
const own_names = js_obj.getOwnPropertyNames() catch break :blk 0;
|
||||||
|
break :blk own_names.len();
|
||||||
|
};
|
||||||
|
|
||||||
if (own_len == 0) {
|
if (own_len == 0) {
|
||||||
const js_val_str = try js_val.toStringSlice();
|
const js_val_str = try js_val.toStringSlice();
|
||||||
if (js_val_str.len > 2000) {
|
if (js_val_str.len > 2000) {
|
||||||
@@ -1312,6 +1348,7 @@ pub const Scope = struct {
|
|||||||
handle_scope: js.HandleScope,
|
handle_scope: js.HandleScope,
|
||||||
|
|
||||||
pub fn deinit(self: *Scope) void {
|
pub fn deinit(self: *Scope) void {
|
||||||
|
v8.v8__Context__Exit(self.local.handle);
|
||||||
self.handle_scope.deinit();
|
self.handle_scope.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,8 +129,14 @@ pub fn isNullOrUndefined(self: Object) bool {
|
|||||||
return v8.v8__Value__IsNullOrUndefined(@ptrCast(self.handle));
|
return v8.v8__Value__IsNullOrUndefined(@ptrCast(self.handle));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getOwnPropertyNames(self: Object) js.Array {
|
pub fn getOwnPropertyNames(self: Object) !js.Array {
|
||||||
const handle = v8.v8__Object__GetOwnPropertyNames(self.handle, self.local.handle).?;
|
const handle = v8.v8__Object__GetOwnPropertyNames(self.handle, self.local.handle) orelse {
|
||||||
|
// This is almost always a fatal error case. Either we're in some exception
|
||||||
|
// and things are messy, or we're shutting down, or someone has messed up
|
||||||
|
// the object (like some WPT tests do).
|
||||||
|
return error.TypeError;
|
||||||
|
};
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.local = self.local,
|
.local = self.local,
|
||||||
.handle = handle,
|
.handle = handle,
|
||||||
@@ -145,8 +151,11 @@ pub fn getPropertyNames(self: Object) js.Array {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nameIterator(self: Object) NameIterator {
|
pub fn nameIterator(self: Object) !NameIterator {
|
||||||
const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle).?;
|
const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle) orelse {
|
||||||
|
// see getOwnPropertyNames above
|
||||||
|
return error.TypeError;
|
||||||
|
};
|
||||||
const count = v8.v8__Array__Length(handle);
|
const count = v8.v8__Array__Length(handle);
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
|
|||||||
42
src/browser/js/Private.zig
Normal file
42
src/browser/js/Private.zig
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Private = @This();
|
||||||
|
|
||||||
|
// Unlike most types, we always store the Private as a Global. It makes more
|
||||||
|
// sense for this type given how it's used.
|
||||||
|
handle: v8.Global,
|
||||||
|
|
||||||
|
pub fn init(isolate: *v8.Isolate, name: []const u8) Private {
|
||||||
|
const v8_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
|
const private_handle = v8.v8__Private__New(isolate, v8_name);
|
||||||
|
|
||||||
|
var global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(isolate, private_handle, &global);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.handle = global,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Private) void {
|
||||||
|
v8.v8__Global__Reset(&self.handle);
|
||||||
|
}
|
||||||
@@ -202,12 +202,16 @@ pub fn create() !Snapshot {
|
|||||||
const name = JsApi.Meta.name;
|
const name = JsApi.Meta.name;
|
||||||
const illegal_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
const illegal_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
var maybe_result2: v8.MaybeBool = undefined;
|
var maybe_result2: v8.MaybeBool = undefined;
|
||||||
v8.v8__Object__Set(global_obj, context, illegal_class_name, func, &maybe_result2);
|
v8.v8__Object__DefineOwnProperty(global_obj, context, illegal_class_name, func, 0, &maybe_result2);
|
||||||
} else {
|
} else {
|
||||||
const name = JsApi.Meta.name;
|
const name = JsApi.Meta.name;
|
||||||
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
var maybe_result: v8.MaybeBool = undefined;
|
var maybe_result: v8.MaybeBool = undefined;
|
||||||
v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result);
|
var properties: v8.PropertyAttribute = v8.None;
|
||||||
|
if (@hasDecl(JsApi.Meta, "enumerable") and JsApi.Meta.enumerable == false) {
|
||||||
|
properties |= v8.DontEnum;
|
||||||
|
}
|
||||||
|
v8.v8__Object__DefineOwnProperty(global_obj, context, v8_class_name, func, properties, &maybe_result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,9 +282,13 @@ fn hasNamedIndexedGetter(comptime JsApi: type) bool {
|
|||||||
fn countExternalReferences() comptime_int {
|
fn countExternalReferences() comptime_int {
|
||||||
@setEvalBranchQuota(100_000);
|
@setEvalBranchQuota(100_000);
|
||||||
|
|
||||||
// +1 for the illegal constructor callback
|
var count: comptime_int = 0;
|
||||||
var count: comptime_int = 1;
|
|
||||||
var has_non_template_property: bool = false;
|
// +1 for the illegal constructor callback shared by various types
|
||||||
|
count += 1;
|
||||||
|
|
||||||
|
// +1 for the noop function shared by various types
|
||||||
|
count += 1;
|
||||||
|
|
||||||
inline for (JsApis) |JsApi| {
|
inline for (JsApis) |JsApi| {
|
||||||
// Constructor (only if explicit)
|
// Constructor (only if explicit)
|
||||||
@@ -300,17 +308,18 @@ fn countExternalReferences() comptime_int {
|
|||||||
const T = @TypeOf(value);
|
const T = @TypeOf(value);
|
||||||
if (T == bridge.Accessor) {
|
if (T == bridge.Accessor) {
|
||||||
count += 1; // getter
|
count += 1; // getter
|
||||||
if (value.setter != null) count += 1; // setter
|
if (value.setter != null) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
} else if (T == bridge.Function) {
|
} else if (T == bridge.Function) {
|
||||||
count += 1;
|
count += 1;
|
||||||
} else if (T == bridge.Property) {
|
|
||||||
if (value.template == false) {
|
|
||||||
has_non_template_property = true;
|
|
||||||
}
|
|
||||||
} else if (T == bridge.Iterator) {
|
} else if (T == bridge.Iterator) {
|
||||||
count += 1;
|
count += 1;
|
||||||
} else if (T == bridge.Indexed) {
|
} else if (T == bridge.Indexed) {
|
||||||
count += 1;
|
count += 1;
|
||||||
|
if (value.enumerator != null) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
} else if (T == bridge.NamedIndexed) {
|
} else if (T == bridge.NamedIndexed) {
|
||||||
count += 1; // getter
|
count += 1; // getter
|
||||||
if (value.setter != null) count += 1;
|
if (value.setter != null) count += 1;
|
||||||
@@ -319,10 +328,6 @@ fn countExternalReferences() comptime_int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (has_non_template_property) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In debug mode, add unknown property callbacks for types without NamedIndexed
|
// In debug mode, add unknown property callbacks for types without NamedIndexed
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
inline for (JsApis) |JsApi| {
|
inline for (JsApis) |JsApi| {
|
||||||
@@ -342,7 +347,8 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
references[idx] = @bitCast(@intFromPtr(&illegalConstructorCallback));
|
references[idx] = @bitCast(@intFromPtr(&illegalConstructorCallback));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
|
|
||||||
var has_non_template_property = false;
|
references[idx] = @bitCast(@intFromPtr(&bridge.Function.noopFunction));
|
||||||
|
idx += 1;
|
||||||
|
|
||||||
inline for (JsApis) |JsApi| {
|
inline for (JsApis) |JsApi| {
|
||||||
if (@hasDecl(JsApi, "constructor")) {
|
if (@hasDecl(JsApi, "constructor")) {
|
||||||
@@ -369,16 +375,16 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
} else if (T == bridge.Function) {
|
} else if (T == bridge.Function) {
|
||||||
references[idx] = @bitCast(@intFromPtr(value.func));
|
references[idx] = @bitCast(@intFromPtr(value.func));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
} else if (T == bridge.Property) {
|
|
||||||
if (value.template == false) {
|
|
||||||
has_non_template_property = true;
|
|
||||||
}
|
|
||||||
} else if (T == bridge.Iterator) {
|
} else if (T == bridge.Iterator) {
|
||||||
references[idx] = @bitCast(@intFromPtr(value.func));
|
references[idx] = @bitCast(@intFromPtr(value.func));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
} else if (T == bridge.Indexed) {
|
} else if (T == bridge.Indexed) {
|
||||||
references[idx] = @bitCast(@intFromPtr(value.getter));
|
references[idx] = @bitCast(@intFromPtr(value.getter));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
|
if (value.enumerator) |enumerator| {
|
||||||
|
references[idx] = @bitCast(@intFromPtr(enumerator));
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
} else if (T == bridge.NamedIndexed) {
|
} else if (T == bridge.NamedIndexed) {
|
||||||
references[idx] = @bitCast(@intFromPtr(value.getter));
|
references[idx] = @bitCast(@intFromPtr(value.getter));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
@@ -394,11 +400,6 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (has_non_template_property) {
|
|
||||||
references[idx] = @bitCast(@intFromPtr(&bridge.Property.getter));
|
|
||||||
idx += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In debug mode, collect unknown property callbacks for types without NamedIndexed
|
// In debug mode, collect unknown property callbacks for types without NamedIndexed
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
inline for (JsApis) |JsApi| {
|
inline for (JsApis) |JsApi| {
|
||||||
@@ -429,9 +430,12 @@ fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionT
|
|||||||
};
|
};
|
||||||
|
|
||||||
const template = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?);
|
const template = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?);
|
||||||
if (!@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
|
{
|
||||||
|
const internal_field_count = comptime countInternalFields(JsApi);
|
||||||
|
if (internal_field_count > 0) {
|
||||||
const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
||||||
v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, 1);
|
v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, internal_field_count);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi);
|
const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi);
|
||||||
const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len));
|
const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len));
|
||||||
@@ -439,10 +443,48 @@ fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionT
|
|||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn countInternalFields(comptime JsApi: type) u8 {
|
||||||
|
var last_used_id = 0;
|
||||||
|
var cache_count: u8 = 0;
|
||||||
|
|
||||||
|
inline for (@typeInfo(JsApi).@"struct".decls) |d| {
|
||||||
|
const name: [:0]const u8 = d.name;
|
||||||
|
const value = @field(JsApi, name);
|
||||||
|
const definition = @TypeOf(value);
|
||||||
|
|
||||||
|
switch (definition) {
|
||||||
|
inline bridge.Accessor, bridge.Function => {
|
||||||
|
const cache = value.cache orelse continue;
|
||||||
|
if (cache != .internal) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// We assert that they are declared in-order. This isn't necessary
|
||||||
|
// but I don't want to do anything fancy to look for gaps or
|
||||||
|
// duplicates.
|
||||||
|
const internal_id = cache.internal;
|
||||||
|
if (internal_id != last_used_id + 1) {
|
||||||
|
@compileError(@typeName(JsApi) ++ "." ++ name ++ " has a non-monotonic cache index");
|
||||||
|
}
|
||||||
|
last_used_id = internal_id;
|
||||||
|
cache_count += 1; // this is just last_used, but it's more explicit this way
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
|
||||||
|
return cache_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need cache_count internal fields, + 1 for the TAO pointer (the v8 -> Zig)
|
||||||
|
// mapping) itself.
|
||||||
|
return cache_count + 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Attaches JsApi members to the prototype template (normal case)
|
// Attaches JsApi members to the prototype template (normal case)
|
||||||
fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.FunctionTemplate) void {
|
fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.FunctionTemplate) void {
|
||||||
const target = v8.v8__FunctionTemplate__PrototypeTemplate(template);
|
|
||||||
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
||||||
|
const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(template);
|
||||||
|
|
||||||
const declarations = @typeInfo(JsApi).@"struct".decls;
|
const declarations = @typeInfo(JsApi).@"struct".decls;
|
||||||
var has_named_index_getter = false;
|
var has_named_index_getter = false;
|
||||||
@@ -460,14 +502,14 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
|||||||
if (value.static) {
|
if (value.static) {
|
||||||
v8.v8__Template__SetAccessorProperty__DEFAULT(@ptrCast(template), js_name, getter_callback);
|
v8.v8__Template__SetAccessorProperty__DEFAULT(@ptrCast(template), js_name, getter_callback);
|
||||||
} else {
|
} else {
|
||||||
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(target, js_name, getter_callback);
|
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(prototype, js_name, getter_callback);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
std.debug.assert(value.static == false);
|
std.debug.assert(value.static == false);
|
||||||
}
|
}
|
||||||
const setter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.setter.? }).?);
|
const setter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.setter.? }).?);
|
||||||
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT2(target, js_name, getter_callback, setter_callback);
|
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT2(prototype, js_name, getter_callback, setter_callback);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bridge.Function => {
|
bridge.Function => {
|
||||||
@@ -476,16 +518,16 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
|||||||
if (value.static) {
|
if (value.static) {
|
||||||
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
|
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
|
||||||
} else {
|
} else {
|
||||||
v8.v8__Template__Set(@ptrCast(target), js_name, @ptrCast(function_template), v8.None);
|
v8.v8__Template__Set(@ptrCast(prototype), js_name, @ptrCast(function_template), v8.None);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bridge.Indexed => {
|
bridge.Indexed => {
|
||||||
var configuration: v8.IndexedPropertyHandlerConfiguration = .{
|
var configuration: v8.IndexedPropertyHandlerConfiguration = .{
|
||||||
.getter = value.getter,
|
.getter = value.getter,
|
||||||
|
.enumerator = value.enumerator,
|
||||||
.setter = null,
|
.setter = null,
|
||||||
.query = null,
|
.query = null,
|
||||||
.deleter = null,
|
.deleter = null,
|
||||||
.enumerator = null,
|
|
||||||
.definer = null,
|
.definer = null,
|
||||||
.descriptor = null,
|
.descriptor = null,
|
||||||
.data = null,
|
.data = null,
|
||||||
@@ -514,7 +556,7 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
|||||||
v8.v8__Symbol__GetAsyncIterator(isolate)
|
v8.v8__Symbol__GetAsyncIterator(isolate)
|
||||||
else
|
else
|
||||||
v8.v8__Symbol__GetIterator(isolate);
|
v8.v8__Symbol__GetIterator(isolate);
|
||||||
v8.v8__Template__Set(@ptrCast(target), js_name, @ptrCast(function_template), v8.None);
|
v8.v8__Template__Set(@ptrCast(prototype), js_name, @ptrCast(function_template), v8.None);
|
||||||
},
|
},
|
||||||
bridge.Property => {
|
bridge.Property => {
|
||||||
const js_value = switch (value.value) {
|
const js_value = switch (value.value) {
|
||||||
@@ -523,22 +565,14 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
|||||||
};
|
};
|
||||||
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
|
|
||||||
if (value.template == false) {
|
{
|
||||||
// not defined on the template, only on the instance. This
|
const flags = if (value.readonly) v8.ReadOnly + v8.DontDelete else 0;
|
||||||
// is like an Accessor, but because the value is known at
|
v8.v8__Template__Set(@ptrCast(prototype), js_name, js_value, flags);
|
||||||
// compile time, we skip _a lot_ of code and quickly return
|
}
|
||||||
// the hard-coded value
|
|
||||||
const getter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{
|
if (value.template) {
|
||||||
.callback = bridge.Property.getter,
|
// apply it both to the type itself (e.g. Node.Elem)
|
||||||
.data = js_value,
|
|
||||||
.side_effect_type = v8.kSideEffectType_HasSideEffectToReceiver,
|
|
||||||
}));
|
|
||||||
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(target, js_name, getter_callback);
|
|
||||||
} else {
|
|
||||||
// apply it both to the type itself
|
|
||||||
v8.v8__Template__Set(@ptrCast(template), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
v8.v8__Template__Set(@ptrCast(template), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
||||||
// and to instances of the type
|
|
||||||
v8.v8__Template__Set(@ptrCast(target), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bridge.Constructor => {}, // already handled in generateConstructor
|
bridge.Constructor => {}, // already handled in generateConstructor
|
||||||
|
|||||||
@@ -95,33 +95,6 @@ pub fn fromJS(comptime R: type, js_obj_handle: *const v8.Object) !R {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const internal_field_count = v8.v8__Object__InternalFieldCount(js_obj_handle);
|
const internal_field_count = v8.v8__Object__InternalFieldCount(js_obj_handle);
|
||||||
// Special case for Window: the global object doesn't have internal fields
|
|
||||||
// Window instance is stored in context.page.window instead
|
|
||||||
if (internal_field_count == 0) {
|
|
||||||
// Normally, this would be an error. All JsObject that map to a Zig type
|
|
||||||
// are either `empty_with_no_proto` (handled above) or have an
|
|
||||||
// interalFieldCount. The only exception to that is the Window...
|
|
||||||
const isolate = v8.v8__Object__GetIsolate(js_obj_handle).?;
|
|
||||||
const context = js.Context.fromIsolate(.{ .handle = isolate });
|
|
||||||
|
|
||||||
const Window = @import("../webapi/Window.zig");
|
|
||||||
if (T == Window) {
|
|
||||||
return context.page.window;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... Or the window's prototype.
|
|
||||||
// We could make this all comptime-fancy, but it's easier to hard-code
|
|
||||||
// the EventTarget
|
|
||||||
|
|
||||||
const EventTarget = @import("../webapi/EventTarget.zig");
|
|
||||||
if (T == EventTarget) {
|
|
||||||
return context.page.window._proto;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type not found in Window's prototype chain
|
|
||||||
return error.InvalidArgument;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if it isn't an empty struct, then the v8.Object should have an
|
// if it isn't an empty struct, then the v8.Object should have an
|
||||||
// InternalFieldCount > 0, since our toa pointer should be embedded
|
// InternalFieldCount > 0, since our toa pointer should be embedded
|
||||||
// at index 0 of the internal field count.
|
// at index 0 of the internal field count.
|
||||||
@@ -133,8 +106,8 @@ pub fn fromJS(comptime R: type, js_obj_handle: *const v8.Object) !R {
|
|||||||
@compileError("unknown Zig type: " ++ @typeName(R));
|
@compileError("unknown Zig type: " ++ @typeName(R));
|
||||||
}
|
}
|
||||||
|
|
||||||
const internal_field_handle = v8.v8__Object__GetInternalField(js_obj_handle, 0).?;
|
const tao_ptr = v8.v8__Object__GetAlignedPointerFromInternalField(js_obj_handle, 0).?;
|
||||||
const tao: *TaggedOpaque = @ptrCast(@alignCast(v8.v8__External__Value(internal_field_handle)));
|
const tao: *TaggedOpaque = @ptrCast(@alignCast(tao_ptr));
|
||||||
const expected_type_index = bridge.JsApiLookup.getId(JsApi);
|
const expected_type_index = bridge.JsApiLookup.getId(JsApi);
|
||||||
|
|
||||||
const prototype_chain = tao.prototype_chain[0..tao.prototype_len];
|
const prototype_chain = tao.prototype_chain[0..tao.prototype_len];
|
||||||
|
|||||||
@@ -134,4 +134,17 @@ pub const Caught = struct {
|
|||||||
try writer.write(prefix ++ ".line", self.line);
|
try writer.write(prefix ++ ".line", self.line);
|
||||||
try writer.write(prefix ++ ".caught", self.caught);
|
try writer.write(prefix ++ ".caught", self.caught);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn jsonStringify(self: Caught, jw: anytype) !void {
|
||||||
|
try jw.beginObject();
|
||||||
|
try jw.objectField("exception");
|
||||||
|
try jw.write(self.exception);
|
||||||
|
try jw.objectField("stack");
|
||||||
|
try jw.write(self.stack);
|
||||||
|
try jw.objectField("line");
|
||||||
|
try jw.write(self.line);
|
||||||
|
try jw.objectField("caught");
|
||||||
|
try jw.write(self.caught);
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,16 +38,16 @@ pub fn Builder(comptime T: type) type {
|
|||||||
return Constructor.init(T, func, opts);
|
return Constructor.init(T, func, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn accessor(comptime getter: anytype, comptime setter: anytype, comptime opts: Accessor.Opts) Accessor {
|
pub fn accessor(comptime getter: anytype, comptime setter: anytype, comptime opts: Caller.Function.Opts) Accessor {
|
||||||
return Accessor.init(T, getter, setter, opts);
|
return Accessor.init(T, getter, setter, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn function(comptime func: anytype, comptime opts: Function.Opts) Function {
|
pub fn function(comptime func: anytype, comptime opts: Caller.Function.Opts) Function {
|
||||||
return Function.init(T, func, opts);
|
return Function.init(T, func, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn indexed(comptime getter_func: anytype, comptime opts: Indexed.Opts) Indexed {
|
pub fn indexed(comptime getter_func: anytype, comptime enumerator_func: anytype, comptime opts: Indexed.Opts) Indexed {
|
||||||
return Indexed.init(T, getter_func, opts);
|
return Indexed.init(T, getter_func, enumerator_func, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn namedIndexed(comptime getter_func: anytype, setter_func: anytype, deleter_func: anytype, comptime opts: NamedIndexed.Opts) NamedIndexed {
|
pub fn namedIndexed(comptime getter_func: anytype, setter_func: anytype, deleter_func: anytype, comptime opts: NamedIndexed.Opts) NamedIndexed {
|
||||||
@@ -104,11 +104,11 @@ pub fn Builder(comptime T: type) type {
|
|||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finalizer(comptime func: *const fn (self: *T, shutdown: bool) void) Finalizer {
|
pub fn finalizer(comptime func: *const fn (self: *T, shutdown: bool, page: *Page) void) Finalizer {
|
||||||
return .{
|
return .{
|
||||||
.from_zig = struct {
|
.from_zig = struct {
|
||||||
fn wrap(ptr: *anyopaque) void {
|
fn wrap(ptr: *anyopaque, page: *Page) void {
|
||||||
func(@ptrCast(@alignCast(ptr)), true);
|
func(@ptrCast(@alignCast(ptr)), true, page);
|
||||||
}
|
}
|
||||||
}.wrap,
|
}.wrap,
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ pub fn Builder(comptime T: type) type {
|
|||||||
const ctx = fc.ctx;
|
const ctx = fc.ctx;
|
||||||
const value_ptr = fc.ptr;
|
const value_ptr = fc.ptr;
|
||||||
if (ctx.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
|
if (ctx.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
|
||||||
func(@ptrCast(@alignCast(value_ptr)), false);
|
func(@ptrCast(@alignCast(value_ptr)), false, ctx.page);
|
||||||
ctx.release(value_ptr);
|
ctx.release(value_ptr);
|
||||||
} else {
|
} else {
|
||||||
// A bit weird, but v8 _requires_ that we release it
|
// A bit weird, but v8 _requires_ that we release it
|
||||||
@@ -160,44 +160,25 @@ pub const Constructor = struct {
|
|||||||
pub const Function = struct {
|
pub const Function = struct {
|
||||||
static: bool,
|
static: bool,
|
||||||
arity: usize,
|
arity: usize,
|
||||||
|
noop: bool = false,
|
||||||
|
cache: ?Caller.Function.Opts.Caching = null,
|
||||||
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
||||||
|
|
||||||
const Opts = struct {
|
fn init(comptime T: type, comptime func: anytype, comptime opts: Caller.Function.Opts) Function {
|
||||||
static: bool = false,
|
|
||||||
dom_exception: bool = false,
|
|
||||||
as_typed_array: bool = false,
|
|
||||||
null_as_undefined: bool = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Function {
|
|
||||||
return .{
|
return .{
|
||||||
|
.cache = opts.cache,
|
||||||
.static = opts.static,
|
.static = opts.static,
|
||||||
.arity = getArity(@TypeOf(func)),
|
.arity = getArity(@TypeOf(func)),
|
||||||
.func = struct {
|
.func = if (opts.noop) noopFunction else struct {
|
||||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
Caller.Function.call(T, handle.?, func, opts);
|
||||||
var caller: Caller = undefined;
|
|
||||||
caller.init(v8_isolate);
|
|
||||||
defer caller.deinit();
|
|
||||||
|
|
||||||
if (comptime opts.static) {
|
|
||||||
caller.function(T, func, handle.?, .{
|
|
||||||
.dom_exception = opts.dom_exception,
|
|
||||||
.as_typed_array = opts.as_typed_array,
|
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
caller.method(T, func, handle.?, .{
|
|
||||||
.dom_exception = opts.dom_exception,
|
|
||||||
.as_typed_array = opts.as_typed_array,
|
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.wrap,
|
}.wrap,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn noopFunction(_: ?*const v8.FunctionCallbackInfo) callconv(.c) void {}
|
||||||
|
|
||||||
fn getArity(comptime T: type) usize {
|
fn getArity(comptime T: type) usize {
|
||||||
var count: usize = 0;
|
var count: usize = 0;
|
||||||
var params = @typeInfo(T).@"fn".params;
|
var params = @typeInfo(T).@"fn".params;
|
||||||
@@ -217,42 +198,20 @@ pub const Function = struct {
|
|||||||
|
|
||||||
pub const Accessor = struct {
|
pub const Accessor = struct {
|
||||||
static: bool = false,
|
static: bool = false,
|
||||||
|
cache: ?Caller.Function.Opts.Caching = null,
|
||||||
getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
|
getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
|
||||||
setter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
|
setter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
|
||||||
|
|
||||||
const Opts = struct {
|
fn init(comptime T: type, comptime getter: anytype, comptime setter: anytype, comptime opts: Caller.Function.Opts) Accessor {
|
||||||
static: bool = false,
|
|
||||||
as_typed_array: bool = false,
|
|
||||||
null_as_undefined: bool = false,
|
|
||||||
dom_exception: bool = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn init(comptime T: type, comptime getter: anytype, comptime setter: anytype, comptime opts: Opts) Accessor {
|
|
||||||
var accessor = Accessor{
|
var accessor = Accessor{
|
||||||
|
.cache = opts.cache,
|
||||||
.static = opts.static,
|
.static = opts.static,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (@typeInfo(@TypeOf(getter)) != .null) {
|
if (@typeInfo(@TypeOf(getter)) != .null) {
|
||||||
accessor.getter = struct {
|
accessor.getter = struct {
|
||||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
Caller.Function.call(T, handle.?, getter, opts);
|
||||||
var caller: Caller = undefined;
|
|
||||||
caller.init(v8_isolate);
|
|
||||||
defer caller.deinit();
|
|
||||||
|
|
||||||
if (comptime opts.static) {
|
|
||||||
caller.function(T, getter, handle.?, .{
|
|
||||||
.dom_exception = opts.dom_exception,
|
|
||||||
.as_typed_array = opts.as_typed_array,
|
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
caller.method(T, getter, handle.?, .{
|
|
||||||
.dom_exception = opts.dom_exception,
|
|
||||||
.as_typed_array = opts.as_typed_array,
|
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.wrap;
|
}.wrap;
|
||||||
}
|
}
|
||||||
@@ -260,16 +219,7 @@ pub const Accessor = struct {
|
|||||||
if (@typeInfo(@TypeOf(setter)) != .null) {
|
if (@typeInfo(@TypeOf(setter)) != .null) {
|
||||||
accessor.setter = struct {
|
accessor.setter = struct {
|
||||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
Caller.Function.call(T, handle.?, setter, opts);
|
||||||
var caller: Caller = undefined;
|
|
||||||
caller.init(v8_isolate);
|
|
||||||
defer caller.deinit();
|
|
||||||
|
|
||||||
caller.method(T, setter, handle.?, .{
|
|
||||||
.dom_exception = opts.dom_exception,
|
|
||||||
.as_typed_array = opts.as_typed_array,
|
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}.wrap;
|
}.wrap;
|
||||||
}
|
}
|
||||||
@@ -280,14 +230,17 @@ pub const Accessor = struct {
|
|||||||
|
|
||||||
pub const Indexed = struct {
|
pub const Indexed = struct {
|
||||||
getter: *const fn (idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
|
getter: *const fn (idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
|
||||||
|
enumerator: ?*const fn (handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
|
||||||
|
|
||||||
const Opts = struct {
|
const Opts = struct {
|
||||||
as_typed_array: bool = false,
|
as_typed_array: bool = false,
|
||||||
null_as_undefined: bool = false,
|
null_as_undefined: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn init(comptime T: type, comptime getter: anytype, comptime opts: Opts) Indexed {
|
fn init(comptime T: type, comptime getter: anytype, comptime enumerator: anytype, comptime opts: Opts) Indexed {
|
||||||
return .{ .getter = struct {
|
var indexed = Indexed{
|
||||||
|
.enumerator = null,
|
||||||
|
.getter = struct {
|
||||||
fn wrap(idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
fn wrap(idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||||
var caller: Caller = undefined;
|
var caller: Caller = undefined;
|
||||||
@@ -299,7 +252,22 @@ pub const Indexed = struct {
|
|||||||
.null_as_undefined = opts.null_as_undefined,
|
.null_as_undefined = opts.null_as_undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}.wrap };
|
}.wrap,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (@typeInfo(@TypeOf(enumerator)) != .null) {
|
||||||
|
indexed.enumerator = struct {
|
||||||
|
fn wrap(handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
|
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||||
|
var caller: Caller = undefined;
|
||||||
|
caller.init(v8_isolate);
|
||||||
|
defer caller.deinit();
|
||||||
|
return caller.getEnumerator(T, enumerator, handle.?, .{});
|
||||||
|
}
|
||||||
|
}.wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexed;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -390,11 +358,9 @@ pub const Iterator = struct {
|
|||||||
.async = opts.async,
|
.async = opts.async,
|
||||||
.func = struct {
|
.func = struct {
|
||||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
return Caller.Function.call(T, handle.?, struct_or_func, .{
|
||||||
var caller: Caller = undefined;
|
.null_as_undefined = opts.null_as_undefined,
|
||||||
caller.init(v8_isolate);
|
});
|
||||||
defer caller.deinit();
|
|
||||||
caller.method(T, struct_or_func, handle.?, .{});
|
|
||||||
}
|
}
|
||||||
}.wrap,
|
}.wrap,
|
||||||
};
|
};
|
||||||
@@ -411,12 +377,7 @@ pub const Callable = struct {
|
|||||||
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Callable {
|
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Callable {
|
||||||
return .{ .func = struct {
|
return .{ .func = struct {
|
||||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
Caller.Function.call(T, handle.?, func, .{
|
||||||
var caller: Caller = undefined;
|
|
||||||
caller.init(v8_isolate);
|
|
||||||
defer caller.deinit();
|
|
||||||
|
|
||||||
caller.method(T, func, handle.?, .{
|
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
.null_as_undefined = opts.null_as_undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -427,6 +388,7 @@ pub const Callable = struct {
|
|||||||
pub const Property = struct {
|
pub const Property = struct {
|
||||||
value: Value,
|
value: Value,
|
||||||
template: bool,
|
template: bool,
|
||||||
|
readonly: bool,
|
||||||
|
|
||||||
const Value = union(enum) {
|
const Value = union(enum) {
|
||||||
null,
|
null,
|
||||||
@@ -438,27 +400,22 @@ pub const Property = struct {
|
|||||||
|
|
||||||
const Opts = struct {
|
const Opts = struct {
|
||||||
template: bool,
|
template: bool,
|
||||||
|
readonly: bool = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn init(value: Value, opts: Opts) Property {
|
fn init(value: Value, opts: Opts) Property {
|
||||||
return .{
|
return .{
|
||||||
.value = value,
|
.value = value,
|
||||||
.template = opts.template,
|
.template = opts.template,
|
||||||
|
.readonly = opts.readonly,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getter(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
|
||||||
const value = v8.v8__FunctionCallbackInfo__Data(handle.?);
|
|
||||||
var rv: v8.ReturnValue = undefined;
|
|
||||||
v8.v8__FunctionCallbackInfo__GetReturnValue(handle.?, &rv);
|
|
||||||
v8.v8__ReturnValue__Set(rv, value);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Finalizer = struct {
|
const Finalizer = struct {
|
||||||
// The finalizer wrapper when called fro Zig. This is only called on
|
// The finalizer wrapper when called fro Zig. This is only called on
|
||||||
// Context.deinit
|
// Context.deinit
|
||||||
from_zig: *const fn (ctx: *anyopaque) void,
|
from_zig: *const fn (ctx: *anyopaque, page: *Page) void,
|
||||||
|
|
||||||
// The finalizer wrapper when called from V8. This may never be called
|
// The finalizer wrapper when called from V8. This may never be called
|
||||||
// (hence why we fallback to calling in Context.denit). If it is called,
|
// (hence why we fallback to calling in Context.denit). If it is called,
|
||||||
@@ -773,6 +730,7 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/css/CSSStyleRule.zig"),
|
@import("../webapi/css/CSSStyleRule.zig"),
|
||||||
@import("../webapi/css/CSSStyleSheet.zig"),
|
@import("../webapi/css/CSSStyleSheet.zig"),
|
||||||
@import("../webapi/css/CSSStyleProperties.zig"),
|
@import("../webapi/css/CSSStyleProperties.zig"),
|
||||||
|
@import("../webapi/css/FontFaceSet.zig"),
|
||||||
@import("../webapi/css/MediaQueryList.zig"),
|
@import("../webapi/css/MediaQueryList.zig"),
|
||||||
@import("../webapi/css/StyleSheetList.zig"),
|
@import("../webapi/css/StyleSheetList.zig"),
|
||||||
@import("../webapi/Document.zig"),
|
@import("../webapi/Document.zig"),
|
||||||
@@ -811,6 +769,7 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/element/html/DataList.zig"),
|
@import("../webapi/element/html/DataList.zig"),
|
||||||
@import("../webapi/element/html/Dialog.zig"),
|
@import("../webapi/element/html/Dialog.zig"),
|
||||||
@import("../webapi/element/html/Directory.zig"),
|
@import("../webapi/element/html/Directory.zig"),
|
||||||
|
@import("../webapi/element/html/DList.zig"),
|
||||||
@import("../webapi/element/html/Div.zig"),
|
@import("../webapi/element/html/Div.zig"),
|
||||||
@import("../webapi/element/html/Embed.zig"),
|
@import("../webapi/element/html/Embed.zig"),
|
||||||
@import("../webapi/element/html/FieldSet.zig"),
|
@import("../webapi/element/html/FieldSet.zig"),
|
||||||
@@ -880,6 +839,9 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/event/MouseEvent.zig"),
|
@import("../webapi/event/MouseEvent.zig"),
|
||||||
@import("../webapi/event/PointerEvent.zig"),
|
@import("../webapi/event/PointerEvent.zig"),
|
||||||
@import("../webapi/event/KeyboardEvent.zig"),
|
@import("../webapi/event/KeyboardEvent.zig"),
|
||||||
|
@import("../webapi/event/FocusEvent.zig"),
|
||||||
|
@import("../webapi/event/WheelEvent.zig"),
|
||||||
|
@import("../webapi/event/TextEvent.zig"),
|
||||||
@import("../webapi/event/PromiseRejectionEvent.zig"),
|
@import("../webapi/event/PromiseRejectionEvent.zig"),
|
||||||
@import("../webapi/MessageChannel.zig"),
|
@import("../webapi/MessageChannel.zig"),
|
||||||
@import("../webapi/MessagePort.zig"),
|
@import("../webapi/MessagePort.zig"),
|
||||||
@@ -913,15 +875,18 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/IdleDeadline.zig"),
|
@import("../webapi/IdleDeadline.zig"),
|
||||||
@import("../webapi/Blob.zig"),
|
@import("../webapi/Blob.zig"),
|
||||||
@import("../webapi/File.zig"),
|
@import("../webapi/File.zig"),
|
||||||
|
@import("../webapi/FileReader.zig"),
|
||||||
@import("../webapi/Screen.zig"),
|
@import("../webapi/Screen.zig"),
|
||||||
@import("../webapi/VisualViewport.zig"),
|
@import("../webapi/VisualViewport.zig"),
|
||||||
@import("../webapi/PerformanceObserver.zig"),
|
@import("../webapi/PerformanceObserver.zig"),
|
||||||
@import("../webapi/navigation/Navigation.zig"),
|
@import("../webapi/navigation/Navigation.zig"),
|
||||||
@import("../webapi/navigation/NavigationEventTarget.zig"),
|
|
||||||
@import("../webapi/navigation/NavigationHistoryEntry.zig"),
|
@import("../webapi/navigation/NavigationHistoryEntry.zig"),
|
||||||
@import("../webapi/navigation/NavigationActivation.zig"),
|
@import("../webapi/navigation/NavigationActivation.zig"),
|
||||||
@import("../webapi/canvas/CanvasRenderingContext2D.zig"),
|
@import("../webapi/canvas/CanvasRenderingContext2D.zig"),
|
||||||
@import("../webapi/canvas/WebGLRenderingContext.zig"),
|
@import("../webapi/canvas/WebGLRenderingContext.zig"),
|
||||||
|
@import("../webapi/canvas/OffscreenCanvas.zig"),
|
||||||
|
@import("../webapi/canvas/OffscreenCanvasRenderingContext2D.zig"),
|
||||||
@import("../webapi/SubtleCrypto.zig"),
|
@import("../webapi/SubtleCrypto.zig"),
|
||||||
@import("../webapi/Selection.zig"),
|
@import("../webapi/Selection.zig"),
|
||||||
|
@import("../webapi/ImageData.zig"),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -77,6 +77,107 @@ pub const ArrayBuffer = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const ArrayType = enum(u8) {
|
||||||
|
int8,
|
||||||
|
uint8,
|
||||||
|
uint8_clamped,
|
||||||
|
int16,
|
||||||
|
uint16,
|
||||||
|
int32,
|
||||||
|
uint32,
|
||||||
|
float16,
|
||||||
|
float32,
|
||||||
|
float64,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn ArrayBufferRef(comptime kind: ArrayType) type {
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
const BackingInt = switch (kind) {
|
||||||
|
.int8 => i8,
|
||||||
|
.uint8, .uint8_clamped => u8,
|
||||||
|
.int16 => i16,
|
||||||
|
.uint16 => u16,
|
||||||
|
.int32 => i32,
|
||||||
|
.uint32 => u32,
|
||||||
|
.float16 => f16,
|
||||||
|
.float32 => f32,
|
||||||
|
.float64 => f64,
|
||||||
|
};
|
||||||
|
|
||||||
|
local: *const Local,
|
||||||
|
handle: *const v8.Value,
|
||||||
|
|
||||||
|
/// Persisted typed array.
|
||||||
|
pub const Global = struct {
|
||||||
|
handle: v8.Global,
|
||||||
|
|
||||||
|
pub fn deinit(self: *Global) void {
|
||||||
|
v8.v8__Global__Reset(&self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local(self: *const Global, l: *const Local) Self {
|
||||||
|
return .{ .local = l, .handle = v8.v8__Global__Get(&self.handle, l.isolate.handle).? };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(local: *const Local, size: usize) Self {
|
||||||
|
const ctx = local.ctx;
|
||||||
|
const isolate = ctx.isolate;
|
||||||
|
const bits = switch (@typeInfo(BackingInt)) {
|
||||||
|
.int => |n| n.bits,
|
||||||
|
.float => |f| f.bits,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
|
||||||
|
var array_buffer: *const v8.ArrayBuffer = undefined;
|
||||||
|
if (size == 0) {
|
||||||
|
array_buffer = v8.v8__ArrayBuffer__New(isolate.handle, 0).?;
|
||||||
|
} else {
|
||||||
|
const buffer_len = size * bits / 8;
|
||||||
|
const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, buffer_len).?;
|
||||||
|
const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
|
||||||
|
array_buffer = v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handle: *const v8.Value = switch (comptime kind) {
|
||||||
|
.int8 => @ptrCast(v8.v8__Int8Array__New(array_buffer, 0, size).?),
|
||||||
|
.uint8 => @ptrCast(v8.v8__Uint8Array__New(array_buffer, 0, size).?),
|
||||||
|
.uint8_clamped => @ptrCast(v8.v8__Uint8ClampedArray__New(array_buffer, 0, size).?),
|
||||||
|
.int16 => @ptrCast(v8.v8__Int16Array__New(array_buffer, 0, size).?),
|
||||||
|
.uint16 => @ptrCast(v8.v8__Uint16Array__New(array_buffer, 0, size).?),
|
||||||
|
.int32 => @ptrCast(v8.v8__Int32Array__New(array_buffer, 0, size).?),
|
||||||
|
.uint32 => @ptrCast(v8.v8__Uint32Array__New(array_buffer, 0, size).?),
|
||||||
|
.float16 => @ptrCast(v8.v8__Float16Array__New(array_buffer, 0, size).?),
|
||||||
|
.float32 => @ptrCast(v8.v8__Float32Array__New(array_buffer, 0, size).?),
|
||||||
|
.float64 => @ptrCast(v8.v8__Float64Array__New(array_buffer, 0, size).?),
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{ .local = local, .handle = handle };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn persist(self: *const Self) !Global {
|
||||||
|
var ctx = self.local.ctx;
|
||||||
|
var global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
|
try ctx.global_values.append(ctx.arena, global);
|
||||||
|
|
||||||
|
return .{ .handle = global };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a WebAPI takes a []const u8, then we'll coerce any JS value to that string
|
||||||
|
// so null -> "null". But if a WebAPI takes an optional string, ?[]const u8,
|
||||||
|
// how should we handle null? If the parameter _isn't_ passed, then it's obvious
|
||||||
|
// that it should be null, but what if `null` is passed? It's ambiguous, should
|
||||||
|
// that be null, or "null"? It could depend on the api. So, `null` passed to
|
||||||
|
// ?[]const u8 will be `null`. If you want it to be "null", use a `.js.NullableString`.
|
||||||
|
pub const NullableString = struct {
|
||||||
|
value: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
pub const Exception = struct {
|
pub const Exception = struct {
|
||||||
local: *const Local,
|
local: *const Local,
|
||||||
handle: *const v8.Value,
|
handle: *const v8.Value,
|
||||||
@@ -134,8 +235,10 @@ pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool,
|
|||||||
const values = value.values;
|
const values = value.values;
|
||||||
const len = values.len;
|
const len = values.len;
|
||||||
const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, len);
|
const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, len);
|
||||||
|
if (len > 0) {
|
||||||
const data: [*]u8 = @ptrCast(@alignCast(v8.v8__BackingStore__Data(backing_store)));
|
const data: [*]u8 = @ptrCast(@alignCast(v8.v8__BackingStore__Data(backing_store)));
|
||||||
@memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]);
|
@memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]);
|
||||||
|
}
|
||||||
const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
|
const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
|
||||||
return @ptrCast(v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?);
|
return @ptrCast(v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?);
|
||||||
},
|
},
|
||||||
|
|||||||
607
src/browser/markdown.zig
Normal file
607
src/browser/markdown.zig
Normal file
@@ -0,0 +1,607 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
const CData = @import("webapi/CData.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
|
||||||
|
pub const Opts = struct {
|
||||||
|
// Options for future customization (e.g., dialect)
|
||||||
|
};
|
||||||
|
|
||||||
|
const State = struct {
|
||||||
|
const ListType = enum { ordered, unordered };
|
||||||
|
const ListState = struct {
|
||||||
|
type: ListType,
|
||||||
|
index: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
list_depth: usize = 0,
|
||||||
|
list_stack: [32]ListState = undefined,
|
||||||
|
pre_node: ?*Node = null,
|
||||||
|
in_code: bool = false,
|
||||||
|
in_table: bool = false,
|
||||||
|
table_row_index: usize = 0,
|
||||||
|
table_col_count: usize = 0,
|
||||||
|
last_char_was_newline: bool = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn isBlock(tag: Element.Tag) bool {
|
||||||
|
return switch (tag) {
|
||||||
|
.p, .div, .section, .article, .main, .header, .footer, .nav, .aside, .h1, .h2, .h3, .h4, .h5, .h6, .ul, .ol, .blockquote, .pre, .table, .hr => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shouldAddSpacing(tag: Element.Tag) bool {
|
||||||
|
return switch (tag) {
|
||||||
|
.p, .h1, .h2, .h3, .h4, .h5, .h6, .blockquote, .pre, .table => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isLayoutBlock(tag: Element.Tag) bool {
|
||||||
|
return switch (tag) {
|
||||||
|
.main, .section, .article, .nav, .aside, .header, .footer, .div, .ul, .ol => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isStandaloneAnchor(el: *Element) bool {
|
||||||
|
const node = el.asNode();
|
||||||
|
const parent = node.parentNode() orelse return false;
|
||||||
|
const parent_el = parent.is(Element) orelse return false;
|
||||||
|
|
||||||
|
if (!isLayoutBlock(parent_el.getTag())) return false;
|
||||||
|
|
||||||
|
var prev = node.previousSibling();
|
||||||
|
while (prev) |p| : (prev = p.previousSibling()) {
|
||||||
|
if (isSignificantText(p)) return false;
|
||||||
|
if (p.is(Element)) |pe| {
|
||||||
|
if (isVisibleElement(pe)) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var next = node.nextSibling();
|
||||||
|
while (next) |n| : (next = n.nextSibling()) {
|
||||||
|
if (isSignificantText(n)) return false;
|
||||||
|
if (n.is(Element)) |ne| {
|
||||||
|
if (isVisibleElement(ne)) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isSignificantText(node: *Node) bool {
|
||||||
|
const text = node.is(Node.CData.Text) orelse return false;
|
||||||
|
return !isAllWhitespace(text.getWholeText());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isVisibleElement(el: *Element) bool {
|
||||||
|
return switch (el.getTag()) {
|
||||||
|
.script, .style, .noscript, .template, .head, .meta, .link, .title, .svg => false,
|
||||||
|
else => true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isAllWhitespace(text: []const u8) bool {
|
||||||
|
return for (text) |c| {
|
||||||
|
if (!std.ascii.isWhitespace(c)) break false;
|
||||||
|
} else true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hasBlockDescendant(node: *Node) bool {
|
||||||
|
var it = node.childrenIterator();
|
||||||
|
return while (it.next()) |child| {
|
||||||
|
if (child.is(Element)) |el| {
|
||||||
|
if (isBlock(el.getTag())) break true;
|
||||||
|
if (hasBlockDescendant(child)) break true;
|
||||||
|
}
|
||||||
|
} else false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensureNewline(state: *State, writer: *std.Io.Writer) !void {
|
||||||
|
if (!state.last_char_was_newline) {
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
state.last_char_was_newline = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
|
_ = opts;
|
||||||
|
var state = State{};
|
||||||
|
try render(node, &state, writer, page);
|
||||||
|
if (!state.last_char_was_newline) {
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(node: *Node, state: *State, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {
|
||||||
|
switch (node._type) {
|
||||||
|
.document, .document_fragment => {
|
||||||
|
try renderChildren(node, state, writer, page);
|
||||||
|
},
|
||||||
|
.element => |el| {
|
||||||
|
try renderElement(el, state, writer, page);
|
||||||
|
},
|
||||||
|
.cdata => |cd| {
|
||||||
|
if (node.is(Node.CData.Text)) |_| {
|
||||||
|
var text = cd.getData().str();
|
||||||
|
if (state.pre_node) |pre| {
|
||||||
|
if (node.parentNode() == pre and node.nextSibling() == null) {
|
||||||
|
text = std.mem.trimRight(u8, text, " \t\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try renderText(text, state, writer);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderChildren(parent: *Node, state: *State, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
|
var it = parent.childrenIterator();
|
||||||
|
while (it.next()) |child| {
|
||||||
|
try render(child, state, writer, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderElement(el: *Element, state: *State, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
|
const tag = el.getTag();
|
||||||
|
|
||||||
|
if (!isVisibleElement(el)) return;
|
||||||
|
|
||||||
|
// --- Opening Tag Logic ---
|
||||||
|
|
||||||
|
// Ensure block elements start on a new line (double newline for paragraphs etc)
|
||||||
|
if (isBlock(tag) and !state.in_table) {
|
||||||
|
try ensureNewline(state, writer);
|
||||||
|
if (shouldAddSpacing(tag)) {
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
} else if (tag == .li or tag == .tr) {
|
||||||
|
try ensureNewline(state, writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefixes
|
||||||
|
switch (tag) {
|
||||||
|
.h1 => try writer.writeAll("# "),
|
||||||
|
.h2 => try writer.writeAll("## "),
|
||||||
|
.h3 => try writer.writeAll("### "),
|
||||||
|
.h4 => try writer.writeAll("#### "),
|
||||||
|
.h5 => try writer.writeAll("##### "),
|
||||||
|
.h6 => try writer.writeAll("###### "),
|
||||||
|
.ul => {
|
||||||
|
if (state.list_depth < state.list_stack.len) {
|
||||||
|
state.list_stack[state.list_depth] = .{ .type = .unordered, .index = 0 };
|
||||||
|
state.list_depth += 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.ol => {
|
||||||
|
if (state.list_depth < state.list_stack.len) {
|
||||||
|
state.list_stack[state.list_depth] = .{ .type = .ordered, .index = 1 };
|
||||||
|
state.list_depth += 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.li => {
|
||||||
|
const indent = if (state.list_depth > 0) state.list_depth - 1 else 0;
|
||||||
|
for (0..indent) |_| try writer.writeAll(" ");
|
||||||
|
|
||||||
|
if (state.list_depth > 0 and state.list_stack[state.list_depth - 1].type == .ordered) {
|
||||||
|
const current_list = &state.list_stack[state.list_depth - 1];
|
||||||
|
try writer.print("{d}. ", .{current_list.index});
|
||||||
|
current_list.index += 1;
|
||||||
|
} else {
|
||||||
|
try writer.writeAll("- ");
|
||||||
|
}
|
||||||
|
state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.table => {
|
||||||
|
state.in_table = true;
|
||||||
|
state.table_row_index = 0;
|
||||||
|
state.table_col_count = 0;
|
||||||
|
},
|
||||||
|
.tr => {
|
||||||
|
state.table_col_count = 0;
|
||||||
|
try writer.writeByte('|');
|
||||||
|
},
|
||||||
|
.td, .th => {
|
||||||
|
// Note: leading pipe handled by previous cell closing or tr opening
|
||||||
|
state.last_char_was_newline = false;
|
||||||
|
try writer.writeByte(' ');
|
||||||
|
},
|
||||||
|
.blockquote => {
|
||||||
|
try writer.writeAll("> ");
|
||||||
|
state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.pre => {
|
||||||
|
try writer.writeAll("```\n");
|
||||||
|
state.pre_node = el.asNode();
|
||||||
|
state.last_char_was_newline = true;
|
||||||
|
},
|
||||||
|
.code => {
|
||||||
|
if (state.pre_node == null) {
|
||||||
|
try writer.writeByte('`');
|
||||||
|
state.in_code = true;
|
||||||
|
state.last_char_was_newline = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.b, .strong => {
|
||||||
|
try writer.writeAll("**");
|
||||||
|
state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.i, .em => {
|
||||||
|
try writer.writeAll("*");
|
||||||
|
state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.s, .del => {
|
||||||
|
try writer.writeAll("~~");
|
||||||
|
state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.hr => {
|
||||||
|
try writer.writeAll("---\n");
|
||||||
|
state.last_char_was_newline = true;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.br => {
|
||||||
|
if (state.in_table) {
|
||||||
|
try writer.writeByte(' ');
|
||||||
|
} else {
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
state.last_char_was_newline = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.img => {
|
||||||
|
try writer.writeAll(";
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("src"))) |src| {
|
||||||
|
try writer.writeAll(src);
|
||||||
|
}
|
||||||
|
try writer.writeAll(")");
|
||||||
|
state.last_char_was_newline = false;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.anchor => {
|
||||||
|
const has_block = hasBlockDescendant(el.asNode());
|
||||||
|
if (has_block) {
|
||||||
|
try renderChildren(el.asNode(), state, writer, page);
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("href"))) |href| {
|
||||||
|
if (!state.last_char_was_newline) try writer.writeByte('\n');
|
||||||
|
try writer.writeAll("([Link](");
|
||||||
|
try writer.writeAll(href);
|
||||||
|
try writer.writeAll("))\n");
|
||||||
|
state.last_char_was_newline = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStandaloneAnchor(el)) {
|
||||||
|
if (!state.last_char_was_newline) try writer.writeByte('\n');
|
||||||
|
try writer.writeByte('[');
|
||||||
|
try renderChildren(el.asNode(), state, writer, page);
|
||||||
|
try writer.writeAll("](");
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("href"))) |href| {
|
||||||
|
try writer.writeAll(href);
|
||||||
|
}
|
||||||
|
try writer.writeAll(")\n");
|
||||||
|
state.last_char_was_newline = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try writer.writeByte('[');
|
||||||
|
try renderChildren(el.asNode(), state, writer, page);
|
||||||
|
try writer.writeAll("](");
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("href"))) |href| {
|
||||||
|
try writer.writeAll(href);
|
||||||
|
}
|
||||||
|
try writer.writeByte(')');
|
||||||
|
state.last_char_was_newline = false;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.input => {
|
||||||
|
const type_attr = el.getAttributeSafe(comptime .wrap("type")) orelse return;
|
||||||
|
if (std.ascii.eqlIgnoreCase(type_attr, "checkbox")) {
|
||||||
|
const checked = el.getAttributeSafe(comptime .wrap("checked")) != null;
|
||||||
|
try writer.writeAll(if (checked) "[x] " else "[ ] ");
|
||||||
|
state.last_char_was_newline = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Render Children ---
|
||||||
|
try renderChildren(el.asNode(), state, writer, page);
|
||||||
|
|
||||||
|
// --- Closing Tag Logic ---
|
||||||
|
|
||||||
|
// Suffixes
|
||||||
|
switch (tag) {
|
||||||
|
.pre => {
|
||||||
|
if (!state.last_char_was_newline) {
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
try writer.writeAll("```\n");
|
||||||
|
state.pre_node = null;
|
||||||
|
state.last_char_was_newline = true;
|
||||||
|
},
|
||||||
|
.code => {
|
||||||
|
if (state.pre_node == null) {
|
||||||
|
try writer.writeByte('`');
|
||||||
|
state.in_code = false;
|
||||||
|
state.last_char_was_newline = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.b, .strong => {
|
||||||
|
try writer.writeAll("**");
|
||||||
|
state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.i, .em => {
|
||||||
|
try writer.writeAll("*");
|
||||||
|
state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.s, .del => {
|
||||||
|
try writer.writeAll("~~");
|
||||||
|
state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.blockquote => {},
|
||||||
|
.ul, .ol => {
|
||||||
|
if (state.list_depth > 0) state.list_depth -= 1;
|
||||||
|
},
|
||||||
|
.table => {
|
||||||
|
state.in_table = false;
|
||||||
|
},
|
||||||
|
.tr => {
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
if (state.table_row_index == 0) {
|
||||||
|
try writer.writeByte('|');
|
||||||
|
for (0..state.table_col_count) |_| {
|
||||||
|
try writer.writeAll("---|");
|
||||||
|
}
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
state.table_row_index += 1;
|
||||||
|
state.last_char_was_newline = true;
|
||||||
|
},
|
||||||
|
.td, .th => {
|
||||||
|
try writer.writeAll(" |");
|
||||||
|
state.table_col_count += 1;
|
||||||
|
state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post-block newlines
|
||||||
|
if (isBlock(tag) and !state.in_table) {
|
||||||
|
try ensureNewline(state, writer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderText(text: []const u8, state: *State, writer: *std.Io.Writer) !void {
|
||||||
|
if (text.len == 0) return;
|
||||||
|
|
||||||
|
if (state.pre_node) |_| {
|
||||||
|
try writer.writeAll(text);
|
||||||
|
state.last_char_was_newline = text[text.len - 1] == '\n';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for pure whitespace
|
||||||
|
if (isAllWhitespace(text)) {
|
||||||
|
if (!state.last_char_was_newline) {
|
||||||
|
try writer.writeByte(' ');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapse whitespace
|
||||||
|
var it = std.mem.tokenizeAny(u8, text, " \t\n\r");
|
||||||
|
var first = true;
|
||||||
|
while (it.next()) |word| {
|
||||||
|
if (!first or (!state.last_char_was_newline and std.ascii.isWhitespace(text[0]))) {
|
||||||
|
try writer.writeByte(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
try escapeMarkdown(writer, word);
|
||||||
|
state.last_char_was_newline = false;
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle trailing whitespace from the original text
|
||||||
|
if (!first and !state.last_char_was_newline and std.ascii.isWhitespace(text[text.len - 1])) {
|
||||||
|
try writer.writeByte(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escapeMarkdown(writer: *std.Io.Writer, text: []const u8) !void {
|
||||||
|
for (text) |c| {
|
||||||
|
switch (c) {
|
||||||
|
'\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '!', '|' => {
|
||||||
|
try writer.writeByte('\\');
|
||||||
|
try writer.writeByte(c);
|
||||||
|
},
|
||||||
|
else => try writer.writeByte(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn testMarkdownHTML(html: []const u8, expected: []const u8) !void {
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
const page = try testing.test_session.createPage();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const doc = page.window._document;
|
||||||
|
|
||||||
|
const div = try doc.createElement("div", null, page);
|
||||||
|
try page.parseHtmlAsChildren(div.asNode(), html);
|
||||||
|
|
||||||
|
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||||
|
defer aw.deinit();
|
||||||
|
try dump(div.asNode(), .{}, &aw.writer, page);
|
||||||
|
|
||||||
|
try testing.expectString(expected, aw.written());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: basic" {
|
||||||
|
try testMarkdownHTML("Hello world", "Hello world\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: whitespace" {
|
||||||
|
try testMarkdownHTML("<span>A</span> <span>B</span>", "A B\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: escaping" {
|
||||||
|
try testMarkdownHTML("<p># Not a header</p>", "\n\\# Not a header\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: strikethrough" {
|
||||||
|
try testMarkdownHTML("<s>deleted</s>", "~~deleted~~\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: task list" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<input type="checkbox" checked><input type="checkbox">
|
||||||
|
, "[x] [ ] \n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: ordered list" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<ol><li>First</li><li>Second</li></ol>
|
||||||
|
, "1. First\n2. Second\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: table" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<table><thead><tr><th>Head 1</th><th>Head 2</th></tr></thead>
|
||||||
|
\\<tbody><tr><td>Cell 1</td><td>Cell 2</td></tr></tbody></table>
|
||||||
|
,
|
||||||
|
\\
|
||||||
|
\\| Head 1 | Head 2 |
|
||||||
|
\\|---|---|
|
||||||
|
\\| Cell 1 | Cell 2 |
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: nested lists" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<ul><li>Parent<ul><li>Child</li></ul></li></ul>
|
||||||
|
,
|
||||||
|
\\- Parent
|
||||||
|
\\ - Child
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: blockquote" {
|
||||||
|
try testMarkdownHTML("<blockquote>Hello world</blockquote>", "\n> Hello world\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: links" {
|
||||||
|
try testMarkdownHTML("<a href=\"https://lightpanda.io\">Lightpanda</a>", "[Lightpanda](https://lightpanda.io)\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: images" {
|
||||||
|
try testMarkdownHTML("<img src=\"logo.png\" alt=\"Logo\">", "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: headings" {
|
||||||
|
try testMarkdownHTML("<h1>Title</h1><h2>Subtitle</h2>",
|
||||||
|
\\
|
||||||
|
\\# Title
|
||||||
|
\\
|
||||||
|
\\## Subtitle
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: code" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<p>Use git push</p>
|
||||||
|
\\<pre><code>line 1
|
||||||
|
\\line 2</code></pre>
|
||||||
|
,
|
||||||
|
\\
|
||||||
|
\\Use git push
|
||||||
|
\\
|
||||||
|
\\```
|
||||||
|
\\line 1
|
||||||
|
\\line 2
|
||||||
|
\\```
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: block link" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<a href="https://example.com">
|
||||||
|
\\ <h3>Title</h3>
|
||||||
|
\\ <p>Description</p>
|
||||||
|
\\</a>
|
||||||
|
,
|
||||||
|
\\
|
||||||
|
\\### Title
|
||||||
|
\\
|
||||||
|
\\Description
|
||||||
|
\\([Link](https://example.com))
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: inline link" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<p>Visit <a href="https://example.com">Example</a>.</p>
|
||||||
|
,
|
||||||
|
\\
|
||||||
|
\\Visit [Example](https://example.com).
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: standalone anchors" {
|
||||||
|
// Inside main, with whitespace between anchors -> treated as blocks
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<main>
|
||||||
|
\\ <a href="1">Link 1</a>
|
||||||
|
\\ <a href="2">Link 2</a>
|
||||||
|
\\</main>
|
||||||
|
,
|
||||||
|
\\[Link 1](1)
|
||||||
|
\\[Link 2](2)
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: mixed anchors in main" {
|
||||||
|
// Anchors surrounded by text should remain inline
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<main>
|
||||||
|
\\ Welcome <a href="1">Link 1</a>.
|
||||||
|
\\</main>
|
||||||
|
,
|
||||||
|
\\Welcome [Link 1](1).
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -421,7 +421,16 @@ fn appendBeforeSiblingCallback(ctx: *anyopaque, sibling_ref: *anyopaque, node_or
|
|||||||
fn _appendBeforeSiblingCallback(self: *Parser, sibling: *Node, node_or_text: h5e.NodeOrText) !void {
|
fn _appendBeforeSiblingCallback(self: *Parser, sibling: *Node, node_or_text: h5e.NodeOrText) !void {
|
||||||
const parent = sibling.parentNode() orelse return error.NoParent;
|
const parent = sibling.parentNode() orelse return error.NoParent;
|
||||||
const node: *Node = switch (node_or_text.toUnion()) {
|
const node: *Node = switch (node_or_text.toUnion()) {
|
||||||
.node => |cpn| getNode(cpn),
|
.node => |cpn| blk: {
|
||||||
|
const child = getNode(cpn);
|
||||||
|
if (child._parent) |previous_parent| {
|
||||||
|
// A custom element constructor may have inserted the node into the
|
||||||
|
// DOM before the parser officially places it (e.g. via foster
|
||||||
|
// parenting). Detach it first so insertNodeRelative's assertion holds.
|
||||||
|
self.page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() });
|
||||||
|
}
|
||||||
|
break :blk child;
|
||||||
|
},
|
||||||
.text => |txt| try self.page.createTextNode(txt),
|
.text => |txt| try self.page.createTextNode(txt),
|
||||||
};
|
};
|
||||||
try self.page.insertNodeRelative(parent, node, .{ .before = sibling }, .{});
|
try self.page.insertNodeRelative(parent, node, .{ .before = sibling }, .{});
|
||||||
|
|||||||
@@ -3,13 +3,67 @@
|
|||||||
|
|
||||||
<script id=animation>
|
<script id=animation>
|
||||||
let a1 = document.createElement('div').animate(null, null);
|
let a1 = document.createElement('div').animate(null, null);
|
||||||
testing.expectEqual('finished', a1.playState);
|
testing.expectEqual('idle', a1.playState);
|
||||||
|
|
||||||
let cb = [];
|
let cb = [];
|
||||||
a1.ready.then(() => { cb.push('ready') });
|
|
||||||
a1.finished.then((x) => {
|
a1.finished.then((x) => {
|
||||||
cb.push('finished');
|
cb.push(a1.playState);
|
||||||
cb.push(x == a1);
|
cb.push(x == a1);
|
||||||
});
|
});
|
||||||
testing.eventually(() => testing.expectEqual(['finished', true], cb));
|
a1.ready.then(() => {
|
||||||
|
cb.push(a1.playState);
|
||||||
|
a1.play();
|
||||||
|
cb.push(a1.playState);
|
||||||
|
});
|
||||||
|
testing.eventually(() => testing.expectEqual(['idle', 'running', 'finished', true], cb));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=startTime>
|
||||||
|
let a2 = document.createElement('div').animate(null, null);
|
||||||
|
// startTime defaults to null
|
||||||
|
testing.expectEqual(null, a2.startTime);
|
||||||
|
// startTime is settable
|
||||||
|
a2.startTime = 42.5;
|
||||||
|
testing.expectEqual(42.5, a2.startTime);
|
||||||
|
// startTime can be reset to null
|
||||||
|
a2.startTime = null;
|
||||||
|
testing.expectEqual(null, a2.startTime);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=onfinish>
|
||||||
|
let a3 = document.createElement('div').animate(null, null);
|
||||||
|
// onfinish defaults to null
|
||||||
|
testing.expectEqual(null, a3.onfinish);
|
||||||
|
|
||||||
|
let calls = [];
|
||||||
|
// onfinish callback should be scheduled and called asynchronously
|
||||||
|
a3.onfinish = function() { calls.push('finish'); };
|
||||||
|
a3.play();
|
||||||
|
testing.eventually(() => testing.expectEqual(['finish'], calls));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=pause>
|
||||||
|
let a4 = document.createElement('div').animate(null, null);
|
||||||
|
let cb4 = [];
|
||||||
|
a4.finished.then((x) => { cb4.push(a4.playState) });
|
||||||
|
a4.ready.then(() => {
|
||||||
|
a4.play();
|
||||||
|
cb4.push(a4.playState)
|
||||||
|
a4.pause();
|
||||||
|
cb4.push(a4.playState)
|
||||||
|
});
|
||||||
|
testing.eventually(() => testing.expectEqual(['running', 'paused'], cb4));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=finish>
|
||||||
|
let a5 = document.createElement('div').animate(null, null);
|
||||||
|
testing.expectEqual('idle', a5.playState);
|
||||||
|
|
||||||
|
let cb5 = [];
|
||||||
|
a5.finished.then((x) => { cb5.push(a5.playState) });
|
||||||
|
a5.ready.then(() => {
|
||||||
|
cb5.push(a5.playState);
|
||||||
|
a5.play();
|
||||||
|
});
|
||||||
|
testing.eventually(() => testing.expectEqual(['idle', 'finished'], cb5));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -33,3 +33,70 @@
|
|||||||
testing.expectEqual(ctx.fillStyle, "rgba(255, 0, 0, 0.06)");
|
testing.expectEqual(ctx.fillStyle, "rgba(255, 0, 0, 0.06)");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="CanvasRenderingContext2D#createImageData(width, height)">
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
|
||||||
|
const imageData = ctx.createImageData(100, 200);
|
||||||
|
testing.expectEqual(true, imageData instanceof ImageData);
|
||||||
|
testing.expectEqual(imageData.width, 100);
|
||||||
|
testing.expectEqual(imageData.height, 200);
|
||||||
|
testing.expectEqual(imageData.data.length, 100 * 200 * 4);
|
||||||
|
testing.expectEqual(true, imageData.data instanceof Uint8ClampedArray);
|
||||||
|
|
||||||
|
// All pixels should be initialized to 0.
|
||||||
|
testing.expectEqual(imageData.data[0], 0);
|
||||||
|
testing.expectEqual(imageData.data[1], 0);
|
||||||
|
testing.expectEqual(imageData.data[2], 0);
|
||||||
|
testing.expectEqual(imageData.data[3], 0);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CanvasRenderingContext2D#createImageData(imageData)">
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
|
||||||
|
const source = ctx.createImageData(50, 75);
|
||||||
|
const imageData = ctx.createImageData(source);
|
||||||
|
testing.expectEqual(true, imageData instanceof ImageData);
|
||||||
|
testing.expectEqual(imageData.width, 50);
|
||||||
|
testing.expectEqual(imageData.height, 75);
|
||||||
|
testing.expectEqual(imageData.data.length, 50 * 75 * 4);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CanvasRenderingContext2D#putImageData">
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
|
||||||
|
const imageData = ctx.createImageData(10, 10);
|
||||||
|
testing.expectEqual(true, imageData instanceof ImageData);
|
||||||
|
// Modify some pixel data.
|
||||||
|
imageData.data[0] = 255;
|
||||||
|
imageData.data[1] = 0;
|
||||||
|
imageData.data[2] = 0;
|
||||||
|
imageData.data[3] = 255;
|
||||||
|
|
||||||
|
// putImageData should not throw.
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
ctx.putImageData(imageData, 10, 20);
|
||||||
|
// With dirty rect parameters.
|
||||||
|
ctx.putImageData(imageData, 0, 0, 0, 0, 5, 5);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script id="getter">
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
testing.expectEqual('10px sans-serif', ctx.font);
|
||||||
|
ctx.font = 'bold 48px serif'
|
||||||
|
testing.expectEqual('bold 48px serif', ctx.font);
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
64
src/browser/tests/canvas/offscreen_canvas.html
Normal file
64
src/browser/tests/canvas/offscreen_canvas.html
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=OffscreenCanvas>
|
||||||
|
{
|
||||||
|
const canvas = new OffscreenCanvas(256, 256);
|
||||||
|
testing.expectEqual(true, canvas instanceof OffscreenCanvas);
|
||||||
|
testing.expectEqual(canvas.width, 256);
|
||||||
|
testing.expectEqual(canvas.height, 256);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=OffscreenCanvas#width>
|
||||||
|
{
|
||||||
|
const canvas = new OffscreenCanvas(100, 200);
|
||||||
|
testing.expectEqual(canvas.width, 100);
|
||||||
|
canvas.width = 300;
|
||||||
|
testing.expectEqual(canvas.width, 300);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=OffscreenCanvas#height>
|
||||||
|
{
|
||||||
|
const canvas = new OffscreenCanvas(100, 200);
|
||||||
|
testing.expectEqual(canvas.height, 200);
|
||||||
|
canvas.height = 400;
|
||||||
|
testing.expectEqual(canvas.height, 400);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=OffscreenCanvas#getContext>
|
||||||
|
{
|
||||||
|
const canvas = new OffscreenCanvas(64, 64);
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
testing.expectEqual(true, ctx instanceof OffscreenCanvasRenderingContext2D);
|
||||||
|
// We can't really test rendering but let's try to call it at least.
|
||||||
|
ctx.fillRect(0, 0, 10, 10);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=OffscreenCanvas#convertToBlob>
|
||||||
|
{
|
||||||
|
const canvas = new OffscreenCanvas(64, 64);
|
||||||
|
const promise = canvas.convertToBlob();
|
||||||
|
testing.expectEqual(true, promise instanceof Promise);
|
||||||
|
// The promise should resolve to a Blob (even if empty)
|
||||||
|
promise.then(blob => {
|
||||||
|
testing.expectEqual(true, blob instanceof Blob);
|
||||||
|
testing.expectEqual(blob.size, 0); // Empty since no rendering
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=HTMLCanvasElement#transferControlToOffscreen>
|
||||||
|
{
|
||||||
|
const htmlCanvas = document.createElement("canvas");
|
||||||
|
htmlCanvas.width = 128;
|
||||||
|
htmlCanvas.height = 96;
|
||||||
|
const offscreen = htmlCanvas.transferControlToOffscreen();
|
||||||
|
testing.expectEqual(true, offscreen instanceof OffscreenCanvas);
|
||||||
|
testing.expectEqual(offscreen.width, 128);
|
||||||
|
testing.expectEqual(offscreen.height, 96);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -4,4 +4,6 @@
|
|||||||
<script id=comment>
|
<script id=comment>
|
||||||
testing.expectEqual('', new Comment().data);
|
testing.expectEqual('', new Comment().data);
|
||||||
testing.expectEqual('over 9000! ', new Comment('over 9000! ').data);
|
testing.expectEqual('over 9000! ', new Comment('over 9000! ').data);
|
||||||
|
|
||||||
|
testing.expectEqual('null', new Comment(null).data);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<a id="link" href="foo" class="ok">OK</a>
|
<a id="link" href="foo" class="ok">OK</a>
|
||||||
|
|
||||||
<script src="../../testing.js"></script>
|
<script src="../testing.js"></script>
|
||||||
<script id=text>
|
<script id=text>
|
||||||
let t = new Text('foo');
|
let t = new Text('foo');
|
||||||
testing.expectEqual('foo', t.data);
|
testing.expectEqual('foo', t.data);
|
||||||
@@ -16,4 +16,7 @@
|
|||||||
let split = text.splitText('OK'.length);
|
let split = text.splitText('OK'.length);
|
||||||
testing.expectEqual(' modified', split.data);
|
testing.expectEqual(' modified', split.data);
|
||||||
testing.expectEqual('OK', text.data);
|
testing.expectEqual('OK', text.data);
|
||||||
|
|
||||||
|
let x = new Text(null);
|
||||||
|
testing.expectEqual("null", x.data);
|
||||||
</script>
|
</script>
|
||||||
@@ -16,44 +16,44 @@
|
|||||||
isRandom(ti8a)
|
isRandom(ti8a)
|
||||||
}
|
}
|
||||||
|
|
||||||
// {
|
{
|
||||||
// let tu16a = new Uint16Array(100)
|
let tu16a = new Uint16Array(100)
|
||||||
// testing.expectEqual(tu16a, crypto.getRandomValues(tu16a))
|
testing.expectEqual(tu16a, crypto.getRandomValues(tu16a))
|
||||||
// isRandom(tu16a)
|
isRandom(tu16a)
|
||||||
|
|
||||||
// let ti16a = new Int16Array(100)
|
let ti16a = new Int16Array(100)
|
||||||
// testing.expectEqual(ti16a, crypto.getRandomValues(ti16a))
|
testing.expectEqual(ti16a, crypto.getRandomValues(ti16a))
|
||||||
// isRandom(ti16a)
|
isRandom(ti16a)
|
||||||
// }
|
}
|
||||||
|
|
||||||
// {
|
{
|
||||||
// let tu32a = new Uint32Array(100)
|
let tu32a = new Uint32Array(100)
|
||||||
// testing.expectEqual(tu32a, crypto.getRandomValues(tu32a))
|
testing.expectEqual(tu32a, crypto.getRandomValues(tu32a))
|
||||||
// isRandom(tu32a)
|
isRandom(tu32a)
|
||||||
|
|
||||||
// let ti32a = new Int32Array(100)
|
let ti32a = new Int32Array(100)
|
||||||
// testing.expectEqual(ti32a, crypto.getRandomValues(ti32a))
|
testing.expectEqual(ti32a, crypto.getRandomValues(ti32a))
|
||||||
// isRandom(ti32a)
|
isRandom(ti32a)
|
||||||
// }
|
}
|
||||||
|
|
||||||
// {
|
{
|
||||||
// let tu64a = new BigUint64Array(100)
|
let tu64a = new BigUint64Array(100)
|
||||||
// testing.expectEqual(tu64a, crypto.getRandomValues(tu64a))
|
testing.expectEqual(tu64a, crypto.getRandomValues(tu64a))
|
||||||
// isRandom(tu64a)
|
isRandom(tu64a)
|
||||||
|
|
||||||
// let ti64a = new BigInt64Array(100)
|
let ti64a = new BigInt64Array(100)
|
||||||
// testing.expectEqual(ti64a, crypto.getRandomValues(ti64a))
|
testing.expectEqual(ti64a, crypto.getRandomValues(ti64a))
|
||||||
// isRandom(ti64a)
|
isRandom(ti64a)
|
||||||
// }
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- <script id="randomUUID">
|
<script id="randomUUID">
|
||||||
const uuid = crypto.randomUUID();
|
const uuid = crypto.randomUUID();
|
||||||
testing.expectEqual('string', typeof uuid);
|
testing.expectEqual('string', typeof uuid);
|
||||||
testing.expectEqual(36, uuid.length);
|
testing.expectEqual(36, uuid.length);
|
||||||
const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||||
testing.expectEqual(true, regex.test(uuid));
|
testing.expectEqual(true, regex.test(uuid));
|
||||||
</script> -->
|
</script>
|
||||||
|
|
||||||
<script id=SubtleCrypto>
|
<script id=SubtleCrypto>
|
||||||
testing.expectEqual(true, crypto.subtle instanceof SubtleCrypto);
|
testing.expectEqual(true, crypto.subtle instanceof SubtleCrypto);
|
||||||
@@ -119,3 +119,16 @@
|
|||||||
testing.expectEqual(16, sharedKey.byteLength);
|
testing.expectEqual(16, sharedKey.byteLength);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="digest">
|
||||||
|
testing.async(async () => {
|
||||||
|
async function hash(algo, data) {
|
||||||
|
const buffer = await window.crypto.subtle.digest(algo, new TextEncoder().encode(data));
|
||||||
|
return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
testing.expectEqual("a6a1e3375239f215f09a156df29c17c7d1ac6722", await hash('sha-1', 'over 9000'));
|
||||||
|
testing.expectEqual("1bc375bb92459685194dda18a4b835f4e2972ec1bde6d9ab3db53fcc584a6580", await hash('sha-256', 'over 9000'));
|
||||||
|
testing.expectEqual("a4260d64c2eea9fd30c1f895c5e48a26d817e19d3a700b61b3ce665864ff4b8e012bd357d345aa614c5f642dab865ea1", await hash('sha-384', 'over 9000'));
|
||||||
|
testing.expectEqual("6cad17e6f3f76680d6dd18ed043b75b4f6e1aa1d08b917294942e882fb6466c3510948c34af8b903ed0725b582b3b39c0e485ae2c1b7dfdb192ee38b79c782b6", await hash('sha-512', 'over 9000'));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -69,3 +69,11 @@
|
|||||||
testing.expectEqual(true, CSS.supports('z-index', '10'));
|
testing.expectEqual(true, CSS.supports('z-index', '10'));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="escape_null_character">
|
||||||
|
{
|
||||||
|
testing.expectEqual('\uFFFD', CSS.escape('\x00'));
|
||||||
|
testing.expectEqual('test\uFFFDvalue', CSS.escape('test\x00value'));
|
||||||
|
testing.expectEqual('\uFFFDabc', CSS.escape('\x00abc'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
58
src/browser/tests/css/font_face_set.html
Normal file
58
src/browser/tests/css/font_face_set.html
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id="document_fonts_exists">
|
||||||
|
{
|
||||||
|
testing.expectTrue(document.fonts !== undefined);
|
||||||
|
testing.expectTrue(document.fonts !== null);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_same_instance">
|
||||||
|
{
|
||||||
|
// Should return same instance each time
|
||||||
|
const f1 = document.fonts;
|
||||||
|
const f2 = document.fonts;
|
||||||
|
testing.expectTrue(f1 === f2);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_status">
|
||||||
|
{
|
||||||
|
testing.expectEqual('loaded', document.fonts.status);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_size">
|
||||||
|
{
|
||||||
|
testing.expectEqual(0, document.fonts.size);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_ready_is_promise">
|
||||||
|
{
|
||||||
|
const ready = document.fonts.ready;
|
||||||
|
testing.expectTrue(ready instanceof Promise);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_ready_resolves">
|
||||||
|
{
|
||||||
|
let resolved = false;
|
||||||
|
document.fonts.ready.then(() => { resolved = true; });
|
||||||
|
// Promise resolution is async; just confirm .then() does not throw
|
||||||
|
testing.expectTrue(typeof document.fonts.ready.then === 'function');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_check">
|
||||||
|
{
|
||||||
|
testing.expectTrue(document.fonts.check('16px sans-serif'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_constructor_name">
|
||||||
|
{
|
||||||
|
testing.expectEqual('FontFaceSet', document.fonts.constructor.name);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -205,3 +205,54 @@
|
|||||||
testing.expectEqual('', style.getPropertyPriority('content'));
|
testing.expectEqual('', style.getPropertyPriority('content'));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_style_syncs_to_attribute">
|
||||||
|
{
|
||||||
|
// JS style modifications must be reflected in getAttribute.
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
// Named property assignment (element.style.X = ...)
|
||||||
|
div.style.opacity = '0';
|
||||||
|
testing.expectEqual('opacity: 0;', div.getAttribute('style'));
|
||||||
|
|
||||||
|
// Update existing property
|
||||||
|
div.style.opacity = '1';
|
||||||
|
testing.expectEqual('opacity: 1;', div.getAttribute('style'));
|
||||||
|
|
||||||
|
// Add a second property
|
||||||
|
div.style.color = 'red';
|
||||||
|
testing.expectTrue(div.getAttribute('style').includes('opacity: 1'));
|
||||||
|
testing.expectTrue(div.getAttribute('style').includes('color: red'));
|
||||||
|
|
||||||
|
// removeProperty syncs back
|
||||||
|
div.style.removeProperty('opacity');
|
||||||
|
testing.expectTrue(!div.getAttribute('style').includes('opacity'));
|
||||||
|
testing.expectTrue(div.getAttribute('style').includes('color: red'));
|
||||||
|
|
||||||
|
// setCssText syncs back
|
||||||
|
div.style.cssText = 'filter: blur(0px)';
|
||||||
|
testing.expectEqual('filter: blur(0px);', div.getAttribute('style'));
|
||||||
|
|
||||||
|
// setCssText with empty string clears attribute
|
||||||
|
div.style.cssText = '';
|
||||||
|
testing.expectEqual('', div.getAttribute('style'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_outerHTML_reflects_style_changes">
|
||||||
|
{
|
||||||
|
// outerHTML must reflect JS-modified styles (regression test for
|
||||||
|
// DOM serialization reading stale HTML-parsed attribute values).
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.setAttribute('style', 'filter:blur(10px);opacity:0');
|
||||||
|
|
||||||
|
div.style.filter = 'blur(0px)';
|
||||||
|
div.style.opacity = '1';
|
||||||
|
|
||||||
|
const html = div.outerHTML;
|
||||||
|
testing.expectTrue(html.includes('filter: blur(0px)'));
|
||||||
|
testing.expectTrue(html.includes('opacity: 1'));
|
||||||
|
testing.expectTrue(!html.includes('blur(10px)'));
|
||||||
|
testing.expectTrue(!html.includes('opacity:0'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -119,3 +119,33 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="constructor_self_insert_foster_parent">
|
||||||
|
{
|
||||||
|
// Regression: custom element constructor inserting itself (via appendChild) during
|
||||||
|
// innerHTML parsing. When the element is not valid table content, the HTML5 parser
|
||||||
|
// foster-parents it before the <table> via appendBeforeSiblingCallback. That callback
|
||||||
|
// previously didn't check for an existing _parent before calling insertNodeRelative,
|
||||||
|
// causing the "Page.insertNodeRelative parent" assertion to fire.
|
||||||
|
let constructorCalled = 0;
|
||||||
|
let container;
|
||||||
|
|
||||||
|
class CtorSelfInsert extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
constructorCalled++;
|
||||||
|
// Insert self into container so _parent is set before the parser
|
||||||
|
// officially places this element via appendBeforeSiblingCallback.
|
||||||
|
if (container) container.appendChild(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('ctor-self-insert', CtorSelfInsert);
|
||||||
|
|
||||||
|
container = document.createElement('div');
|
||||||
|
// ctor-self-insert is not valid table content; the parser foster-parents it
|
||||||
|
// before the <table>, calling appendBeforeSiblingCallback(sibling=table, node=element).
|
||||||
|
// At that point the element already has _parent=container from the constructor.
|
||||||
|
container.innerHTML = '<table><ctor-self-insert></ctor-self-insert></table>';
|
||||||
|
|
||||||
|
testing.expectEqual(1, constructorCalled);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
testing.expectEqual(true, htmlDiv1 instanceof HTMLDivElement);
|
testing.expectEqual(true, htmlDiv1 instanceof HTMLDivElement);
|
||||||
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv1.namespaceURI);
|
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv1.namespaceURI);
|
||||||
|
|
||||||
|
// Per spec, createElementNS does NOT lowercase — 'DIV' != 'div', so this
|
||||||
|
// creates an HTMLUnknownElement, not an HTMLDivElement.
|
||||||
const htmlDiv2 = document.createElementNS('http://www.w3.org/1999/xhtml', 'DIV');
|
const htmlDiv2 = document.createElementNS('http://www.w3.org/1999/xhtml', 'DIV');
|
||||||
testing.expectEqual('DIV', htmlDiv2.tagName);
|
testing.expectEqual('DIV', htmlDiv2.tagName);
|
||||||
testing.expectEqual(true, htmlDiv2 instanceof HTMLDivElement);
|
testing.expectEqual(false, htmlDiv2 instanceof HTMLDivElement);
|
||||||
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv2.namespaceURI);
|
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv2.namespaceURI);
|
||||||
|
|
||||||
const svgRect = document.createElementNS('http://www.w3.org/2000/svg', 'RecT');
|
const svgRect = document.createElementNS('http://www.w3.org/2000/svg', 'RecT');
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<head id="the_head">
|
<head id="the_head">
|
||||||
|
<meta charset="UTF-8">
|
||||||
<title>Test Document Title</title>
|
<title>Test Document Title</title>
|
||||||
<script src="../testing.js"></script>
|
<script src="../testing.js"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -11,7 +12,7 @@
|
|||||||
testing.expectEqual(10, document.childNodes[0].nodeType);
|
testing.expectEqual(10, document.childNodes[0].nodeType);
|
||||||
testing.expectEqual(null, document.parentNode);
|
testing.expectEqual(null, document.parentNode);
|
||||||
testing.expectEqual(undefined, document.getCurrentScript);
|
testing.expectEqual(undefined, document.getCurrentScript);
|
||||||
testing.expectEqual("http://127.0.0.1:9582/src/browser/tests/document/document.html", document.URL);
|
testing.expectEqual(testing.BASE_URL + 'document/document.html', document.URL);
|
||||||
testing.expectEqual(window, document.defaultView);
|
testing.expectEqual(window, document.defaultView);
|
||||||
testing.expectEqual(false, document.hidden);
|
testing.expectEqual(false, document.hidden);
|
||||||
testing.expectEqual("visible", document.visibilityState);
|
testing.expectEqual("visible", document.visibilityState);
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
|
|
||||||
<script id=documentElement>
|
<script id=documentElement>
|
||||||
testing.expectEqual($('#the_body').parentNode, document.documentElement);
|
testing.expectEqual($('#the_body').parentNode, document.documentElement);
|
||||||
|
testing.expectEqual(document.documentElement, document.scrollingElement);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=title>
|
<script id=title>
|
||||||
@@ -55,7 +57,7 @@
|
|||||||
testing.expectEqual('CSS1Compat', document.compatMode);
|
testing.expectEqual('CSS1Compat', document.compatMode);
|
||||||
testing.expectEqual(document.URL, document.documentURI);
|
testing.expectEqual(document.URL, document.documentURI);
|
||||||
testing.expectEqual('', document.referrer);
|
testing.expectEqual('', document.referrer);
|
||||||
testing.expectEqual('127.0.0.1', document.domain);
|
testing.expectEqual(testing.HOST, document.domain);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=programmatic_document_metadata>
|
<script id=programmatic_document_metadata>
|
||||||
@@ -68,7 +70,7 @@
|
|||||||
testing.expectEqual('CSS1Compat', doc.compatMode);
|
testing.expectEqual('CSS1Compat', doc.compatMode);
|
||||||
testing.expectEqual('', doc.referrer);
|
testing.expectEqual('', doc.referrer);
|
||||||
// Programmatic document should have empty domain (no URL/origin)
|
// Programmatic document should have empty domain (no URL/origin)
|
||||||
testing.expectEqual('127.0.0.1', doc.domain);
|
testing.expectEqual(testing.HOST, doc.domain);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Test anchors and links -->
|
<!-- Test anchors and links -->
|
||||||
@@ -175,15 +177,111 @@
|
|||||||
testing.expectEqual(initialLength, anchors.length);
|
testing.expectEqual(initialLength, anchors.length);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=cookie>
|
<script id=cookie_basic>
|
||||||
testing.expectEqual('', document.cookie);
|
// Basic cookie operations
|
||||||
document.cookie = 'name=Oeschger;';
|
document.cookie = 'testbasic1=Oeschger';
|
||||||
document.cookie = 'favorite_food=tripe;';
|
testing.expectEqual(true, document.cookie.includes('testbasic1=Oeschger'));
|
||||||
|
|
||||||
testing.expectEqual('name=Oeschger; favorite_food=tripe', document.cookie);
|
document.cookie = 'testbasic2=tripe';
|
||||||
// "" should be returned, but the framework overrules it atm
|
testing.expectEqual(true, document.cookie.includes('testbasic1=Oeschger'));
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testbasic2=tripe'));
|
||||||
|
|
||||||
|
// HttpOnly should be ignored from JavaScript
|
||||||
|
const beforeHttp = document.cookie;
|
||||||
document.cookie = 'IgnoreMy=Ghost; HttpOnly';
|
document.cookie = 'IgnoreMy=Ghost; HttpOnly';
|
||||||
testing.expectEqual('name=Oeschger; favorite_food=tripe', document.cookie);
|
testing.expectEqual(false, document.cookie.includes('IgnoreMy=Ghost'));
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
document.cookie = 'testbasic1=; Max-Age=0';
|
||||||
|
document.cookie = 'testbasic2=; Max-Age=0';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=cookie_special_chars>
|
||||||
|
// Test special characters in cookie values
|
||||||
|
document.cookie = 'testspaces=hello world';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testspaces=hello world'));
|
||||||
|
document.cookie = 'testspaces=; Max-Age=0';
|
||||||
|
|
||||||
|
// Test various allowed special characters
|
||||||
|
document.cookie = 'testspecial=!#$%&\'()*+-./';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testspecial='));
|
||||||
|
document.cookie = 'testspecial=; Max-Age=0';
|
||||||
|
|
||||||
|
// Semicolon terminates the cookie value
|
||||||
|
document.cookie = 'testsemi=before;after';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testsemi=before'));
|
||||||
|
testing.expectEqual(false, document.cookie.includes('after'));
|
||||||
|
document.cookie = 'testsemi=; Max-Age=0';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=cookie_empty_name>
|
||||||
|
// Cookie with empty name (just a value)
|
||||||
|
document.cookie = 'teststandalone';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('teststandalone'));
|
||||||
|
document.cookie = 'teststandalone; Max-Age=0';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=cookie_whitespace>
|
||||||
|
// Names and values should be trimmed
|
||||||
|
document.cookie = ' testtrim = trimmed_value ';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testtrim=trimmed_value'));
|
||||||
|
document.cookie = 'testtrim=; Max-Age=0';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=cookie_max_age>
|
||||||
|
// Max-Age=0 should immediately delete
|
||||||
|
document.cookie = 'testtemp0=value; Max-Age=0';
|
||||||
|
testing.expectEqual(false, document.cookie.includes('testtemp0=value'));
|
||||||
|
|
||||||
|
// Negative Max-Age should also delete
|
||||||
|
document.cookie = 'testinstant=value';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testinstant=value'));
|
||||||
|
document.cookie = 'testinstant=value; Max-Age=-1';
|
||||||
|
testing.expectEqual(false, document.cookie.includes('testinstant=value'));
|
||||||
|
|
||||||
|
// Positive Max-Age should keep cookie
|
||||||
|
document.cookie = 'testkept=value; Max-Age=3600';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testkept=value'));
|
||||||
|
document.cookie = 'testkept=; Max-Age=0';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=cookie_overwrite>
|
||||||
|
// Setting a cookie with the same name should overwrite
|
||||||
|
document.cookie = 'testoverwrite=first';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testoverwrite=first'));
|
||||||
|
|
||||||
|
document.cookie = 'testoverwrite=second';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testoverwrite=second'));
|
||||||
|
testing.expectEqual(false, document.cookie.includes('testoverwrite=first'));
|
||||||
|
|
||||||
|
document.cookie = 'testoverwrite=; Max-Age=0';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=cookie_path>
|
||||||
|
// Path attribute
|
||||||
|
document.cookie = 'testpath1=value; Path=/';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testpath1=value'));
|
||||||
|
|
||||||
|
// Different path cookie should coexist
|
||||||
|
document.cookie = 'testpath2=value2; Path=/src';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testpath1=value'));
|
||||||
|
|
||||||
|
document.cookie = 'testpath1=; Max-Age=0; Path=/';
|
||||||
|
document.cookie = 'testpath2=; Max-Age=0; Path=/src';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=cookie_invalid_chars>
|
||||||
|
// Control characters (< 32 or > 126) should be rejected
|
||||||
|
const beforeBad = document.cookie;
|
||||||
|
|
||||||
|
document.cookie = 'testbad1\x00=value';
|
||||||
|
testing.expectEqual(false, document.cookie.includes('testbad1'));
|
||||||
|
|
||||||
|
document.cookie = 'testbad2\x1F=value';
|
||||||
|
testing.expectEqual(false, document.cookie.includes('testbad2'));
|
||||||
|
|
||||||
|
document.cookie = 'testbad3=val\x7F';
|
||||||
|
testing.expectEqual(false, document.cookie.includes('testbad3'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=createAttribute>
|
<script id=createAttribute>
|
||||||
|
|||||||
@@ -81,6 +81,172 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script id="focusin_focusout_events">
|
||||||
|
{
|
||||||
|
const input1 = $('#input1');
|
||||||
|
const input2 = $('#input2');
|
||||||
|
|
||||||
|
if (document.activeElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
let events = [];
|
||||||
|
|
||||||
|
input1.addEventListener('focus', () => events.push('focus1'));
|
||||||
|
input1.addEventListener('focusin', () => events.push('focusin1'));
|
||||||
|
input1.addEventListener('blur', () => events.push('blur1'));
|
||||||
|
input1.addEventListener('focusout', () => events.push('focusout1'));
|
||||||
|
input2.addEventListener('focus', () => events.push('focus2'));
|
||||||
|
input2.addEventListener('focusin', () => events.push('focusin2'));
|
||||||
|
|
||||||
|
// Focus input1 — should fire focus then focusin
|
||||||
|
input1.focus();
|
||||||
|
testing.expectEqual('focus1,focusin1', events.join(','));
|
||||||
|
|
||||||
|
// Focus input2 — should fire blur, focusout on input1, then focus, focusin on input2
|
||||||
|
events = [];
|
||||||
|
input2.focus();
|
||||||
|
testing.expectEqual('blur1,focusout1,focus2,focusin2', events.join(','));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="focusin_bubbles">
|
||||||
|
{
|
||||||
|
const input1 = $('#input1');
|
||||||
|
|
||||||
|
if (document.activeElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
let bodyFocusin = 0;
|
||||||
|
let bodyFocus = 0;
|
||||||
|
|
||||||
|
document.body.addEventListener('focusin', () => bodyFocusin++);
|
||||||
|
document.body.addEventListener('focus', () => bodyFocus++);
|
||||||
|
|
||||||
|
input1.focus();
|
||||||
|
|
||||||
|
// focusin should bubble to body, focus should not
|
||||||
|
testing.expectEqual(1, bodyFocusin);
|
||||||
|
testing.expectEqual(0, bodyFocus);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="focusout_bubbles">
|
||||||
|
{
|
||||||
|
const input1 = $('#input1');
|
||||||
|
|
||||||
|
input1.focus();
|
||||||
|
|
||||||
|
let bodyFocusout = 0;
|
||||||
|
let bodyBlur = 0;
|
||||||
|
|
||||||
|
document.body.addEventListener('focusout', () => bodyFocusout++);
|
||||||
|
document.body.addEventListener('blur', () => bodyBlur++);
|
||||||
|
|
||||||
|
input1.blur();
|
||||||
|
|
||||||
|
// focusout should bubble to body, blur should not
|
||||||
|
testing.expectEqual(1, bodyFocusout);
|
||||||
|
testing.expectEqual(0, bodyBlur);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="focus_relatedTarget">
|
||||||
|
{
|
||||||
|
const input1 = $('#input1');
|
||||||
|
const input2 = $('#input2');
|
||||||
|
|
||||||
|
if (document.activeElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
let focusRelated = null;
|
||||||
|
let blurRelated = null;
|
||||||
|
let focusinRelated = null;
|
||||||
|
let focusoutRelated = null;
|
||||||
|
|
||||||
|
input1.addEventListener('blur', (e) => { blurRelated = e.relatedTarget; });
|
||||||
|
input1.addEventListener('focusout', (e) => { focusoutRelated = e.relatedTarget; });
|
||||||
|
input2.addEventListener('focus', (e) => { focusRelated = e.relatedTarget; });
|
||||||
|
input2.addEventListener('focusin', (e) => { focusinRelated = e.relatedTarget; });
|
||||||
|
|
||||||
|
input1.focus();
|
||||||
|
input2.focus();
|
||||||
|
|
||||||
|
// blur/focusout on input1 should have relatedTarget = input2 (gaining focus)
|
||||||
|
testing.expectEqual(input2, blurRelated);
|
||||||
|
testing.expectEqual(input2, focusoutRelated);
|
||||||
|
|
||||||
|
// focus/focusin on input2 should have relatedTarget = input1 (losing focus)
|
||||||
|
testing.expectEqual(input1, focusRelated);
|
||||||
|
testing.expectEqual(input1, focusinRelated);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="blur_relatedTarget_null">
|
||||||
|
{
|
||||||
|
const btn = $('#btn1');
|
||||||
|
|
||||||
|
btn.focus();
|
||||||
|
|
||||||
|
let blurRelated = 'not_set';
|
||||||
|
btn.addEventListener('blur', (e) => { blurRelated = e.relatedTarget; });
|
||||||
|
btn.blur();
|
||||||
|
|
||||||
|
// blur without moving to another element should have relatedTarget = null
|
||||||
|
testing.expectEqual(null, blurRelated);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="focus_event_properties">
|
||||||
|
{
|
||||||
|
const input1 = $('#input1');
|
||||||
|
const input2 = $('#input2');
|
||||||
|
|
||||||
|
if (document.activeElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
let focusEvent = null;
|
||||||
|
let focusinEvent = null;
|
||||||
|
let blurEvent = null;
|
||||||
|
let focusoutEvent = null;
|
||||||
|
|
||||||
|
input1.addEventListener('blur', (e) => { blurEvent = e; });
|
||||||
|
input1.addEventListener('focusout', (e) => { focusoutEvent = e; });
|
||||||
|
input2.addEventListener('focus', (e) => { focusEvent = e; });
|
||||||
|
input2.addEventListener('focusin', (e) => { focusinEvent = e; });
|
||||||
|
|
||||||
|
input1.focus();
|
||||||
|
input2.focus();
|
||||||
|
|
||||||
|
// All four should be FocusEvent instances
|
||||||
|
testing.expectEqual(true, blurEvent instanceof FocusEvent);
|
||||||
|
testing.expectEqual(true, focusoutEvent instanceof FocusEvent);
|
||||||
|
testing.expectEqual(true, focusEvent instanceof FocusEvent);
|
||||||
|
testing.expectEqual(true, focusinEvent instanceof FocusEvent);
|
||||||
|
|
||||||
|
// All four should be composed per spec
|
||||||
|
testing.expectEqual(true, blurEvent.composed);
|
||||||
|
testing.expectEqual(true, focusoutEvent.composed);
|
||||||
|
testing.expectEqual(true, focusEvent.composed);
|
||||||
|
testing.expectEqual(true, focusinEvent.composed);
|
||||||
|
|
||||||
|
// None should be cancelable
|
||||||
|
testing.expectEqual(false, blurEvent.cancelable);
|
||||||
|
testing.expectEqual(false, focusoutEvent.cancelable);
|
||||||
|
testing.expectEqual(false, focusEvent.cancelable);
|
||||||
|
testing.expectEqual(false, focusinEvent.cancelable);
|
||||||
|
|
||||||
|
// blur/focus don't bubble, focusin/focusout do
|
||||||
|
testing.expectEqual(false, blurEvent.bubbles);
|
||||||
|
testing.expectEqual(true, focusoutEvent.bubbles);
|
||||||
|
testing.expectEqual(false, focusEvent.bubbles);
|
||||||
|
testing.expectEqual(true, focusinEvent.bubbles);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id="focus_disconnected">
|
<script id="focus_disconnected">
|
||||||
{
|
{
|
||||||
const focused = document.activeElement;
|
const focused = document.activeElement;
|
||||||
@@ -88,3 +254,68 @@
|
|||||||
testing.expectEqual(focused, document.activeElement);
|
testing.expectEqual(focused, document.activeElement);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="click_focuses_element">
|
||||||
|
{
|
||||||
|
const input1 = $('#input1');
|
||||||
|
const input2 = $('#input2');
|
||||||
|
|
||||||
|
if (document.activeElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
let focusCount = 0;
|
||||||
|
let blurCount = 0;
|
||||||
|
|
||||||
|
input1.addEventListener('focus', () => focusCount++);
|
||||||
|
input1.addEventListener('blur', () => blurCount++);
|
||||||
|
input2.addEventListener('focus', () => focusCount++);
|
||||||
|
|
||||||
|
// Click input1 — should focus it and fire focus event
|
||||||
|
input1.click();
|
||||||
|
testing.expectEqual(input1, document.activeElement);
|
||||||
|
testing.expectEqual(1, focusCount);
|
||||||
|
testing.expectEqual(0, blurCount);
|
||||||
|
|
||||||
|
// Click input2 — should move focus, fire blur on input1 and focus on input2
|
||||||
|
input2.click();
|
||||||
|
testing.expectEqual(input2, document.activeElement);
|
||||||
|
testing.expectEqual(2, focusCount);
|
||||||
|
testing.expectEqual(1, blurCount);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="click_focuses_button">
|
||||||
|
{
|
||||||
|
const btn = $('#btn1');
|
||||||
|
|
||||||
|
if (document.activeElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.click();
|
||||||
|
testing.expectEqual(btn, document.activeElement);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="focus_disconnected_no_blur">
|
||||||
|
{
|
||||||
|
const input1 = $('#input1');
|
||||||
|
|
||||||
|
if (document.activeElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
input1.focus();
|
||||||
|
testing.expectEqual(input1, document.activeElement);
|
||||||
|
|
||||||
|
let blurCount = 0;
|
||||||
|
input1.addEventListener('blur', () => { blurCount++ });
|
||||||
|
|
||||||
|
// Focusing a disconnected element should be a no-op:
|
||||||
|
// blur must not fire on the currently focused element
|
||||||
|
document.createElement('a').focus();
|
||||||
|
testing.expectEqual(input1, document.activeElement);
|
||||||
|
testing.expectEqual(0, blurCount);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -270,3 +270,36 @@
|
|||||||
testing.expectEqual('rect', document.querySelector('svg g rect').tagName);
|
testing.expectEqual('rect', document.querySelector('svg g rect').tagName);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=special>
|
||||||
|
testing.expectEqual(null, document.querySelector('\\'));
|
||||||
|
|
||||||
|
testing.expectEqual(null, document.querySelector('div\\'));
|
||||||
|
testing.expectEqual(null, document.querySelector('.test-class\\'));
|
||||||
|
testing.expectEqual(null, document.querySelector('#byId\\'));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="café">Non-ASCII class 1</div>
|
||||||
|
<div class="日本語">Non-ASCII class 2</div>
|
||||||
|
<span id="niño">Non-ASCII ID 1</span>
|
||||||
|
<p id="🎨">Non-ASCII ID 2</p>
|
||||||
|
|
||||||
|
<script id=nonAsciiSelectors>
|
||||||
|
testing.expectEqual('Non-ASCII class 1', document.querySelector('.café').textContent);
|
||||||
|
testing.expectEqual('Non-ASCII class 2', document.querySelector('.日本語').textContent);
|
||||||
|
|
||||||
|
testing.expectEqual('Non-ASCII ID 1', document.querySelector('#niño').textContent);
|
||||||
|
testing.expectEqual('Non-ASCII ID 2', document.querySelector('#🎨').textContent);
|
||||||
|
|
||||||
|
testing.expectEqual('Non-ASCII class 1', document.querySelector('div.café').textContent);
|
||||||
|
testing.expectEqual('Non-ASCII ID 1', document.querySelector('span#niño').textContent);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span id=".,:!">Punctuation test</span>
|
||||||
|
|
||||||
|
<script id=escapedPunctuation>
|
||||||
|
{
|
||||||
|
// Test escaped punctuation in ID selectors
|
||||||
|
testing.expectEqual('Punctuation test', document.querySelector('#\\.\\,\\:\\!').textContent);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -27,7 +27,9 @@
|
|||||||
testing.expectEqual(expected.length, result.length);
|
testing.expectEqual(expected.length, result.length);
|
||||||
testing.expectEqual(expected, Array.from(result).map((e) => e.textContent));
|
testing.expectEqual(expected, Array.from(result).map((e) => e.textContent));
|
||||||
testing.expectEqual(expected, Array.from(result.values()).map((e) => e.textContent));
|
testing.expectEqual(expected, Array.from(result.values()).map((e) => e.textContent));
|
||||||
|
|
||||||
testing.expectEqual(expected.map((e, i) => i), Array.from(result.keys()));
|
testing.expectEqual(expected.map((e, i) => i), Array.from(result.keys()));
|
||||||
|
testing.expectEqual(expected.map((e, i) => i.toString()), Object.keys(result));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -376,3 +378,93 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<form id="form-validity-test">
|
||||||
|
<input id="vi-required-empty" type="text" required>
|
||||||
|
<input id="vi-optional" type="text">
|
||||||
|
<input id="vi-hidden-required" type="hidden" required>
|
||||||
|
<fieldset id="vi-fieldset">
|
||||||
|
<input id="vi-nested-required" type="text" required>
|
||||||
|
<select id="vi-select-required" required>
|
||||||
|
<option value="">Pick one</option>
|
||||||
|
<option value="a">A</option>
|
||||||
|
</select>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
<input id="vi-checkbox" type="checkbox">
|
||||||
|
|
||||||
|
<script id=invalidPseudo>
|
||||||
|
{
|
||||||
|
// Inputs with required + empty value are :invalid
|
||||||
|
testing.expectEqual(true, document.getElementById('vi-required-empty').matches(':invalid'));
|
||||||
|
testing.expectEqual(false, document.getElementById('vi-required-empty').matches(':valid'));
|
||||||
|
|
||||||
|
// Inputs without required are :valid
|
||||||
|
testing.expectEqual(false, document.getElementById('vi-optional').matches(':invalid'));
|
||||||
|
testing.expectEqual(true, document.getElementById('vi-optional').matches(':valid'));
|
||||||
|
|
||||||
|
// hidden inputs are not candidates for constraint validation
|
||||||
|
testing.expectEqual(false, document.getElementById('vi-hidden-required').matches(':invalid'));
|
||||||
|
testing.expectEqual(false, document.getElementById('vi-hidden-required').matches(':valid'));
|
||||||
|
|
||||||
|
// select with required and empty selected value is :invalid
|
||||||
|
testing.expectEqual(true, document.getElementById('vi-select-required').matches(':invalid'));
|
||||||
|
testing.expectEqual(false, document.getElementById('vi-select-required').matches(':valid'));
|
||||||
|
|
||||||
|
// fieldset containing invalid controls is :invalid
|
||||||
|
testing.expectEqual(true, document.getElementById('vi-fieldset').matches(':invalid'));
|
||||||
|
testing.expectEqual(false, document.getElementById('vi-fieldset').matches(':valid'));
|
||||||
|
|
||||||
|
// form containing invalid controls is :invalid
|
||||||
|
testing.expectEqual(true, document.getElementById('form-validity-test').matches(':invalid'));
|
||||||
|
testing.expectEqual(false, document.getElementById('form-validity-test').matches(':valid'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=validAfterValueSet>
|
||||||
|
{
|
||||||
|
// After setting a value, a required input becomes :valid
|
||||||
|
const input = document.getElementById('vi-required-empty');
|
||||||
|
input.value = 'hello';
|
||||||
|
testing.expectEqual(false, input.matches(':invalid'));
|
||||||
|
testing.expectEqual(true, input.matches(':valid'));
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=indeterminatePseudo>
|
||||||
|
{
|
||||||
|
const cb = document.getElementById('vi-checkbox');
|
||||||
|
testing.expectEqual(false, cb.matches(':indeterminate'));
|
||||||
|
cb.indeterminate = true;
|
||||||
|
testing.expectEqual(true, cb.matches(':indeterminate'));
|
||||||
|
cb.indeterminate = false;
|
||||||
|
testing.expectEqual(false, cb.matches(':indeterminate'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=iterator_list_lifetime>
|
||||||
|
// This test is intended to ensure that a list remains alive as long as it
|
||||||
|
// must, i.e. as long as any iterator referencing the list is alive.
|
||||||
|
// This test depends on being able to force the v8 GC to cleanup, which
|
||||||
|
// we have no way of controlling. At worst, the test will pass without
|
||||||
|
// actually testing correct lifetime. But it was at least manually verified
|
||||||
|
// for me that this triggers plenty of GCs.
|
||||||
|
const expected = Array.from(document.querySelectorAll('*')).length;
|
||||||
|
{
|
||||||
|
let keys = [];
|
||||||
|
|
||||||
|
// Phase 1: Create many lists+iterators to fill up the arena pool
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
let list = document.querySelectorAll('*');
|
||||||
|
keys.push(list.keys());
|
||||||
|
|
||||||
|
// Create an Event every iteration to compete for arenas
|
||||||
|
new Event('arena_compete');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let k of keys) {
|
||||||
|
const result = Array.from(k);
|
||||||
|
testing.expectEqual(expected, result.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -108,6 +108,20 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=createHTMLDocument_nulll_title>
|
||||||
|
{
|
||||||
|
const impl = document.implementation;
|
||||||
|
const doc = impl.createHTMLDocument(null);
|
||||||
|
|
||||||
|
testing.expectEqual('null', doc.title);
|
||||||
|
|
||||||
|
// Should have title element in head
|
||||||
|
const titleElement = doc.head.querySelector('title');
|
||||||
|
testing.expectEqual(true, titleElement !== null);
|
||||||
|
testing.expectEqual('null', titleElement.textContent);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id=createHTMLDocument_structure>
|
<script id=createHTMLDocument_structure>
|
||||||
{
|
{
|
||||||
const impl = document.implementation;
|
const impl = document.implementation;
|
||||||
|
|||||||
@@ -93,6 +93,29 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_errors>
|
||||||
|
{
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'foo bar';
|
||||||
|
|
||||||
|
testing.withError((err) => {
|
||||||
|
testing.expectEqual('SyntaxError', err.name);
|
||||||
|
}, () => div.classList.replace('', 'baz'));
|
||||||
|
|
||||||
|
testing.withError((err) => {
|
||||||
|
testing.expectEqual('SyntaxError', err.name);
|
||||||
|
}, () => div.classList.replace('foo', ''));
|
||||||
|
|
||||||
|
testing.withError((err) => {
|
||||||
|
testing.expectEqual('InvalidCharacterError', err.name);
|
||||||
|
}, () => div.classList.replace('foo bar', 'baz'));
|
||||||
|
|
||||||
|
testing.withError((err) => {
|
||||||
|
testing.expectEqual('InvalidCharacterError', err.name);
|
||||||
|
}, () => div.classList.replace('foo', 'bar baz'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id=item>
|
<script id=item>
|
||||||
{
|
{
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
@@ -166,6 +189,29 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=classList_assignment>
|
||||||
|
{
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
// Direct assignment should work (equivalent to classList.value = ...)
|
||||||
|
div.classList = 'foo bar baz';
|
||||||
|
testing.expectEqual('foo bar baz', div.className);
|
||||||
|
testing.expectEqual(3, div.classList.length);
|
||||||
|
testing.expectEqual(true, div.classList.contains('foo'));
|
||||||
|
|
||||||
|
// Assigning again should replace
|
||||||
|
div.classList = 'qux';
|
||||||
|
testing.expectEqual('qux', div.className);
|
||||||
|
testing.expectEqual(1, div.classList.length);
|
||||||
|
testing.expectEqual(false, div.classList.contains('foo'));
|
||||||
|
|
||||||
|
// Empty assignment
|
||||||
|
div.classList = '';
|
||||||
|
testing.expectEqual('', div.className);
|
||||||
|
testing.expectEqual(0, div.classList.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id=errors>
|
<script id=errors>
|
||||||
{
|
{
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
|
|||||||
@@ -121,6 +121,29 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="propertyAssignment">
|
||||||
|
{
|
||||||
|
const div = $('#test-div');
|
||||||
|
div.style.cssText = '';
|
||||||
|
|
||||||
|
// camelCase assignment
|
||||||
|
div.style.opacity = '0.5';
|
||||||
|
testing.expectEqual('0.5', div.style.opacity);
|
||||||
|
|
||||||
|
// bracket notation assignment
|
||||||
|
div.style['filter'] = 'blur(5px)';
|
||||||
|
testing.expectEqual('blur(5px)', div.style.filter);
|
||||||
|
|
||||||
|
// numeric value coerced to string
|
||||||
|
div.style.opacity = 1;
|
||||||
|
testing.expectEqual('1', div.style.opacity);
|
||||||
|
|
||||||
|
// assigning method names should be ignored (not intercepted)
|
||||||
|
div.style.setProperty('color', 'blue');
|
||||||
|
testing.expectEqual('blue', div.style.color);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id="prototypeChainCheck">
|
<script id="prototypeChainCheck">
|
||||||
{
|
{
|
||||||
const div = $('#test-div');
|
const div = $('#test-div');
|
||||||
@@ -131,3 +154,11 @@
|
|||||||
testing.expectEqual(true, typeof div.style.getPropertyPriority === 'function');
|
testing.expectEqual(true, typeof div.style.getPropertyPriority === 'function');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div id=crash1 style="background-position: 5% .1em"></div>
|
||||||
|
<script id="crash_case_1">
|
||||||
|
{
|
||||||
|
testing.expectEqual('5% .1em', $('#crash1').style.backgroundPosition);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
123
src/browser/tests/element/get_elements_by_tag_name_ns.html
Normal file
123
src/browser/tests/element/get_elements_by_tag_name_ns.html
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<div id="container" xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<div id="div1">div1</div>
|
||||||
|
<p id="p1">p1</p>
|
||||||
|
<div id="div2">div2</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<svg id="svgContainer" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
|
||||||
|
<circle id="circle1" cx="50" cy="50" r="40"/>
|
||||||
|
<rect id="rect1" x="10" y="10" width="30" height="30"/>
|
||||||
|
<circle id="circle2" cx="25" cy="25" r="10"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div id="mixed">
|
||||||
|
<div id="htmlDiv" xmlns="http://www.w3.org/1999/xhtml">HTML div</div>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle id="svgCircle" cx="10" cy="10" r="5"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script id=basic>
|
||||||
|
{
|
||||||
|
const htmlNS = "http://www.w3.org/1999/xhtml";
|
||||||
|
const svgNS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
|
// Test HTML namespace
|
||||||
|
const htmlDivs = document.getElementsByTagNameNS(htmlNS, 'div');
|
||||||
|
testing.expectEqual(true, htmlDivs instanceof HTMLCollection);
|
||||||
|
testing.expectEqual(5, htmlDivs.length); // container, div1, div2, mixed, htmlDiv
|
||||||
|
|
||||||
|
const htmlPs = document.getElementsByTagNameNS(htmlNS, 'p');
|
||||||
|
testing.expectEqual(1, htmlPs.length);
|
||||||
|
testing.expectEqual('p1', htmlPs[0].id);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=svgNamespace>
|
||||||
|
{
|
||||||
|
const svgNS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
|
const circles = document.getElementsByTagNameNS(svgNS, 'circle');
|
||||||
|
testing.expectEqual(3, circles.length); // circle1, circle2, svgCircle
|
||||||
|
testing.expectEqual('circle1', circles[0].id);
|
||||||
|
testing.expectEqual('circle2', circles[1].id);
|
||||||
|
testing.expectEqual('svgCircle', circles[2].id);
|
||||||
|
|
||||||
|
const rects = document.getElementsByTagNameNS(svgNS, 'rect');
|
||||||
|
testing.expectEqual(1, rects.length);
|
||||||
|
testing.expectEqual('rect1', rects[0].id);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=nullNamespace>
|
||||||
|
{
|
||||||
|
// Null namespace should match elements with null namespace
|
||||||
|
const nullNsElements = document.getElementsByTagNameNS(null, 'div');
|
||||||
|
testing.expectEqual(0, nullNsElements.length); // Our divs are in HTML namespace
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=wildcardNamespace>
|
||||||
|
{
|
||||||
|
// Wildcard namespace "*" should match all namespaces
|
||||||
|
const allDivs = document.getElementsByTagNameNS('*', 'div');
|
||||||
|
testing.expectEqual(5, allDivs.length); // All divs regardless of namespace
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=wildcardLocalName>
|
||||||
|
{
|
||||||
|
const htmlNS = "http://www.w3.org/1999/xhtml";
|
||||||
|
|
||||||
|
// Wildcard local name should match all elements in that namespace
|
||||||
|
const allHtmlElements = document.getElementsByTagNameNS(htmlNS, '*');
|
||||||
|
testing.expectEqual(true, allHtmlElements.length > 0);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=caseSensitive>
|
||||||
|
{
|
||||||
|
const htmlNS = "http://www.w3.org/1999/xhtml";
|
||||||
|
|
||||||
|
// getElementsByTagNameNS is case-sensitive for local names
|
||||||
|
const lowerDivs = document.getElementsByTagNameNS(htmlNS, 'div');
|
||||||
|
const upperDivs = document.getElementsByTagNameNS(htmlNS, 'DIV');
|
||||||
|
|
||||||
|
testing.expectEqual(5, lowerDivs.length);
|
||||||
|
testing.expectEqual(0, upperDivs.length); // Should be 0 because it's case-sensitive
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=unknownNamespace>
|
||||||
|
{
|
||||||
|
// Unknown namespace should still work
|
||||||
|
const unknownNs = document.getElementsByTagNameNS('http://example.com/unknown', 'div');
|
||||||
|
testing.expectEqual(0, unknownNs.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=emptyResult>
|
||||||
|
{
|
||||||
|
const htmlNS = "http://www.w3.org/1999/xhtml";
|
||||||
|
const svgNS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
|
testing.expectEqual(0, document.getElementsByTagNameNS(htmlNS, 'nonexistent').length);
|
||||||
|
testing.expectEqual(0, document.getElementsByTagNameNS(svgNS, 'nonexistent').length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=elementMethod>
|
||||||
|
{
|
||||||
|
const htmlNS = "http://www.w3.org/1999/xhtml";
|
||||||
|
const container = document.getElementById('container');
|
||||||
|
|
||||||
|
// getElementsByTagNameNS on element should only search descendants
|
||||||
|
const divsInContainer = container.getElementsByTagNameNS(htmlNS, 'div');
|
||||||
|
testing.expectEqual(2, divsInContainer.length); // div1, div2 (not container itself)
|
||||||
|
testing.expectEqual('div1', divsInContainer[0].id);
|
||||||
|
testing.expectEqual('div2', divsInContainer[1].id);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -11,11 +11,11 @@
|
|||||||
<script id=empty_href>
|
<script id=empty_href>
|
||||||
testing.expectEqual('', $('#a0').href);
|
testing.expectEqual('', $('#a0').href);
|
||||||
|
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/anchor1.html', $('#a1').href);
|
testing.expectEqual(testing.BASE_URL + 'element/anchor1.html', $('#a1').href);
|
||||||
testing.expectEqual('http://127.0.0.1:9582/hello/world/anchor2.html', $('#a2').href);
|
testing.expectEqual(testing.ORIGIN + 'hello/world/anchor2.html', $('#a2').href);
|
||||||
testing.expectEqual('https://www.openmymind.net/Elixirs-With-Statement/', $('#a3').href);
|
testing.expectEqual('https://www.openmymind.net/Elixirs-With-Statement/', $('#a3').href);
|
||||||
|
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/foo', $('#link').href);
|
testing.expectEqual(testing.BASE_URL + 'element/html/foo', $('#link').href);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=dynamic_anchor_defaults>
|
<script id=dynamic_anchor_defaults>
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
|
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
|
||||||
|
|
||||||
link.href = 'foo';
|
link.href = 'foo';
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/foo', link.href);
|
testing.expectEqual(testing.BASE_URL + 'element/html/foo', link.href);
|
||||||
|
|
||||||
testing.expectEqual('', link.type);
|
testing.expectEqual('', link.type);
|
||||||
link.type = 'text/html';
|
link.type = 'text/html';
|
||||||
@@ -245,3 +245,11 @@
|
|||||||
testing.expectEqual('', b.toString());
|
testing.expectEqual('', b.toString());
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=url_encode>
|
||||||
|
{
|
||||||
|
let a = document.createElement('a');
|
||||||
|
a.href = 'over 9000!';
|
||||||
|
testing.expectEqual(testing.BASE_URL + 'element/html/over%209000!', a.href);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -143,6 +143,29 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="js_setter_null_clears_listener">
|
||||||
|
{
|
||||||
|
// Setting an event handler property to null must silently clear it (not throw).
|
||||||
|
// Browsers also accept undefined and non-function values without throwing.
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
div.onload = () => 42;
|
||||||
|
testing.expectEqual('function', typeof div.onload);
|
||||||
|
|
||||||
|
// Setting to null removes the listener; getter returns null
|
||||||
|
div.onload = null;
|
||||||
|
testing.expectEqual(null, div.onload);
|
||||||
|
|
||||||
|
div.onerror = () => {};
|
||||||
|
div.onerror = null;
|
||||||
|
testing.expectEqual(null, div.onerror);
|
||||||
|
|
||||||
|
div.onclick = () => {};
|
||||||
|
div.onclick = null;
|
||||||
|
testing.expectEqual(null, div.onclick);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id="different_event_types_independent">
|
<script id="different_event_types_independent">
|
||||||
{
|
{
|
||||||
// Test that different event types are stored independently
|
// Test that different event types are stored independently
|
||||||
|
|||||||
35
src/browser/tests/element/html/fieldset.html
Normal file
35
src/browser/tests/element/html/fieldset.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<fieldset id="fs1" disabled name="group1">
|
||||||
|
<input type="text">
|
||||||
|
</fieldset>
|
||||||
|
<fieldset id="fs2">
|
||||||
|
<input type="text">
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<script id="disabled">
|
||||||
|
{
|
||||||
|
const fs1 = document.getElementById('fs1');
|
||||||
|
testing.expectEqual(true, fs1.disabled);
|
||||||
|
|
||||||
|
fs1.disabled = false;
|
||||||
|
testing.expectEqual(false, fs1.disabled);
|
||||||
|
|
||||||
|
const fs2 = document.getElementById('fs2');
|
||||||
|
testing.expectEqual(false, fs2.disabled);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="name">
|
||||||
|
{
|
||||||
|
const fs1 = document.getElementById('fs1');
|
||||||
|
testing.expectEqual('group1', fs1.name);
|
||||||
|
|
||||||
|
fs1.name = 'updated';
|
||||||
|
testing.expectEqual('updated', fs1.name);
|
||||||
|
|
||||||
|
const fs2 = document.getElementById('fs2');
|
||||||
|
testing.expectEqual('', fs2.name);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
56
src/browser/tests/element/html/htmlelement-props.html
Normal file
56
src/browser/tests/element/html/htmlelement-props.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<div id="d1" hidden>Hidden div</div>
|
||||||
|
<div id="d2">Visible div</div>
|
||||||
|
<input id="i1" tabindex="5">
|
||||||
|
<div id="d3">No tabindex</div>
|
||||||
|
|
||||||
|
<script id="hidden">
|
||||||
|
{
|
||||||
|
const d1 = document.getElementById('d1');
|
||||||
|
testing.expectEqual(true, d1.hidden);
|
||||||
|
|
||||||
|
d1.hidden = false;
|
||||||
|
testing.expectEqual(false, d1.hidden);
|
||||||
|
|
||||||
|
const d2 = document.getElementById('d2');
|
||||||
|
testing.expectEqual(false, d2.hidden);
|
||||||
|
|
||||||
|
d2.hidden = true;
|
||||||
|
testing.expectEqual(true, d2.hidden);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="tabIndex">
|
||||||
|
{
|
||||||
|
const i1 = document.getElementById('i1');
|
||||||
|
testing.expectEqual(5, i1.tabIndex);
|
||||||
|
|
||||||
|
i1.tabIndex = 10;
|
||||||
|
testing.expectEqual(10, i1.tabIndex);
|
||||||
|
|
||||||
|
// Non-interactive elements default to -1
|
||||||
|
const d3 = document.getElementById('d3');
|
||||||
|
testing.expectEqual(-1, d3.tabIndex);
|
||||||
|
|
||||||
|
d3.tabIndex = 0;
|
||||||
|
testing.expectEqual(0, d3.tabIndex);
|
||||||
|
|
||||||
|
// Interactive elements default to 0 per spec
|
||||||
|
const input = document.createElement('input');
|
||||||
|
testing.expectEqual(0, input.tabIndex);
|
||||||
|
|
||||||
|
const button = document.createElement('button');
|
||||||
|
testing.expectEqual(0, button.tabIndex);
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
testing.expectEqual(0, a.tabIndex);
|
||||||
|
|
||||||
|
const select = document.createElement('select');
|
||||||
|
testing.expectEqual(0, select.tabIndex);
|
||||||
|
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
testing.expectEqual(0, textarea.tabIndex);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -32,12 +32,12 @@
|
|||||||
|
|
||||||
img.src = 'test.png';
|
img.src = 'test.png';
|
||||||
// src property returns resolved absolute URL
|
// src property returns resolved absolute URL
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/test.png', img.src);
|
testing.expectEqual(testing.BASE_URL + 'element/html/test.png', img.src);
|
||||||
// getAttribute returns the raw attribute value
|
// getAttribute returns the raw attribute value
|
||||||
testing.expectEqual('test.png', img.getAttribute('src'));
|
testing.expectEqual('test.png', img.getAttribute('src'));
|
||||||
|
|
||||||
img.src = '/absolute/path.png';
|
img.src = '/absolute/path.png';
|
||||||
testing.expectEqual('http://127.0.0.1:9582/absolute/path.png', img.src);
|
testing.expectEqual(testing.ORIGIN + 'absolute/path.png', img.src);
|
||||||
testing.expectEqual('/absolute/path.png', img.getAttribute('src'));
|
testing.expectEqual('/absolute/path.png', img.getAttribute('src'));
|
||||||
|
|
||||||
img.src = 'https://example.com/image.png';
|
img.src = 'https://example.com/image.png';
|
||||||
@@ -98,48 +98,31 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id="load-trigger-event">
|
<script id="complete">
|
||||||
{
|
{
|
||||||
|
// Image with no src is complete per spec
|
||||||
|
const img = document.createElement('img');
|
||||||
|
testing.expectEqual(true, img.complete);
|
||||||
|
|
||||||
|
// Image with src is also complete (headless browser, no actual fetch)
|
||||||
|
img.src = 'test.png';
|
||||||
|
testing.expectEqual(true, img.complete);
|
||||||
|
|
||||||
|
// Image constructor also complete
|
||||||
|
const img2 = new Image();
|
||||||
|
testing.expectEqual(true, img2.complete);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<body></body>
|
||||||
|
|
||||||
|
<script id="img-load-event">
|
||||||
|
{
|
||||||
|
// An img fires a load event when src is set.
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
let count = 0;
|
let result = false;
|
||||||
img.addEventListener("load", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
|
|
||||||
testing.expectEqual(true, count < 3);
|
|
||||||
count++;
|
|
||||||
|
|
||||||
testing.expectEqual(false, bubbles);
|
|
||||||
testing.expectEqual(false, cancelBubble);
|
|
||||||
testing.expectEqual(false, cancelable);
|
|
||||||
testing.expectEqual(false, composed);
|
|
||||||
testing.expectEqual(true, isTrusted);
|
|
||||||
testing.expectEqual(img, target);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
img.src = "https://cdn.lightpanda.io/website/assets/images/docs/hn.png";
|
|
||||||
testing.expectEqual("https://cdn.lightpanda.io/website/assets/images/docs/hn.png", img.src);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure count is incremented asynchronously.
|
|
||||||
testing.expectEqual(0, count);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<img
|
|
||||||
id="inline-img"
|
|
||||||
src="https://cdn.lightpanda.io/website/assets/images/docs/hn.png"
|
|
||||||
onload="(() => testing.expectEqual(true, true))()"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<script id="inline-on-load">
|
|
||||||
{
|
|
||||||
const img = document.getElementById("inline-img");
|
|
||||||
testing.expectEqual(true, img.onload instanceof Function);
|
|
||||||
// Also call inline to double check.
|
|
||||||
img.onload();
|
|
||||||
|
|
||||||
// Make sure ones attached with `addEventListener` also executed.
|
|
||||||
testing.async(async () => {
|
testing.async(async () => {
|
||||||
const result = await new Promise(resolve => {
|
await new Promise(resolve => {
|
||||||
img.addEventListener("load", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
|
img.addEventListener("load", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
|
||||||
testing.expectEqual(false, bubbles);
|
testing.expectEqual(false, bubbles);
|
||||||
testing.expectEqual(false, cancelBubble);
|
testing.expectEqual(false, cancelBubble);
|
||||||
@@ -147,12 +130,46 @@
|
|||||||
testing.expectEqual(false, composed);
|
testing.expectEqual(false, composed);
|
||||||
testing.expectEqual(true, isTrusted);
|
testing.expectEqual(true, isTrusted);
|
||||||
testing.expectEqual(img, target);
|
testing.expectEqual(img, target);
|
||||||
|
result = true;
|
||||||
return resolve(true);
|
return resolve();
|
||||||
|
});
|
||||||
|
img.src = "https://cdn.lightpanda.io/website/assets/images/docs/hn.png";
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
testing.expectEqual(true, result);
|
testing.eventually(() => testing.expectEqual(true, result));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="img-no-load-without-src">
|
||||||
|
{
|
||||||
|
// An img without src should not fire a load event.
|
||||||
|
let fired = false;
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.addEventListener("load", () => { fired = true; });
|
||||||
|
document.body.appendChild(img);
|
||||||
|
testing.eventually(() => testing.expectEqual(false, fired));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="lazy-src-set">
|
||||||
|
{
|
||||||
|
// Append to DOM first, then set src — load should still fire.
|
||||||
|
const img = document.createElement("img");
|
||||||
|
let result = false;
|
||||||
|
img.onload = () => result = true;
|
||||||
|
document.body.appendChild(img);
|
||||||
|
img.src = "https://cdn.lightpanda.io/website/assets/images/docs/hn.png";
|
||||||
|
|
||||||
|
testing.eventually(() => testing.expectEqual(true, result));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=url_encode>
|
||||||
|
{
|
||||||
|
let img = document.createElement('img');
|
||||||
|
img.src = 'over 9000!?hello=world !';
|
||||||
|
testing.expectEqual('over 9000!?hello=world !', img.getAttribute('src'));
|
||||||
|
testing.expectEqual(testing.BASE_URL + 'element/html/over%209000!?hello=world%20!', img.src);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
87
src/browser/tests/element/html/input-attrs.html
Normal file
87
src/browser/tests/element/html/input-attrs.html
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<input id="i1" placeholder="Enter name" min="0" max="100" step="5" autocomplete="email">
|
||||||
|
<input id="i2" type="file" multiple>
|
||||||
|
<input id="i3">
|
||||||
|
|
||||||
|
<script id="placeholder">
|
||||||
|
{
|
||||||
|
const i1 = document.getElementById('i1');
|
||||||
|
testing.expectEqual('Enter name', i1.placeholder);
|
||||||
|
|
||||||
|
i1.placeholder = 'Updated';
|
||||||
|
testing.expectEqual('Updated', i1.placeholder);
|
||||||
|
|
||||||
|
const i3 = document.getElementById('i3');
|
||||||
|
testing.expectEqual('', i3.placeholder);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="min">
|
||||||
|
{
|
||||||
|
const i1 = document.getElementById('i1');
|
||||||
|
testing.expectEqual('0', i1.min);
|
||||||
|
|
||||||
|
i1.min = '10';
|
||||||
|
testing.expectEqual('10', i1.min);
|
||||||
|
|
||||||
|
const i3 = document.getElementById('i3');
|
||||||
|
testing.expectEqual('', i3.min);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="max">
|
||||||
|
{
|
||||||
|
const i1 = document.getElementById('i1');
|
||||||
|
testing.expectEqual('100', i1.max);
|
||||||
|
|
||||||
|
i1.max = '200';
|
||||||
|
testing.expectEqual('200', i1.max);
|
||||||
|
|
||||||
|
const i3 = document.getElementById('i3');
|
||||||
|
testing.expectEqual('', i3.max);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="step">
|
||||||
|
{
|
||||||
|
const i1 = document.getElementById('i1');
|
||||||
|
testing.expectEqual('5', i1.step);
|
||||||
|
|
||||||
|
i1.step = '0.5';
|
||||||
|
testing.expectEqual('0.5', i1.step);
|
||||||
|
|
||||||
|
const i3 = document.getElementById('i3');
|
||||||
|
testing.expectEqual('', i3.step);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="multiple">
|
||||||
|
{
|
||||||
|
const i2 = document.getElementById('i2');
|
||||||
|
testing.expectEqual(true, i2.multiple);
|
||||||
|
|
||||||
|
i2.multiple = false;
|
||||||
|
testing.expectEqual(false, i2.multiple);
|
||||||
|
|
||||||
|
const i3 = document.getElementById('i3');
|
||||||
|
testing.expectEqual(false, i3.multiple);
|
||||||
|
|
||||||
|
i3.multiple = true;
|
||||||
|
testing.expectEqual(true, i3.multiple);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="autocomplete">
|
||||||
|
{
|
||||||
|
const i1 = document.getElementById('i1');
|
||||||
|
testing.expectEqual('email', i1.autocomplete);
|
||||||
|
|
||||||
|
i1.autocomplete = 'off';
|
||||||
|
testing.expectEqual('off', i1.autocomplete);
|
||||||
|
|
||||||
|
const i3 = document.getElementById('i3');
|
||||||
|
testing.expectEqual('', i3.autocomplete);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
testing.expectEqual(5, input.maxLength);
|
testing.expectEqual(5, input.maxLength);
|
||||||
input.maxLength = 'banana';
|
input.maxLength = 'banana';
|
||||||
testing.expectEqual(0, input.maxLength);
|
testing.expectEqual(0, input.maxLength);
|
||||||
testing.expectError('Error: NegativeValueNotAllowed', () => { input.maxLength = -45;});
|
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => { input.maxLength = -45;});
|
||||||
|
|
||||||
testing.expectEqual(20, input.size);
|
testing.expectEqual(20, input.size);
|
||||||
input.size = 5;
|
input.size = 5;
|
||||||
@@ -57,9 +57,9 @@
|
|||||||
|
|
||||||
testing.expectEqual('', input.src);
|
testing.expectEqual('', input.src);
|
||||||
input.src = 'foo'
|
input.src = 'foo'
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/foo', input.src);
|
testing.expectEqual(testing.BASE_URL + 'element/html/foo', input.src);
|
||||||
input.src = '-3'
|
input.src = '-3'
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/-3', input.src);
|
testing.expectEqual(testing.BASE_URL + 'element/html/-3', input.src);
|
||||||
input.src = ''
|
input.src = ''
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -221,7 +221,45 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- <script id="defaultChecked">
|
<script id="select_event">
|
||||||
|
{
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.value = 'Hello World';
|
||||||
|
document.body.appendChild(input);
|
||||||
|
|
||||||
|
let eventCount = 0;
|
||||||
|
let lastEvent = null;
|
||||||
|
|
||||||
|
input.addEventListener('select', (e) => {
|
||||||
|
eventCount++;
|
||||||
|
lastEvent = e;
|
||||||
|
});
|
||||||
|
|
||||||
|
let onselectFired = false;
|
||||||
|
input.onselect = () => { onselectFired = true; };
|
||||||
|
|
||||||
|
let bubbledToBody = false;
|
||||||
|
document.body.addEventListener('select', () => {
|
||||||
|
bubbledToBody = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.expectEqual(0, eventCount);
|
||||||
|
|
||||||
|
input.select();
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual(1, eventCount);
|
||||||
|
testing.expectEqual('select', lastEvent.type);
|
||||||
|
testing.expectEqual(input, lastEvent.target);
|
||||||
|
testing.expectEqual(true, lastEvent.bubbles);
|
||||||
|
testing.expectEqual(false, lastEvent.cancelable);
|
||||||
|
testing.expectEqual(true, bubbledToBody);
|
||||||
|
testing.expectEqual(true, onselectFired);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="defaultChecked">
|
||||||
testing.expectEqual(true, $('#check1').defaultChecked)
|
testing.expectEqual(true, $('#check1').defaultChecked)
|
||||||
testing.expectEqual(false, $('#check2').defaultChecked)
|
testing.expectEqual(false, $('#check2').defaultChecked)
|
||||||
testing.expectEqual(true, $('#radio1').defaultChecked)
|
testing.expectEqual(true, $('#radio1').defaultChecked)
|
||||||
@@ -493,4 +531,4 @@
|
|||||||
input_checked.defaultChecked = true;
|
input_checked.defaultChecked = true;
|
||||||
testing.expectEqual(false, input_checked.checked);
|
testing.expectEqual(false, input_checked.checked);
|
||||||
}
|
}
|
||||||
</script> -->
|
</script>
|
||||||
|
|||||||
283
src/browser/tests/element/html/input_click.html
Normal file
283
src/browser/tests/element/html/input_click.html
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<!-- Checkbox click tests -->
|
||||||
|
<input id="checkbox1" type="checkbox">
|
||||||
|
<input id="checkbox2" type="checkbox" checked>
|
||||||
|
<input id="checkbox_disabled" type="checkbox" disabled>
|
||||||
|
|
||||||
|
<!-- Radio click tests -->
|
||||||
|
<input id="radio1" type="radio" name="clickgroup" checked>
|
||||||
|
<input id="radio2" type="radio" name="clickgroup">
|
||||||
|
<input id="radio3" type="radio" name="clickgroup">
|
||||||
|
<input id="radio_disabled" type="radio" name="clickgroup" disabled>
|
||||||
|
|
||||||
|
<script id="checkbox_click_toggles">
|
||||||
|
{
|
||||||
|
const cb = $('#checkbox1');
|
||||||
|
testing.expectEqual(false, cb.checked);
|
||||||
|
|
||||||
|
cb.click();
|
||||||
|
testing.expectEqual(true, cb.checked);
|
||||||
|
|
||||||
|
cb.click();
|
||||||
|
testing.expectEqual(false, cb.checked);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="checkbox_click_preventDefault_reverts">
|
||||||
|
{
|
||||||
|
const cb = document.createElement('input');
|
||||||
|
cb.type = 'checkbox';
|
||||||
|
testing.expectEqual(false, cb.checked);
|
||||||
|
|
||||||
|
cb.addEventListener('click', (e) => {
|
||||||
|
testing.expectEqual(true, cb.checked, 'checkbox should be checked during click handler');
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
cb.click();
|
||||||
|
testing.expectEqual(false, cb.checked, 'checkbox should revert after preventDefault');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="checkbox_click_events_order">
|
||||||
|
{
|
||||||
|
const cb = document.createElement('input');
|
||||||
|
cb.type = 'checkbox';
|
||||||
|
document.body.appendChild(cb);
|
||||||
|
|
||||||
|
const events = [];
|
||||||
|
|
||||||
|
cb.addEventListener('click', () => events.push('click'));
|
||||||
|
cb.addEventListener('input', () => events.push('input'));
|
||||||
|
cb.addEventListener('change', () => events.push('change'));
|
||||||
|
|
||||||
|
cb.click();
|
||||||
|
|
||||||
|
testing.expectEqual(3, events.length);
|
||||||
|
testing.expectEqual('click', events[0]);
|
||||||
|
testing.expectEqual('input', events[1]);
|
||||||
|
testing.expectEqual('change', events[2]);
|
||||||
|
|
||||||
|
document.body.removeChild(cb);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="checkbox_click_preventDefault_no_input_change">
|
||||||
|
{
|
||||||
|
const cb = document.createElement('input');
|
||||||
|
cb.type = 'checkbox';
|
||||||
|
document.body.appendChild(cb);
|
||||||
|
|
||||||
|
const events = [];
|
||||||
|
|
||||||
|
cb.addEventListener('click', (e) => {
|
||||||
|
events.push('click');
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
cb.addEventListener('input', () => events.push('input'));
|
||||||
|
cb.addEventListener('change', () => events.push('change'));
|
||||||
|
|
||||||
|
cb.click();
|
||||||
|
|
||||||
|
testing.expectEqual(1, events.length, 'only click event should fire');
|
||||||
|
testing.expectEqual('click', events[0]);
|
||||||
|
|
||||||
|
document.body.removeChild(cb);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="checkbox_click_state_visible_in_handler">
|
||||||
|
{
|
||||||
|
const cb = document.createElement('input');
|
||||||
|
cb.type = 'checkbox';
|
||||||
|
cb.checked = true;
|
||||||
|
|
||||||
|
cb.addEventListener('click', (e) => {
|
||||||
|
testing.expectEqual(false, cb.checked, 'should see toggled state in handler');
|
||||||
|
e.preventDefault();
|
||||||
|
testing.expectEqual(false, cb.checked, 'should still be toggled after preventDefault in handler');
|
||||||
|
});
|
||||||
|
|
||||||
|
cb.click();
|
||||||
|
testing.expectEqual(true, cb.checked, 'should revert to original state after handler completes');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="radio_click_checks_clicked">
|
||||||
|
{
|
||||||
|
const r1 = $('#radio1');
|
||||||
|
const r2 = $('#radio2');
|
||||||
|
|
||||||
|
testing.expectEqual(true, r1.checked);
|
||||||
|
testing.expectEqual(false, r2.checked);
|
||||||
|
|
||||||
|
r2.click();
|
||||||
|
testing.expectEqual(false, r1.checked);
|
||||||
|
testing.expectEqual(true, r2.checked);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="radio_click_preventDefault_reverts">
|
||||||
|
{
|
||||||
|
const r1 = document.createElement('input');
|
||||||
|
r1.type = 'radio';
|
||||||
|
r1.name = 'testgroup';
|
||||||
|
r1.checked = true;
|
||||||
|
|
||||||
|
const r2 = document.createElement('input');
|
||||||
|
r2.type = 'radio';
|
||||||
|
r2.name = 'testgroup';
|
||||||
|
|
||||||
|
document.body.appendChild(r1);
|
||||||
|
document.body.appendChild(r2);
|
||||||
|
|
||||||
|
r2.addEventListener('click', (e) => {
|
||||||
|
testing.expectEqual(false, r1.checked, 'r1 should be unchecked during click handler');
|
||||||
|
testing.expectEqual(true, r2.checked, 'r2 should be checked during click handler');
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
r2.click();
|
||||||
|
|
||||||
|
testing.expectEqual(true, r1.checked, 'r1 should be restored after preventDefault');
|
||||||
|
testing.expectEqual(false, r2.checked, 'r2 should revert after preventDefault');
|
||||||
|
|
||||||
|
document.body.removeChild(r1);
|
||||||
|
document.body.removeChild(r2);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="radio_click_events_order">
|
||||||
|
{
|
||||||
|
const r = document.createElement('input');
|
||||||
|
r.type = 'radio';
|
||||||
|
r.name = 'eventtest';
|
||||||
|
document.body.appendChild(r);
|
||||||
|
|
||||||
|
const events = [];
|
||||||
|
|
||||||
|
r.addEventListener('click', () => events.push('click'));
|
||||||
|
r.addEventListener('input', () => events.push('input'));
|
||||||
|
r.addEventListener('change', () => events.push('change'));
|
||||||
|
|
||||||
|
r.click();
|
||||||
|
|
||||||
|
testing.expectEqual(3, events.length);
|
||||||
|
testing.expectEqual('click', events[0]);
|
||||||
|
testing.expectEqual('input', events[1]);
|
||||||
|
testing.expectEqual('change', events[2]);
|
||||||
|
|
||||||
|
document.body.removeChild(r);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="radio_click_already_checked_no_events">
|
||||||
|
{
|
||||||
|
const r = document.createElement('input');
|
||||||
|
r.type = 'radio';
|
||||||
|
r.name = 'alreadytest';
|
||||||
|
r.checked = true;
|
||||||
|
document.body.appendChild(r);
|
||||||
|
|
||||||
|
const events = [];
|
||||||
|
|
||||||
|
r.addEventListener('click', () => events.push('click'));
|
||||||
|
r.addEventListener('input', () => events.push('input'));
|
||||||
|
r.addEventListener('change', () => events.push('change'));
|
||||||
|
|
||||||
|
r.click();
|
||||||
|
|
||||||
|
testing.expectEqual(1, events.length, 'only click event should fire for already-checked radio');
|
||||||
|
testing.expectEqual('click', events[0]);
|
||||||
|
|
||||||
|
document.body.removeChild(r);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="disabled_checkbox_no_click">
|
||||||
|
{
|
||||||
|
const cb = $('#checkbox_disabled');
|
||||||
|
const events = [];
|
||||||
|
|
||||||
|
cb.addEventListener('click', () => events.push('click'));
|
||||||
|
cb.addEventListener('input', () => events.push('input'));
|
||||||
|
cb.addEventListener('change', () => events.push('change'));
|
||||||
|
|
||||||
|
cb.click();
|
||||||
|
|
||||||
|
testing.expectEqual(0, events.length, 'disabled checkbox should not fire any events');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="disabled_radio_no_click">
|
||||||
|
{
|
||||||
|
const r = $('#radio_disabled');
|
||||||
|
const events = [];
|
||||||
|
|
||||||
|
r.addEventListener('click', () => events.push('click'));
|
||||||
|
r.addEventListener('input', () => events.push('input'));
|
||||||
|
r.addEventListener('change', () => events.push('change'));
|
||||||
|
|
||||||
|
r.click();
|
||||||
|
|
||||||
|
testing.expectEqual(0, events.length, 'disabled radio should not fire any events');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="input_and_change_are_trusted">
|
||||||
|
{
|
||||||
|
const cb = document.createElement('input');
|
||||||
|
cb.type = 'checkbox';
|
||||||
|
document.body.appendChild(cb);
|
||||||
|
|
||||||
|
let inputEvent = null;
|
||||||
|
let changeEvent = null;
|
||||||
|
|
||||||
|
cb.addEventListener('input', (e) => inputEvent = e);
|
||||||
|
cb.addEventListener('change', (e) => changeEvent = e);
|
||||||
|
|
||||||
|
cb.click();
|
||||||
|
|
||||||
|
testing.expectEqual(true, inputEvent.isTrusted, 'input event should be trusted');
|
||||||
|
testing.expectEqual(true, inputEvent.bubbles, 'input event should bubble');
|
||||||
|
testing.expectEqual(false, inputEvent.cancelable, 'input event should not be cancelable');
|
||||||
|
|
||||||
|
testing.expectEqual(true, changeEvent.isTrusted, 'change event should be trusted');
|
||||||
|
testing.expectEqual(true, changeEvent.bubbles, 'change event should bubble');
|
||||||
|
testing.expectEqual(false, changeEvent.cancelable, 'change event should not be cancelable');
|
||||||
|
|
||||||
|
document.body.removeChild(cb);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="multiple_radios_click_sequence">
|
||||||
|
{
|
||||||
|
const r1 = $('#radio1');
|
||||||
|
const r2 = $('#radio2');
|
||||||
|
const r3 = $('#radio3');
|
||||||
|
|
||||||
|
// Reset to known state
|
||||||
|
r1.checked = true;
|
||||||
|
|
||||||
|
testing.expectEqual(true, r1.checked);
|
||||||
|
testing.expectEqual(false, r2.checked);
|
||||||
|
testing.expectEqual(false, r3.checked);
|
||||||
|
|
||||||
|
r2.click();
|
||||||
|
testing.expectEqual(false, r1.checked);
|
||||||
|
testing.expectEqual(true, r2.checked);
|
||||||
|
testing.expectEqual(false, r3.checked);
|
||||||
|
|
||||||
|
r3.click();
|
||||||
|
testing.expectEqual(false, r1.checked);
|
||||||
|
testing.expectEqual(false, r2.checked);
|
||||||
|
testing.expectEqual(true, r3.checked);
|
||||||
|
|
||||||
|
r1.click();
|
||||||
|
testing.expectEqual(true, r1.checked);
|
||||||
|
testing.expectEqual(false, r2.checked);
|
||||||
|
testing.expectEqual(false, r3.checked);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
74
src/browser/tests/element/html/label.html
Normal file
74
src/browser/tests/element/html/label.html
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<label id="l1" for="input1">Name</label>
|
||||||
|
<input id="input1">
|
||||||
|
|
||||||
|
<script id="htmlFor">
|
||||||
|
{
|
||||||
|
const l1 = document.getElementById('l1');
|
||||||
|
testing.expectEqual('input1', l1.htmlFor);
|
||||||
|
|
||||||
|
l1.htmlFor = 'input2';
|
||||||
|
testing.expectEqual('input2', l1.htmlFor);
|
||||||
|
|
||||||
|
const l2 = document.createElement('label');
|
||||||
|
testing.expectEqual('', l2.htmlFor);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<label id="l2" for="input1"><span>Name</span></label>
|
||||||
|
<input id="input2" type="text">
|
||||||
|
<input id="input-hidden" type="hidden">
|
||||||
|
<select id="sel1"><option>a</option></select>
|
||||||
|
<button id="btn1">Click</button>
|
||||||
|
<label id="l3"><input id="input3"><span>desc</span></label>
|
||||||
|
<label id="l4"><span>no control here</span></label>
|
||||||
|
<label id="l5"><label id="l5-inner"><input id="input5"></label></label>
|
||||||
|
|
||||||
|
<script id="control">
|
||||||
|
{
|
||||||
|
// for attribute pointing to a text input
|
||||||
|
const l2 = document.getElementById('l2');
|
||||||
|
testing.expectEqual('input1', l2.control.id);
|
||||||
|
|
||||||
|
// for attribute pointing to a non-existent id
|
||||||
|
const lMissing = document.createElement('label');
|
||||||
|
lMissing.htmlFor = 'does-not-exist';
|
||||||
|
testing.expectEqual(null, lMissing.control);
|
||||||
|
|
||||||
|
// for attribute pointing to a hidden input -> not labelable, returns null
|
||||||
|
const lHidden = document.createElement('label');
|
||||||
|
lHidden.htmlFor = 'input-hidden';
|
||||||
|
document.body.appendChild(lHidden);
|
||||||
|
testing.expectEqual(null, lHidden.control);
|
||||||
|
|
||||||
|
// for attribute pointing to a select
|
||||||
|
const lSel = document.createElement('label');
|
||||||
|
lSel.htmlFor = 'sel1';
|
||||||
|
document.body.appendChild(lSel);
|
||||||
|
testing.expectEqual('sel1', lSel.control.id);
|
||||||
|
|
||||||
|
// for attribute pointing to a button
|
||||||
|
const lBtn = document.createElement('label');
|
||||||
|
lBtn.htmlFor = 'btn1';
|
||||||
|
document.body.appendChild(lBtn);
|
||||||
|
testing.expectEqual('btn1', lBtn.control.id);
|
||||||
|
|
||||||
|
// no for attribute: first labelable descendant
|
||||||
|
const l3 = document.getElementById('l3');
|
||||||
|
testing.expectEqual('input3', l3.control.id);
|
||||||
|
|
||||||
|
// no for attribute: no labelable descendant -> null
|
||||||
|
const l4 = document.getElementById('l4');
|
||||||
|
testing.expectEqual(null, l4.control);
|
||||||
|
|
||||||
|
// no for attribute: nested labels, first labelable in tree order
|
||||||
|
const l5 = document.getElementById('l5');
|
||||||
|
testing.expectEqual('input5', l5.control.id);
|
||||||
|
|
||||||
|
// label with no for and not in document -> null
|
||||||
|
const lDetached = document.createElement('label');
|
||||||
|
testing.expectEqual(null, lDetached.control);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
23
src/browser/tests/element/html/li.html
Normal file
23
src/browser/tests/element/html/li.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li id="li1" value="5">Item</li>
|
||||||
|
<li id="li2">Item</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<script id="value">
|
||||||
|
{
|
||||||
|
const li1 = document.getElementById('li1');
|
||||||
|
testing.expectEqual(5, li1.value);
|
||||||
|
|
||||||
|
li1.value = 10;
|
||||||
|
testing.expectEqual(10, li1.value);
|
||||||
|
|
||||||
|
const li2 = document.getElementById('li2');
|
||||||
|
testing.expectEqual(0, li2.value);
|
||||||
|
|
||||||
|
li2.value = -3;
|
||||||
|
testing.expectEqual(-3, li2.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -8,5 +8,79 @@
|
|||||||
testing.expectEqual('https://lightpanda.io/opensource-browser/15', l2.href);
|
testing.expectEqual('https://lightpanda.io/opensource-browser/15', l2.href);
|
||||||
|
|
||||||
l2.href = '/over/9000';
|
l2.href = '/over/9000';
|
||||||
testing.expectEqual('http://127.0.0.1:9582/over/9000', l2.href);
|
testing.expectEqual(testing.ORIGIN + 'over/9000', l2.href);
|
||||||
|
|
||||||
|
l2.crossOrigin = 'nope';
|
||||||
|
testing.expectEqual('anonymous', l2.crossOrigin);
|
||||||
|
|
||||||
|
l2.crossOrigin = 'use-Credentials';
|
||||||
|
testing.expectEqual('use-credentials', l2.crossOrigin);
|
||||||
|
|
||||||
|
l2.crossOrigin = '';
|
||||||
|
testing.expectEqual('anonymous', l2.crossOrigin);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="link-load-event">
|
||||||
|
{
|
||||||
|
// A link with rel=stylesheet and a non-empty href fires a load event when appended to the DOM
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.href = 'https://lightpanda.io/opensource-browser/15';
|
||||||
|
|
||||||
|
testing.async(async () => {
|
||||||
|
const result = await new Promise(resolve => {
|
||||||
|
link.addEventListener('load', ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
|
||||||
|
testing.expectEqual(false, bubbles);
|
||||||
|
testing.expectEqual(false, cancelBubble);
|
||||||
|
testing.expectEqual(false, cancelable);
|
||||||
|
testing.expectEqual(false, composed);
|
||||||
|
testing.expectEqual(true, isTrusted);
|
||||||
|
testing.expectEqual(link, target);
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
document.head.appendChild(link);
|
||||||
|
});
|
||||||
|
testing.expectEqual(true, result);
|
||||||
|
});
|
||||||
|
testing.expectEqual(true, true);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="link-no-load-without-href">
|
||||||
|
{
|
||||||
|
// A link with rel=stylesheet but no href should not fire a load event
|
||||||
|
let fired = false;
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.addEventListener('load', () => { fired = true; });
|
||||||
|
document.head.appendChild(link);
|
||||||
|
testing.eventually(() => testing.expectEqual(false, fired));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="link-no-load-wrong-rel">
|
||||||
|
{
|
||||||
|
// A link without rel=stylesheet should not fire a load event
|
||||||
|
let fired = false;
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.href = 'https://lightpanda.io/opensource-browser/15';
|
||||||
|
link.addEventListener('load', () => { fired = true; });
|
||||||
|
document.head.appendChild(link);
|
||||||
|
testing.eventually(() => testing.expectEqual(false, fired));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="lazy-href-set">
|
||||||
|
{
|
||||||
|
let result = false;
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.onload = () => result = true;
|
||||||
|
// Append to DOM,
|
||||||
|
document.head.appendChild(link);
|
||||||
|
// then set href.
|
||||||
|
link.href = 'https://lightpanda.io/opensource-browser/15';
|
||||||
|
|
||||||
|
testing.eventually(() => testing.expectEqual(true, result));
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -50,6 +50,50 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="play_pause_events">
|
||||||
|
{
|
||||||
|
const audio = document.createElement('audio');
|
||||||
|
const events = [];
|
||||||
|
|
||||||
|
audio.addEventListener('play', () => events.push('play'));
|
||||||
|
audio.addEventListener('playing', () => events.push('playing'));
|
||||||
|
audio.addEventListener('pause', () => events.push('pause'));
|
||||||
|
audio.addEventListener('emptied', () => events.push('emptied'));
|
||||||
|
|
||||||
|
// First play: paused -> playing, fires play + playing
|
||||||
|
audio.play();
|
||||||
|
testing.expectEqual('play,playing', events.join(','));
|
||||||
|
|
||||||
|
// Second play: already playing, no events
|
||||||
|
audio.play();
|
||||||
|
testing.expectEqual('play,playing', events.join(','));
|
||||||
|
|
||||||
|
// Pause: playing -> paused, fires pause
|
||||||
|
audio.pause();
|
||||||
|
testing.expectEqual('play,playing,pause', events.join(','));
|
||||||
|
|
||||||
|
// Second pause: already paused, no event
|
||||||
|
audio.pause();
|
||||||
|
testing.expectEqual('play,playing,pause', events.join(','));
|
||||||
|
|
||||||
|
// Third play: resume from pause, fires play + playing (verified in Chrome)
|
||||||
|
audio.play();
|
||||||
|
testing.expectEqual('play,playing,pause,play,playing', events.join(','));
|
||||||
|
|
||||||
|
// Pause again
|
||||||
|
audio.pause();
|
||||||
|
testing.expectEqual('play,playing,pause,play,playing,pause', events.join(','));
|
||||||
|
|
||||||
|
// Load: resets state, fires emptied
|
||||||
|
audio.load();
|
||||||
|
testing.expectEqual('play,playing,pause,play,playing,pause,emptied', events.join(','));
|
||||||
|
|
||||||
|
// Play after load: fires play + playing
|
||||||
|
audio.play();
|
||||||
|
testing.expectEqual('play,playing,pause,play,playing,pause,emptied,play,playing', events.join(','));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id="volume_muted">
|
<script id="volume_muted">
|
||||||
{
|
{
|
||||||
const audio = document.getElementById('audio1');
|
const audio = document.getElementById('audio1');
|
||||||
@@ -194,7 +238,7 @@
|
|||||||
testing.expectEqual('', audio.src);
|
testing.expectEqual('', audio.src);
|
||||||
|
|
||||||
audio.src = 'test.mp3';
|
audio.src = 'test.mp3';
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/test.mp3', audio.src);
|
testing.expectEqual(testing.BASE_URL + 'element/html/test.mp3', audio.src);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -204,7 +248,7 @@
|
|||||||
testing.expectEqual('', video.poster);
|
testing.expectEqual('', video.poster);
|
||||||
|
|
||||||
video.poster = 'poster.jpg';
|
video.poster = 'poster.jpg';
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/poster.jpg', video.poster);
|
testing.expectEqual(testing.BASE_URL + 'element/html/poster.jpg', video.poster);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
51
src/browser/tests/element/html/ol.html
Normal file
51
src/browser/tests/element/html/ol.html
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<ol id="ol1" start="5" reversed type="a">
|
||||||
|
<li>Item</li>
|
||||||
|
</ol>
|
||||||
|
<ol id="ol2">
|
||||||
|
<li>Item</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<script id="start">
|
||||||
|
{
|
||||||
|
const ol1 = document.getElementById('ol1');
|
||||||
|
testing.expectEqual(5, ol1.start);
|
||||||
|
|
||||||
|
ol1.start = 10;
|
||||||
|
testing.expectEqual(10, ol1.start);
|
||||||
|
|
||||||
|
const ol2 = document.getElementById('ol2');
|
||||||
|
testing.expectEqual(1, ol2.start);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="reversed">
|
||||||
|
{
|
||||||
|
const ol1 = document.getElementById('ol1');
|
||||||
|
testing.expectEqual(true, ol1.reversed);
|
||||||
|
|
||||||
|
ol1.reversed = false;
|
||||||
|
testing.expectEqual(false, ol1.reversed);
|
||||||
|
|
||||||
|
const ol2 = document.getElementById('ol2');
|
||||||
|
testing.expectEqual(false, ol2.reversed);
|
||||||
|
|
||||||
|
ol2.reversed = true;
|
||||||
|
testing.expectEqual(true, ol2.reversed);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="type">
|
||||||
|
{
|
||||||
|
const ol1 = document.getElementById('ol1');
|
||||||
|
testing.expectEqual('a', ol1.type);
|
||||||
|
|
||||||
|
ol1.type = '1';
|
||||||
|
testing.expectEqual('1', ol1.type);
|
||||||
|
|
||||||
|
const ol2 = document.getElementById('ol2');
|
||||||
|
testing.expectEqual('1', ol2.type);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
40
src/browser/tests/element/html/optgroup.html
Normal file
40
src/browser/tests/element/html/optgroup.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<select>
|
||||||
|
<optgroup id="og1" label="Group 1" disabled>
|
||||||
|
<option>A</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup id="og2" label="Group 2">
|
||||||
|
<option>B</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<script id="disabled">
|
||||||
|
{
|
||||||
|
const og1 = document.getElementById('og1');
|
||||||
|
testing.expectEqual(true, og1.disabled);
|
||||||
|
|
||||||
|
og1.disabled = false;
|
||||||
|
testing.expectEqual(false, og1.disabled);
|
||||||
|
|
||||||
|
const og2 = document.getElementById('og2');
|
||||||
|
testing.expectEqual(false, og2.disabled);
|
||||||
|
|
||||||
|
og2.disabled = true;
|
||||||
|
testing.expectEqual(true, og2.disabled);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="label">
|
||||||
|
{
|
||||||
|
const og1 = document.getElementById('og1');
|
||||||
|
testing.expectEqual('Group 1', og1.label);
|
||||||
|
|
||||||
|
og1.label = 'Updated';
|
||||||
|
testing.expectEqual('Updated', og1.label);
|
||||||
|
|
||||||
|
const og = document.createElement('optgroup');
|
||||||
|
testing.expectEqual('', og.label);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -29,6 +29,12 @@
|
|||||||
testing.expectEqual('Text 3', $('#opt3').text)
|
testing.expectEqual('Text 3', $('#opt3').text)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="text_set">
|
||||||
|
$('#opt1').text = 'New Text 1'
|
||||||
|
testing.expectEqual('New Text 1', $('#opt1').text)
|
||||||
|
testing.expectEqual('New Text 1', $('#opt1').textContent)
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id="selected">
|
<script id="selected">
|
||||||
testing.expectEqual(false, $('#opt1').selected)
|
testing.expectEqual(false, $('#opt1').selected)
|
||||||
testing.expectEqual(true, $('#opt2').selected)
|
testing.expectEqual(true, $('#opt2').selected)
|
||||||
|
|||||||
17
src/browser/tests/element/html/quote.html
Normal file
17
src/browser/tests/element/html/quote.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<blockquote id="q1" cite="https://example.com/source">Quote</blockquote>
|
||||||
|
|
||||||
|
<script id="cite">
|
||||||
|
{
|
||||||
|
const q1 = document.getElementById('q1');
|
||||||
|
testing.expectEqual('https://example.com/source', q1.cite);
|
||||||
|
|
||||||
|
q1.cite = 'https://example.com/other';
|
||||||
|
testing.expectEqual('https://example.com/other', q1.cite);
|
||||||
|
|
||||||
|
const q2 = document.createElement('blockquote');
|
||||||
|
testing.expectEqual('', q2.cite);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
0
src/browser/tests/element/html/script/empty.js
Normal file
0
src/browser/tests/element/html/script/empty.js
Normal file
@@ -2,11 +2,28 @@
|
|||||||
<script src="../../../testing.js"></script>
|
<script src="../../../testing.js"></script>
|
||||||
|
|
||||||
<script id="script">
|
<script id="script">
|
||||||
{
|
{
|
||||||
let s = document.createElement('script');
|
let dom_load = false;
|
||||||
testing.expectEqual('', s.src);
|
let attribute_load = false;
|
||||||
|
|
||||||
s.src = '/over.9000.js';
|
let s = document.createElement('script');
|
||||||
testing.expectEqual('http://127.0.0.1:9582/over.9000.js', s.src);
|
document.documentElement.addEventListener('load', (e) => {
|
||||||
|
testing.expectEqual(s, e.target);
|
||||||
|
dom_load = true;
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
testing.expectEqual('', s.src);
|
||||||
|
s.onload = function(e) {
|
||||||
|
testing.expectEqual(s, e.target);
|
||||||
|
attribute_load = true;
|
||||||
}
|
}
|
||||||
|
s.src = 'empty.js';
|
||||||
|
testing.expectEqual(testing.BASE_URL + 'element/html/script/empty.js', s.src);
|
||||||
|
document.head.appendChild(s);
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual(true, dom_load);
|
||||||
|
testing.expectEqual(true, attribute_load);
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,7 +3,46 @@
|
|||||||
|
|
||||||
<script id="sheet">
|
<script id="sheet">
|
||||||
{
|
{
|
||||||
|
// Disconnected style element should have no sheet
|
||||||
testing.expectEqual(null, document.createElement('style').sheet);
|
testing.expectEqual(null, document.createElement('style').sheet);
|
||||||
|
|
||||||
|
// Connected style element should have a CSSStyleSheet
|
||||||
|
const style = document.createElement('style');
|
||||||
|
document.head.appendChild(style);
|
||||||
|
testing.expectEqual(true, style.sheet instanceof CSSStyleSheet);
|
||||||
|
|
||||||
|
// Same sheet instance on repeated access
|
||||||
|
testing.expectEqual(true, style.sheet === style.sheet);
|
||||||
|
|
||||||
|
// Non-CSS type should have no sheet
|
||||||
|
const lessStyle = document.createElement('style');
|
||||||
|
lessStyle.type = 'text/less';
|
||||||
|
document.head.appendChild(lessStyle);
|
||||||
|
testing.expectEqual(null, lessStyle.sheet);
|
||||||
|
|
||||||
|
// Empty type attribute is valid (defaults to text/css per spec)
|
||||||
|
const emptyType = document.createElement('style');
|
||||||
|
emptyType.setAttribute('type', '');
|
||||||
|
document.head.appendChild(emptyType);
|
||||||
|
testing.expectEqual(true, emptyType.sheet instanceof CSSStyleSheet);
|
||||||
|
|
||||||
|
// Case-insensitive type check
|
||||||
|
const upperType = document.createElement('style');
|
||||||
|
upperType.type = 'TEXT/CSS';
|
||||||
|
document.head.appendChild(upperType);
|
||||||
|
testing.expectEqual(true, upperType.sheet instanceof CSSStyleSheet);
|
||||||
|
|
||||||
|
// Disconnection clears sheet
|
||||||
|
const tempStyle = document.createElement('style');
|
||||||
|
document.head.appendChild(tempStyle);
|
||||||
|
testing.expectEqual(true, tempStyle.sheet instanceof CSSStyleSheet);
|
||||||
|
document.head.removeChild(tempStyle);
|
||||||
|
testing.expectEqual(null, tempStyle.sheet);
|
||||||
|
|
||||||
|
// ownerNode points back to the style element
|
||||||
|
const ownStyle = document.createElement('style');
|
||||||
|
document.head.appendChild(ownStyle);
|
||||||
|
testing.expectEqual(true, ownStyle.sheet.ownerNode === ownStyle);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -67,3 +106,28 @@
|
|||||||
testing.expectEqual(true, style.disabled);
|
testing.expectEqual(true, style.disabled);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="style-load-event">
|
||||||
|
{
|
||||||
|
// A style element fires a load event when appended to the DOM.
|
||||||
|
const style = document.createElement("style");
|
||||||
|
let result = false;
|
||||||
|
testing.async(async () => {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
style.addEventListener("load", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
|
||||||
|
testing.expectEqual(false, bubbles);
|
||||||
|
testing.expectEqual(false, cancelBubble);
|
||||||
|
testing.expectEqual(false, cancelable);
|
||||||
|
testing.expectEqual(false, composed);
|
||||||
|
testing.expectEqual(true, isTrusted);
|
||||||
|
testing.expectEqual(style, target);
|
||||||
|
result = true;
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
document.head.appendChild(style);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.eventually(() => testing.expectEqual(true, result));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
51
src/browser/tests/element/html/tablecell.html
Normal file
51
src/browser/tests/element/html/tablecell.html
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td id="td1" colspan="3" rowspan="2">Cell</td>
|
||||||
|
<td id="td2">Cell</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<script id="colSpan">
|
||||||
|
{
|
||||||
|
const td1 = document.getElementById('td1');
|
||||||
|
testing.expectEqual(3, td1.colSpan);
|
||||||
|
|
||||||
|
td1.colSpan = 5;
|
||||||
|
testing.expectEqual(5, td1.colSpan);
|
||||||
|
|
||||||
|
const td2 = document.getElementById('td2');
|
||||||
|
testing.expectEqual(1, td2.colSpan);
|
||||||
|
|
||||||
|
// colSpan 0 clamps to 1
|
||||||
|
td2.colSpan = 0;
|
||||||
|
testing.expectEqual(1, td2.colSpan);
|
||||||
|
|
||||||
|
// colSpan > 1000 clamps to 1000
|
||||||
|
td2.colSpan = 9999;
|
||||||
|
testing.expectEqual(1000, td2.colSpan);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="rowSpan">
|
||||||
|
{
|
||||||
|
const td1 = document.getElementById('td1');
|
||||||
|
testing.expectEqual(2, td1.rowSpan);
|
||||||
|
|
||||||
|
td1.rowSpan = 4;
|
||||||
|
testing.expectEqual(4, td1.rowSpan);
|
||||||
|
|
||||||
|
const td2 = document.getElementById('td2');
|
||||||
|
testing.expectEqual(1, td2.rowSpan);
|
||||||
|
|
||||||
|
// rowSpan 0 is valid per spec (span remaining rows)
|
||||||
|
td2.rowSpan = 0;
|
||||||
|
testing.expectEqual(0, td2.rowSpan);
|
||||||
|
|
||||||
|
// rowSpan > 65534 clamps to 65534
|
||||||
|
td2.rowSpan = 99999;
|
||||||
|
testing.expectEqual(65534, td2.rowSpan);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -230,6 +230,44 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="select_event">
|
||||||
|
{
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = 'Hello World';
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
|
||||||
|
let eventCount = 0;
|
||||||
|
let lastEvent = null;
|
||||||
|
|
||||||
|
textarea.addEventListener('select', (e) => {
|
||||||
|
eventCount++;
|
||||||
|
lastEvent = e;
|
||||||
|
});
|
||||||
|
|
||||||
|
let onselectFired = false;
|
||||||
|
textarea.onselect = () => { onselectFired = true; };
|
||||||
|
|
||||||
|
let bubbledToBody = false;
|
||||||
|
document.body.addEventListener('select', () => {
|
||||||
|
bubbledToBody = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.expectEqual(0, eventCount);
|
||||||
|
|
||||||
|
textarea.select();
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual(1, eventCount);
|
||||||
|
testing.expectEqual('select', lastEvent.type);
|
||||||
|
testing.expectEqual(textarea, lastEvent.target);
|
||||||
|
testing.expectEqual(true, lastEvent.bubbles);
|
||||||
|
testing.expectEqual(false, lastEvent.cancelable);
|
||||||
|
testing.expectEqual(true, bubbledToBody);
|
||||||
|
testing.expectEqual(true, onselectFired);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id="selectionchange_event">
|
<script id="selectionchange_event">
|
||||||
{
|
{
|
||||||
const textarea = document.createElement('textarea');
|
const textarea = document.createElement('textarea');
|
||||||
|
|||||||
17
src/browser/tests/element/html/time.html
Normal file
17
src/browser/tests/element/html/time.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<time id="t1" datetime="2024-01-15">January 15</time>
|
||||||
|
|
||||||
|
<script id="dateTime">
|
||||||
|
{
|
||||||
|
const t = document.getElementById('t1');
|
||||||
|
testing.expectEqual('2024-01-15', t.dateTime);
|
||||||
|
|
||||||
|
t.dateTime = '2024-12-25T10:00';
|
||||||
|
testing.expectEqual('2024-12-25T10:00', t.dateTime);
|
||||||
|
|
||||||
|
const t2 = document.createElement('time');
|
||||||
|
testing.expectEqual('', t2.dateTime);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -45,7 +45,6 @@
|
|||||||
testing.expectEqual('hi', $('#link').innerText);
|
testing.expectEqual('hi', $('#link').innerText);
|
||||||
|
|
||||||
d1.innerHTML = '';
|
d1.innerHTML = '';
|
||||||
testing.todo(null, $('#link'));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=attributeSerialization>
|
<script id=attributeSerialization>
|
||||||
|
|||||||
116
src/browser/tests/element/position.html
Normal file
116
src/browser/tests/element/position.html
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<div id="test1">Test Element</div>
|
||||||
|
<div id="test2">Another Element</div>
|
||||||
|
|
||||||
|
<script id="clientDimensions">
|
||||||
|
{
|
||||||
|
const test1 = $('#test1');
|
||||||
|
|
||||||
|
// clientWidth/Height - default is 5px in dummy layout
|
||||||
|
testing.expectEqual('number', typeof test1.clientWidth);
|
||||||
|
testing.expectEqual('number', typeof test1.clientHeight);
|
||||||
|
testing.expectTrue(test1.clientWidth >= 0);
|
||||||
|
testing.expectTrue(test1.clientHeight >= 0);
|
||||||
|
|
||||||
|
// clientTop/Left should be 0 (no borders in dummy layout)
|
||||||
|
testing.expectEqual(0, test1.clientTop);
|
||||||
|
testing.expectEqual(0, test1.clientLeft);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="scrollDimensions">
|
||||||
|
{
|
||||||
|
const test1 = $('#test1');
|
||||||
|
|
||||||
|
// In dummy layout, scroll dimensions equal client dimensions (no overflow)
|
||||||
|
testing.expectEqual(test1.clientWidth, test1.scrollWidth);
|
||||||
|
testing.expectEqual(test1.clientHeight, test1.scrollHeight);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="scrollPosition">
|
||||||
|
{
|
||||||
|
const test1 = $('#test1');
|
||||||
|
|
||||||
|
// Initial scroll position should be 0
|
||||||
|
testing.expectEqual(0, test1.scrollTop);
|
||||||
|
testing.expectEqual(0, test1.scrollLeft);
|
||||||
|
|
||||||
|
// Setting scroll position
|
||||||
|
test1.scrollTop = 50;
|
||||||
|
testing.expectEqual(50, test1.scrollTop);
|
||||||
|
|
||||||
|
test1.scrollLeft = 25;
|
||||||
|
testing.expectEqual(25, test1.scrollLeft);
|
||||||
|
|
||||||
|
// Negative values should be clamped to 0
|
||||||
|
test1.scrollTop = -10;
|
||||||
|
testing.expectEqual(0, test1.scrollTop);
|
||||||
|
|
||||||
|
test1.scrollLeft = -5;
|
||||||
|
testing.expectEqual(0, test1.scrollLeft);
|
||||||
|
|
||||||
|
// Each element has independent scroll position
|
||||||
|
const test2 = $('#test2');
|
||||||
|
testing.expectEqual(0, test2.scrollTop);
|
||||||
|
testing.expectEqual(0, test2.scrollLeft);
|
||||||
|
|
||||||
|
test2.scrollTop = 100;
|
||||||
|
testing.expectEqual(100, test2.scrollTop);
|
||||||
|
testing.expectEqual(0, test1.scrollTop); // test1 should still be 0
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="offsetDimensions">
|
||||||
|
{
|
||||||
|
const test1 = $('#test1');
|
||||||
|
|
||||||
|
// offsetWidth/Height should be numbers
|
||||||
|
testing.expectEqual('number', typeof test1.offsetWidth);
|
||||||
|
testing.expectEqual('number', typeof test1.offsetHeight);
|
||||||
|
testing.expectTrue(test1.offsetWidth >= 0);
|
||||||
|
testing.expectTrue(test1.offsetHeight >= 0);
|
||||||
|
|
||||||
|
// Should equal client dimensions
|
||||||
|
testing.expectEqual(test1.clientWidth, test1.offsetWidth);
|
||||||
|
testing.expectEqual(test1.clientHeight, test1.offsetHeight);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="offsetPosition">
|
||||||
|
{
|
||||||
|
const test1 = $('#test1');
|
||||||
|
const test2 = $('#test2');
|
||||||
|
|
||||||
|
// offsetTop/Left should be calculated from tree position
|
||||||
|
// These values are based on the heuristic layout engine
|
||||||
|
const top1 = test1.offsetTop;
|
||||||
|
const left1 = test1.offsetLeft;
|
||||||
|
const top2 = test2.offsetTop;
|
||||||
|
const left2 = test2.offsetLeft;
|
||||||
|
|
||||||
|
// Position values should be numbers
|
||||||
|
testing.expectEqual('number', typeof top1);
|
||||||
|
testing.expectEqual('number', typeof left1);
|
||||||
|
testing.expectEqual('number', typeof top2);
|
||||||
|
testing.expectEqual('number', typeof left2);
|
||||||
|
|
||||||
|
// Siblings should have different positions (either different x or y)
|
||||||
|
testing.expectTrue(top1 !== top2 || left1 !== left2);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="offsetVsBounding">
|
||||||
|
{
|
||||||
|
const test1 = $('#test1');
|
||||||
|
|
||||||
|
// offsetTop/Left should match getBoundingClientRect
|
||||||
|
const rect = test1.getBoundingClientRect();
|
||||||
|
testing.expectEqual(rect.y, test1.offsetTop);
|
||||||
|
testing.expectEqual(rect.x, test1.offsetLeft);
|
||||||
|
testing.expectEqual(rect.width, test1.offsetWidth);
|
||||||
|
testing.expectEqual(rect.height, test1.offsetHeight);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
22
src/browser/tests/event/focus.html
Normal file
22
src/browser/tests/event/focus.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script id=default>
|
||||||
|
let event = new FocusEvent('focus');
|
||||||
|
testing.expectEqual('focus', event.type);
|
||||||
|
testing.expectEqual(true, event instanceof FocusEvent);
|
||||||
|
testing.expectEqual(true, event instanceof UIEvent);
|
||||||
|
testing.expectEqual(true, event instanceof Event);
|
||||||
|
testing.expectEqual(null, event.relatedTarget);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=parameters>
|
||||||
|
let div = document.createElement('div');
|
||||||
|
let focusEvent = new FocusEvent('blur', { relatedTarget: div });
|
||||||
|
testing.expectEqual(div, focusEvent.relatedTarget);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=createEvent>
|
||||||
|
let evt = document.createEvent('focusevent');
|
||||||
|
testing.expectEqual(true, evt instanceof FocusEvent);
|
||||||
|
testing.expectEqual(true, evt instanceof UIEvent);
|
||||||
|
</script>
|
||||||
@@ -103,3 +103,16 @@
|
|||||||
document.dispatchEvent(new KeyboardEvent('keytest', {key: 'b'}));
|
document.dispatchEvent(new KeyboardEvent('keytest', {key: 'b'}));
|
||||||
testing.expectEqual(false, keyIsTrusted);
|
testing.expectEqual(false, keyIsTrusted);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=non_keyboard_keydown>
|
||||||
|
// this used to crash
|
||||||
|
{
|
||||||
|
let called = false;
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.addEventListener('keydown', () => {
|
||||||
|
called = true;
|
||||||
|
});
|
||||||
|
div.dispatchEvent(new Event('keydown'));
|
||||||
|
testing.expectEqual(true, called);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -10,11 +10,13 @@
|
|||||||
testing.expectEqual(0, event.clientY);
|
testing.expectEqual(0, event.clientY);
|
||||||
testing.expectEqual(0, event.screenX);
|
testing.expectEqual(0, event.screenX);
|
||||||
testing.expectEqual(0, event.screenY);
|
testing.expectEqual(0, event.screenY);
|
||||||
|
testing.expectEqual(0, event.buttons);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=parameters>
|
<script id=parameters>
|
||||||
let new_event = new MouseEvent('click', { 'button': 0, 'clientX': 10, 'clientY': 20, screenX: 200, screenY: 500 });
|
let new_event = new MouseEvent('click', { 'button': 0, 'clientX': 10, 'clientY': 20, screenX: 200, screenY: 500, buttons: 5 });
|
||||||
testing.expectEqual(0, new_event.button);
|
testing.expectEqual(0, new_event.button);
|
||||||
|
testing.expectEqual(5, new_event.buttons);
|
||||||
testing.expectEqual(10, new_event.x);
|
testing.expectEqual(10, new_event.x);
|
||||||
testing.expectEqual(20, new_event.y);
|
testing.expectEqual(20, new_event.y);
|
||||||
testing.expectEqual(10, new_event.pageX);
|
testing.expectEqual(10, new_event.pageX);
|
||||||
|
|||||||
17
src/browser/tests/event/text.html
Normal file
17
src/browser/tests/event/text.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script id=createEvent>
|
||||||
|
let evt = document.createEvent('TextEvent');
|
||||||
|
testing.expectEqual(true, evt instanceof TextEvent);
|
||||||
|
testing.expectEqual(true, evt instanceof UIEvent);
|
||||||
|
testing.expectEqual('', evt.data);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=initTextEvent>
|
||||||
|
let textEvent = document.createEvent('TextEvent');
|
||||||
|
textEvent.initTextEvent('textInput', true, false, window, 'test data');
|
||||||
|
testing.expectEqual('textInput', textEvent.type);
|
||||||
|
testing.expectEqual('test data', textEvent.data);
|
||||||
|
testing.expectEqual(true, textEvent.bubbles);
|
||||||
|
testing.expectEqual(false, textEvent.cancelable);
|
||||||
|
</script>
|
||||||
30
src/browser/tests/event/wheel.html
Normal file
30
src/browser/tests/event/wheel.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script id=default>
|
||||||
|
let event = new WheelEvent('wheel');
|
||||||
|
testing.expectEqual('wheel', event.type);
|
||||||
|
testing.expectEqual(true, event instanceof WheelEvent);
|
||||||
|
testing.expectEqual(true, event instanceof MouseEvent);
|
||||||
|
testing.expectEqual(true, event instanceof UIEvent);
|
||||||
|
testing.expectEqual(0, event.deltaX);
|
||||||
|
testing.expectEqual(0, event.deltaY);
|
||||||
|
testing.expectEqual(0, event.deltaZ);
|
||||||
|
testing.expectEqual(0, event.deltaMode);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=parameters>
|
||||||
|
let wheelEvent = new WheelEvent('wheel', {
|
||||||
|
deltaX: 10,
|
||||||
|
deltaY: 20,
|
||||||
|
deltaMode: WheelEvent.DOM_DELTA_LINE
|
||||||
|
});
|
||||||
|
testing.expectEqual(10, wheelEvent.deltaX);
|
||||||
|
testing.expectEqual(20, wheelEvent.deltaY);
|
||||||
|
testing.expectEqual(1, wheelEvent.deltaMode);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=constants>
|
||||||
|
testing.expectEqual(0, WheelEvent.DOM_DELTA_PIXEL);
|
||||||
|
testing.expectEqual(1, WheelEvent.DOM_DELTA_LINE);
|
||||||
|
testing.expectEqual(2, WheelEvent.DOM_DELTA_PAGE);
|
||||||
|
</script>
|
||||||
@@ -635,3 +635,130 @@
|
|||||||
// https://github.com/lightpanda-io/browser/pull/1316
|
// https://github.com/lightpanda-io/browser/pull/1316
|
||||||
testing.expectError('TypeError', () => MessageEvent(''));
|
testing.expectError('TypeError', () => MessageEvent(''));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div id=inline_parent><div id=inline_child></div></div>
|
||||||
|
<script id=inlineHandlerReceivesEvent>
|
||||||
|
// Test that inline onclick handler receives the event object
|
||||||
|
{
|
||||||
|
const inline_child = $('#inline_child');
|
||||||
|
let receivedType = null;
|
||||||
|
let receivedTarget = null;
|
||||||
|
let receivedCurrentTarget = null;
|
||||||
|
|
||||||
|
inline_child.onclick = function(e) {
|
||||||
|
// Capture values DURING handler execution
|
||||||
|
receivedType = e.type;
|
||||||
|
receivedTarget = e.target;
|
||||||
|
receivedCurrentTarget = e.currentTarget;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline_child.click();
|
||||||
|
|
||||||
|
testing.expectEqual('click', receivedType);
|
||||||
|
testing.expectEqual(inline_child, receivedTarget);
|
||||||
|
testing.expectEqual(inline_child, receivedCurrentTarget);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id=inline_order_parent><div id=inline_order_child></div></div>
|
||||||
|
<script id=inlineHandlerOrder>
|
||||||
|
// Test that inline handler executes in proper order with addEventListener
|
||||||
|
{
|
||||||
|
const inline_order_child = $('#inline_order_child');
|
||||||
|
const inline_order_parent = $('#inline_order_parent');
|
||||||
|
const order = [];
|
||||||
|
|
||||||
|
// Capture listener on parent
|
||||||
|
inline_order_parent.addEventListener('click', () => order.push('parent-capture'), true);
|
||||||
|
|
||||||
|
// Inline handler on child (should execute at target phase)
|
||||||
|
inline_order_child.onclick = () => order.push('child-onclick');
|
||||||
|
|
||||||
|
// addEventListener on child (should execute at target phase, after onclick)
|
||||||
|
inline_order_child.addEventListener('click', () => order.push('child-listener'));
|
||||||
|
|
||||||
|
// Bubble listener on parent
|
||||||
|
inline_order_parent.addEventListener('click', () => order.push('parent-bubble'));
|
||||||
|
|
||||||
|
inline_order_child.click();
|
||||||
|
|
||||||
|
// Expected order: capture, then onclick, then addEventListener, then bubble
|
||||||
|
testing.expectEqual('parent-capture', order[0]);
|
||||||
|
testing.expectEqual('child-onclick', order[1]);
|
||||||
|
testing.expectEqual('child-listener', order[2]);
|
||||||
|
testing.expectEqual('parent-bubble', order[3]);
|
||||||
|
testing.expectEqual(4, order.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id=inline_prevent><div id=inline_prevent_child></div></div>
|
||||||
|
<script id=inlineHandlerPreventDefault>
|
||||||
|
// Test that inline handler can preventDefault and it affects addEventListener listeners
|
||||||
|
{
|
||||||
|
const inline_prevent_child = $('#inline_prevent_child');
|
||||||
|
let preventDefaultCalled = false;
|
||||||
|
let listenerSawPrevented = false;
|
||||||
|
|
||||||
|
inline_prevent_child.onclick = function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
preventDefaultCalled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline_prevent_child.addEventListener('click', (e) => {
|
||||||
|
listenerSawPrevented = e.defaultPrevented;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = inline_prevent_child.dispatchEvent(new MouseEvent('click', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
testing.expectEqual(true, preventDefaultCalled);
|
||||||
|
testing.expectEqual(true, listenerSawPrevented);
|
||||||
|
testing.expectEqual(false, result); // dispatchEvent returns false when prevented
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id=inline_stop_parent><div id=inline_stop_child></div></div>
|
||||||
|
<script id=inlineHandlerStopPropagation>
|
||||||
|
// Test that inline handler can stopPropagation
|
||||||
|
{
|
||||||
|
const inline_stop_child = $('#inline_stop_child');
|
||||||
|
const inline_stop_parent = $('#inline_stop_parent');
|
||||||
|
let childCalled = false;
|
||||||
|
let parentCalled = false;
|
||||||
|
|
||||||
|
inline_stop_child.onclick = function(e) {
|
||||||
|
childCalled = true;
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
inline_stop_parent.addEventListener('click', () => {
|
||||||
|
parentCalled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
inline_stop_child.click();
|
||||||
|
|
||||||
|
testing.expectEqual(true, childCalled);
|
||||||
|
testing.expectEqual(false, parentCalled); // Should not bubble to parent
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id=inline_replace_test></div>
|
||||||
|
<script id=inlineHandlerReplacement>
|
||||||
|
// Test that setting onclick property replaces previous handler
|
||||||
|
{
|
||||||
|
const inline_replace_test = $('#inline_replace_test');
|
||||||
|
let calls = [];
|
||||||
|
|
||||||
|
inline_replace_test.onclick = () => calls.push('first');
|
||||||
|
inline_replace_test.click();
|
||||||
|
|
||||||
|
inline_replace_test.onclick = () => calls.push('second');
|
||||||
|
inline_replace_test.click();
|
||||||
|
|
||||||
|
testing.expectEqual('first', calls[0]);
|
||||||
|
testing.expectEqual('second', calls[1]);
|
||||||
|
testing.expectEqual(2, calls.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
197
src/browser/tests/file_reader.html
Normal file
197
src/browser/tests/file_reader.html
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<script src="./testing.js"></script>
|
||||||
|
|
||||||
|
<script id=basic>
|
||||||
|
{
|
||||||
|
const reader = new FileReader();
|
||||||
|
testing.expectEqual(0, reader.readyState);
|
||||||
|
testing.expectEqual(null, reader.result);
|
||||||
|
testing.expectEqual(null, reader.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
testing.expectEqual(0, FileReader.EMPTY);
|
||||||
|
testing.expectEqual(1, FileReader.LOADING);
|
||||||
|
testing.expectEqual(2, FileReader.DONE);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=readAsText>
|
||||||
|
testing.async(async () => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const blob = new Blob(["Hello, World!"], { type: "text/plain" });
|
||||||
|
|
||||||
|
let loadstartFired = false;
|
||||||
|
let progressFired = false;
|
||||||
|
let loadFired = false;
|
||||||
|
let loadendFired = false;
|
||||||
|
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
reader.onloadstart = function(e) {
|
||||||
|
loadstartFired = true;
|
||||||
|
testing.expectEqual("loadstart", e.type);
|
||||||
|
testing.expectEqual(1, reader.readyState);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onprogress = function(e) {
|
||||||
|
progressFired = true;
|
||||||
|
testing.expectEqual(13, e.loaded);
|
||||||
|
testing.expectEqual(13, e.total);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onload = function(e) {
|
||||||
|
loadFired = true;
|
||||||
|
testing.expectEqual(2, reader.readyState);
|
||||||
|
testing.expectEqual("Hello, World!", reader.result);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onloadend = function(e) {
|
||||||
|
loadendFired = true;
|
||||||
|
testing.expectEqual(true, loadstartFired);
|
||||||
|
testing.expectEqual(true, progressFired);
|
||||||
|
testing.expectEqual(true, loadFired);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsText(blob);
|
||||||
|
await promise;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=readAsDataURL>
|
||||||
|
testing.async(async () => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const blob = new Blob(["test"], { type: "text/plain" });
|
||||||
|
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
reader.onload = function() {
|
||||||
|
testing.expectEqual("data:text/plain;base64,dGVzdA==", reader.result);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
await promise;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Empty MIME type
|
||||||
|
testing.async(async () => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const blob = new Blob(["test"]);
|
||||||
|
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
reader.onload = function() {
|
||||||
|
testing.expectEqual("data:application/octet-stream;base64,dGVzdA==", reader.result);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
await promise;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=readAsArrayBuffer>
|
||||||
|
testing.async(async () => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const blob = new Blob([new Uint8Array([65, 66, 67])]);
|
||||||
|
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
reader.onload = function() {
|
||||||
|
const result = reader.result;
|
||||||
|
testing.expectEqual(true, result instanceof ArrayBuffer);
|
||||||
|
testing.expectEqual(3, result.byteLength);
|
||||||
|
|
||||||
|
const view = new Uint8Array(result);
|
||||||
|
testing.expectEqual(65, view[0]);
|
||||||
|
testing.expectEqual(66, view[1]);
|
||||||
|
testing.expectEqual(67, view[2]);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsArrayBuffer(blob);
|
||||||
|
await promise;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=readAsBinaryString>
|
||||||
|
testing.async(async () => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const blob = new Blob(["ABC"]);
|
||||||
|
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
reader.onload = function() {
|
||||||
|
testing.expectEqual("ABC", reader.result);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsBinaryString(blob);
|
||||||
|
await promise;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=abort>
|
||||||
|
// Test aborting when not loading (should do nothing)
|
||||||
|
{
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.abort(); // Should not throw
|
||||||
|
testing.expectEqual(0, reader.readyState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Testing abort during read is implementation-dependent.
|
||||||
|
// In synchronous implementations (like ours), the read completes before abort can be called.
|
||||||
|
// In async implementations (like Firefox), you can abort during the read.
|
||||||
|
// We test that abort() at least doesn't throw and maintains correct state.
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=multipleReads>
|
||||||
|
testing.async(async () => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const blob1 = new Blob(["first"]);
|
||||||
|
const blob2 = new Blob(["second"]);
|
||||||
|
|
||||||
|
// First read
|
||||||
|
const promise1 = new Promise((resolve) => {
|
||||||
|
reader.onload = function() {
|
||||||
|
testing.expectEqual("first", reader.result);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
reader.readAsText(blob1);
|
||||||
|
await promise1;
|
||||||
|
|
||||||
|
// Second read - should work after first completes
|
||||||
|
const promise2 = new Promise((resolve) => {
|
||||||
|
reader.onload = function() {
|
||||||
|
testing.expectEqual("second", reader.result);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
reader.readAsText(blob2);
|
||||||
|
await promise2;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=addEventListener>
|
||||||
|
testing.async(async () => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const blob = new Blob(["test"]);
|
||||||
|
|
||||||
|
let loadFired = false;
|
||||||
|
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
reader.addEventListener("load", function() {
|
||||||
|
loadFired = true;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsText(blob);
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
testing.expectEqual(true, loadFired);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
76
src/browser/tests/frames/frames.html
Normal file
76
src/browser/tests/frames/frames.html
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function frame1Onload() {
|
||||||
|
window.f1_onload = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<iframe id=f1 onload="frame1Onload" src="support/sub 1.html"></iframe>
|
||||||
|
<iframe id=f2 src="support/sub2.html"></iframe>
|
||||||
|
|
||||||
|
<script id="basic">
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual(undefined, window[10]);
|
||||||
|
|
||||||
|
testing.expectEqual(window, window[0].top);
|
||||||
|
testing.expectEqual(window, window[0].parent);
|
||||||
|
testing.expectEqual(false, window === window[0]);
|
||||||
|
|
||||||
|
testing.expectEqual(window, window[1].top);
|
||||||
|
testing.expectEqual(window, window[1].parent);
|
||||||
|
testing.expectEqual(false, window === window[1]);
|
||||||
|
testing.expectEqual(false, window[0] === window[1]);
|
||||||
|
|
||||||
|
testing.expectEqual(0, $('#f1').childNodes.length);
|
||||||
|
|
||||||
|
testing.expectEqual(testing.BASE_URL + 'frames/support/sub%201.html', $('#f1').src);
|
||||||
|
testing.expectEqual(window[0], $('#f1').contentWindow);
|
||||||
|
testing.expectEqual(window[1], $('#f2').contentWindow);
|
||||||
|
|
||||||
|
testing.expectEqual(window[0].document, $('#f1').contentDocument);
|
||||||
|
testing.expectEqual(window[1].document, $('#f2').contentDocument);
|
||||||
|
|
||||||
|
// sibling frames share the same top
|
||||||
|
testing.expectEqual(window[0].top, window[1].top);
|
||||||
|
|
||||||
|
// child frames have no sub-frames
|
||||||
|
testing.expectEqual(0, window[0].length);
|
||||||
|
testing.expectEqual(0, window[1].length);
|
||||||
|
|
||||||
|
// self and window are self-referential on child frames
|
||||||
|
testing.expectEqual(window[0], window[0].self);
|
||||||
|
testing.expectEqual(window[0], window[0].window);
|
||||||
|
testing.expectEqual(window[1], window[1].self);
|
||||||
|
|
||||||
|
// child frame's top.parent is itself (root has no parent)
|
||||||
|
testing.expectEqual(window, window[0].top.parent);
|
||||||
|
|
||||||
|
// testing.expectEqual(true, window.sub1_loaded);
|
||||||
|
// testing.expectEqual(true, window.sub2_loaded);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=onload>
|
||||||
|
{
|
||||||
|
let f3_load_event = false;
|
||||||
|
let f3 = document.createElement('iframe');
|
||||||
|
f3.addEventListener('load', () => {
|
||||||
|
f3_load_event = true;
|
||||||
|
});
|
||||||
|
f3.src = 'invalid'; // still fires load!
|
||||||
|
document.documentElement.appendChild(f3);
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual(true, window.f1_onload);
|
||||||
|
testing.expectEqual(true, f3_load_event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=count>
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual(3, window.length);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
6
src/browser/tests/frames/support/sub 1.html
Normal file
6
src/browser/tests/frames/support/sub 1.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<div id=div-1>sub1 div1</div>
|
||||||
|
<script>
|
||||||
|
// should not have access to the parent's JS context
|
||||||
|
window.top.sub1_loaded = window.testing == undefined;
|
||||||
|
</script>
|
||||||
7
src/browser/tests/frames/support/sub2.html
Normal file
7
src/browser/tests/frames/support/sub2.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<div id=div-1>sub2 div1</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// should not have access to the parent's JS context
|
||||||
|
window.top.sub2_loaded = window.testing == undefined;
|
||||||
|
</script>
|
||||||
75
src/browser/tests/image_data.html
Normal file
75
src/browser/tests/image_data.html
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="testing.js"></script>
|
||||||
|
|
||||||
|
<script id=constructor-basic>
|
||||||
|
{
|
||||||
|
const img = new ImageData(10, 20);
|
||||||
|
testing.expectEqual(10, img.width);
|
||||||
|
testing.expectEqual(20, img.height);
|
||||||
|
testing.expectEqual("srgb", img.colorSpace);
|
||||||
|
testing.expectEqual("rgba-unorm8", img.pixelFormat);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=data-property>
|
||||||
|
{
|
||||||
|
const img = new ImageData(2, 3);
|
||||||
|
const data = img.data;
|
||||||
|
testing.expectEqual(true, data instanceof Uint8ClampedArray);
|
||||||
|
// 2 * 3 * 4 (RGBA) = 24 bytes
|
||||||
|
testing.expectEqual(24, data.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=data-initialized-to-zero>
|
||||||
|
{
|
||||||
|
const img = new ImageData(2, 2);
|
||||||
|
const data = img.data;
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
testing.expectEqual(0, data[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=data-mutability>
|
||||||
|
{
|
||||||
|
const img = new ImageData(1, 1);
|
||||||
|
const data = img.data;
|
||||||
|
// Set pixel to red (RGBA)
|
||||||
|
data[0] = 255;
|
||||||
|
data[1] = 0;
|
||||||
|
data[2] = 0;
|
||||||
|
data[3] = 255;
|
||||||
|
|
||||||
|
// Read back through the same accessor
|
||||||
|
const data2 = img.data;
|
||||||
|
testing.expectEqual(255, data2[0]);
|
||||||
|
testing.expectEqual(0, data2[1]);
|
||||||
|
testing.expectEqual(0, data2[2]);
|
||||||
|
testing.expectEqual(255, data2[3]);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=constructor-with-settings>
|
||||||
|
{
|
||||||
|
const img = new ImageData(5, 5, { colorSpace: "srgb" });
|
||||||
|
testing.expectEqual(5, img.width);
|
||||||
|
testing.expectEqual(5, img.height);
|
||||||
|
testing.expectEqual("srgb", img.colorSpace);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=single-pixel>
|
||||||
|
{
|
||||||
|
const img = new ImageData(1, 1);
|
||||||
|
testing.expectEqual(4, img.data.length);
|
||||||
|
testing.expectEqual(1, img.width);
|
||||||
|
testing.expectEqual(1, img.height);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=too-large>
|
||||||
|
testing.expectError("IndexSizeError", () => new ImageData(2_147_483_648, 2_147_483_648));
|
||||||
|
</script>
|
||||||
@@ -25,6 +25,8 @@
|
|||||||
testing.expectEqual('number', typeof entry.intersectionRatio);
|
testing.expectEqual('number', typeof entry.intersectionRatio);
|
||||||
testing.expectEqual('object', typeof entry.boundingClientRect);
|
testing.expectEqual('object', typeof entry.boundingClientRect);
|
||||||
testing.expectEqual('object', typeof entry.intersectionRect);
|
testing.expectEqual('object', typeof entry.intersectionRect);
|
||||||
|
testing.expectEqual('number', typeof entry.time);
|
||||||
|
testing.expectEqual(true, entry.time > 0);
|
||||||
|
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
testing.expectEqual(5, input.maxLength);
|
testing.expectEqual(5, input.maxLength);
|
||||||
input.maxLength = 'banana';
|
input.maxLength = 'banana';
|
||||||
testing.expectEqual(0, input.maxLength);
|
testing.expectEqual(0, input.maxLength);
|
||||||
testing.expectError('Error: NegativeValueNotAllowed', () => { input.maxLength = -45;});
|
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => { input.maxLength = -45;});
|
||||||
|
|
||||||
testing.expectEqual(20, input.size);
|
testing.expectEqual(20, input.size);
|
||||||
input.size = 5;
|
input.size = 5;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user