mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-29 16:10:04 +00:00
Compare commits
1355 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe3faa0a5a | ||
|
|
39d5a25258 | ||
|
|
f4044230fd | ||
|
|
4d6d8d9a83 | ||
|
|
c4176a282f | ||
|
|
535128da71 | ||
|
|
7fe26bc966 | ||
|
|
cc6587d6e5 | ||
|
|
be8ba53263 | ||
|
|
043d48d1c7 | ||
|
|
e8fe80189b | ||
|
|
0e48f317cb | ||
|
|
867745c71d | ||
|
|
a1a7919f74 | ||
|
|
c3de47de90 | ||
|
|
dd35bdfeb4 | ||
|
|
07c3aec34f | ||
|
|
bce3e8f7c6 | ||
|
|
ba9777e754 | ||
|
|
7040801dfa | ||
|
|
4f8a6b62b8 | ||
|
|
d3dad772cf | ||
|
|
944b672fea | ||
|
|
b1c54aa92d | ||
|
|
4ca6f43aeb | ||
|
|
f09e66e1cc | ||
|
|
8b7a4ceaaa | ||
|
|
51e90f5971 | ||
|
|
8db64772b7 | ||
|
|
bf0be60b89 | ||
|
|
172481dd72 | ||
|
|
c6c0492c33 | ||
|
|
fca29a8be2 | ||
|
|
d365240f91 | ||
|
|
1ed61d4783 | ||
|
|
a1fb11ae33 | ||
|
|
9971816711 | ||
|
|
c38d9a3098 | ||
|
|
02198de455 | ||
|
|
6cd8202310 | ||
|
|
4d7b7d1d42 | ||
|
|
e4e21f52b5 | ||
|
|
84e1cd08b6 | ||
|
|
7796753e7a | ||
|
|
880205e874 | ||
|
|
1b96087b08 | ||
|
|
aa246c9e9f | ||
|
|
f1d311d232 | ||
|
|
e4f7fca10d | ||
|
|
3d6d669a50 | ||
|
|
c4097e2b7e | ||
|
|
619d27c773 | ||
|
|
1522c90294 | ||
|
|
794e15ce21 | ||
|
|
34771b835e | ||
|
|
8df51b232a | ||
|
|
13b8ce18b2 | ||
|
|
448386e52b | ||
|
|
bf07659dd5 | ||
|
|
16dfad0895 | ||
|
|
f61449c31c | ||
|
|
60699229ca | ||
|
|
e1dd26b307 | ||
|
|
7d835ef99d | ||
|
|
0971df4dfc | ||
|
|
9fb57fbac0 | ||
|
|
48ead90850 | ||
|
|
cc88bb7feb | ||
|
|
a725e2aa6a | ||
|
|
ee637c3662 | ||
|
|
65d7a39554 | ||
|
|
37735b1caa | ||
|
|
c8f8d79f45 | ||
|
|
1866e7141e | ||
|
|
feccc9f5ce | ||
|
|
af803da5c8 | ||
|
|
25c89c9940 | ||
|
|
697a2834c2 | ||
|
|
056b8bb536 | ||
|
|
625d424199 | ||
|
|
5329d05005 | ||
|
|
d2c55da6c9 | ||
|
|
2e6dd3edfe | ||
|
|
a95b4ea7b9 | ||
|
|
c891eff664 | ||
|
|
68564ca714 | ||
|
|
ca931a11be | ||
|
|
6c7272061c | ||
|
|
4f262e5bed | ||
|
|
ff26b0d5a4 | ||
|
|
a6ccc72d15 | ||
|
|
487ee18358 | ||
|
|
dc3d2e9790 | ||
|
|
f6d0e484b0 | ||
|
|
4cea9aba3c | ||
|
|
7348a68c84 | ||
|
|
7d90c3f582 | ||
|
|
2a103fc94a | ||
|
|
753391b7e2 | ||
|
|
94ce5edd20 | ||
|
|
3626f70d3e | ||
|
|
24cc24ed50 | ||
|
|
dd29ba4664 | ||
|
|
7927ad8fcf | ||
|
|
d23453ce45 | ||
|
|
a22040efa9 | ||
|
|
ba3da32ce6 | ||
|
|
9d2ba52160 | ||
|
|
e610506df4 | ||
|
|
dd91d28bfa | ||
|
|
1ebf7460fe | ||
|
|
8c930e5c33 | ||
|
|
4fb2f7474c | ||
|
|
5301f79989 | ||
|
|
6a7f7fdf15 | ||
|
|
11fb5f990e | ||
|
|
d1ee0442ea | ||
|
|
62f31ea24a | ||
|
|
f4ca5313e6 | ||
|
|
064e7b404b | ||
|
|
dfd90bd564 | ||
|
|
55508eb418 | ||
|
|
2a4fa4ed6f | ||
|
|
cf7c9f6372 | ||
|
|
ec68c3207d | ||
|
|
ecf140f3d6 | ||
|
|
13f73b7b87 | ||
|
|
12c5bcd24f | ||
|
|
56f47ee574 | ||
|
|
74f0436ac7 | ||
|
|
22d31b1527 | ||
|
|
9f3bca771a | ||
|
|
4e16d90a81 | ||
|
|
d669d5c153 | ||
|
|
343d985e96 | ||
|
|
dc3958356d | ||
|
|
c4e85c3277 | ||
|
|
89e46376dc | ||
|
|
8eeb34dba8 | ||
|
|
7171305972 | ||
|
|
2b0c223425 | ||
|
|
8f960ab0f7 | ||
|
|
60350efa10 | ||
|
|
687f577562 | ||
|
|
8e59ce9e9f | ||
|
|
33d75354a2 | ||
|
|
a318c6263d | ||
|
|
0e4a65efb7 | ||
|
|
b88134cf04 | ||
|
|
2aaa212dbc | ||
|
|
1e37990938 | ||
|
|
a417c73bf7 | ||
|
|
37c34351ee | ||
|
|
8672232ee2 | ||
|
|
83ba974f94 | ||
|
|
85ebbe8759 | ||
|
|
61cba3f6eb | ||
|
|
3ad10ff8d0 | ||
|
|
183643547b | ||
|
|
5568340b9a | ||
|
|
1399bd3065 | ||
|
|
9172e16e80 | ||
|
|
3e5f602396 | ||
|
|
3c97332fd8 | ||
|
|
379a3f27b8 | ||
|
|
ecec932a47 | ||
|
|
e239f69f69 | ||
|
|
c77cb317c4 | ||
|
|
034b089433 | ||
|
|
c0db96482c | ||
|
|
ffa8fa0a6f | ||
|
|
7e1d459a2d | ||
|
|
71c4fce87f | ||
|
|
e91da78ebb | ||
|
|
8adad6fa61 | ||
|
|
b47004bb7c | ||
|
|
08a7fb4de0 | ||
|
|
c17a9b11cc | ||
|
|
245a92a644 | ||
|
|
6b313946fe | ||
|
|
4586fb1d13 | ||
|
|
aa051434cb | ||
|
|
c3a53752e7 | ||
|
|
f3e1204fa1 | ||
|
|
0a5eb93565 | ||
|
|
b8a3135835 | ||
|
|
330dfccb89 | ||
|
|
d80e926015 | ||
|
|
2a2b067633 | ||
|
|
be73c14395 | ||
|
|
9cd5afe5b6 | ||
|
|
1cb5d26344 | ||
|
|
ec9a2d8155 | ||
|
|
4ba40f2295 | ||
|
|
b674c2e448 | ||
|
|
0227afffc8 | ||
|
|
b8139a6e83 | ||
|
|
bde5fc9264 | ||
|
|
6a421a1d96 | ||
|
|
4f55a0f1d0 | ||
|
|
3de55899fa | ||
|
|
ae4ad713ec | ||
|
|
21313adf9c | ||
|
|
9c1293ca45 | ||
|
|
1cb1e6b680 | ||
|
|
ed6ddeaa4c | ||
|
|
de08a89e6b | ||
|
|
dd42ef1920 | ||
|
|
dd192be689 | ||
|
|
52250ed10e | ||
|
|
4a26cd8d68 | ||
|
|
2ca972c3c8 | ||
|
|
74c0d55a6c | ||
|
|
3271e1464e | ||
|
|
cabd62b48f | ||
|
|
58c2355c8b | ||
|
|
bfe2065b9f | ||
|
|
9332b1355e | ||
|
|
45705a3e29 | ||
|
|
e0f0b9f210 | ||
|
|
f2832447d4 | ||
|
|
471ba5baf6 | ||
|
|
248851701f | ||
|
|
0f46277b1f | ||
|
|
679e703754 | ||
|
|
7322f90af4 | ||
|
|
e869df98c9 | ||
|
|
e499d36126 | ||
|
|
cac66d7fad | ||
|
|
320aaf0e33 | ||
|
|
178a175e99 | ||
|
|
5fdf1cb2d1 | ||
|
|
c64500dd85 | ||
|
|
812ad3f49e | ||
|
|
8e8a1a7541 | ||
|
|
4863b3df6e | ||
|
|
768c3a533b | ||
|
|
3dea554e9e | ||
|
|
16d4f6e4e1 | ||
|
|
9c7ecf221e | ||
|
|
26db481d46 | ||
|
|
3256a57230 | ||
|
|
cbc30587ff | ||
|
|
a27de38c03 | ||
|
|
e2f1609116 | ||
|
|
ea66a91a95 | ||
|
|
0d87c352b2 | ||
|
|
918f6ce0e6 | ||
|
|
6c5efe6ce0 | ||
|
|
f0be6675e7 | ||
|
|
6a8174a15c | ||
|
|
40c3f1b618 | ||
|
|
6dd2dac049 | ||
|
|
b39bbb557f | ||
|
|
f7682cba67 | ||
|
|
f94c07160a | ||
|
|
bbe6692580 | ||
|
|
9266a1c4d9 | ||
|
|
220d80f05f | ||
|
|
9144c909dd | ||
|
|
7981fcec84 | ||
|
|
71264c56fc | ||
|
|
ca0f77bdee | ||
|
|
fc8b1b8549 | ||
|
|
bc8c44f62f | ||
|
|
01fab5c92a | ||
|
|
1c07d786a0 | ||
|
|
6f0cd87d1c | ||
|
|
e44308cba2 | ||
|
|
50245c5157 | ||
|
|
9ca5188e12 | ||
|
|
e25c33eaa6 | ||
|
|
56cc881ac0 | ||
|
|
7bddc0a89c | ||
|
|
50896bdc9d | ||
|
|
8dd4567828 | ||
|
|
06ef6d3e6a | ||
|
|
14b58e8062 | ||
|
|
eee232c12c | ||
|
|
febe321aef | ||
|
|
28777ac717 | ||
|
|
13b008b56c | ||
|
|
403f42bf38 | ||
|
|
523efbd85a | ||
|
|
fcacc8bfc6 | ||
|
|
b2e301418f | ||
|
|
334a2e44a1 | ||
|
|
252b3c3bf6 | ||
|
|
c9121a03d2 | ||
|
|
cc93180d57 | ||
|
|
24221748e1 | ||
|
|
4062a425cb | ||
|
|
cce533ebb6 | ||
|
|
48df38cbfe | ||
|
|
f982f073df | ||
|
|
34999f12ca | ||
|
|
c8d5665653 | ||
|
|
ddebaf87d0 | ||
|
|
6b80cd6109 | ||
|
|
7635d8d2a5 | ||
|
|
141ae053db | ||
|
|
10ec4ff814 | ||
|
|
d2da0b7c0e | ||
|
|
7d0548406e | ||
|
|
634e3e35a0 | ||
|
|
da3dc58199 | ||
|
|
4f99df694b | ||
|
|
c121dbbd67 | ||
|
|
c1c0a7d494 | ||
|
|
0749f60702 | ||
|
|
982b8e2d72 | ||
|
|
6e7c8d7ae2 | ||
|
|
3c858f522b | ||
|
|
f2a30f8cdd | ||
|
|
43785bfab4 | ||
|
|
78edf6d324 | ||
|
|
73565c4493 | ||
|
|
ca0ef18bdf | ||
|
|
6ed011e2f8 | ||
|
|
23d322452a | ||
|
|
5d3b965d28 | ||
|
|
d9794d72c7 | ||
|
|
524b5be937 | ||
|
|
ac2e276a6a | ||
|
|
4f4dbc0c22 | ||
|
|
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 | ||
|
|
d56e63a91b | ||
|
|
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 | ||
|
|
77aa2241dc | ||
|
|
0766d08479 | ||
|
|
14112ed294 | ||
|
|
f6ed0d43a2 | ||
|
|
c8413cb029 | ||
|
|
97d53b81a7 | ||
|
|
ab888f5cd0 | ||
|
|
f54246eac1 | ||
|
|
7de9422b75 | ||
|
|
f02a37d3f0 | ||
|
|
28815a0ae6 | ||
|
|
70c7dfd0f4 | ||
|
|
9c2ebd308b | ||
|
|
11d8412591 | ||
|
|
32ca170c4d | ||
|
|
388ed08b0e | ||
|
|
3e1909b645 | ||
|
|
b408f88b8c | ||
|
|
09087401b4 | ||
|
|
c68692d78e | ||
|
|
ee2a4d0a5d | ||
|
|
a15885fe80 | ||
|
|
24111570cf | ||
|
|
ded203b1c1 | ||
|
|
e43fc98c0d | ||
|
|
1efd13545e | ||
|
|
1193ee1ab9 | ||
|
|
a6ba801738 | ||
|
|
e7958f2910 | ||
|
|
cbac9a7703 | ||
|
|
60d8f2323e | ||
|
|
70ae6b8d72 | ||
|
|
a4b1fbd6ee | ||
|
|
e1850440b0 | ||
|
|
d5c2aaeea3 | ||
|
|
a06b7acc85 | ||
|
|
615168423a | ||
|
|
73abf7d20e | ||
|
|
fcea42e91e | ||
|
|
14f7574c41 | ||
|
|
8f15ded650 | ||
|
|
4aec4ef80a | ||
|
|
ecb8f1de30 | ||
|
|
4c28180125 | ||
|
|
4138180f43 | ||
|
|
0d508a88f6 | ||
|
|
7c8fcf73f6 | ||
|
|
5904d72776 | ||
|
|
5e32ccbf12 | ||
|
|
6ce136bede | ||
|
|
b9f8ce5729 | ||
|
|
115530a104 | ||
|
|
65c9b2a5f7 | ||
|
|
46c73a05a9 | ||
|
|
c5f7e72ca8 | ||
|
|
e6fb63ddba | ||
|
|
e2645e4126 | ||
|
|
36d267ca40 | ||
|
|
2e5d04389b | ||
|
|
6130ed17a6 | ||
|
|
c4e82407ec | ||
|
|
2e28e68c48 | ||
|
|
a7882fa32b | ||
|
|
82f9e70406 | ||
|
|
3fa27c7ffa | ||
|
|
1aa50dee20 | ||
|
|
f58f7257be | ||
|
|
e17f1269a2 | ||
|
|
82f48b84b3 | ||
|
|
926bd20281 | ||
|
|
a6cd019118 | ||
|
|
bbfc476d7e | ||
|
|
8d49515a3c | ||
|
|
0a410a5544 | ||
|
|
eef203633b | ||
|
|
122255058e | ||
|
|
b1681b2213 | ||
|
|
73d79f55d8 | ||
|
|
a02fcd97d6 | ||
|
|
016708b338 | ||
|
|
9f26fc28c4 | ||
|
|
7c1b354fc3 | ||
|
|
abeda1935d | ||
|
|
403ee9ff9e | ||
|
|
cccb45fe13 | ||
|
|
8d43becb27 | ||
|
|
ee22e07fff | ||
|
|
37464e2d95 | ||
|
|
5abcecbc9b | ||
|
|
cecdf0d511 | ||
|
|
a451fe4248 | ||
|
|
d6f801f764 | ||
|
|
a35e772a6b | ||
|
|
aca3fae6b1 | ||
|
|
17891f0209 | ||
|
|
aea2b3c8e5 | ||
|
|
6d2ef9be5d | ||
|
|
57fb167a9c | ||
|
|
0406bba384 | ||
|
|
7c4c80fe4a | ||
|
|
bfb267e164 | ||
|
|
a0720948a1 | ||
|
|
9f00159a84 | ||
|
|
34067a1d70 | ||
|
|
3f6917fdcb | ||
|
|
c04a6e501e | ||
|
|
661b564399 | ||
|
|
761c103373 | ||
|
|
f4bd9e3d24 | ||
|
|
b9ddac878c | ||
|
|
f304ce5ccf | ||
|
|
828401f057 | ||
|
|
445d77a220 | ||
|
|
4d768bb5eb | ||
|
|
4e3b87d338 | ||
|
|
00740b6117 | ||
|
|
7775f203fc | ||
|
|
945af879ec | ||
|
|
b2506f0afe | ||
|
|
2eab4b84c9 | ||
|
|
7746d9968d | ||
|
|
da49d918d6 | ||
|
|
804ed758c9 | ||
|
|
17aac58e08 | ||
|
|
a7095d7dec | ||
|
|
3afbb6fcc2 | ||
|
|
8ecbd8e71c | ||
|
|
988f499723 | ||
|
|
50aeb9ff21 | ||
|
|
e620c28a1c | ||
|
|
29ee7d41f5 | ||
|
|
f9104c71f6 | ||
|
|
b6af5884b1 | ||
|
|
e4f250435d | ||
|
|
1a246f2e38 | ||
|
|
48ebc46c5f | ||
|
|
e27803038c | ||
|
|
babf8ba3e7 | ||
|
|
6ccd3f277b | ||
|
|
9d6f9aae9a | ||
|
|
95a000c279 | ||
|
|
b19debff14 | ||
|
|
39c9024747 | ||
|
|
3c660f2cb0 | ||
|
|
13dbdc7dc7 | ||
|
|
f903e4b2de | ||
|
|
b96cb2142b | ||
|
|
cc51cd4476 | ||
|
|
8a995fc515 | ||
|
|
078eccea2d | ||
|
|
190119bcd4 | ||
|
|
7672b42fbc | ||
|
|
c590658f16 | ||
|
|
017d4e792b | ||
|
|
0671be870d | ||
|
|
2f9ed37db2 | ||
|
|
2cf2db3eef | ||
|
|
11ad025e5d | ||
|
|
630cf05b2f | ||
|
|
a72782f91e | ||
|
|
fbd554a15f | ||
|
|
f71aa1cad2 | ||
|
|
6d9517f6ea | ||
|
|
fd8c488dbd | ||
|
|
dbf18b90a7 | ||
|
|
d318fe24b8 | ||
|
|
1352315441 | ||
|
|
3c635532c4 | ||
|
|
f8703bf884 | ||
|
|
eea3aa7a27 | ||
|
|
6eff448508 | ||
|
|
eb8cac5980 | ||
|
|
1a4086c98c | ||
|
|
5c91076660 | ||
|
|
5467b8dd0d | ||
|
|
d46a9d6286 | ||
|
|
2fa7810128 | ||
|
|
8249725ae7 | ||
|
|
c07b83335b | ||
|
|
7e575c501a | ||
|
|
933e2fb0ef | ||
|
|
8d51383fb2 | ||
|
|
80f4c83b83 | ||
|
|
0d739e4af7 | ||
|
|
58f9027002 | ||
|
|
990f2e2892 | ||
|
|
ce7989c171 | ||
|
|
4efb0229d4 | ||
|
|
5dd6dc2d69 | ||
|
|
20931eb9d6 | ||
|
|
c11fa122af | ||
|
|
e9141c8300 | ||
|
|
1d03b688d9 | ||
|
|
176d42f625 | ||
|
|
7c98a27c53 | ||
|
|
020b30783e | ||
|
|
fafbdb0714 | ||
|
|
466cdb4ee7 | ||
|
|
fa66f0b509 | ||
|
|
12a566c07e | ||
|
|
bf7a1c6b1f | ||
|
|
55891aa5f8 | ||
|
|
7c0acd9fcb | ||
|
|
333f1e2c47 | ||
|
|
9d30cdfefc | ||
|
|
324f6fe16e | ||
|
|
5d96304332 | ||
|
|
e6e32b5fd2 | ||
|
|
181f265de5 | ||
|
|
e5fc8bb27c | ||
|
|
34dda780d9 | ||
|
|
c7cf4eeb7a | ||
|
|
a6e5d9f6dc | ||
|
|
ea1017584e | ||
|
|
6aef32d7a8 | ||
|
|
4a1d71b6b8 | ||
|
|
a18b61cb1d | ||
|
|
e31e19aeba | ||
|
|
ef6d8a6554 | ||
|
|
100764d79e | ||
|
|
75abe7da1b | ||
|
|
a19a125aec | ||
|
|
f02fc95958 | ||
|
|
175edca8c7 | ||
|
|
f1f0a66f41 | ||
|
|
496c6905af | ||
|
|
c84106570f | ||
|
|
1a05da9e55 | ||
|
|
232e7a1759 | ||
|
|
c440d41d57 | ||
|
|
dfe5c24404 | ||
|
|
eba5773d56 | ||
|
|
5d56fea2d3 | ||
|
|
946f02b7a2 | ||
|
|
8e8ffd21d5 | ||
|
|
d02d974cd0 | ||
|
|
0a68be695d | ||
|
|
335e781d0c | ||
|
|
9f5c2e4ca7 | ||
|
|
76a53bedbe | ||
|
|
b0bc84ed21 | ||
|
|
ae298fc2e6 | ||
|
|
3b809b2910 | ||
|
|
68fbc0bde3 | ||
|
|
9d8e5263a6 | ||
|
|
7eb026cc0d | ||
|
|
e51e6aa2b0 | ||
|
|
bc700d2044 | ||
|
|
30ed58ff07 | ||
|
|
066069baad | ||
|
|
068ec68917 | ||
|
|
560f028bda | ||
|
|
fd1e77df8f | ||
|
|
864ac08f16 | ||
|
|
6ad1a11593 | ||
|
|
89174ba0b6 | ||
|
|
fc5496e570 | ||
|
|
fd21d952ac | ||
|
|
073fea2bde | ||
|
|
e548712f5e | ||
|
|
c3ba83ff93 | ||
|
|
451dd0fd64 | ||
|
|
aa805c2428 | ||
|
|
58a7590aff | ||
|
|
563ab30564 | ||
|
|
5050b34361 | ||
|
|
3bb86f196b | ||
|
|
51dca3be11 | ||
|
|
adeda6cd75 | ||
|
|
09665c3a4a | ||
|
|
8f5f6212d2 | ||
|
|
a11ae912b4 | ||
|
|
3b12240615 | ||
|
|
862520e4b1 | ||
|
|
a3d2dd8366 | ||
|
|
16ef487871 | ||
|
|
54c45a0cfd | ||
|
|
1f14eb62d4 | ||
|
|
0db86a8b3d | ||
|
|
c63c85071a | ||
|
|
b63d93e325 | ||
|
|
12c6e50e16 | ||
|
|
53ccc2e04c | ||
|
|
2d3234b54d | ||
|
|
9a57c2a0d4 | ||
|
|
fc64abee8f | ||
|
|
d5f26f6d15 | ||
|
|
97f9c2991b | ||
|
|
81378d4353 | ||
|
|
9f0c902030 | ||
|
|
3c0c75be10 | ||
|
|
90d23abe18 | ||
|
|
82eccf36d4 | ||
|
|
342cb52887 | ||
|
|
cafa4f5173 | ||
|
|
67cff5af8b | ||
|
|
6d23d91aa5 | ||
|
|
3a0699fc1d | ||
|
|
027e569087 | ||
|
|
830f759f0b | ||
|
|
969891c71c | ||
|
|
4eb5c3e907 | ||
|
|
23303a759b | ||
|
|
d1e7f46994 | ||
|
|
65ea70ae90 | ||
|
|
7522b71c86 | ||
|
|
70625c86c3 | ||
|
|
74354d2027 | ||
|
|
f6397e2731 | ||
|
|
065ca39d60 | ||
|
|
b4759ae261 | ||
|
|
c095950ef9 | ||
|
|
24b7035b1b | ||
|
|
7b1f157cf8 | ||
|
|
8b8bee4e9c | ||
|
|
c27ab35600 | ||
|
|
446b4dc461 | ||
|
|
ff8ed24622 | ||
|
|
ae2d6a122b | ||
|
|
3cac375f21 | ||
|
|
7d806dd161 | ||
|
|
db037c704e | ||
|
|
954184f742 | ||
|
|
7650e0b61a | ||
|
|
4a5c93988f | ||
|
|
8ceaf0ac66 | ||
|
|
ca60aa1cc6 | ||
|
|
596d5906a0 | ||
|
|
c02db94522 | ||
|
|
3970803575 | ||
|
|
43805ad698 | ||
|
|
2498e12f19 | ||
|
|
6f3cb4b48e | ||
|
|
fbd047599e | ||
|
|
da00117622 | ||
|
|
e44c73bdf6 | ||
|
|
e3cb7bd9f0 | ||
|
|
08f5889ee5 | ||
|
|
d5bfe74e1a | ||
|
|
d7015fa3b6 | ||
|
|
9092651b5b | ||
|
|
2c53b48e0a | ||
|
|
319a1c3367 | ||
|
|
80dd590e8f | ||
|
|
992a8e8774 | ||
|
|
f56d3bd193 | ||
|
|
4ecc59d0c0 | ||
|
|
5ebf82874b | ||
|
|
12670a3153 | ||
|
|
fa3a23134e | ||
|
|
8291044abc | ||
|
|
505e0799da | ||
|
|
be1d463775 | ||
|
|
a6fc5aa345 | ||
|
|
0e6e4db08b | ||
|
|
a84708e99d | ||
|
|
6b6c0e930e | ||
|
|
926892be01 | ||
|
|
2894bef9ef | ||
|
|
a6e7ecd9e5 | ||
|
|
9b000a002e | ||
|
|
0f9c9e2089 | ||
|
|
0edc1fcec7 | ||
|
|
b46d3b22e2 | ||
|
|
412c881cd4 | ||
|
|
48f07a110f | ||
|
|
5c1b7935e2 | ||
|
|
62aa564df1 | ||
|
|
798ee4a4d5 | ||
|
|
7d87fb80ec | ||
|
|
393227a786 | ||
|
|
c5870353e3 | ||
|
|
7c9941c629 | ||
|
|
c7dbb6792d | ||
|
|
728b2b7089 | ||
|
|
5def997bed | ||
|
|
a30c65966b | ||
|
|
cd67ed8a27 | ||
|
|
5400dc783e | ||
|
|
2880e9867d | ||
|
|
58f9469a6f | ||
|
|
30d052db99 | ||
|
|
744311f107 | ||
|
|
656674a477 | ||
|
|
0e4aa38aaa | ||
|
|
fdc267fa1f | ||
|
|
4325b80d64 | ||
|
|
fbe07836f9 | ||
|
|
304681bd21 | ||
|
|
05a01bb7c4 | ||
|
|
cbc028b040 | ||
|
|
2074c0149f | ||
|
|
61ed97dd45 | ||
|
|
a358c46b9f | ||
|
|
50c1e2472b | ||
|
|
ea2fc76d3c | ||
|
|
58634b54ec | ||
|
|
4b4bc1a4d3 | ||
|
|
0549e07a90 | ||
|
|
42666b1d30 | ||
|
|
0a8be77233 | ||
|
|
b26fb0e6c7 | ||
|
|
1699a92822 | ||
|
|
7ae3e8cb47 | ||
|
|
fd26ae4b5b | ||
|
|
9945a5f9cc | ||
|
|
d5e9ae23ef | ||
|
|
d50e056114 | ||
|
|
d7d956d966 | ||
|
|
bd3966bf8d | ||
|
|
74578ba274 | ||
|
|
cb89742d2f | ||
|
|
6d0f991c17 | ||
|
|
d126d2a0f9 | ||
|
|
b51cca5617 | ||
|
|
dc54dad290 | ||
|
|
7d6ab5a708 | ||
|
|
07acb9308d | ||
|
|
ef315a46bc | ||
|
|
eb45bd051c | ||
|
|
65102edc98 | ||
|
|
04eda96416 | ||
|
|
f5036bdf5e | ||
|
|
b6df85da7a | ||
|
|
9775b39a8d | ||
|
|
d6d74c5024 | ||
|
|
e09d15b12a | ||
|
|
6d33d23935 | ||
|
|
47760e00f7 | ||
|
|
72e8421099 | ||
|
|
844b0ed457 | ||
|
|
7e37db796f | ||
|
|
3e5b506675 | ||
|
|
d356dbfc06 | ||
|
|
f5aee1f4c0 | ||
|
|
de4926d87d | ||
|
|
56a39e2cc7 | ||
|
|
8e14dacc32 | ||
|
|
05102c673a | ||
|
|
db2ecfe159 | ||
|
|
640cb0d489 | ||
|
|
223a6170d5 | ||
|
|
63f1c85964 | ||
|
|
c252c8e870 | ||
|
|
801c019150 | ||
|
|
d77a6620f3 | ||
|
|
4e4a615df8 | ||
|
|
1b0ea44519 | ||
|
|
86f4ea108d | ||
|
|
2322cb9b83 | ||
|
|
4720268426 | ||
|
|
b4f134bff6 | ||
|
|
f2a9125b99 | ||
|
|
8438b7d561 | ||
|
|
18c846757b | ||
|
|
bc11a48e6b | ||
|
|
01ecd725b8 | ||
|
|
e6af7d1bd0 | ||
|
|
701de08e8a | ||
|
|
363b95bdef | ||
|
|
ca5a385b51 | ||
|
|
93f0d24673 | ||
|
|
a5038893fe | ||
|
|
3442f99a49 | ||
|
|
6ecf52cc03 | ||
|
|
8aaef674fe | ||
|
|
3b1cd06615 | ||
|
|
4841f8cc8f | ||
|
|
d9d8f68bf8 | ||
|
|
cf726d9813 | ||
|
|
92be2c45d6 | ||
|
|
914092b538 | ||
|
|
a8cd5fc266 | ||
|
|
643f07fa10 | ||
|
|
0d77ff661b | ||
|
|
70d84b2f72 | ||
|
|
41905ef735 | ||
|
|
2a468cc750 | ||
|
|
32520000c6 | ||
|
|
14db7a8eb3 | ||
|
|
8460e9a385 | ||
|
|
933a93a703 | ||
|
|
c2e09d3084 | ||
|
|
98397401b8 | ||
|
|
e042b1105a | ||
|
|
ee4775eb1a | ||
|
|
6ff6232316 | ||
|
|
10035ab2f4 | ||
|
|
2679175ae9 | ||
|
|
8d3aa1f3fa | ||
|
|
75e78795ec | ||
|
|
05f0f8901e | ||
|
|
6917aeb47b | ||
|
|
516a86e33f | ||
|
|
7184a91c95 | ||
|
|
83e9d705cf | ||
|
|
bb907f5adb | ||
|
|
f1b60453bd | ||
|
|
0ef339f12a | ||
|
|
5c0169ee05 | ||
|
|
daf959ee90 | ||
|
|
89b43b6102 | ||
|
|
d3b05201b9 | ||
|
|
127e53cf3a | ||
|
|
29281fe3ec | ||
|
|
a0fb55802f | ||
|
|
90ec068367 | ||
|
|
f57cf1be75 | ||
|
|
3f44dee367 | ||
|
|
82161ce94c | ||
|
|
27b8e2a38c | ||
|
|
e5f2fbdcb2 | ||
|
|
cdf0cdd0ea | ||
|
|
f12ff2c7bd | ||
|
|
6c7c507d32 | ||
|
|
0c97b8238b | ||
|
|
967a2030e6 | ||
|
|
78ebd5faf8 | ||
|
|
9d498fa069 | ||
|
|
0db1ceaea7 | ||
|
|
df27aeef6c | ||
|
|
5ae0df53bb | ||
|
|
48df6ae159 | ||
|
|
6cae2fcea7 | ||
|
|
d1d4d4894d | ||
|
|
adfcf7bb2c | ||
|
|
c8f75cd266 | ||
|
|
282a9bbf65 | ||
|
|
d4c8af2a61 | ||
|
|
3930524bbf | ||
|
|
622ca3121f |
14
.github/actions/install/action.yml
vendored
14
.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.2'
|
default: 'v0.3.3'
|
||||||
v8:
|
v8:
|
||||||
description: 'v8 version to install'
|
description: 'v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
@@ -22,6 +22,10 @@ inputs:
|
|||||||
description: 'cache dir to use'
|
description: 'cache dir to use'
|
||||||
required: false
|
required: false
|
||||||
default: '~/.cache'
|
default: '~/.cache'
|
||||||
|
debug:
|
||||||
|
description: 'enable v8 pre-built debug version, only available for linux x86_64'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
@@ -32,7 +36,7 @@ runs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang
|
sudo apt-get install -y wget xz-utils ca-certificates clang make git
|
||||||
|
|
||||||
# Zig version used from the `minimum_zig_version` field in build.zig.zon
|
# Zig version used from the `minimum_zig_version` field in build.zig.zon
|
||||||
- uses: mlugg/setup-zig@v2
|
- uses: mlugg/setup-zig@v2
|
||||||
@@ -47,17 +51,17 @@ runs:
|
|||||||
cache-name: cache-v8
|
cache-name: cache-v8
|
||||||
with:
|
with:
|
||||||
path: ${{ inputs.cache-dir }}/v8
|
path: ${{ inputs.cache-dir }}/v8
|
||||||
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}_${{ inputs.zig-v8 }}.a
|
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}_${{ inputs.zig-v8 }}${{inputs.debug == 'true' && '_debug' || '' }}.a
|
||||||
|
|
||||||
- if: ${{ steps.cache-v8.outputs.cache-hit != 'true' }}
|
- if: ${{ steps.cache-v8.outputs.cache-hit != 'true' }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ${{ inputs.cache-dir }}/v8
|
mkdir -p ${{ inputs.cache-dir }}/v8
|
||||||
|
|
||||||
wget -O ${{ inputs.cache-dir }}/v8/libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${{ inputs.zig-v8 }}/libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}.a
|
wget -O ${{ inputs.cache-dir }}/v8/libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${{ inputs.zig-v8 }}/libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}${{inputs.debug == 'true' && '_debug' || '' }}.a
|
||||||
|
|
||||||
- name: install v8
|
- name: install v8
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
mkdir -p v8
|
mkdir -p v8
|
||||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/libc_v8.a
|
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/libc_v8${{inputs.debug == 'true' && '_debug' || '' }}.a
|
||||||
|
|||||||
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
OS: linux
|
OS: linux
|
||||||
|
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
timeout-minutes: 15
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
@@ -40,7 +40,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
os: ${{env.OS}}
|
os: ${{env.OS}}
|
||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
mode: 'release'
|
|
||||||
|
|
||||||
- name: v8 snapshot
|
- name: v8 snapshot
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
@@ -62,6 +61,7 @@ jobs:
|
|||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
tag: ${{ env.RELEASE }}
|
tag: ${{ env.RELEASE }}
|
||||||
|
makeLatest: true
|
||||||
|
|
||||||
build-linux-aarch64:
|
build-linux-aarch64:
|
||||||
env:
|
env:
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
OS: linux
|
OS: linux
|
||||||
|
|
||||||
runs-on: ubuntu-22.04-arm
|
runs-on: ubuntu-22.04-arm
|
||||||
timeout-minutes: 15
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -82,7 +82,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
os: ${{env.OS}}
|
os: ${{env.OS}}
|
||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
mode: 'release'
|
|
||||||
|
|
||||||
- name: v8 snapshot
|
- name: v8 snapshot
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
@@ -104,6 +103,7 @@ jobs:
|
|||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
tag: ${{ env.RELEASE }}
|
tag: ${{ env.RELEASE }}
|
||||||
|
makeLatest: true
|
||||||
|
|
||||||
build-macos-aarch64:
|
build-macos-aarch64:
|
||||||
env:
|
env:
|
||||||
@@ -113,7 +113,7 @@ jobs:
|
|||||||
# macos-14 runs on arm CPU. see
|
# macos-14 runs on arm CPU. see
|
||||||
# https://github.com/actions/runner-images?tab=readme-ov-file
|
# https://github.com/actions/runner-images?tab=readme-ov-file
|
||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
timeout-minutes: 15
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -126,7 +126,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
os: ${{env.OS}}
|
os: ${{env.OS}}
|
||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
mode: 'release'
|
|
||||||
|
|
||||||
- name: v8 snapshot
|
- name: v8 snapshot
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
@@ -148,6 +147,7 @@ jobs:
|
|||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
tag: ${{ env.RELEASE }}
|
tag: ${{ env.RELEASE }}
|
||||||
|
makeLatest: true
|
||||||
|
|
||||||
build-macos-x86_64:
|
build-macos-x86_64:
|
||||||
env:
|
env:
|
||||||
@@ -155,7 +155,7 @@ jobs:
|
|||||||
OS: macos
|
OS: macos
|
||||||
|
|
||||||
runs-on: macos-14-large
|
runs-on: macos-14-large
|
||||||
timeout-minutes: 15
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -168,7 +168,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
os: ${{env.OS}}
|
os: ${{env.OS}}
|
||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
mode: 'release'
|
|
||||||
|
|
||||||
- name: v8 snapshot
|
- name: v8 snapshot
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
@@ -190,3 +189,4 @@ jobs:
|
|||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
tag: ${{ env.RELEASE }}
|
tag: ${{ env.RELEASE }}
|
||||||
|
makeLatest: true
|
||||||
|
|||||||
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`
|
||||||
|
|||||||
172
.github/workflows/e2e-test.yml
vendored
172
.github/workflows/e2e-test.yml
vendored
@@ -56,8 +56,6 @@ jobs:
|
|||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
with:
|
|
||||||
mode: 'release'
|
|
||||||
|
|
||||||
- name: zig build release
|
- name: zig build release
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||||
@@ -119,15 +117,123 @@ jobs:
|
|||||||
BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js
|
BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js
|
||||||
kill `cat LPD.pid` `cat PROXY.id`
|
kill `cat LPD.pid` `cat PROXY.id`
|
||||||
|
|
||||||
|
# e2e tests w/ web-bot-auth configuration on.
|
||||||
|
wba-demo-scripts:
|
||||||
|
name: wba-demo-scripts
|
||||||
|
needs: zig-build-release
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: 'lightpanda-io/demo'
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- run: npm install
|
||||||
|
|
||||||
|
- name: download artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: lightpanda-build-release
|
||||||
|
|
||||||
|
- run: chmod a+x ./lightpanda
|
||||||
|
|
||||||
|
- run: echo "${{ secrets.WBA_PRIVATE_KEY_PEM }}" > private_key.pem
|
||||||
|
|
||||||
|
- name: run end to end tests
|
||||||
|
run: |
|
||||||
|
./lightpanda serve \
|
||||||
|
--web_bot_auth_key_file private_key.pem \
|
||||||
|
--web_bot_auth_keyid ${{ vars.WBA_KEY_ID }} \
|
||||||
|
--web_bot_auth_domain ${{ vars.WBA_DOMAIN }} \
|
||||||
|
& echo $! > LPD.pid
|
||||||
|
go run runner/main.go
|
||||||
|
kill `cat LPD.pid`
|
||||||
|
|
||||||
|
- name: build proxy
|
||||||
|
run: |
|
||||||
|
cd proxy
|
||||||
|
go build
|
||||||
|
|
||||||
|
- name: run end to end tests through proxy
|
||||||
|
run: |
|
||||||
|
./proxy/proxy & echo $! > PROXY.id
|
||||||
|
./lightpanda serve \
|
||||||
|
--web_bot_auth_key_file private_key.pem \
|
||||||
|
--web_bot_auth_keyid ${{ vars.WBA_KEY_ID }} \
|
||||||
|
--web_bot_auth_domain ${{ vars.WBA_DOMAIN }} \
|
||||||
|
--http_proxy 'http://127.0.0.1:3000' \
|
||||||
|
& echo $! > LPD.pid
|
||||||
|
go run runner/main.go
|
||||||
|
kill `cat LPD.pid` `cat PROXY.id`
|
||||||
|
|
||||||
|
- name: run request interception through proxy
|
||||||
|
run: |
|
||||||
|
export PROXY_USERNAME=username PROXY_PASSWORD=password
|
||||||
|
./proxy/proxy & echo $! > PROXY.id
|
||||||
|
./lightpanda serve & echo $! > LPD.pid
|
||||||
|
URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js
|
||||||
|
BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js
|
||||||
|
kill `cat LPD.pid` `cat PROXY.id`
|
||||||
|
|
||||||
|
wba-test:
|
||||||
|
name: wba-test
|
||||||
|
needs: zig-build-release
|
||||||
|
|
||||||
|
env:
|
||||||
|
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: 'lightpanda-io/demo'
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- run: echo "${{ secrets.WBA_PRIVATE_KEY_PEM }}" > private_key.pem
|
||||||
|
|
||||||
|
- name: download artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: lightpanda-build-release
|
||||||
|
|
||||||
|
- run: chmod a+x ./lightpanda
|
||||||
|
|
||||||
|
- name: run wba test
|
||||||
|
run: |
|
||||||
|
node webbotauth/validator.js &
|
||||||
|
VALIDATOR_PID=$!
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
./lightpanda fetch http://127.0.0.1:8989/ \
|
||||||
|
--web_bot_auth_key_file private_key.pem \
|
||||||
|
--web_bot_auth_keyid ${{ vars.WBA_KEY_ID }} \
|
||||||
|
--web_bot_auth_domain ${{ vars.WBA_DOMAIN }}
|
||||||
|
|
||||||
|
wait $VALIDATOR_PID
|
||||||
|
|
||||||
cdp-and-hyperfine-bench:
|
cdp-and-hyperfine-bench:
|
||||||
name: cdp-and-hyperfine-bench
|
name: cdp-and-hyperfine-bench
|
||||||
needs: zig-build-release
|
needs: zig-build-release
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MAX_MEMORY: 28000
|
MAX_VmHWM: 28000 # 28MB (KB)
|
||||||
MAX_AVG_DURATION: 23
|
MAX_CG_PEAK: 8000 # 8MB (KB)
|
||||||
|
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
|
||||||
@@ -152,22 +258,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: |
|
||||||
@@ -180,7 +317,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
|
||||||
@@ -232,3 +370,19 @@ jobs:
|
|||||||
|
|
||||||
- name: format and send json result
|
- name: format and send json result
|
||||||
run: /perf-fmt hyperfine ${{ github.sha }} hyperfine.json
|
run: /perf-fmt hyperfine ${{ github.sha }} hyperfine.json
|
||||||
|
|
||||||
|
browser-fetch:
|
||||||
|
name: browser fetch
|
||||||
|
needs: zig-build-release
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: download artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: lightpanda-build-release
|
||||||
|
|
||||||
|
- run: chmod a+x ./lightpanda
|
||||||
|
|
||||||
|
- run: ./lightpanda fetch https://demo-browser.lightpanda.io/campfire-commerce/
|
||||||
|
|||||||
90
.github/workflows/wpt.yml
vendored
90
.github/workflows/wpt.yml
vendored
@@ -5,6 +5,7 @@ env:
|
|||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
||||||
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
||||||
|
AWS_CF_DISTRIBUTION: ${{ vars.AWS_CF_DISTRIBUTION }}
|
||||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -15,11 +16,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,8 +31,85 @@ jobs:
|
|||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
- name: json output
|
- name: zig build release
|
||||||
run: zig build wpt -- --json > wpt.json
|
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: 180
|
||||||
|
|
||||||
|
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
|
||||||
|
run: |
|
||||||
|
./wpt serve 2> /dev/null & echo $! > WPT.pid
|
||||||
|
sleep 10s
|
||||||
|
./wptrunner -lpd-path ./lightpanda -json -concurrency 10 -pool 3 > wpt.json
|
||||||
|
kill `cat WPT.pid`
|
||||||
|
|
||||||
- name: write commit
|
- name: write commit
|
||||||
run: |
|
run: |
|
||||||
@@ -48,7 +126,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
|
||||||
|
|||||||
47
.github/workflows/zig-test.yml
vendored
47
.github/workflows/zig-test.yml
vendored
@@ -12,8 +12,7 @@ on:
|
|||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- "build.zig"
|
- "build.zig"
|
||||||
- "src/**/*.zig"
|
- "src/**"
|
||||||
- "src/*.zig"
|
|
||||||
- "vendor/zig-js-runtime"
|
- "vendor/zig-js-runtime"
|
||||||
- ".github/**"
|
- ".github/**"
|
||||||
- "vendor/**"
|
- "vendor/**"
|
||||||
@@ -38,51 +37,25 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
zig-build-dev:
|
zig-test-debug:
|
||||||
name: zig build dev
|
name: zig test using v8 in debug mode
|
||||||
|
timeout-minutes: 15
|
||||||
# Don't run the CI with draft PR.
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
- name: zig build debug
|
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a
|
|
||||||
|
|
||||||
- name: upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-dev
|
debug: true
|
||||||
path: |
|
|
||||||
zig-out/bin/lightpanda
|
|
||||||
retention-days: 1
|
|
||||||
|
|
||||||
browser-fetch:
|
- name: zig build test
|
||||||
name: browser fetch
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8_debug.a -Dtsan=true test
|
||||||
needs: zig-build-dev
|
|
||||||
|
|
||||||
# Don't run the CI with draft PR.
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: download artifact
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: lightpanda-build-dev
|
|
||||||
|
|
||||||
- run: chmod a+x ./lightpanda
|
|
||||||
|
|
||||||
- run: ./lightpanda fetch https://httpbin.io/xhr/get
|
|
||||||
|
|
||||||
zig-test:
|
zig-test:
|
||||||
name: zig test
|
name: zig test
|
||||||
@@ -103,7 +76,7 @@ jobs:
|
|||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
- name: zig build test
|
- name: zig build test
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a test -- --json > bench.json
|
run: METRICS=true zig build -Dprebuilt_v8_path=v8/libc_v8.a test > bench.json
|
||||||
|
|
||||||
- name: write commit
|
- name: write commit
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
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,11 +3,12 @@ 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.2
|
ARG ZIG_V8=v0.3.3
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
RUN apt-get update -yq && \
|
RUN apt-get update -yq && \
|
||||||
apt-get install -yq xz-utils ca-certificates \
|
apt-get install -yq xz-utils ca-certificates \
|
||||||
|
pkg-config libglib2.0-dev \
|
||||||
clang make curl git
|
clang make curl git
|
||||||
|
|
||||||
# Get Rust
|
# Get Rust
|
||||||
@@ -35,10 +36,6 @@ RUN ZIG=$(grep '\.minimum_zig_version = "' "build.zig.zon" | cut -d'"' -f2) && \
|
|||||||
mv zig-${ARCH}-linux-${ZIG} /usr/local/lib && \
|
mv zig-${ARCH}-linux-${ZIG} /usr/local/lib && \
|
||||||
ln -s /usr/local/lib/zig-${ARCH}-linux-${ZIG}/zig /usr/local/bin/zig
|
ln -s /usr/local/lib/zig-${ARCH}-linux-${ZIG}/zig /usr/local/bin/zig
|
||||||
|
|
||||||
# install deps
|
|
||||||
RUN git submodule init && \
|
|
||||||
git submodule update --recursive
|
|
||||||
|
|
||||||
# download and install v8
|
# download and install v8
|
||||||
RUN case $TARGETPLATFORM in \
|
RUN case $TARGETPLATFORM in \
|
||||||
"linux/arm64") ARCH="aarch64" ;; \
|
"linux/arm64") ARCH="aarch64" ;; \
|
||||||
|
|||||||
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
|
|
||||||
|
|||||||
120
README.md
120
README.md
@@ -78,23 +78,49 @@ docker run -d --name lightpanda -p 9222:9222 lightpanda/browser:nightly
|
|||||||
### Dump a URL
|
### Dump a URL
|
||||||
|
|
||||||
```console
|
```console
|
||||||
./lightpanda fetch --dump https://lightpanda.io
|
./lightpanda fetch --obey_robots --log_format pretty --log_level info https://demo-browser.lightpanda.io/campfire-commerce/
|
||||||
```
|
```
|
||||||
```console
|
```console
|
||||||
info(browser): GET https://lightpanda.io/ http.Status.ok
|
INFO telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
||||||
info(browser): fetch script https://api.website.lightpanda.io/js/script.js: http.Status.ok
|
disabled = false
|
||||||
info(browser): eval remote https://api.website.lightpanda.io/js/script.js: TypeError: Cannot read properties of undefined (reading 'pushState')
|
|
||||||
|
INFO page : navigate . . . . . . . . . . . . . . . . . . . . [+6ms]
|
||||||
|
url = https://demo-browser.lightpanda.io/campfire-commerce/
|
||||||
|
method = GET
|
||||||
|
reason = address_bar
|
||||||
|
body = false
|
||||||
|
req_id = 1
|
||||||
|
|
||||||
|
INFO browser : executing script . . . . . . . . . . . . . . [+118ms]
|
||||||
|
src = https://demo-browser.lightpanda.io/campfire-commerce/script.js
|
||||||
|
kind = javascript
|
||||||
|
cacheable = true
|
||||||
|
|
||||||
|
INFO http : request complete . . . . . . . . . . . . . . . . [+140ms]
|
||||||
|
source = xhr
|
||||||
|
url = https://demo-browser.lightpanda.io/campfire-commerce/json/product.json
|
||||||
|
status = 200
|
||||||
|
len = 4770
|
||||||
|
|
||||||
|
INFO http : request complete . . . . . . . . . . . . . . . . [+141ms]
|
||||||
|
source = fetch
|
||||||
|
url = https://demo-browser.lightpanda.io/campfire-commerce/json/reviews.json
|
||||||
|
status = 200
|
||||||
|
len = 1615
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Start a CDP server
|
### Start a CDP server
|
||||||
|
|
||||||
```console
|
```console
|
||||||
./lightpanda serve --host 127.0.0.1 --port 9222
|
./lightpanda serve --obey_robots --log_format pretty --log_level info --host 127.0.0.1 --port 9222
|
||||||
```
|
```
|
||||||
```console
|
```console
|
||||||
info(websocket): starting blocking worker to listen on 127.0.0.1:9222
|
INFO telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
||||||
info(server): accepting new conn...
|
disabled = false
|
||||||
|
|
||||||
|
INFO app : server running . . . . . . . . . . . . . . . . . [+0ms]
|
||||||
|
address = 127.0.0.1:9222
|
||||||
```
|
```
|
||||||
|
|
||||||
Once the CDP server started, you can run a Puppeteer script by configuring the
|
Once the CDP server started, you can run a Puppeteer script by configuring the
|
||||||
@@ -115,7 +141,7 @@ const context = await browser.createBrowserContext();
|
|||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
||||||
// Dump all the links from the page.
|
// Dump all the links from the page.
|
||||||
await page.goto('https://wikipedia.com/');
|
await page.goto('https://demo-browser.lightpanda.io/amiibo/', {waitUntil: "networkidle0"});
|
||||||
|
|
||||||
const links = await page.evaluate(() => {
|
const links = await page.evaluate(() => {
|
||||||
return Array.from(document.querySelectorAll('a')).map(row => {
|
return Array.from(document.querySelectorAll('a')).map(row => {
|
||||||
@@ -156,6 +182,7 @@ Here are the key features we have implemented:
|
|||||||
- [x] Custom HTTP headers
|
- [x] Custom HTTP headers
|
||||||
- [x] Proxy support
|
- [x] Proxy support
|
||||||
- [x] Network interception
|
- [x] Network interception
|
||||||
|
- [x] Respect `robots.txt` with option `--obey_robots`
|
||||||
|
|
||||||
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
||||||
|
|
||||||
@@ -178,6 +205,7 @@ For **Debian/Ubuntu based Linux**:
|
|||||||
|
|
||||||
```
|
```
|
||||||
sudo apt install xz-utils ca-certificates \
|
sudo apt install xz-utils ca-certificates \
|
||||||
|
pkg-config libglib2.0-dev \
|
||||||
clang make curl git
|
clang make curl git
|
||||||
```
|
```
|
||||||
You also need to [install Rust](https://rust-lang.org/tools/install/).
|
You also need to [install Rust](https://rust-lang.org/tools/install/).
|
||||||
@@ -192,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
|
||||||
@@ -253,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,35 @@
|
|||||||
.{
|
.{
|
||||||
.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/d6b5f89cfc7feece29359e8c848bb916e8ecfab6.tar.gz",
|
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.3.tar.gz",
|
||||||
.hash = "v8-0.0.0-xddH6_0gBABrJc5cL6-P2wGvvweTTCgWdpmClr9r-C-s",
|
.hash = "v8-0.0.0-xddH6yx3BAAGD9jSoq_ttt_bk9MectTU44s_HZxxE5LD",
|
||||||
},
|
},
|
||||||
// .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 = .{""},
|
||||||
}
|
}
|
||||||
|
|||||||
24
flake.lock
generated
24
flake.lock
generated
@@ -8,11 +8,11 @@
|
|||||||
"rust-analyzer-src": "rust-analyzer-src"
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763016383,
|
"lastModified": 1770708269,
|
||||||
"narHash": "sha256-eYmo7FNvm3q08iROzwIi8i9dWuUbJJl3uLR3OLnSmdI=",
|
"narHash": "sha256-OnZW86app7hHJJoB5lC9GNXY5QBBIESJB+sIdwEyld0=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "fenix",
|
"repo": "fenix",
|
||||||
"rev": "0fad5c0e5c531358e7174cd666af4608f08bc3ba",
|
"rev": "6b5325a017a9a9fe7e6252ccac3680cc7181cd63",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -96,11 +96,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763043403,
|
"lastModified": 1768649915,
|
||||||
"narHash": "sha256-DgCTbHdIpzbXSlQlOZEWj8oPt2lrRMlSk03oIstvkVQ=",
|
"narHash": "sha256-jc21hKogFnxU7KXSVTRmxC7u5D4RHwm9BAvDf5/Z1Uo=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "75e04ecd084f93d4105ce68c07dac7656291fe2e",
|
"rev": "3e3f3c7f9977dc123c23ee21e8085ed63daf8c37",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -122,11 +122,11 @@
|
|||||||
"rust-analyzer-src": {
|
"rust-analyzer-src": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1762860488,
|
"lastModified": 1770668050,
|
||||||
"narHash": "sha256-rMfWMCOo/pPefM2We0iMBLi2kLBAnYoB9thi4qS7uk4=",
|
"narHash": "sha256-Q05yaIZtQrBKHpyWaPmyJmDRj0lojnVf8nUFE0vydcY=",
|
||||||
"owner": "rust-lang",
|
"owner": "rust-lang",
|
||||||
"repo": "rust-analyzer",
|
"repo": "rust-analyzer",
|
||||||
"rev": "2efc80078029894eec0699f62ec8d5c1a56af763",
|
"rev": "9efc1f709f3c8134c3acac5d3592a8e4c184a0c6",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -175,11 +175,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1762907712,
|
"lastModified": 1770598090,
|
||||||
"narHash": "sha256-VNW/+VYIg6N4b9Iq+F0YZmm22n74IdFS7hsPLblWuOY=",
|
"narHash": "sha256-k+82IDgTd9o5sxHIqGlvfwseKln3Ejx1edGtDltuPXo=",
|
||||||
"owner": "mitchellh",
|
"owner": "mitchellh",
|
||||||
"repo": "zig-overlay",
|
"repo": "zig-overlay",
|
||||||
"rev": "d16453ee78765e49527c56d23386cead799b6b53",
|
"rev": "142495696982c88edddc8e17e4da90d8164acadf",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
88
src/App.zig
88
src/App.zig
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -21,99 +21,75 @@ const std = @import("std");
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const log = @import("log.zig");
|
const log = @import("log.zig");
|
||||||
const Http = @import("http/Http.zig");
|
const Config = @import("Config.zig");
|
||||||
const Snapshot = @import("browser/js/Snapshot.zig");
|
const Snapshot = @import("browser/js/Snapshot.zig");
|
||||||
const Platform = @import("browser/js/Platform.zig");
|
const Platform = @import("browser/js/Platform.zig");
|
||||||
|
|
||||||
const Notification = @import("Notification.zig");
|
|
||||||
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
||||||
|
|
||||||
// Container for global state / objects that various parts of the system
|
const Network = @import("network/Runtime.zig");
|
||||||
// might need.
|
pub const ArenaPool = @import("ArenaPool.zig");
|
||||||
|
|
||||||
const App = @This();
|
const App = @This();
|
||||||
|
|
||||||
http: Http,
|
network: Network,
|
||||||
config: Config,
|
config: *const Config,
|
||||||
platform: Platform,
|
platform: Platform,
|
||||||
snapshot: Snapshot,
|
snapshot: Snapshot,
|
||||||
telemetry: Telemetry,
|
telemetry: Telemetry,
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
|
arena_pool: ArenaPool,
|
||||||
app_dir_path: ?[]const u8,
|
app_dir_path: ?[]const u8,
|
||||||
notification: *Notification,
|
|
||||||
shutdown: bool = false,
|
|
||||||
|
|
||||||
pub const RunMode = enum {
|
pub fn init(allocator: Allocator, config: *const Config) !*App {
|
||||||
help,
|
|
||||||
fetch,
|
|
||||||
serve,
|
|
||||||
version,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Config = struct {
|
|
||||||
run_mode: RunMode,
|
|
||||||
tls_verify_host: bool = true,
|
|
||||||
http_proxy: ?[:0]const u8 = null,
|
|
||||||
proxy_bearer_token: ?[:0]const u8 = null,
|
|
||||||
http_timeout_ms: ?u31 = null,
|
|
||||||
http_connect_timeout_ms: ?u31 = null,
|
|
||||||
http_max_host_open: ?u8 = null,
|
|
||||||
http_max_concurrent: ?u8 = null,
|
|
||||||
user_agent: [:0]const u8,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, config: Config) !*App {
|
|
||||||
const app = try allocator.create(App);
|
const app = try allocator.create(App);
|
||||||
errdefer allocator.destroy(app);
|
errdefer allocator.destroy(app);
|
||||||
|
|
||||||
app.config = config;
|
app.* = .{
|
||||||
app.allocator = allocator;
|
.config = config,
|
||||||
|
.allocator = allocator,
|
||||||
|
.network = undefined,
|
||||||
|
.platform = undefined,
|
||||||
|
.snapshot = undefined,
|
||||||
|
.app_dir_path = undefined,
|
||||||
|
.telemetry = undefined,
|
||||||
|
.arena_pool = undefined,
|
||||||
|
};
|
||||||
|
|
||||||
app.notification = try Notification.init(allocator, null);
|
app.network = try Network.init(allocator, config);
|
||||||
errdefer app.notification.deinit();
|
errdefer app.network.deinit();
|
||||||
|
|
||||||
app.http = try Http.init(allocator, .{
|
|
||||||
.max_host_open = config.http_max_host_open orelse 4,
|
|
||||||
.max_concurrent = config.http_max_concurrent orelse 10,
|
|
||||||
.timeout_ms = config.http_timeout_ms orelse 5000,
|
|
||||||
.connect_timeout_ms = config.http_connect_timeout_ms orelse 0,
|
|
||||||
.http_proxy = config.http_proxy,
|
|
||||||
.tls_verify_host = config.tls_verify_host,
|
|
||||||
.proxy_bearer_token = config.proxy_bearer_token,
|
|
||||||
.user_agent = config.user_agent,
|
|
||||||
});
|
|
||||||
errdefer app.http.deinit();
|
|
||||||
|
|
||||||
app.platform = try Platform.init();
|
app.platform = try Platform.init();
|
||||||
errdefer app.platform.deinit();
|
errdefer app.platform.deinit();
|
||||||
|
|
||||||
app.snapshot = try Snapshot.load(allocator);
|
app.snapshot = try Snapshot.load();
|
||||||
errdefer app.snapshot.deinit(allocator);
|
errdefer app.snapshot.deinit();
|
||||||
|
|
||||||
app.app_dir_path = getAndMakeAppDir(allocator);
|
app.app_dir_path = getAndMakeAppDir(allocator);
|
||||||
|
|
||||||
app.telemetry = try Telemetry.init(app, config.run_mode);
|
app.telemetry = try Telemetry.init(app, config.mode);
|
||||||
errdefer app.telemetry.deinit();
|
errdefer app.telemetry.deinit();
|
||||||
|
|
||||||
try app.telemetry.register(app.notification);
|
app.arena_pool = ArenaPool.init(allocator, 512, 1024 * 16);
|
||||||
|
errdefer app.arena_pool.deinit();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *App) void {
|
pub fn shutdown(self: *const App) bool {
|
||||||
if (@atomicRmw(bool, &self.shutdown, .Xchg, true, .monotonic)) {
|
return self.network.shutdown.load(.acquire);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *App) void {
|
||||||
const allocator = self.allocator;
|
const allocator = self.allocator;
|
||||||
if (self.app_dir_path) |app_dir_path| {
|
if (self.app_dir_path) |app_dir_path| {
|
||||||
allocator.free(app_dir_path);
|
allocator.free(app_dir_path);
|
||||||
self.app_dir_path = null;
|
self.app_dir_path = null;
|
||||||
}
|
}
|
||||||
self.telemetry.deinit();
|
self.telemetry.deinit();
|
||||||
self.notification.deinit();
|
self.network.deinit();
|
||||||
self.http.deinit();
|
self.snapshot.deinit();
|
||||||
self.snapshot.deinit(allocator);
|
|
||||||
self.platform.deinit();
|
self.platform.deinit();
|
||||||
|
self.arena_pool.deinit();
|
||||||
|
|
||||||
allocator.destroy(self);
|
allocator.destroy(self);
|
||||||
}
|
}
|
||||||
|
|||||||
212
src/ArenaPool.zig
Normal file
212
src/ArenaPool.zig
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
// 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 Allocator = std.mem.Allocator;
|
||||||
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
|
const ArenaPool = @This();
|
||||||
|
|
||||||
|
allocator: Allocator,
|
||||||
|
retain_bytes: usize,
|
||||||
|
free_list_len: u16 = 0,
|
||||||
|
free_list: ?*Entry = null,
|
||||||
|
free_list_max: u16,
|
||||||
|
entry_pool: std.heap.MemoryPool(Entry),
|
||||||
|
mutex: std.Thread.Mutex = .{},
|
||||||
|
|
||||||
|
const Entry = struct {
|
||||||
|
next: ?*Entry,
|
||||||
|
arena: ArenaAllocator,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, free_list_max: u16, retain_bytes: usize) ArenaPool {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.free_list_max = free_list_max,
|
||||||
|
.retain_bytes = retain_bytes,
|
||||||
|
.entry_pool = .init(allocator),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *ArenaPool) void {
|
||||||
|
var entry = self.free_list;
|
||||||
|
while (entry) |e| {
|
||||||
|
entry = e.next;
|
||||||
|
e.arena.deinit();
|
||||||
|
}
|
||||||
|
self.entry_pool.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn acquire(self: *ArenaPool) !Allocator {
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
if (self.free_list) |entry| {
|
||||||
|
self.free_list = entry.next;
|
||||||
|
self.free_list_len -= 1;
|
||||||
|
return entry.arena.allocator();
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = try self.entry_pool.create();
|
||||||
|
entry.* = .{
|
||||||
|
.next = null,
|
||||||
|
.arena = ArenaAllocator.init(self.allocator),
|
||||||
|
};
|
||||||
|
|
||||||
|
return entry.arena.allocator();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn release(self: *ArenaPool, allocator: Allocator) void {
|
||||||
|
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
||||||
|
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;
|
||||||
|
if (free_list_len == self.free_list_max) {
|
||||||
|
arena.deinit();
|
||||||
|
self.entry_pool.destroy(entry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.next = self.free_list;
|
||||||
|
self.free_list_len = free_list_len + 1;
|
||||||
|
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();
|
||||||
|
}
|
||||||
911
src/Config.zig
Normal file
911
src/Config.zig
Normal file
@@ -0,0 +1,911 @@
|
|||||||
|
// 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 builtin = @import("builtin");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const log = @import("log.zig");
|
||||||
|
const dump = @import("browser/dump.zig");
|
||||||
|
|
||||||
|
const WebBotAuthConfig = @import("network/WebBotAuth.zig").Config;
|
||||||
|
|
||||||
|
pub const RunMode = enum {
|
||||||
|
help,
|
||||||
|
fetch,
|
||||||
|
serve,
|
||||||
|
version,
|
||||||
|
mcp,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const MAX_LISTENERS = 16;
|
||||||
|
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,
|
||||||
|
exec_name: []const u8,
|
||||||
|
http_headers: HttpHeaders,
|
||||||
|
|
||||||
|
const Config = @This();
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, exec_name: []const u8, mode: Mode) !Config {
|
||||||
|
var config = Config{
|
||||||
|
.mode = mode,
|
||||||
|
.exec_name = exec_name,
|
||||||
|
.http_headers = undefined,
|
||||||
|
};
|
||||||
|
config.http_headers = try HttpHeaders.init(allocator, &config);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *const Config, allocator: Allocator) void {
|
||||||
|
self.http_headers.deinit(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tlsVerifyHost(self: *const Config) bool {
|
||||||
|
return switch (self.mode) {
|
||||||
|
inline .serve, .fetch, .mcp => |opts| opts.common.tls_verify_host,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn obeyRobots(self: *const Config) bool {
|
||||||
|
return switch (self.mode) {
|
||||||
|
inline .serve, .fetch, .mcp => |opts| opts.common.obey_robots,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn httpProxy(self: *const Config) ?[:0]const u8 {
|
||||||
|
return switch (self.mode) {
|
||||||
|
inline .serve, .fetch, .mcp => |opts| opts.common.http_proxy,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn proxyBearerToken(self: *const Config) ?[:0]const u8 {
|
||||||
|
return switch (self.mode) {
|
||||||
|
inline .serve, .fetch, .mcp => |opts| opts.common.proxy_bearer_token,
|
||||||
|
.help, .version => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn httpMaxConcurrent(self: *const Config) u8 {
|
||||||
|
return switch (self.mode) {
|
||||||
|
inline .serve, .fetch, .mcp => |opts| opts.common.http_max_concurrent orelse 10,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn httpMaxHostOpen(self: *const Config) u8 {
|
||||||
|
return switch (self.mode) {
|
||||||
|
inline .serve, .fetch, .mcp => |opts| opts.common.http_max_host_open orelse 4,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn httpConnectTimeout(self: *const Config) u31 {
|
||||||
|
return switch (self.mode) {
|
||||||
|
inline .serve, .fetch, .mcp => |opts| opts.common.http_connect_timeout orelse 0,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn httpTimeout(self: *const Config) u31 {
|
||||||
|
return switch (self.mode) {
|
||||||
|
inline .serve, .fetch, .mcp => |opts| opts.common.http_timeout orelse 5000,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn httpMaxRedirects(_: *const Config) u8 {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn httpMaxResponseSize(self: *const Config) ?usize {
|
||||||
|
return switch (self.mode) {
|
||||||
|
inline .serve, .fetch, .mcp => |opts| opts.common.http_max_response_size,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logLevel(self: *const Config) ?log.Level {
|
||||||
|
return switch (self.mode) {
|
||||||
|
inline .serve, .fetch, .mcp => |opts| opts.common.log_level,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logFormat(self: *const Config) ?log.Format {
|
||||||
|
return switch (self.mode) {
|
||||||
|
inline .serve, .fetch, .mcp => |opts| opts.common.log_format,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logFilterScopes(self: *const Config) ?[]const log.Scope {
|
||||||
|
return switch (self.mode) {
|
||||||
|
inline .serve, .fetch, .mcp => |opts| opts.common.log_filter_scopes,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn userAgentSuffix(self: *const Config) ?[]const u8 {
|
||||||
|
return switch (self.mode) {
|
||||||
|
inline .serve, .fetch, .mcp => |opts| opts.common.user_agent_suffix,
|
||||||
|
.help, .version => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cdpTimeout(self: *const Config) usize {
|
||||||
|
return switch (self.mode) {
|
||||||
|
.serve => |opts| if (opts.timeout > 604_800) 604_800_000 else @as(usize, opts.timeout) * 1000,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn webBotAuth(self: *const Config) ?WebBotAuthConfig {
|
||||||
|
return switch (self.mode) {
|
||||||
|
inline .serve, .fetch, .mcp => |opts| WebBotAuthConfig{
|
||||||
|
.key_file = opts.common.web_bot_auth_key_file orelse return null,
|
||||||
|
.keyid = opts.common.web_bot_auth_keyid orelse return null,
|
||||||
|
.domain = opts.common.web_bot_auth_domain orelse return 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) {
|
||||||
|
help: bool, // false when being printed because of an error
|
||||||
|
fetch: Fetch,
|
||||||
|
serve: Serve,
|
||||||
|
version: void,
|
||||||
|
mcp: Mcp,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Serve = struct {
|
||||||
|
host: []const u8 = "127.0.0.1",
|
||||||
|
port: u16 = 9222,
|
||||||
|
timeout: u31 = 10,
|
||||||
|
cdp_max_connections: u16 = 16,
|
||||||
|
cdp_max_pending_connections: u16 = 128,
|
||||||
|
common: Common = .{},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Mcp = struct {
|
||||||
|
common: Common = .{},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const DumpFormat = enum {
|
||||||
|
html,
|
||||||
|
markdown,
|
||||||
|
wpt,
|
||||||
|
semantic_tree,
|
||||||
|
semantic_tree_text,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Fetch = struct {
|
||||||
|
url: [:0]const u8,
|
||||||
|
dump_mode: ?DumpFormat = null,
|
||||||
|
common: Common = .{},
|
||||||
|
with_base: bool = false,
|
||||||
|
with_frames: bool = false,
|
||||||
|
strip: dump.Opts.Strip = .{},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Common = struct {
|
||||||
|
obey_robots: bool = false,
|
||||||
|
proxy_bearer_token: ?[:0]const u8 = null,
|
||||||
|
http_proxy: ?[:0]const u8 = null,
|
||||||
|
http_max_concurrent: ?u8 = null,
|
||||||
|
http_max_host_open: ?u8 = null,
|
||||||
|
http_timeout: ?u31 = null,
|
||||||
|
http_connect_timeout: ?u31 = null,
|
||||||
|
http_max_response_size: ?usize = null,
|
||||||
|
tls_verify_host: bool = true,
|
||||||
|
log_level: ?log.Level = null,
|
||||||
|
log_format: ?log.Format = null,
|
||||||
|
log_filter_scopes: ?[]log.Scope = null,
|
||||||
|
user_agent_suffix: ?[]const u8 = null,
|
||||||
|
|
||||||
|
web_bot_auth_key_file: ?[]const u8 = null,
|
||||||
|
web_bot_auth_keyid: ?[]const u8 = null,
|
||||||
|
web_bot_auth_domain: ?[]const u8 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Pre-formatted HTTP headers for reuse across Http and Client.
|
||||||
|
/// Must be initialized with an allocator that outlives all HTTP connections.
|
||||||
|
pub const HttpHeaders = struct {
|
||||||
|
const user_agent_base: [:0]const u8 = "Lightpanda/1.0";
|
||||||
|
|
||||||
|
user_agent: [:0]const u8, // User agent value (e.g. "Lightpanda/1.0")
|
||||||
|
user_agent_header: [:0]const u8,
|
||||||
|
|
||||||
|
proxy_bearer_header: ?[:0]const u8,
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, config: *const Config) !HttpHeaders {
|
||||||
|
const user_agent: [:0]const u8 = if (config.userAgentSuffix()) |suffix|
|
||||||
|
try std.fmt.allocPrintSentinel(allocator, "{s} {s}", .{ user_agent_base, suffix }, 0)
|
||||||
|
else
|
||||||
|
user_agent_base;
|
||||||
|
errdefer if (config.userAgentSuffix() != null) allocator.free(user_agent);
|
||||||
|
|
||||||
|
const user_agent_header = try std.fmt.allocPrintSentinel(allocator, "User-Agent: {s}", .{user_agent}, 0);
|
||||||
|
errdefer allocator.free(user_agent_header);
|
||||||
|
|
||||||
|
const proxy_bearer_header: ?[:0]const u8 = if (config.proxyBearerToken()) |token|
|
||||||
|
try std.fmt.allocPrintSentinel(allocator, "Proxy-Authorization: Bearer {s}", .{token}, 0)
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.user_agent = user_agent,
|
||||||
|
.user_agent_header = user_agent_header,
|
||||||
|
.proxy_bearer_header = proxy_bearer_header,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *const HttpHeaders, allocator: Allocator) void {
|
||||||
|
if (self.proxy_bearer_header) |hdr| {
|
||||||
|
allocator.free(hdr);
|
||||||
|
}
|
||||||
|
allocator.free(self.user_agent_header);
|
||||||
|
if (self.user_agent.ptr != user_agent_base.ptr) {
|
||||||
|
allocator.free(self.user_agent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
||||||
|
// MAX_HELP_LEN|
|
||||||
|
const common_options =
|
||||||
|
\\
|
||||||
|
\\--insecure_disable_tls_host_verification
|
||||||
|
\\ Disables host verification on all HTTP requests. This is an
|
||||||
|
\\ advanced option which should only be set if you understand
|
||||||
|
\\ and accept the risk of disabling host verification.
|
||||||
|
\\
|
||||||
|
\\--obey_robots
|
||||||
|
\\ Fetches and obeys the robots.txt (if available) of the web pages
|
||||||
|
\\ we make requests towards.
|
||||||
|
\\ Defaults to false.
|
||||||
|
\\
|
||||||
|
\\--http_proxy The HTTP proxy to use for all HTTP requests.
|
||||||
|
\\ A username:password can be included for basic authentication.
|
||||||
|
\\ Defaults to none.
|
||||||
|
\\
|
||||||
|
\\--proxy_bearer_token
|
||||||
|
\\ The <token> to send for bearer authentication with the proxy
|
||||||
|
\\ Proxy-Authorization: Bearer <token>
|
||||||
|
\\
|
||||||
|
\\--http_max_concurrent
|
||||||
|
\\ The maximum number of concurrent HTTP requests.
|
||||||
|
\\ Defaults to 10.
|
||||||
|
\\
|
||||||
|
\\--http_max_host_open
|
||||||
|
\\ The maximum number of open connection to a given host:port.
|
||||||
|
\\ Defaults to 4.
|
||||||
|
\\
|
||||||
|
\\--http_connect_timeout
|
||||||
|
\\ The time, in milliseconds, for establishing an HTTP connection
|
||||||
|
\\ before timing out. 0 means it never times out.
|
||||||
|
\\ Defaults to 0.
|
||||||
|
\\
|
||||||
|
\\--http_timeout
|
||||||
|
\\ The maximum time, in milliseconds, the transfer is allowed
|
||||||
|
\\ to complete. 0 means it never times out.
|
||||||
|
\\ Defaults to 10000.
|
||||||
|
\\
|
||||||
|
\\--http_max_response_size
|
||||||
|
\\ Limits the acceptable response size for any request
|
||||||
|
\\ (e.g. XHR, fetch, script loading, ...).
|
||||||
|
\\ Defaults to no limit.
|
||||||
|
\\
|
||||||
|
\\--log_level The log level: debug, info, warn, error or fatal.
|
||||||
|
\\ Defaults to
|
||||||
|
++ (if (builtin.mode == .Debug) " info." else "warn.") ++
|
||||||
|
\\
|
||||||
|
\\
|
||||||
|
\\--log_format The log format: pretty or logfmt.
|
||||||
|
\\ Defaults to
|
||||||
|
++ (if (builtin.mode == .Debug) " pretty." else " logfmt.") ++
|
||||||
|
\\
|
||||||
|
\\
|
||||||
|
\\--log_filter_scopes
|
||||||
|
\\ Filter out too verbose logs per scope:
|
||||||
|
\\ http, unknown_prop, event, ...
|
||||||
|
\\
|
||||||
|
\\--user_agent_suffix
|
||||||
|
\\ Suffix to append to the Lightpanda/X.Y User-Agent
|
||||||
|
\\
|
||||||
|
\\--web_bot_auth_key_file
|
||||||
|
\\ Path to the Ed25519 private key PEM file.
|
||||||
|
\\
|
||||||
|
\\--web_bot_auth_keyid
|
||||||
|
\\ The JWK thumbprint of your public key.
|
||||||
|
\\
|
||||||
|
\\--web_bot_auth_domain
|
||||||
|
\\ Your domain e.g. yourdomain.com
|
||||||
|
;
|
||||||
|
|
||||||
|
// MAX_HELP_LEN|
|
||||||
|
const usage =
|
||||||
|
\\usage: {s} command [options] [URL]
|
||||||
|
\\
|
||||||
|
\\Command can be either 'fetch', 'serve', 'mcp' or 'help'
|
||||||
|
\\
|
||||||
|
\\fetch command
|
||||||
|
\\Fetches the specified URL
|
||||||
|
\\Example: {s} fetch --dump html https://lightpanda.io/
|
||||||
|
\\
|
||||||
|
\\Options:
|
||||||
|
\\--dump Dumps document to stdout.
|
||||||
|
\\ Argument must be 'html', 'markdown', 'semantic_tree', or 'semantic_tree_text'.
|
||||||
|
\\ Defaults to no dump.
|
||||||
|
\\
|
||||||
|
\\--strip_mode Comma separated list of tag groups to remove from dump
|
||||||
|
\\ the dump. e.g. --strip_mode js,css
|
||||||
|
\\ - "js" script and link[as=script, rel=preload]
|
||||||
|
\\ - "ui" includes img, picture, video, css and svg
|
||||||
|
\\ - "css" includes style and link[rel=stylesheet]
|
||||||
|
\\ - "full" includes js, ui and css
|
||||||
|
\\
|
||||||
|
\\--with_base Add a <base> tag in dump. Defaults to false.
|
||||||
|
\\
|
||||||
|
\\--with_frames Includes the contents of iframes. Defaults to false.
|
||||||
|
\\
|
||||||
|
++ common_options ++
|
||||||
|
\\
|
||||||
|
\\serve command
|
||||||
|
\\Starts a websocket CDP server
|
||||||
|
\\Example: {s} serve --host 127.0.0.1 --port 9222
|
||||||
|
\\
|
||||||
|
\\Options:
|
||||||
|
\\--host Host of the CDP server
|
||||||
|
\\ Defaults to "127.0.0.1"
|
||||||
|
\\
|
||||||
|
\\--port Port of the CDP server
|
||||||
|
\\ Defaults to 9222
|
||||||
|
\\
|
||||||
|
\\--timeout Inactivity timeout in seconds before disconnecting clients
|
||||||
|
\\ Defaults to 10 (seconds). Limited to 604800 (1 week).
|
||||||
|
\\
|
||||||
|
\\--cdp_max_connections
|
||||||
|
\\ Maximum number of simultaneous CDP connections.
|
||||||
|
\\ Defaults to 16.
|
||||||
|
\\
|
||||||
|
\\--cdp_max_pending_connections
|
||||||
|
\\ Maximum pending connections in the accept queue.
|
||||||
|
\\ Defaults to 128.
|
||||||
|
\\
|
||||||
|
++ common_options ++
|
||||||
|
\\
|
||||||
|
\\mcp command
|
||||||
|
\\Starts an MCP (Model Context Protocol) server over stdio
|
||||||
|
\\Example: {s} mcp
|
||||||
|
\\
|
||||||
|
++ common_options ++
|
||||||
|
\\
|
||||||
|
\\version command
|
||||||
|
\\Displays the version of {s}
|
||||||
|
\\
|
||||||
|
\\help command
|
||||||
|
\\Displays this message
|
||||||
|
\\
|
||||||
|
;
|
||||||
|
std.debug.print(usage, .{ self.exec_name, self.exec_name, self.exec_name, self.exec_name, self.exec_name });
|
||||||
|
if (success) {
|
||||||
|
return std.process.cleanExit();
|
||||||
|
}
|
||||||
|
std.process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parseArgs(allocator: Allocator) !Config {
|
||||||
|
var args = try std.process.argsWithAllocator(allocator);
|
||||||
|
defer args.deinit();
|
||||||
|
|
||||||
|
const exec_name = try allocator.dupe(u8, std.fs.path.basename(args.next().?));
|
||||||
|
|
||||||
|
const mode_string = args.next() orelse "";
|
||||||
|
const run_mode = std.meta.stringToEnum(RunMode, mode_string) orelse blk: {
|
||||||
|
const inferred_mode = inferMode(mode_string) orelse
|
||||||
|
return init(allocator, exec_name, .{ .help = false });
|
||||||
|
// "command" wasn't a command but an option. We can't reset args, but
|
||||||
|
// we can create a new one. Not great, but this fallback is temporary
|
||||||
|
// as we transition to this command mode approach.
|
||||||
|
args.deinit();
|
||||||
|
|
||||||
|
args = try std.process.argsWithAllocator(allocator);
|
||||||
|
// skip the exec_name
|
||||||
|
_ = args.skip();
|
||||||
|
|
||||||
|
break :blk inferred_mode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mode: Mode = switch (run_mode) {
|
||||||
|
.help => .{ .help = true },
|
||||||
|
.serve => .{ .serve = parseServeArgs(allocator, &args) catch
|
||||||
|
return init(allocator, exec_name, .{ .help = false }) },
|
||||||
|
.fetch => .{ .fetch = parseFetchArgs(allocator, &args) catch
|
||||||
|
return init(allocator, exec_name, .{ .help = false }) },
|
||||||
|
.mcp => .{ .mcp = parseMcpArgs(allocator, &args) catch
|
||||||
|
return init(allocator, exec_name, .{ .help = false }) },
|
||||||
|
.version => .{ .version = {} },
|
||||||
|
};
|
||||||
|
return init(allocator, exec_name, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inferMode(opt: []const u8) ?RunMode {
|
||||||
|
if (opt.len == 0) {
|
||||||
|
return .serve;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, opt, "--") == false) {
|
||||||
|
return .fetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, opt, "--dump")) {
|
||||||
|
return .fetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, opt, "--noscript")) {
|
||||||
|
return .fetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, opt, "--strip_mode")) {
|
||||||
|
return .fetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, opt, "--with_base")) {
|
||||||
|
return .fetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, opt, "--with_frames")) {
|
||||||
|
return .fetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, opt, "--host")) {
|
||||||
|
return .serve;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, opt, "--port")) {
|
||||||
|
return .serve;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, opt, "--timeout")) {
|
||||||
|
return .serve;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseServeArgs(
|
||||||
|
allocator: Allocator,
|
||||||
|
args: *std.process.ArgIterator,
|
||||||
|
) !Serve {
|
||||||
|
var serve: Serve = .{};
|
||||||
|
|
||||||
|
while (args.next()) |opt| {
|
||||||
|
if (std.mem.eql(u8, "--host", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--host" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
serve.host = try allocator.dupe(u8, str);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--port", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--port" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
|
||||||
|
serve.port = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||||
|
log.fatal(.app, "invalid argument value", .{ .arg = "--port", .err = err });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--timeout", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--timeout" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
|
||||||
|
serve.timeout = std.fmt.parseInt(u31, str, 10) catch |err| {
|
||||||
|
log.fatal(.app, "invalid argument value", .{ .arg = "--timeout", .err = err });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--cdp_max_connections", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--cdp_max_connections" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
|
||||||
|
serve.cdp_max_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||||
|
log.fatal(.app, "invalid argument value", .{ .arg = "--cdp_max_connections", .err = err });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--cdp_max_pending_connections", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--cdp_max_pending_connections" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
|
||||||
|
serve.cdp_max_pending_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||||
|
log.fatal(.app, "invalid argument value", .{ .arg = "--cdp_max_pending_connections", .err = err });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (try parseCommonArg(allocator, opt, args, &serve.common)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.fatal(.app, "unknown argument", .{ .mode = "serve", .arg = opt });
|
||||||
|
return error.UnkownOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
allocator: Allocator,
|
||||||
|
args: *std.process.ArgIterator,
|
||||||
|
) !Fetch {
|
||||||
|
var dump_mode: ?DumpFormat = null;
|
||||||
|
var with_base: bool = false;
|
||||||
|
var with_frames: bool = false;
|
||||||
|
var url: ?[:0]const u8 = null;
|
||||||
|
var common: Common = .{};
|
||||||
|
var strip: dump.Opts.Strip = .{};
|
||||||
|
|
||||||
|
while (args.next()) |opt| {
|
||||||
|
if (std.mem.eql(u8, "--dump", opt)) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--noscript", opt)) {
|
||||||
|
log.warn(.app, "deprecation warning", .{
|
||||||
|
.feature = "--noscript argument",
|
||||||
|
.hint = "use '--strip_mode js' instead",
|
||||||
|
});
|
||||||
|
strip.js = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--with_base", opt)) {
|
||||||
|
with_base = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--with_frames", opt)) {
|
||||||
|
with_frames = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--strip_mode", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--strip_mode" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
|
||||||
|
var it = std.mem.splitScalar(u8, str, ',');
|
||||||
|
while (it.next()) |part| {
|
||||||
|
const trimmed = std.mem.trim(u8, part, &std.ascii.whitespace);
|
||||||
|
if (std.mem.eql(u8, trimmed, "js")) {
|
||||||
|
strip.js = true;
|
||||||
|
} else if (std.mem.eql(u8, trimmed, "ui")) {
|
||||||
|
strip.ui = true;
|
||||||
|
} else if (std.mem.eql(u8, trimmed, "css")) {
|
||||||
|
strip.css = true;
|
||||||
|
} else if (std.mem.eql(u8, trimmed, "full")) {
|
||||||
|
strip.js = true;
|
||||||
|
strip.ui = true;
|
||||||
|
strip.css = true;
|
||||||
|
} else {
|
||||||
|
log.fatal(.app, "invalid option choice", .{ .arg = "--strip_mode", .value = trimmed });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (try parseCommonArg(allocator, opt, args, &common)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, opt, "--")) {
|
||||||
|
log.fatal(.app, "unknown argument", .{ .mode = "fetch", .arg = opt });
|
||||||
|
return error.UnkownOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url != null) {
|
||||||
|
log.fatal(.app, "duplicate fetch url", .{ .help = "only 1 URL can be specified" });
|
||||||
|
return error.TooManyURLs;
|
||||||
|
}
|
||||||
|
url = try allocator.dupeZ(u8, opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url == null) {
|
||||||
|
log.fatal(.app, "missing fetch url", .{ .help = "URL to fetch must be provided" });
|
||||||
|
return error.MissingURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.url = url.?,
|
||||||
|
.dump_mode = dump_mode,
|
||||||
|
.strip = strip,
|
||||||
|
.common = common,
|
||||||
|
.with_base = with_base,
|
||||||
|
.with_frames = with_frames,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseCommonArg(
|
||||||
|
allocator: Allocator,
|
||||||
|
opt: []const u8,
|
||||||
|
args: *std.process.ArgIterator,
|
||||||
|
common: *Common,
|
||||||
|
) !bool {
|
||||||
|
if (std.mem.eql(u8, "--insecure_disable_tls_host_verification", opt)) {
|
||||||
|
common.tls_verify_host = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--obey_robots", opt)) {
|
||||||
|
common.obey_robots = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--http_proxy", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--http_proxy" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
common.http_proxy = try allocator.dupeZ(u8, str);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--proxy_bearer_token", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--proxy_bearer_token" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
common.proxy_bearer_token = try allocator.dupeZ(u8, str);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--http_max_concurrent", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--http_max_concurrent" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
|
||||||
|
common.http_max_concurrent = std.fmt.parseInt(u8, str, 10) catch |err| {
|
||||||
|
log.fatal(.app, "invalid argument value", .{ .arg = "--http_max_concurrent", .err = err });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--http_max_host_open", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--http_max_host_open" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
|
||||||
|
common.http_max_host_open = std.fmt.parseInt(u8, str, 10) catch |err| {
|
||||||
|
log.fatal(.app, "invalid argument value", .{ .arg = "--http_max_host_open", .err = err });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--http_connect_timeout", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--http_connect_timeout" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
|
||||||
|
common.http_connect_timeout = std.fmt.parseInt(u31, str, 10) catch |err| {
|
||||||
|
log.fatal(.app, "invalid argument value", .{ .arg = "--http_connect_timeout", .err = err });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--http_timeout", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--http_timeout" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
|
||||||
|
common.http_timeout = std.fmt.parseInt(u31, str, 10) catch |err| {
|
||||||
|
log.fatal(.app, "invalid argument value", .{ .arg = "--http_timeout", .err = err });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--http_max_response_size", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--http_max_response_size" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
|
||||||
|
common.http_max_response_size = std.fmt.parseInt(usize, str, 10) catch |err| {
|
||||||
|
log.fatal(.app, "invalid argument value", .{ .arg = "--http_max_response_size", .err = err });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--log_level", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--log_level" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
|
||||||
|
common.log_level = std.meta.stringToEnum(log.Level, str) orelse blk: {
|
||||||
|
if (std.mem.eql(u8, str, "error")) {
|
||||||
|
break :blk .err;
|
||||||
|
}
|
||||||
|
log.fatal(.app, "invalid option choice", .{ .arg = "--log_level", .value = str });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--log_format", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--log_format" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
|
||||||
|
common.log_format = std.meta.stringToEnum(log.Format, str) orelse {
|
||||||
|
log.fatal(.app, "invalid option choice", .{ .arg = "--log_format", .value = str });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--log_filter_scopes", opt)) {
|
||||||
|
if (builtin.mode != .Debug) {
|
||||||
|
log.fatal(.app, "experimental", .{ .help = "log scope filtering is only available in debug builds" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const str = args.next() orelse {
|
||||||
|
// disables the default filters
|
||||||
|
common.log_filter_scopes = &.{};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
var arr: std.ArrayList(log.Scope) = .empty;
|
||||||
|
|
||||||
|
var it = std.mem.splitScalar(u8, str, ',');
|
||||||
|
while (it.next()) |part| {
|
||||||
|
try arr.append(allocator, std.meta.stringToEnum(log.Scope, part) orelse {
|
||||||
|
log.fatal(.app, "invalid option choice", .{ .arg = "--log_filter_scopes", .value = part });
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
common.log_filter_scopes = arr.items;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--user_agent_suffix", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--user_agent_suffix" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
for (str) |c| {
|
||||||
|
if (!std.ascii.isPrint(c)) {
|
||||||
|
log.fatal(.app, "not printable character", .{ .arg = "--user_agent_suffix" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
common.user_agent_suffix = try allocator.dupe(u8, str);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--web_bot_auth_key_file", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--web_bot_auth_key_file" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
common.web_bot_auth_key_file = try allocator.dupe(u8, str);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--web_bot_auth_keyid", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--web_bot_auth_keyid" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
common.web_bot_auth_keyid = try allocator.dupe(u8, str);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--web_bot_auth_domain", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--web_bot_auth_domain" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
common.web_bot_auth_domain = try allocator.dupe(u8, str);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -17,10 +17,11 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const lp = @import("lightpanda");
|
||||||
|
|
||||||
const log = @import("log.zig");
|
const log = @import("log.zig");
|
||||||
const Page = @import("browser/Page.zig");
|
const Page = @import("browser/Page.zig");
|
||||||
const Transfer = @import("http/Client.zig").Transfer;
|
const Transfer = @import("browser/HttpClient.zig").Transfer;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
@@ -38,10 +39,9 @@ const List = std.DoublyLinkedList;
|
|||||||
// CDP code registers for the "network_bytes_sent" event, because it needs to
|
// CDP code registers for the "network_bytes_sent" event, because it needs to
|
||||||
// send messages to the client when this happens. Our HTTP client could then
|
// send messages to the client when this happens. Our HTTP client could then
|
||||||
// emit a "network_bytes_sent" message. It would be easy, and it would work.
|
// emit a "network_bytes_sent" message. It would be easy, and it would work.
|
||||||
// That is, it would work until the Telemetry code makes an HTTP request, and
|
// That is, it would work until multiple CDP clients connect, and because
|
||||||
// because everything's just one big global, that gets picked up by the
|
// everything's just one big global, events from one CDP session would be sent
|
||||||
// registered CDP listener, and the telemetry network activity gets sent to the
|
// to all CDP clients.
|
||||||
// CDP client.
|
|
||||||
//
|
//
|
||||||
// To avoid this, one way or another, we need scoping. We could still have
|
// To avoid this, one way or another, we need scoping. We could still have
|
||||||
// a global registry but every "register" and every "emit" has some type of
|
// a global registry but every "register" and every "emit" has some type of
|
||||||
@@ -49,14 +49,10 @@ const List = std.DoublyLinkedList;
|
|||||||
// between components to share a common scope.
|
// between components to share a common scope.
|
||||||
//
|
//
|
||||||
// Instead, the approach that we take is to have a notification instance per
|
// Instead, the approach that we take is to have a notification instance per
|
||||||
// scope. This makes some things harder, but we only plan on having 2
|
// CDP connection (BrowserContext). Each CDP connection has its own notification
|
||||||
// notification instances at a given time: one in a Browser and one in the App.
|
// that is shared across all Sessions (tabs) within that connection. This ensures
|
||||||
// What about something like Telemetry, which lives outside of a Browser but
|
// proper isolation between different CDP clients while allowing a single client
|
||||||
// still cares about Browser-events (like .page_navigate)? When the Browser
|
// to receive events from all its tabs.
|
||||||
// notification is created, a `notification_created` event is raised in the
|
|
||||||
// App's notification, which Telemetry is registered for. This allows Telemetry
|
|
||||||
// to register for events in the Browser notification. See the Telemetry's
|
|
||||||
// register function.
|
|
||||||
const Notification = @This();
|
const Notification = @This();
|
||||||
// Every event type (which are hard-coded), has a list of Listeners.
|
// Every event type (which are hard-coded), has a list of Listeners.
|
||||||
// When the event happens, we dispatch to those listener.
|
// When the event happens, we dispatch to those listener.
|
||||||
@@ -65,7 +61,7 @@ event_listeners: EventListeners,
|
|||||||
// list of listeners for a specified receiver
|
// list of listeners for a specified receiver
|
||||||
// @intFromPtr(receiver) -> [listener1, listener2, ...]
|
// @intFromPtr(receiver) -> [listener1, listener2, ...]
|
||||||
// Used when `unregisterAll` is called.
|
// Used when `unregisterAll` is called.
|
||||||
listeners: std.AutoHashMapUnmanaged(usize, std.ArrayListUnmanaged(*Listener)),
|
listeners: std.AutoHashMapUnmanaged(usize, std.ArrayList(*Listener)),
|
||||||
|
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
mem_pool: std.heap.MemoryPool(Listener),
|
mem_pool: std.heap.MemoryPool(Listener),
|
||||||
@@ -77,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 = .{},
|
||||||
@@ -84,7 +81,6 @@ const EventListeners = struct {
|
|||||||
http_request_auth_required: List = .{},
|
http_request_auth_required: List = .{},
|
||||||
http_response_data: List = .{},
|
http_response_data: List = .{},
|
||||||
http_response_header_done: List = .{},
|
http_response_header_done: List = .{},
|
||||||
notification_created: List = .{},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Events = union(enum) {
|
const Events = union(enum) {
|
||||||
@@ -94,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,
|
||||||
@@ -101,31 +98,42 @@ const Events = union(enum) {
|
|||||||
http_request_done: *const RequestDone,
|
http_request_done: *const RequestDone,
|
||||||
http_response_data: *const ResponseData,
|
http_response_data: *const ResponseData,
|
||||||
http_response_header_done: *const ResponseHeaderDone,
|
http_response_header_done: *const ResponseHeaderDone,
|
||||||
notification_created: *Notification,
|
|
||||||
};
|
};
|
||||||
const EventType = std.meta.FieldEnum(Events);
|
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -161,12 +169,7 @@ pub const RequestFail = struct {
|
|||||||
err: anyerror,
|
err: anyerror,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, parent: ?*Notification) !*Notification {
|
pub fn init(allocator: Allocator) !*Notification {
|
||||||
|
|
||||||
// This is put on the heap because we want to raise a .notification_created
|
|
||||||
// event, so that, something like Telemetry, can receive the
|
|
||||||
// .page_navigate event on all notification instances. That can only work
|
|
||||||
// if we dispatch .notification_created with a *Notification.
|
|
||||||
const notification = try allocator.create(Notification);
|
const notification = try allocator.create(Notification);
|
||||||
errdefer allocator.destroy(notification);
|
errdefer allocator.destroy(notification);
|
||||||
|
|
||||||
@@ -177,10 +180,6 @@ pub fn init(allocator: Allocator, parent: ?*Notification) !*Notification {
|
|||||||
.mem_pool = std.heap.MemoryPool(Listener).init(allocator),
|
.mem_pool = std.heap.MemoryPool(Listener).init(allocator),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (parent) |pn| {
|
|
||||||
pn.dispatch(.notification_created, notification);
|
|
||||||
}
|
|
||||||
|
|
||||||
return notification;
|
return notification;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +240,7 @@ pub fn unregister(self: *Notification, comptime event: EventType, receiver: anyt
|
|||||||
if (listeners.items.len == 0) {
|
if (listeners.items.len == 0) {
|
||||||
listeners.deinit(self.allocator);
|
listeners.deinit(self.allocator);
|
||||||
const removed = self.listeners.remove(@intFromPtr(receiver));
|
const removed = self.listeners.remove(@intFromPtr(receiver));
|
||||||
std.debug.assert(removed == true);
|
lp.assert(removed == true, "Notification.unregister", .{ .type = event });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,6 +254,9 @@ pub fn unregisterAll(self: *Notification, receiver: *anyopaque) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn dispatch(self: *Notification, comptime event: EventType, data: ArgType(event)) void {
|
pub fn dispatch(self: *Notification, comptime event: EventType, data: ArgType(event)) void {
|
||||||
|
if (self.listeners.count() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const list = &@field(self.event_listeners, @tagName(event));
|
const list = &@field(self.event_listeners, @tagName(event));
|
||||||
|
|
||||||
var node = list.first;
|
var node = list.first;
|
||||||
@@ -312,11 +314,12 @@ const Listener = struct {
|
|||||||
|
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
test "Notification" {
|
test "Notification" {
|
||||||
var notifier = try Notification.init(testing.allocator, null);
|
var notifier = try Notification.init(testing.allocator);
|
||||||
defer notifier.deinit();
|
defer notifier.deinit();
|
||||||
|
|
||||||
// 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,
|
||||||
@@ -327,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,
|
||||||
@@ -336,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,
|
||||||
@@ -346,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);
|
||||||
|
|
||||||
@@ -370,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);
|
||||||
}
|
}
|
||||||
|
|||||||
450
src/SemanticTree.zig
Normal file
450
src/SemanticTree.zig
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
// 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. See <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const lp = @import("lightpanda");
|
||||||
|
const log = @import("log.zig");
|
||||||
|
const isAllWhitespace = @import("string.zig").isAllWhitespace;
|
||||||
|
const Page = lp.Page;
|
||||||
|
const interactive = @import("browser/interactive.zig");
|
||||||
|
|
||||||
|
const CData = @import("browser/webapi/CData.zig");
|
||||||
|
const Element = @import("browser/webapi/Element.zig");
|
||||||
|
const Node = @import("browser/webapi/Node.zig");
|
||||||
|
const AXNode = @import("cdp/AXNode.zig");
|
||||||
|
const CDPNode = @import("cdp/Node.zig");
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
dom_node: *Node,
|
||||||
|
registry: *CDPNode.Registry,
|
||||||
|
page: *Page,
|
||||||
|
arena: std.mem.Allocator,
|
||||||
|
prune: bool = false,
|
||||||
|
|
||||||
|
pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!void {
|
||||||
|
var visitor = JsonVisitor{ .jw = jw, .tree = self };
|
||||||
|
var xpath_buffer: std.ArrayList(u8) = .{};
|
||||||
|
const listener_targets = interactive.buildListenerTargetMap(self.page, self.arena) catch |err| {
|
||||||
|
log.err(.app, "listener map failed", .{ .err = err });
|
||||||
|
return error.WriteFailed;
|
||||||
|
};
|
||||||
|
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets) catch |err| {
|
||||||
|
log.err(.app, "semantic tree json dump failed", .{ .err = err });
|
||||||
|
return error.WriteFailed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn textStringify(self: @This(), writer: *std.Io.Writer) error{WriteFailed}!void {
|
||||||
|
var visitor = TextVisitor{ .writer = writer, .tree = self, .depth = 0 };
|
||||||
|
var xpath_buffer: std.ArrayList(u8) = .empty;
|
||||||
|
const listener_targets = interactive.buildListenerTargetMap(self.page, self.arena) catch |err| {
|
||||||
|
log.err(.app, "listener map failed", .{ .err = err });
|
||||||
|
return error.WriteFailed;
|
||||||
|
};
|
||||||
|
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets) catch |err| {
|
||||||
|
log.err(.app, "semantic tree text dump failed", .{ .err = err });
|
||||||
|
return error.WriteFailed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const OptionData = struct {
|
||||||
|
value: []const u8,
|
||||||
|
text: []const u8,
|
||||||
|
selected: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
const NodeData = struct {
|
||||||
|
id: u32,
|
||||||
|
axn: AXNode,
|
||||||
|
role: []const u8,
|
||||||
|
name: ?[]const u8,
|
||||||
|
value: ?[]const u8,
|
||||||
|
options: ?[]OptionData = null,
|
||||||
|
xpath: []const u8,
|
||||||
|
is_interactive: bool,
|
||||||
|
node_name: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_name: ?[]const u8, visitor: anytype, index: usize, listener_targets: interactive.ListenerTargetMap) !void {
|
||||||
|
// 1. Skip non-content nodes
|
||||||
|
if (node.is(Element)) |el| {
|
||||||
|
const tag = el.getTag();
|
||||||
|
if (tag.isMetadata() or tag == .svg) return;
|
||||||
|
|
||||||
|
// We handle options/optgroups natively inside their parents, skip them in the general walk
|
||||||
|
if (tag == .datalist or tag == .option or tag == .optgroup) return;
|
||||||
|
|
||||||
|
// Check visibility using the engine's checkVisibility which handles CSS display: none
|
||||||
|
if (!el.checkVisibility(self.page)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.is(Element.Html)) |html_el| {
|
||||||
|
if (html_el.getHidden()) return;
|
||||||
|
}
|
||||||
|
} else if (node.is(CData.Text)) |text_node| {
|
||||||
|
const text = text_node.getWholeText();
|
||||||
|
if (isAllWhitespace(text)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (node._type != .document and node._type != .document_fragment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cdp_node = try self.registry.register(node);
|
||||||
|
const axn = AXNode.fromNode(node);
|
||||||
|
const role = try axn.getRole();
|
||||||
|
|
||||||
|
var is_interactive = false;
|
||||||
|
var value: ?[]const u8 = null;
|
||||||
|
var options: ?[]OptionData = null;
|
||||||
|
var node_name: []const u8 = "text";
|
||||||
|
|
||||||
|
if (node.is(Element)) |el| {
|
||||||
|
node_name = el.getTagNameLower();
|
||||||
|
|
||||||
|
if (el.is(Element.Html.Input)) |input| {
|
||||||
|
value = input.getValue();
|
||||||
|
if (el.getAttributeSafe(comptime lp.String.wrap("list"))) |list_id| {
|
||||||
|
options = try extractDataListOptions(list_id, self.page, self.arena);
|
||||||
|
}
|
||||||
|
} else if (el.is(Element.Html.TextArea)) |textarea| {
|
||||||
|
value = textarea.getValue();
|
||||||
|
} else if (el.is(Element.Html.Select)) |select| {
|
||||||
|
value = select.getValue(self.page);
|
||||||
|
options = try extractSelectOptions(el.asNode(), self.page, self.arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.is(Element.Html)) |html_el| {
|
||||||
|
if (interactive.classifyInteractivity(el, html_el, listener_targets) != null) {
|
||||||
|
is_interactive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (node._type == .document or node._type == .document_fragment) {
|
||||||
|
node_name = "root";
|
||||||
|
}
|
||||||
|
|
||||||
|
const initial_xpath_len = xpath_buffer.items.len;
|
||||||
|
try appendXPathSegment(node, xpath_buffer.writer(self.arena), index);
|
||||||
|
const xpath = xpath_buffer.items;
|
||||||
|
|
||||||
|
var name = try axn.getName(self.page, self.arena);
|
||||||
|
|
||||||
|
const has_explicit_label = if (node.is(Element)) |el|
|
||||||
|
el.getAttributeSafe(.wrap("aria-label")) != null or el.getAttributeSafe(.wrap("title")) != null
|
||||||
|
else
|
||||||
|
false;
|
||||||
|
|
||||||
|
const structural = isStructuralRole(role);
|
||||||
|
|
||||||
|
// Filter out computed concatenated names for generic containers without explicit labels.
|
||||||
|
// This prevents token bloat and ensures their StaticText children aren't incorrectly pruned.
|
||||||
|
// We ignore interactivity because a generic wrapper with an event listener still shouldn't hoist all text.
|
||||||
|
if (name != null and structural and !has_explicit_label) {
|
||||||
|
name = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = NodeData{
|
||||||
|
.id = cdp_node.id,
|
||||||
|
.axn = axn,
|
||||||
|
.role = role,
|
||||||
|
.name = name,
|
||||||
|
.value = value,
|
||||||
|
.options = options,
|
||||||
|
.xpath = xpath,
|
||||||
|
.is_interactive = is_interactive,
|
||||||
|
.node_name = node_name,
|
||||||
|
};
|
||||||
|
|
||||||
|
var should_visit = true;
|
||||||
|
if (self.prune) {
|
||||||
|
if (structural and !is_interactive and !has_explicit_label) {
|
||||||
|
should_visit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, role, "StaticText") and node._parent != null) {
|
||||||
|
if (parent_name != null and name != null and std.mem.indexOf(u8, parent_name.?, name.?) != null) {
|
||||||
|
should_visit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var did_visit = false;
|
||||||
|
var should_walk_children = true;
|
||||||
|
if (should_visit) {
|
||||||
|
should_walk_children = try visitor.visit(node, &data);
|
||||||
|
did_visit = true; // Always true if should_visit was true, because visit() executed and opened structures
|
||||||
|
} else {
|
||||||
|
// If we skip the node, we must NOT tell the visitor to close it later
|
||||||
|
did_visit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (should_walk_children) {
|
||||||
|
// If we are printing this node normally OR skipping it and unrolling its children,
|
||||||
|
// we walk the children iterator.
|
||||||
|
var it = node.childrenIterator();
|
||||||
|
var tag_counts = std.StringArrayHashMap(usize).init(self.arena);
|
||||||
|
while (it.next()) |child| {
|
||||||
|
var tag: []const u8 = "text()";
|
||||||
|
if (child.is(Element)) |el| {
|
||||||
|
tag = el.getTagNameLower();
|
||||||
|
}
|
||||||
|
|
||||||
|
const gop = try tag_counts.getOrPut(tag);
|
||||||
|
if (!gop.found_existing) {
|
||||||
|
gop.value_ptr.* = 0;
|
||||||
|
}
|
||||||
|
gop.value_ptr.* += 1;
|
||||||
|
|
||||||
|
try self.walk(child, xpath_buffer, name, visitor, gop.value_ptr.*, listener_targets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (did_visit) {
|
||||||
|
try visitor.leave();
|
||||||
|
}
|
||||||
|
|
||||||
|
xpath_buffer.shrinkRetainingCapacity(initial_xpath_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extractSelectOptions(node: *Node, page: *Page, arena: std.mem.Allocator) ![]OptionData {
|
||||||
|
var options = std.ArrayListUnmanaged(OptionData){};
|
||||||
|
var it = node.childrenIterator();
|
||||||
|
while (it.next()) |child| {
|
||||||
|
if (child.is(Element)) |el| {
|
||||||
|
if (el.getTag() == .option) {
|
||||||
|
if (el.is(Element.Html.Option)) |opt| {
|
||||||
|
const text = opt.getText(page);
|
||||||
|
const value = opt.getValue(page);
|
||||||
|
const selected = opt.getSelected();
|
||||||
|
try options.append(arena, .{ .text = text, .value = value, .selected = selected });
|
||||||
|
}
|
||||||
|
} else if (el.getTag() == .optgroup) {
|
||||||
|
var group_it = child.childrenIterator();
|
||||||
|
while (group_it.next()) |group_child| {
|
||||||
|
if (group_child.is(Element.Html.Option)) |opt| {
|
||||||
|
const text = opt.getText(page);
|
||||||
|
const value = opt.getValue(page);
|
||||||
|
const selected = opt.getSelected();
|
||||||
|
try options.append(arena, .{ .text = text, .value = value, .selected = selected });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options.toOwnedSlice(arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extractDataListOptions(list_id: []const u8, page: *Page, arena: std.mem.Allocator) !?[]OptionData {
|
||||||
|
if (page.document.getElementById(list_id, page)) |referenced_el| {
|
||||||
|
if (referenced_el.getTag() == .datalist) {
|
||||||
|
return try extractSelectOptions(referenced_el.asNode(), page, arena);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn appendXPathSegment(node: *Node, writer: anytype, index: usize) !void {
|
||||||
|
if (node.is(Element)) |el| {
|
||||||
|
const tag = el.getTagNameLower();
|
||||||
|
try std.fmt.format(writer, "/{s}[{d}]", .{ tag, index });
|
||||||
|
} else if (node.is(CData.Text)) |_| {
|
||||||
|
try std.fmt.format(writer, "/text()[{d}]", .{index});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const JsonVisitor = struct {
|
||||||
|
jw: *std.json.Stringify,
|
||||||
|
tree: Self,
|
||||||
|
|
||||||
|
pub fn visit(self: *JsonVisitor, node: *Node, data: *NodeData) !bool {
|
||||||
|
try self.jw.beginObject();
|
||||||
|
|
||||||
|
try self.jw.objectField("nodeId");
|
||||||
|
try self.jw.write(try std.fmt.allocPrint(self.tree.arena, "{d}", .{data.id}));
|
||||||
|
|
||||||
|
try self.jw.objectField("backendDOMNodeId");
|
||||||
|
try self.jw.write(data.id);
|
||||||
|
|
||||||
|
try self.jw.objectField("nodeName");
|
||||||
|
try self.jw.write(data.node_name);
|
||||||
|
|
||||||
|
try self.jw.objectField("xpath");
|
||||||
|
try self.jw.write(data.xpath);
|
||||||
|
|
||||||
|
if (node.is(Element)) |el| {
|
||||||
|
try self.jw.objectField("nodeType");
|
||||||
|
try self.jw.write(1);
|
||||||
|
|
||||||
|
try self.jw.objectField("isInteractive");
|
||||||
|
try self.jw.write(data.is_interactive);
|
||||||
|
|
||||||
|
try self.jw.objectField("role");
|
||||||
|
try self.jw.write(data.role);
|
||||||
|
|
||||||
|
if (data.name) |name| {
|
||||||
|
if (name.len > 0) {
|
||||||
|
try self.jw.objectField("name");
|
||||||
|
try self.jw.write(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.value) |value| {
|
||||||
|
try self.jw.objectField("value");
|
||||||
|
try self.jw.write(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el._attributes) |attrs| {
|
||||||
|
try self.jw.objectField("attributes");
|
||||||
|
try self.jw.beginObject();
|
||||||
|
var iter = attrs.iterator();
|
||||||
|
while (iter.next()) |attr| {
|
||||||
|
try self.jw.objectField(attr._name.str());
|
||||||
|
try self.jw.write(attr._value.str());
|
||||||
|
}
|
||||||
|
try self.jw.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.options) |options| {
|
||||||
|
try self.jw.objectField("options");
|
||||||
|
try self.jw.beginArray();
|
||||||
|
for (options) |opt| {
|
||||||
|
try self.jw.beginObject();
|
||||||
|
try self.jw.objectField("value");
|
||||||
|
try self.jw.write(opt.value);
|
||||||
|
try self.jw.objectField("text");
|
||||||
|
try self.jw.write(opt.text);
|
||||||
|
try self.jw.objectField("selected");
|
||||||
|
try self.jw.write(opt.selected);
|
||||||
|
try self.jw.endObject();
|
||||||
|
}
|
||||||
|
try self.jw.endArray();
|
||||||
|
}
|
||||||
|
} else if (node.is(CData.Text)) |text_node| {
|
||||||
|
try self.jw.objectField("nodeType");
|
||||||
|
try self.jw.write(3);
|
||||||
|
try self.jw.objectField("nodeValue");
|
||||||
|
try self.jw.write(text_node.getWholeText());
|
||||||
|
} else {
|
||||||
|
try self.jw.objectField("nodeType");
|
||||||
|
try self.jw.write(9);
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.jw.objectField("children");
|
||||||
|
try self.jw.beginArray();
|
||||||
|
|
||||||
|
if (data.options != null) {
|
||||||
|
// Signal to not walk children, as we handled them natively
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn leave(self: *JsonVisitor) !void {
|
||||||
|
try self.jw.endArray();
|
||||||
|
try self.jw.endObject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn isStructuralRole(role: []const u8) bool {
|
||||||
|
const structural_roles = std.StaticStringMap(void).initComptime(.{
|
||||||
|
.{ "none", {} },
|
||||||
|
.{ "generic", {} },
|
||||||
|
.{ "InlineTextBox", {} },
|
||||||
|
.{ "banner", {} },
|
||||||
|
.{ "navigation", {} },
|
||||||
|
.{ "main", {} },
|
||||||
|
.{ "list", {} },
|
||||||
|
.{ "listitem", {} },
|
||||||
|
.{ "table", {} },
|
||||||
|
.{ "rowgroup", {} },
|
||||||
|
.{ "row", {} },
|
||||||
|
.{ "cell", {} },
|
||||||
|
.{ "region", {} },
|
||||||
|
});
|
||||||
|
return structural_roles.has(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextVisitor = struct {
|
||||||
|
writer: *std.Io.Writer,
|
||||||
|
tree: Self,
|
||||||
|
depth: usize,
|
||||||
|
|
||||||
|
pub fn visit(self: *TextVisitor, node: *Node, data: *NodeData) !bool {
|
||||||
|
// Format: " [12] link: Hacker News (value)"
|
||||||
|
for (0..(self.depth * 2)) |_| {
|
||||||
|
try self.writer.writeByte(' ');
|
||||||
|
}
|
||||||
|
try self.writer.print("[{d}] {s}: ", .{ data.id, data.role });
|
||||||
|
|
||||||
|
if (data.name) |n| {
|
||||||
|
if (n.len > 0) {
|
||||||
|
try self.writer.writeAll(n);
|
||||||
|
}
|
||||||
|
} else if (node.is(CData.Text)) |text_node| {
|
||||||
|
const trimmed = std.mem.trim(u8, text_node.getWholeText(), " \t\r\n");
|
||||||
|
if (trimmed.len > 0) {
|
||||||
|
try self.writer.writeAll(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.value) |v| {
|
||||||
|
if (v.len > 0) {
|
||||||
|
try self.writer.print(" (value: {s})", .{v});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.options) |options| {
|
||||||
|
try self.writer.writeAll(" options: [");
|
||||||
|
for (options, 0..) |opt, i| {
|
||||||
|
if (i > 0) try self.writer.writeAll(", ");
|
||||||
|
try self.writer.print("'{s}'", .{opt.value});
|
||||||
|
if (opt.selected) {
|
||||||
|
try self.writer.writeAll(" (selected)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try self.writer.writeAll("]\n");
|
||||||
|
self.depth += 1;
|
||||||
|
return false; // Native handling complete, do not walk children
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
self.depth += 1;
|
||||||
|
|
||||||
|
// If this is a leaf-like semantic node and we already have a name,
|
||||||
|
// skip children to avoid redundant StaticText or noise.
|
||||||
|
const is_leaf_semantic = std.mem.eql(u8, data.role, "link") or
|
||||||
|
std.mem.eql(u8, data.role, "button") or
|
||||||
|
std.mem.eql(u8, data.role, "heading") or
|
||||||
|
std.mem.eql(u8, data.role, "code");
|
||||||
|
if (is_leaf_semantic and data.name != null and data.name.?.len > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn leave(self: *TextVisitor) !void {
|
||||||
|
if (self.depth > 0) {
|
||||||
|
self.depth -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
1041
src/Server.zig
1041
src/Server.zig
File diff suppressed because it is too large
Load Diff
@@ -17,10 +17,11 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const URL = @import("browser/URL.zig");
|
||||||
|
|
||||||
const TestHTTPServer = @This();
|
const TestHTTPServer = @This();
|
||||||
|
|
||||||
shutdown: bool,
|
shutdown: std.atomic.Value(bool),
|
||||||
listener: ?std.net.Server,
|
listener: ?std.net.Server,
|
||||||
handler: Handler,
|
handler: Handler,
|
||||||
|
|
||||||
@@ -28,16 +29,23 @@ const Handler = *const fn (req: *std.http.Server.Request) anyerror!void;
|
|||||||
|
|
||||||
pub fn init(handler: Handler) TestHTTPServer {
|
pub fn init(handler: Handler) TestHTTPServer {
|
||||||
return .{
|
return .{
|
||||||
.shutdown = true,
|
.shutdown = .init(true),
|
||||||
.listener = null,
|
.listener = null,
|
||||||
.handler = handler,
|
.handler = handler,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *TestHTTPServer) void {
|
pub fn deinit(self: *TestHTTPServer) void {
|
||||||
self.shutdown = true;
|
self.listener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(self: *TestHTTPServer) void {
|
||||||
|
self.shutdown.store(true, .release);
|
||||||
if (self.listener) |*listener| {
|
if (self.listener) |*listener| {
|
||||||
listener.deinit();
|
switch (@import("builtin").target.os.tag) {
|
||||||
|
.linux => std.posix.shutdown(listener.stream.handle, .recv) catch {},
|
||||||
|
else => std.posix.close(listener.stream.handle),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,12 +54,13 @@ pub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void {
|
|||||||
|
|
||||||
self.listener = try address.listen(.{ .reuse_address = true });
|
self.listener = try address.listen(.{ .reuse_address = true });
|
||||||
var listener = &self.listener.?;
|
var listener = &self.listener.?;
|
||||||
|
self.shutdown.store(false, .release);
|
||||||
|
|
||||||
wg.finish();
|
wg.finish();
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const conn = listener.accept() catch |err| {
|
const conn = listener.accept() catch |err| {
|
||||||
if (self.shutdown) {
|
if (self.shutdown.load(.acquire) or err == error.SocketNotListening) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return err;
|
return err;
|
||||||
@@ -89,7 +98,10 @@ fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !voi
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {
|
pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {
|
||||||
var file = std.fs.cwd().openFile(file_path, .{}) catch |err| switch (err) {
|
var url_buf: [1024]u8 = undefined;
|
||||||
|
var fba = std.heap.FixedBufferAllocator.init(&url_buf);
|
||||||
|
const unescaped_file_path = try URL.unescape(fba.allocator(), file_path);
|
||||||
|
var file = std.fs.cwd().openFile(unescaped_file_path, .{}) catch |err| switch (err) {
|
||||||
error.FileNotFound => return req.respond("server error", .{ .status = .not_found }),
|
error.FileNotFound => return req.respond("server error", .{ .status = .not_found }),
|
||||||
else => return err,
|
else => return err,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,12 +24,14 @@ 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 HttpClient = @import("HttpClient.zig");
|
||||||
const Notification = @import("../Notification.zig");
|
|
||||||
|
const ArenaPool = App.ArenaPool;
|
||||||
|
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
const Session = @import("Session.zig");
|
const Session = @import("Session.zig");
|
||||||
|
const Notification = @import("../Notification.zig");
|
||||||
|
|
||||||
// Browser is an instance of the browser.
|
// Browser is an instance of the browser.
|
||||||
// You can create multiple browser instances.
|
// You can create multiple browser instances.
|
||||||
@@ -40,54 +42,40 @@ env: js.Env,
|
|||||||
app: *App,
|
app: *App,
|
||||||
session: ?Session,
|
session: ?Session,
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
|
arena_pool: *ArenaPool,
|
||||||
http_client: *HttpClient,
|
http_client: *HttpClient,
|
||||||
call_arena: ArenaAllocator,
|
|
||||||
page_arena: ArenaAllocator,
|
|
||||||
session_arena: ArenaAllocator,
|
|
||||||
transfer_arena: ArenaAllocator,
|
|
||||||
notification: *Notification,
|
|
||||||
|
|
||||||
pub fn init(app: *App) !Browser {
|
const InitOpts = struct {
|
||||||
|
env: js.Env.InitOpts = .{},
|
||||||
|
http_client: *HttpClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(app: *App, opts: InitOpts) !Browser {
|
||||||
const allocator = app.allocator;
|
const allocator = app.allocator;
|
||||||
|
|
||||||
var env = try js.Env.init(allocator, &app.platform, &app.snapshot);
|
var env = try js.Env.init(app, opts.env);
|
||||||
errdefer env.deinit();
|
errdefer env.deinit();
|
||||||
|
|
||||||
const notification = try Notification.init(allocator, app.notification);
|
|
||||||
app.http.client.notification = notification;
|
|
||||||
app.http.client.next_request_id = 0; // Should we track ids in CDP only?
|
|
||||||
errdefer notification.deinit();
|
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.app = app,
|
.app = app,
|
||||||
.env = env,
|
.env = env,
|
||||||
.session = null,
|
.session = null,
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.notification = notification,
|
.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();
|
|
||||||
self.http_client.notification = null;
|
|
||||||
self.notification.deinit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn newSession(self: *Browser) !*Session {
|
pub fn newSession(self: *Browser, notification: *Notification) !*Session {
|
||||||
self.closeSession();
|
self.closeSession();
|
||||||
self.session = @as(Session, undefined);
|
self.session = @as(Session, undefined);
|
||||||
const session = &self.session.?;
|
const session = &self.session.?;
|
||||||
try Session.init(session, self);
|
try Session.init(session, self, notification);
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,20 +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.lowMemoryNotification();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMicrotasks(self: *const Browser) void {
|
pub fn runMicrotasks(self: *Browser) void {
|
||||||
self.env.runMicrotasks();
|
self.env.runMicrotasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMessageLoop(self: *const Browser) void {
|
pub fn runMacrotasks(self: *Browser) !?u64 {
|
||||||
while (self.env.pumpMessageLoop()) {
|
const env = &self.env;
|
||||||
if (comptime IS_DEBUG) {
|
|
||||||
log.debug(.browser, "pumpMessageLoop", .{});
|
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 hasBackgroundTasks(self: *Browser) bool {
|
||||||
|
return self.env.hasBackgroundTasks();
|
||||||
}
|
}
|
||||||
|
pub fn waitForBackgroundTasks(self: *Browser) void {
|
||||||
|
self.env.waitForBackgroundTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runIdleTasks(self: *const Browser) void {
|
||||||
self.env.runIdleTasks();
|
self.env.runIdleTasks();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,30 +28,61 @@ 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;
|
||||||
|
|
||||||
const IS_DEBUG = builtin.mode == .Debug;
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
|
const EventKey = struct {
|
||||||
|
event_target: usize,
|
||||||
|
type_string: String,
|
||||||
|
};
|
||||||
|
|
||||||
|
const EventKeyContext = struct {
|
||||||
|
pub fn hash(_: @This(), key: EventKey) u64 {
|
||||||
|
var hasher = std.hash.Wyhash.init(0);
|
||||||
|
hasher.update(std.mem.asBytes(&key.event_target));
|
||||||
|
hasher.update(key.type_string.str());
|
||||||
|
return hasher.final();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eql(_: @This(), a: EventKey, b: EventKey) bool {
|
||||||
|
return a.event_target == b.event_target and a.type_string.eql(b.type_string);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub const EventManager = @This();
|
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.AutoHashMapUnmanaged(usize, *std.DoublyLinkedList),
|
lookup: std.HashMapUnmanaged(
|
||||||
|
EventKey,
|
||||||
|
*std.DoublyLinkedList,
|
||||||
|
EventKeyContext,
|
||||||
|
std.hash_map.default_max_load_percentage,
|
||||||
|
),
|
||||||
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +100,7 @@ pub const Callback = union(enum) {
|
|||||||
|
|
||||||
pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, opts: RegisterOptions) !void {
|
pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, opts: RegisterOptions) !void {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.event, "eventManager.register", .{ .type = typ, .capture = opts.capture, .once = opts.once, .target = target });
|
log.debug(.event, "eventManager.register", .{ .type = typ, .capture = opts.capture, .once = opts.once, .target = target.toString() });
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a signal is provided and already aborted, don't register the listener
|
// If a signal is provided and already aborted, don't register the listener
|
||||||
@@ -79,13 +110,22 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const gop = try self.lookup.getOrPut(self.arena, @intFromPtr(target));
|
// Allocate the type string we'll use in both listener and key
|
||||||
|
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, .{
|
||||||
|
.type_string = type_string,
|
||||||
|
.event_target = @intFromPtr(target),
|
||||||
|
});
|
||||||
if (gop.found_existing) {
|
if (gop.found_existing) {
|
||||||
// check for duplicate callbacks already registered
|
// check for duplicate callbacks already registered
|
||||||
var node = gop.value_ptr.*.first;
|
var node = gop.value_ptr.*.first;
|
||||||
while (node) |n| {
|
while (node) |n| {
|
||||||
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||||
if (listener.typ.eqlSlice(typ)) {
|
|
||||||
const is_duplicate = switch (callback) {
|
const is_duplicate = switch (callback) {
|
||||||
.object => |obj| listener.function.eqlObject(obj),
|
.object => |obj| listener.function.eqlObject(obj),
|
||||||
.function => |func| listener.function.eqlFunction(func),
|
.function => |func| listener.function.eqlFunction(func),
|
||||||
@@ -93,7 +133,6 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
|||||||
if (is_duplicate and listener.capture == opts.capture) {
|
if (is_duplicate and listener.capture == opts.capture) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
node = n.next;
|
node = n.next;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -102,8 +141,8 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
|||||||
}
|
}
|
||||||
|
|
||||||
const func = switch (callback) {
|
const func = switch (callback) {
|
||||||
.function => |f| Function{ .value = f },
|
.function => |f| Function{ .value = try f.persist() },
|
||||||
.object => |o| Function{ .object = o },
|
.object => |o| Function{ .object = try o.persist() },
|
||||||
};
|
};
|
||||||
|
|
||||||
const listener = try self.listener_pool.create();
|
const listener = try self.listener_pool.create();
|
||||||
@@ -114,48 +153,67 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
|||||||
.passive = opts.passive,
|
.passive = opts.passive,
|
||||||
.function = func,
|
.function = func,
|
||||||
.signal = opts.signal,
|
.signal = opts.signal,
|
||||||
.typ = try String.init(self.arena, typ, .{}),
|
.typ = type_string,
|
||||||
};
|
};
|
||||||
// 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 {
|
||||||
const list = self.lookup.get(@intFromPtr(target)) orelse return;
|
const list = self.lookup.get(.{
|
||||||
if (findListener(list, typ, callback, use_capture)) |listener| {
|
.type_string = .wrap(typ),
|
||||||
|
.event_target = @intFromPtr(target),
|
||||||
|
}) orelse return;
|
||||||
|
if (findListener(list, callback, use_capture)) |listener| {
|
||||||
self.removeListener(list, listener);
|
self.removeListener(list, listener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void {
|
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
|
||||||
|
// give it an explicit error set so that other parts of the code can use and
|
||||||
|
// inferred error.
|
||||||
|
const DispatchError = error{
|
||||||
|
OutOfMemory,
|
||||||
|
StringTooLarge,
|
||||||
|
JSExecCallback,
|
||||||
|
CompilationError,
|
||||||
|
ExecutionError,
|
||||||
|
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 {
|
||||||
|
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._session);
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
|
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
|
||||||
}
|
}
|
||||||
|
|
||||||
event._target = target;
|
|
||||||
event._dispatch_target = target; // Store original target for composedPath()
|
|
||||||
var was_handled = false;
|
|
||||||
|
|
||||||
defer if (was_handled) {
|
|
||||||
self.page.js.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,
|
|
||||||
.generic,
|
|
||||||
=> {
|
|
||||||
const list = self.lookup.get(@intFromPtr(target)) orelse return;
|
|
||||||
try self.dispatchAll(list, target, event, &was_handled);
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,13 +222,22 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void
|
|||||||
// 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._session);
|
||||||
|
|
||||||
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) {
|
||||||
@@ -179,11 +246,15 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
|
|||||||
}
|
}
|
||||||
|
|
||||||
var was_dispatched = false;
|
var was_dispatched = false;
|
||||||
defer if (was_dispatched) {
|
|
||||||
self.page.js.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;
|
||||||
@@ -193,110 +264,15 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = self.lookup.get(@intFromPtr(target)) orelse return;
|
// listeners reigstered via addEventListener
|
||||||
try self.dispatchAll(list, target, event, &was_dispatched);
|
const list = self.lookup.get(.{
|
||||||
}
|
.event_target = @intFromPtr(target),
|
||||||
|
.type_string = event._type_string,
|
||||||
|
}) orelse return;
|
||||||
|
|
||||||
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(@intFromPtr(current_target))) |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(@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(@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;
|
|
||||||
const typ = event._type_string;
|
|
||||||
|
|
||||||
// Track dispatch depth for deferred removal
|
// Track dispatch depth for deferred removal
|
||||||
self.dispatch_depth += 1;
|
self.dispatch_depth += 1;
|
||||||
@@ -330,16 +306,6 @@ 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 non-matching listeners
|
|
||||||
if (!listener.typ.eql(typ)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (comptime capture_only) |capture| {
|
|
||||||
if (listener.capture != capture) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip removed listeners
|
// Skip removed listeners
|
||||||
if (listener.removed) {
|
if (listener.removed) {
|
||||||
continue;
|
continue;
|
||||||
@@ -358,6 +324,304 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
self.removeListener(list, listener);
|
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))?"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if there are any listeners for a direct dispatch (non-DOM target).
|
||||||
|
/// Use this to avoid creating an event when there are no listeners.
|
||||||
|
pub fn hasDirectListeners(self: *EventManager, target: *EventTarget, typ: []const u8, handler: anytype) bool {
|
||||||
|
if (hasHandler(handler)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return self.lookup.get(.{
|
||||||
|
.event_target = @intFromPtr(target),
|
||||||
|
.type_string = .wrap(typ),
|
||||||
|
}) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hasHandler(handler: anytype) bool {
|
||||||
|
const ti = @typeInfo(@TypeOf(handler));
|
||||||
|
if (ti == .null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ti == .optional) {
|
||||||
|
return handler != null;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Create a single scope for all event handlers in this dispatch.
|
||||||
|
// This ensures function handles passed to queueMicrotask remain valid
|
||||||
|
// throughout the entire dispatch, preventing crashes when microtasks run.
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer {
|
||||||
|
if (was_handled) {
|
||||||
|
ls.local.runMicrotasks();
|
||||||
|
}
|
||||||
|
ls.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
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, &ls.local, 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;
|
||||||
|
|
||||||
|
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, &ls.local, 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, &ls.local, 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, local: *const js.Local, 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
|
||||||
|
if (comptime opts.capture_only) |capture| {
|
||||||
|
if (listener.capture != capture) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
if (listener.once) {
|
||||||
|
self.removeListener(list, listener);
|
||||||
|
}
|
||||||
|
|
||||||
was_handled.* = true;
|
was_handled.* = true;
|
||||||
event._current_target = current_target;
|
event._current_target = current_target;
|
||||||
|
|
||||||
@@ -368,12 +632,13 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (listener.function) {
|
switch (listener.function) {
|
||||||
.value => |value| try value.callWithThis(void, current_target, .{event}),
|
.value => |value| try local.toLocal(value).callWithThis(void, current_target, .{event}),
|
||||||
.string => |string| {
|
.string => |string| {
|
||||||
const str = try page.call_arena.dupeZ(u8, string.str());
|
const str = try page.call_arena.dupeZ(u8, string.str());
|
||||||
try self.page.js.eval(str, null);
|
try local.eval(str, null);
|
||||||
},
|
},
|
||||||
.object => |obj| {
|
.object => |obj_global| {
|
||||||
|
const obj = local.toLocal(obj_global);
|
||||||
if (try obj.getFunction("handleEvent")) |handleEvent| {
|
if (try obj.getFunction("handleEvent")) |handleEvent| {
|
||||||
try handleEvent.callWithThis(void, obj, .{event});
|
try handleEvent.callWithThis(void, obj, .{event});
|
||||||
}
|
}
|
||||||
@@ -391,9 +656,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 {
|
||||||
@@ -408,7 +684,7 @@ fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *L
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Callback, capture: bool) ?*Listener {
|
fn findListener(list: *const std.DoublyLinkedList, callback: Callback, capture: bool) ?*Listener {
|
||||||
var node = list.first;
|
var node = list.first;
|
||||||
while (node) |n| {
|
while (node) |n| {
|
||||||
node = n.next;
|
node = n.next;
|
||||||
@@ -423,9 +699,6 @@ fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Ca
|
|||||||
if (listener.capture != capture) {
|
if (listener.capture != capture) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!listener.typ.eqlSlice(typ)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return listener;
|
return listener;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -443,20 +716,20 @@ const Listener = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Function = union(enum) {
|
const Function = union(enum) {
|
||||||
value: js.Function,
|
value: js.Function.Global,
|
||||||
string: String,
|
string: String,
|
||||||
object: js.Object,
|
object: js.Object.Global,
|
||||||
|
|
||||||
fn eqlFunction(self: Function, func: js.Function) bool {
|
fn eqlFunction(self: Function, func: js.Function) bool {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.value => |v| return v.id == func.id,
|
.value => |v| v.isEqual(func),
|
||||||
else => false,
|
else => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eqlObject(self: Function, obj: js.Object) bool {
|
fn eqlObject(self: Function, obj: js.Object) bool {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.object => |o| return o.getId() == obj.getId(),
|
.object => |o| return o.isEqual(obj),
|
||||||
else => false,
|
else => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -514,3 +787,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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -17,10 +17,8 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const assert = std.debug.assert;
|
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const reflect = @import("reflect.zig");
|
const reflect = @import("reflect.zig");
|
||||||
const IS_DEBUG = builtin.mode == .Debug;
|
|
||||||
|
|
||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
const String = @import("../string.zig").String;
|
const String = @import("../string.zig").String;
|
||||||
@@ -31,6 +29,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 UIEvent = @import("webapi/event/UIEvent.zig");
|
const UIEvent = @import("webapi/event/UIEvent.zig");
|
||||||
|
const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
||||||
const Element = @import("webapi/Element.zig");
|
const Element = @import("webapi/Element.zig");
|
||||||
const Document = @import("webapi/Document.zig");
|
const Document = @import("webapi/Document.zig");
|
||||||
const EventTarget = @import("webapi/EventTarget.zig");
|
const EventTarget = @import("webapi/EventTarget.zig");
|
||||||
@@ -38,10 +37,99 @@ const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.
|
|||||||
const Blob = @import("webapi/Blob.zig");
|
const Blob = @import("webapi/Blob.zig");
|
||||||
const AbstractRange = @import("webapi/AbstractRange.zig");
|
const AbstractRange = @import("webapi/AbstractRange.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
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 {
|
||||||
|
return .{
|
||||||
|
._arena = arena,
|
||||||
|
._slab = SlabAllocator.init(arena, 128),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
@@ -145,83 +233,29 @@ 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn eventInit(typ: []const u8, value: anytype, page: *Page) !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,
|
||||||
._type = unionInit(Event.Type, value),
|
._type = unionInit(Event.Type, value),
|
||||||
._type_string = try String.init(page.arena, typ, .{}),
|
._type_string = typ,
|
||||||
._time_stamp = time_stamp,
|
._time_stamp = time_stamp,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is a root object
|
pub fn blob(_: *const Factory, arena: Allocator, child: anytype) !*@TypeOf(child) {
|
||||||
pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
|
|
||||||
const allocator = self._slab.allocator();
|
|
||||||
|
|
||||||
const chain = try PrototypeChain(
|
|
||||||
&.{ Event, @TypeOf(child) },
|
|
||||||
).allocate(allocator);
|
|
||||||
|
|
||||||
// Special case: Event has a _type_string field, so we need manual setup
|
|
||||||
const event_ptr = chain.get(0);
|
|
||||||
event_ptr.* = try eventInit(typ, chain.get(1), self._page);
|
|
||||||
chain.setLeaf(1, child);
|
|
||||||
|
|
||||||
return chain.get(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn uiEvent(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
|
|
||||||
const allocator = self._slab.allocator();
|
|
||||||
|
|
||||||
const chain = try PrototypeChain(
|
|
||||||
&.{ Event, UIEvent, @TypeOf(child) },
|
|
||||||
).allocate(allocator);
|
|
||||||
|
|
||||||
// Special case: Event has a _type_string field, so we need manual setup
|
|
||||||
const event_ptr = chain.get(0);
|
|
||||||
event_ptr.* = try eventInit(typ, chain.get(1), self._page);
|
|
||||||
chain.setMiddle(1, UIEvent.Type);
|
|
||||||
chain.setLeaf(2, child);
|
|
||||||
|
|
||||||
return chain.get(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) {
|
|
||||||
const allocator = self._slab.allocator();
|
|
||||||
|
|
||||||
// Special case: Blob has slice and mime fields, so we need manual setup
|
// Special case: Blob has slice and mime fields, so we need manual setup
|
||||||
const chain = try PrototypeChain(
|
const chain = try PrototypeChain(
|
||||||
&.{ Blob, @TypeOf(child) },
|
&.{ Blob, @TypeOf(child) },
|
||||||
).allocate(allocator);
|
).allocate(arena);
|
||||||
|
|
||||||
const blob_ptr = chain.get(0);
|
const blob_ptr = chain.get(0);
|
||||||
blob_ptr.* = .{
|
blob_ptr.* = .{
|
||||||
|
._arena = arena,
|
||||||
._type = unionInit(Blob.Type, chain.get(1)),
|
._type = unionInit(Blob.Type, chain.get(1)),
|
||||||
._slice = "",
|
._slice = "",
|
||||||
._mime = "",
|
._mime = "",
|
||||||
@@ -231,19 +265,23 @@ pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) {
|
|||||||
return chain.get(1);
|
return chain.get(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn abstractRange(self: *Factory, child: anytype, page: *Page) !*@TypeOf(child) {
|
pub fn abstractRange(_: *const Factory, arena: Allocator, child: anytype, page: *Page) !*@TypeOf(child) {
|
||||||
const allocator = self._slab.allocator();
|
const chain = try PrototypeChain(&.{ AbstractRange, @TypeOf(child) }).allocate(arena);
|
||||||
const chain = try PrototypeChain(&.{ AbstractRange, @TypeOf(child) }).allocate(allocator);
|
|
||||||
|
|
||||||
const doc = page.document.asNode();
|
const doc = page.document.asNode();
|
||||||
chain.set(0, AbstractRange{
|
const abstract_range = chain.get(0);
|
||||||
|
abstract_range.* = AbstractRange{
|
||||||
|
._rc = 0,
|
||||||
|
._arena = arena,
|
||||||
|
._page_id = page.id,
|
||||||
._type = unionInit(AbstractRange.Type, chain.get(1)),
|
._type = unionInit(AbstractRange.Type, chain.get(1)),
|
||||||
._end_offset = 0,
|
._end_offset = 0,
|
||||||
._start_offset = 0,
|
._start_offset = 0,
|
||||||
._end_container = doc,
|
._end_container = doc,
|
||||||
._start_container = doc,
|
._start_container = doc,
|
||||||
});
|
};
|
||||||
chain.setLeaf(1, child);
|
chain.setLeaf(1, child);
|
||||||
|
page._live_ranges.append(&abstract_range._range_link);
|
||||||
return chain.get(1);
|
return chain.get(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +344,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, .{
|
||||||
@@ -319,9 +357,7 @@ pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeO
|
|||||||
return chain.get(4);
|
return chain.get(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn xhrEventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
pub fn xhrEventTarget(_: *const Factory, allocator: Allocator, child: anytype) !*@TypeOf(child) {
|
||||||
const allocator = self._slab.allocator();
|
|
||||||
|
|
||||||
return try AutoPrototypeChain(
|
return try AutoPrototypeChain(
|
||||||
&.{ EventTarget, XMLHttpRequestEventTarget, @TypeOf(child) },
|
&.{ EventTarget, XMLHttpRequestEventTarget, @TypeOf(child) },
|
||||||
).create(allocator, child);
|
).create(allocator, child);
|
||||||
@@ -336,32 +372,6 @@ pub fn textTrackCue(self: *Factory, child: anytype) !*@TypeOf(child) {
|
|||||||
).create(allocator, child);
|
).create(allocator, child);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hasChainRoot(comptime T: type) bool {
|
|
||||||
// Check if this is a root
|
|
||||||
if (@hasDecl(T, "_prototype_root")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no _proto field, we're at the top but not a recognized root
|
|
||||||
if (!@hasField(T, "_proto")) return false;
|
|
||||||
|
|
||||||
// Get the _proto field's type and recurse
|
|
||||||
const fields = @typeInfo(T).@"struct".fields;
|
|
||||||
inline for (fields) |field| {
|
|
||||||
if (std.mem.eql(u8, field.name, "_proto")) {
|
|
||||||
const ProtoType = reflect.Struct(field.type);
|
|
||||||
return hasChainRoot(ProtoType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isChainType(comptime T: type) bool {
|
|
||||||
if (@hasField(T, "_proto")) return false;
|
|
||||||
return comptime hasChainRoot(T);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn destroy(self: *Factory, value: anytype) void {
|
pub fn destroy(self: *Factory, value: anytype) void {
|
||||||
const S = reflect.Struct(@TypeOf(value));
|
const S = reflect.Struct(@TypeOf(value));
|
||||||
|
|
||||||
@@ -378,35 +388,21 @@ pub fn destroy(self: *Factory, value: anytype) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comptime isChainType(S)) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn destroyStandalone(self: *Factory, value: anytype) void {
|
pub fn destroyStandalone(self: *Factory, value: anytype) void {
|
||||||
const S = reflect.Struct(@TypeOf(value));
|
|
||||||
assert(!@hasDecl(S, "_prototype_root"));
|
|
||||||
|
|
||||||
const allocator = self._slab.allocator();
|
const allocator = self._slab.allocator();
|
||||||
|
|
||||||
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"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allocator.destroy(value);
|
allocator.destroy(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -415,42 +411,20 @@ fn destroyChain(
|
|||||||
|
|
||||||
// aligns the old size to the alignment of this element
|
// aligns the old size to the alignment of this element
|
||||||
const current_size = std.mem.alignForward(usize, old_size, @alignOf(S));
|
const current_size = std.mem.alignForward(usize, old_size, @alignOf(S));
|
||||||
const alignment = std.mem.Alignment.fromByteUnits(@alignOf(S));
|
|
||||||
|
|
||||||
const new_align = std.mem.Alignment.max(old_align, alignment);
|
|
||||||
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));
|
||||||
// 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 if (@hasDecl(S, "JsApi")) {
|
|
||||||
// Doesn't have a _proto, but has a JsApi.
|
|
||||||
if (self._page.js.removeTaggedMapping(@intFromPtr(value))) |tagged| {
|
|
||||||
allocator.destroy(tagged);
|
|
||||||
}
|
|
||||||
} 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.
|
||||||
// and we have summed up the length.
|
// and we have summed up the length.
|
||||||
assert(@hasDecl(S, "_prototype_root"));
|
assert(@hasDecl(S, "_prototype_root"));
|
||||||
|
|
||||||
const memory_ptr: [*]const u8 = @ptrCast(value);
|
const memory_ptr: [*]u8 = @ptrCast(@constCast(value));
|
||||||
const len = std.mem.alignForward(usize, new_size, new_align.toByteUnits());
|
const len = std.mem.alignForward(usize, new_size, new_align.toByteUnits());
|
||||||
allocator.free(memory_ptr[0..len]);
|
allocator.rawFree(memory_ptr[0..len], new_align, @returnAddress());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1540
src/browser/HttpClient.zig
Normal file
1540
src/browser/HttpClient.zig
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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" {
|
||||||
|
|||||||
1952
src/browser/Page.zig
1952
src/browser/Page.zig
File diff suppressed because it is too large
Load Diff
@@ -17,20 +17,23 @@
|
|||||||
// 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 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 HttpClient = @import("HttpClient.zig");
|
||||||
|
const net_http = @import("../network/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");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArrayListUnmanaged = std.ArrayListUnmanaged;
|
const ArrayList = std.ArrayList;
|
||||||
|
|
||||||
const IS_DEBUG = builtin.mode == .Debug;
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
@@ -58,7 +61,7 @@ ready_scripts: std.DoublyLinkedList,
|
|||||||
|
|
||||||
shutdown: bool = false,
|
shutdown: bool = false,
|
||||||
|
|
||||||
client: *Http.Client,
|
client: *HttpClient,
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
buffer_pool: BufferPool,
|
buffer_pool: BufferPool,
|
||||||
|
|
||||||
@@ -82,10 +85,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: *HttpClient, page: *Page) ScriptManager {
|
||||||
return .{
|
return .{
|
||||||
.page = page,
|
.page = page,
|
||||||
.async_scripts = .{},
|
.async_scripts = .{},
|
||||||
@@ -95,9 +99,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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -137,6 +142,12 @@ fn clearList(list: *std.DoublyLinkedList) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getHeaders(self: *ScriptManager, url: [:0]const u8) !net_http.Headers {
|
||||||
|
var headers = try self.client.newHeaders();
|
||||||
|
try self.page.headersForRequest(self.page.arena, url, &headers);
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_element: *Element.Html.Script, comptime ctx: []const u8) !void {
|
pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_element: *Element.Html.Script, comptime ctx: []const u8) !void {
|
||||||
if (script_element._executed) {
|
if (script_element._executed) {
|
||||||
// If a script tag gets dynamically created and added to the dom:
|
// If a script tag gets dynamically created and added to the dom:
|
||||||
@@ -148,17 +159,16 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
// <script> has already been processed.
|
// <script> has already been processed.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
script_element._executed = true;
|
|
||||||
|
|
||||||
const element = script_element.asElement();
|
const element = script_element.asElement();
|
||||||
if (element.getAttributeSafe("nomodule") != null) {
|
if (element.getAttributeSafe(comptime .wrap("nomodule")) != null) {
|
||||||
// these scripts should only be loaded if we don't support modules
|
// these scripts should only be loaded if we don't support modules
|
||||||
// but since we do support modules, we can just skip them.
|
// but since we do support modules, we can just skip them.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const kind: Script.Kind = blk: {
|
const kind: Script.Kind = blk: {
|
||||||
const script_type = element.getAttributeSafe("type") orelse break :blk .javascript;
|
const script_type = element.getAttributeSafe(comptime .wrap("type")) orelse break :blk .javascript;
|
||||||
if (script_type.len == 0) {
|
if (script_type.len == 0) {
|
||||||
break :blk .javascript;
|
break :blk .javascript;
|
||||||
}
|
}
|
||||||
@@ -185,7 +195,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
var source: Script.Source = undefined;
|
var source: Script.Source = undefined;
|
||||||
var remote_url: ?[:0]const u8 = null;
|
var remote_url: ?[:0]const u8 = null;
|
||||||
const base_url = page.base();
|
const base_url = page.base();
|
||||||
if (element.getAttributeSafe("src")) |src| {
|
if (element.getAttributeSafe(comptime .wrap("src"))) |src| {
|
||||||
if (try parseDataURI(page.arena, src)) |data_uri| {
|
if (try parseDataURI(page.arena, src)) |data_uri| {
|
||||||
source = .{ .@"inline" = data_uri };
|
source = .{ .@"inline" = data_uri };
|
||||||
} else {
|
} else {
|
||||||
@@ -193,10 +203,22 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
source = .{ .remote = .{} };
|
source = .{ .remote = .{} };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const inline_source = try element.asNode().getTextContentAlloc(page.arena);
|
var buf = std.Io.Writer.Allocating.init(page.arena);
|
||||||
|
try element.asNode().getChildTextContent(&buf.writer);
|
||||||
|
try buf.writer.writeByte(0);
|
||||||
|
const data = buf.written();
|
||||||
|
const inline_source: [:0]const u8 = data[0 .. data.len - 1 :0];
|
||||||
|
if (inline_source.len == 0) {
|
||||||
|
// we haven't set script_element._executed = true yet, which is good.
|
||||||
|
// If content is appended to the script, we will execute it then.
|
||||||
|
return;
|
||||||
|
}
|
||||||
source = .{ .@"inline" = inline_source };
|
source = .{ .@"inline" = inline_source };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only set _executed (already-started) when we actually have content to execute
|
||||||
|
script_element._executed = true;
|
||||||
|
|
||||||
const script = try self.script_pool.create();
|
const script = try self.script_pool.create();
|
||||||
errdefer self.script_pool.destroy(script);
|
errdefer self.script_pool.destroy(script);
|
||||||
|
|
||||||
@@ -216,12 +238,12 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
break :blk if (kind == .module) .@"defer" else .normal;
|
break :blk if (kind == .module) .@"defer" else .normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element.getAttributeSafe("async") != null) {
|
if (element.getAttributeSafe(comptime .wrap("async")) != null) {
|
||||||
break :blk .async;
|
break :blk .async;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for defer or module (before checking dynamic script default)
|
// Check for defer or module (before checking dynamic script default)
|
||||||
if (kind == .module or element.getAttributeSafe("defer") != null) {
|
if (kind == .module or element.getAttributeSafe(comptime .wrap("defer")) != null) {
|
||||||
break :blk .@"defer";
|
break :blk .@"defer";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,17 +273,16 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
script.deinit(true);
|
script.deinit(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
var headers = try self.client.newHeaders();
|
|
||||||
try page.requestCookie(.{}).headersForRequest(page.arena, url, &headers);
|
|
||||||
|
|
||||||
try self.client.request(.{
|
try self.client.request(.{
|
||||||
.url = url,
|
.url = url,
|
||||||
.ctx = script,
|
.ctx = script,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.headers = headers,
|
.frame_id = page._frame_id,
|
||||||
|
.headers = try self.getHeaders(url),
|
||||||
.blocking = is_blocking,
|
.blocking = is_blocking,
|
||||||
.cookie_jar = &page._session.cookie_jar,
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
|
.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,
|
||||||
@@ -270,11 +291,15 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
log.debug(.http, "script queue", .{
|
log.debug(.http, "script queue", .{
|
||||||
.ctx = ctx,
|
.ctx = ctx,
|
||||||
.url = remote_url.?,
|
.url = remote_url.?,
|
||||||
.element = element,
|
.element = element,
|
||||||
.stack = page.js.stackTrace() catch "???",
|
.stack = ls.local.stackTrace() catch "???",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,15 +377,18 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
.manager = self,
|
.manager = self,
|
||||||
};
|
};
|
||||||
|
|
||||||
var headers = try self.client.newHeaders();
|
const page = self.page;
|
||||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
log.debug(.http, "script queue", .{
|
log.debug(.http, "script queue", .{
|
||||||
.url = url,
|
.url = url,
|
||||||
.ctx = "module",
|
.ctx = "module",
|
||||||
.referrer = referrer,
|
.referrer = referrer,
|
||||||
.stack = self.page.js.stackTrace() catch "???",
|
.stack = ls.local.stackTrace() catch "???",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,9 +396,11 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
.url = url,
|
.url = url,
|
||||||
.ctx = script,
|
.ctx = script,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.headers = headers,
|
.frame_id = page._frame_id,
|
||||||
.cookie_jar = &self.page._session.cookie_jar,
|
.headers = try self.getHeaders(url),
|
||||||
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
|
.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,
|
||||||
@@ -443,15 +473,17 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
|||||||
} },
|
} },
|
||||||
};
|
};
|
||||||
|
|
||||||
var headers = try self.client.newHeaders();
|
const page = self.page;
|
||||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
log.debug(.http, "script queue", .{
|
log.debug(.http, "script queue", .{
|
||||||
.url = url,
|
.url = url,
|
||||||
.ctx = "dynamic module",
|
.ctx = "dynamic module",
|
||||||
.referrer = referrer,
|
.referrer = referrer,
|
||||||
.stack = self.page.js.stackTrace() catch "???",
|
.stack = ls.local.stackTrace() catch "???",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,10 +499,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,
|
||||||
.headers = headers,
|
.frame_id = page._frame_id,
|
||||||
|
.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 = 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,
|
||||||
@@ -484,7 +518,7 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
|||||||
// Called from the Page to let us know it's done parsing the HTML. Necessary that
|
// Called from the Page to let us know it's done parsing the HTML. Necessary that
|
||||||
// we know this so that we know that we can start evaluating deferred scripts.
|
// we know this so that we know that we can start evaluating deferred scripts.
|
||||||
pub fn staticScriptsDone(self: *ScriptManager) void {
|
pub fn staticScriptsDone(self: *ScriptManager) void {
|
||||||
std.debug.assert(self.static_scripts_done == false);
|
lp.assert(self.static_scripts_done == false, "ScriptManager.staticScriptsDone", .{});
|
||||||
self.static_scripts_done = true;
|
self.static_scripts_done = true;
|
||||||
self.evaluate();
|
self.evaluate();
|
||||||
}
|
}
|
||||||
@@ -554,19 +588,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();
|
||||||
|
|
||||||
@@ -608,6 +635,20 @@ 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,
|
||||||
|
debug_transfer_auth_challenge: bool = false,
|
||||||
|
debug_transfer_easy_id: usize = 0,
|
||||||
|
|
||||||
const Kind = enum {
|
const Kind = enum {
|
||||||
module,
|
module,
|
||||||
javascript,
|
javascript,
|
||||||
@@ -621,7 +662,7 @@ pub const Script = struct {
|
|||||||
|
|
||||||
const Source = union(enum) {
|
const Source = union(enum) {
|
||||||
@"inline": []const u8,
|
@"inline": []const u8,
|
||||||
remote: std.ArrayListUnmanaged(u8),
|
remote: std.ArrayList(u8),
|
||||||
|
|
||||||
fn content(self: Source) []const u8 {
|
fn content(self: Source) []const u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
@@ -646,11 +687,11 @@ pub const Script = struct {
|
|||||||
self.manager.script_pool.destroy(self);
|
self.manager.script_pool.destroy(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn startCallback(transfer: *Http.Transfer) !void {
|
fn startCallback(transfer: *HttpClient.Transfer) !void {
|
||||||
log.debug(.http, "script fetch start", .{ .req = transfer });
|
log.debug(.http, "script fetch start", .{ .req = transfer });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn headerCallback(transfer: *Http.Transfer) !void {
|
fn headerCallback(transfer: *HttpClient.Transfer) !bool {
|
||||||
const self: *Script = @ptrCast(@alignCast(transfer.ctx));
|
const self: *Script = @ptrCast(@alignCast(transfer.ctx));
|
||||||
const header = &transfer.response_header.?;
|
const header = &transfer.response_header.?;
|
||||||
self.status = header.status;
|
self.status = header.status;
|
||||||
@@ -660,7 +701,7 @@ pub const Script = struct {
|
|||||||
.status = header.status,
|
.status = header.status,
|
||||||
.content_type = header.contentType(),
|
.content_type = header.contentType(),
|
||||||
});
|
});
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
@@ -671,26 +712,60 @@ pub const Script = struct {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this isn't true, then we'll likely leak memory. If you don't
|
{
|
||||||
// set `CURLOPT_SUPPRESS_CONNECT_HEADERS` and CONNECT to a proxy, this
|
// temp debug, trying to figure out why the next assert sometimes
|
||||||
// will fail. This assertion exists to catch incorrect assumptions about
|
// fails. Is the buffer just corrupt or is headerCallback really
|
||||||
// how libcurl works, or about how we've configured it.
|
// being called twice?
|
||||||
std.debug.assert(self.source.remote.capacity == 0);
|
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,
|
||||||
|
.a8 = self.debug_transfer_auth_challenge,
|
||||||
|
.a9 = self.debug_transfer_easy_id,
|
||||||
|
.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),
|
||||||
|
.b8 = transfer._auth_challenge != null,
|
||||||
|
.b9 = if (transfer._conn) |c| @intFromPtr(c.easy) else 0,
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
self.debug_transfer_auth_challenge = transfer._auth_challenge != null;
|
||||||
|
self.debug_transfer_easy_id = if (transfer._conn) |c| @intFromPtr(c.easy) else 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity });
|
||||||
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);
|
||||||
}
|
}
|
||||||
self.source = .{ .remote = buffer };
|
self.source = .{ .remote = buffer };
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
fn dataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
|
||||||
const self: *Script = @ptrCast(@alignCast(transfer.ctx));
|
const self: *Script = @ptrCast(@alignCast(transfer.ctx));
|
||||||
self._dataCallback(transfer, data) catch |err| {
|
self._dataCallback(transfer, data) catch |err| {
|
||||||
log.err(.http, "SM.dataCallback", .{ .err = err, .transfer = transfer, .len = data.len });
|
log.err(.http, "SM.dataCallback", .{ .err = err, .transfer = transfer, .len = data.len });
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
fn _dataCallback(self: *Script, _: *Http.Transfer, data: []const u8) !void {
|
fn _dataCallback(self: *Script, _: *HttpClient.Transfer, data: []const u8) !void {
|
||||||
try self.source.remote.appendSlice(self.manager.allocator, data);
|
try self.source.remote.appendSlice(self.manager.allocator, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -720,7 +795,7 @@ pub const Script = struct {
|
|||||||
log.warn(.http, "script fetch error", .{
|
log.warn(.http, "script fetch error", .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.req = self.url,
|
.req = self.url,
|
||||||
.mode = self.mode,
|
.mode = std.meta.activeTag(self.mode),
|
||||||
.kind = self.kind,
|
.kind = self.kind,
|
||||||
.status = self.status,
|
.status = self.status,
|
||||||
});
|
});
|
||||||
@@ -740,9 +815,13 @@ pub const Script = struct {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.mode == .import) {
|
switch (self.mode) {
|
||||||
const entry = self.manager.imported_modules.getPtr(self.url).?;
|
.import_async => |ia| ia.callback(ia.data, error.FailedToLoad),
|
||||||
|
.import => {
|
||||||
|
const entry = manager.imported_modules.getPtr(self.url).?;
|
||||||
entry.state = .err;
|
entry.state = .err;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
}
|
}
|
||||||
self.deinit(true);
|
self.deinit(true);
|
||||||
manager.evaluate();
|
manager.evaluate();
|
||||||
@@ -750,10 +829,12 @@ pub const Script = struct {
|
|||||||
|
|
||||||
fn eval(self: *Script, page: *Page) void {
|
fn eval(self: *Script, page: *Page) void {
|
||||||
// never evaluated, source is passed back to v8, via callbacks.
|
// never evaluated, source is passed back to v8, via callbacks.
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
std.debug.assert(self.mode != .import_async);
|
std.debug.assert(self.mode != .import_async);
|
||||||
|
|
||||||
// never evaluated, source is passed back to v8 when asked for it.
|
// never evaluated, source is passed back to v8 when asked for it.
|
||||||
std.debug.assert(self.mode != .import);
|
std.debug.assert(self.mode != .import);
|
||||||
|
}
|
||||||
|
|
||||||
if (page.isGoingAway()) {
|
if (page.isGoingAway()) {
|
||||||
// don't evaluate scripts for a dying page.
|
// don't evaluate scripts for a dying page.
|
||||||
@@ -782,6 +863,12 @@ pub const Script = struct {
|
|||||||
.cacheable = cacheable,
|
.cacheable = cacheable,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
|
const local = &ls.local;
|
||||||
|
|
||||||
// Handle importmap special case here: the content is a JSON containing
|
// Handle importmap special case here: the content is a JSON containing
|
||||||
// imports.
|
// imports.
|
||||||
if (self.kind == .importmap) {
|
if (self.kind == .importmap) {
|
||||||
@@ -792,25 +879,26 @@ pub const Script = struct {
|
|||||||
.kind = self.kind,
|
.kind = self.kind,
|
||||||
.cacheable = cacheable,
|
.cacheable = cacheable,
|
||||||
});
|
});
|
||||||
self.executeCallback("error", script_element._on_error, page);
|
self.executeCallback(comptime .wrap("error"), page);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.executeCallback("load", script_element._on_load, page);
|
self.executeCallback(comptime .wrap("load"), page);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const js_context = page.js;
|
defer page._event_manager.clearIgnoreList();
|
||||||
|
|
||||||
var try_catch: js.TryCatch = undefined;
|
var try_catch: js.TryCatch = undefined;
|
||||||
try_catch.init(js_context);
|
try_catch.init(local);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
const success = blk: {
|
const success = blk: {
|
||||||
const content = self.source.content();
|
const content = self.source.content();
|
||||||
switch (self.kind) {
|
switch (self.kind) {
|
||||||
.javascript => _ = js_context.eval(content, url) catch break :blk false,
|
.javascript => _ = local.eval(content, url) catch break :blk false,
|
||||||
.module => {
|
.module => {
|
||||||
// We don't care about waiting for the evaluation here.
|
// We don't care about waiting for the evaluation here.
|
||||||
js_context.module(false, content, url, cacheable) catch break :blk false;
|
page.js.module(false, local, content, url, cacheable) catch break :blk false;
|
||||||
},
|
},
|
||||||
.importmap => unreachable, // handled before the try/catch.
|
.importmap => unreachable, // handled before the try/catch.
|
||||||
}
|
}
|
||||||
@@ -818,37 +906,32 @@ 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
|
||||||
page.js.runMicrotasks();
|
_ = page.js.scheduler.run() catch |err| {
|
||||||
_ = page.scheduler.run() catch |err| {
|
|
||||||
log.err(.page, "scheduler", .{ .err = err });
|
log.err(.page, "scheduler", .{ .err = err });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
self.executeCallback("load", script_element._on_load, page);
|
self.executeCallback(comptime .wrap("load"), page);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const msg = try_catch.err(page.arena) catch |err| @errorName(err) orelse "unknown";
|
const caught = try_catch.caughtOrError(page.call_arena, error.Unknown);
|
||||||
log.warn(.js, "eval script", .{
|
log.warn(.js, "eval script", .{
|
||||||
.url = url,
|
.url = url,
|
||||||
.err = msg,
|
.caught = caught,
|
||||||
.stack = try_catch.stack(page.call_arena) catch null,
|
|
||||||
.line = try_catch.sourceLineNumber() orelse 0,
|
|
||||||
.cacheable = cacheable,
|
.cacheable = cacheable,
|
||||||
});
|
});
|
||||||
|
|
||||||
self.executeCallback("error", 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(typ, .{}, page) catch |err| {
|
const event = Event.initTrusted(typ, .{}, page) catch |err| {
|
||||||
log.warn(.js, "script internal callback", .{
|
log.warn(.js, "script internal callback", .{
|
||||||
@@ -858,14 +941,11 @@ pub const Script = struct {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
page._event_manager.dispatchOpts(self.script_element.?.asNode().asEventTarget(), event, .{ .apply_ignore = true }) catch |err| {
|
||||||
var result: js.Function.Result = undefined;
|
|
||||||
cb.tryCall(void, .{event}, &result) catch {
|
|
||||||
log.warn(.js, "script callback", .{
|
log.warn(.js, "script callback", .{
|
||||||
.url = self.url,
|
.url = self.url,
|
||||||
.type = typ,
|
.type = typ,
|
||||||
.err = result.exception,
|
.err = err,
|
||||||
.stack = result.stack,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -878,11 +958,11 @@ 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,
|
||||||
buf: std.ArrayListUnmanaged(u8),
|
buf: std.ArrayList(u8),
|
||||||
};
|
};
|
||||||
|
|
||||||
fn init(allocator: Allocator, max_concurrent_transfers: u8) BufferPool {
|
fn init(allocator: Allocator, max_concurrent_transfers: u8) BufferPool {
|
||||||
@@ -907,7 +987,7 @@ const BufferPool = struct {
|
|||||||
self.mem_pool.deinit();
|
self.mem_pool.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(self: *BufferPool) std.ArrayListUnmanaged(u8) {
|
fn get(self: *BufferPool) std.ArrayList(u8) {
|
||||||
const node = self.available.popFirst() orelse {
|
const node = self.available.popFirst() orelse {
|
||||||
// return a new buffer
|
// return a new buffer
|
||||||
return .{};
|
return .{};
|
||||||
@@ -919,7 +999,7 @@ const BufferPool = struct {
|
|||||||
return container.buf;
|
return container.buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn release(self: *BufferPool, buffer: ArrayListUnmanaged(u8)) void {
|
fn release(self: *BufferPool, buffer: ArrayList(u8)) void {
|
||||||
// create mutable copy
|
// create mutable copy
|
||||||
var b = buffer;
|
var b = buffer;
|
||||||
|
|
||||||
@@ -937,7 +1017,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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -985,23 +1065,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");
|
||||||
|
|||||||
@@ -17,8 +17,11 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const lp = @import("lightpanda");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
|
const App = @import("../App.zig");
|
||||||
|
|
||||||
const js = @import("js/js.zig");
|
const js = @import("js/js.zig");
|
||||||
const storage = @import("webapi/storage/storage.zig");
|
const storage = @import("webapi/storage/storage.zig");
|
||||||
@@ -27,56 +30,90 @@ const History = @import("webapi/History.zig");
|
|||||||
|
|
||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
const Browser = @import("Browser.zig");
|
const Browser = @import("Browser.zig");
|
||||||
|
const Factory = @import("Factory.zig");
|
||||||
|
const Notification = @import("../Notification.zig");
|
||||||
|
const QueuedNavigation = Page.QueuedNavigation;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const ArenaPool = App.ArenaPool;
|
||||||
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
// Session is like a browser's tab.
|
|
||||||
// It owns the js env and the loader for all the pages of the session.
|
|
||||||
// You can create successively multiple pages for a session, but you must
|
// You can create successively multiple pages for a session, but you must
|
||||||
// deinit a page before running another one.
|
// deinit a page before running another one. It manages two distinct lifetimes.
|
||||||
|
//
|
||||||
|
// The first is the lifetime of the Session itself, where pages are created and
|
||||||
|
// removed, but share the same cookie jar and navigation history (etc...)
|
||||||
|
//
|
||||||
|
// The second is as a container the data needed by the full page hierarchy, i.e. \
|
||||||
|
// the root page and all of its frames (and all of their frames.)
|
||||||
const Session = @This();
|
const Session = @This();
|
||||||
|
|
||||||
|
// These are the fields that remain intact for the duration of the Session
|
||||||
browser: *Browser,
|
browser: *Browser,
|
||||||
|
|
||||||
// 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,
|
|
||||||
|
|
||||||
executor: js.ExecutionWorld,
|
|
||||||
cookie_jar: storage.Cookie.Jar,
|
|
||||||
storage_shed: storage.Shed,
|
|
||||||
|
|
||||||
history: History,
|
history: History,
|
||||||
navigation: Navigation,
|
navigation: Navigation,
|
||||||
|
storage_shed: storage.Shed,
|
||||||
|
notification: *Notification,
|
||||||
|
cookie_jar: storage.Cookie.Jar,
|
||||||
|
|
||||||
page: ?*Page = null,
|
// These are the fields that get reset whenever the Session's page (the root) is reset.
|
||||||
|
factory: Factory,
|
||||||
|
|
||||||
pub fn init(self: *Session, browser: *Browser) !void {
|
page_arena: Allocator,
|
||||||
var executor = try browser.env.newExecutionWorld();
|
|
||||||
errdefer executor.deinit();
|
|
||||||
|
|
||||||
|
// Origin map for same-origin context sharing. Scoped to the root page lifetime.
|
||||||
|
origins: std.StringHashMapUnmanaged(*js.Origin) = .empty,
|
||||||
|
|
||||||
|
// Shared resources for all pages in this session.
|
||||||
|
// These live for the duration of the page tree (root + frames).
|
||||||
|
arena_pool: *ArenaPool,
|
||||||
|
|
||||||
|
// In Debug, we use this to see if anything fails to release an arena back to
|
||||||
|
// the pool.
|
||||||
|
_arena_pool_leak_track: if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, struct {
|
||||||
|
owner: []const u8,
|
||||||
|
count: usize,
|
||||||
|
}) else void = if (IS_DEBUG) .empty else {},
|
||||||
|
|
||||||
|
page: ?Page,
|
||||||
|
|
||||||
|
queued_navigation: std.ArrayList(*Page),
|
||||||
|
// Temporary buffer for about:blank navigations during processing.
|
||||||
|
// We process async navigations first (safe from re-entrance), then sync
|
||||||
|
// about:blank navigations (which may add to queued_navigation).
|
||||||
|
queued_queued_navigation: std.ArrayList(*Page),
|
||||||
|
|
||||||
|
page_id_gen: u32,
|
||||||
|
frame_id_gen: u32,
|
||||||
|
|
||||||
|
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_pool = browser.arena_pool;
|
||||||
|
|
||||||
|
const arena = try arena_pool.acquire();
|
||||||
|
errdefer arena_pool.release(arena);
|
||||||
|
|
||||||
|
const page_arena = try arena_pool.acquire();
|
||||||
|
errdefer arena_pool.release(page_arena);
|
||||||
|
|
||||||
self.* = .{
|
self.* = .{
|
||||||
.browser = browser,
|
.page = null,
|
||||||
.executor = executor,
|
.arena = arena,
|
||||||
.storage_shed = .{},
|
.arena_pool = arena_pool,
|
||||||
.arena = session_allocator,
|
.page_arena = page_arena,
|
||||||
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
.factory = Factory.init(page_arena),
|
||||||
.navigation = .{},
|
|
||||||
.history = .{},
|
.history = .{},
|
||||||
.transfer_arena = browser.transfer_arena.allocator(),
|
.page_id_gen = 0,
|
||||||
|
.frame_id_gen = 0,
|
||||||
|
// The prototype (EventTarget) for Navigation is created when a Page is created.
|
||||||
|
.navigation = .{ ._proto = undefined },
|
||||||
|
.storage_shed = .{},
|
||||||
|
.browser = browser,
|
||||||
|
.queued_navigation = .{},
|
||||||
|
.queued_queued_navigation = .{},
|
||||||
|
.notification = notification,
|
||||||
|
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,20 +122,20 @@ pub fn deinit(self: *Session) void {
|
|||||||
self.removePage();
|
self.removePage();
|
||||||
}
|
}
|
||||||
self.cookie_jar.deinit();
|
self.cookie_jar.deinit();
|
||||||
|
|
||||||
self.storage_shed.deinit(self.browser.app.allocator);
|
self.storage_shed.deinit(self.browser.app.allocator);
|
||||||
self.executor.deinit();
|
self.arena_pool.release(self.page_arena);
|
||||||
|
self.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,
|
||||||
// the pointer on Page is just returned as a convenience
|
// the pointer on Page is just returned as a convenience
|
||||||
pub fn createPage(self: *Session) !*Page {
|
pub fn createPage(self: *Session) !*Page {
|
||||||
std.debug.assert(self.page == null);
|
lp.assert(self.page == null, "Session.createPage - page not null", .{});
|
||||||
|
|
||||||
const page_arena = &self.browser.page_arena;
|
self.page = @as(Page, undefined);
|
||||||
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
const page = &self.page.?;
|
||||||
|
try Page.init(page, self.nextFrameId(), self, null);
|
||||||
self.page = try Page.init(page_arena.allocator(), self.browser.call_arena.allocator(), self);
|
|
||||||
const page = self.page.?;
|
|
||||||
|
|
||||||
// Creates a new NavigationEventTarget for this page.
|
// Creates a new NavigationEventTarget for this page.
|
||||||
try self.navigation.onNewPage(page);
|
try self.navigation.onNewPage(page);
|
||||||
@@ -108,69 +145,517 @@ pub fn createPage(self: *Session) !*Page {
|
|||||||
}
|
}
|
||||||
// start JS env
|
// start JS env
|
||||||
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
||||||
self.browser.notification.dispatch(.page_created, page);
|
self.notification.dispatch(.page_created, page);
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn removePage(self: *Session) void {
|
pub fn removePage(self: *Session) void {
|
||||||
// Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one
|
// Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one
|
||||||
self.browser.notification.dispatch(.page_remove, .{});
|
self.notification.dispatch(.page_remove, .{});
|
||||||
|
lp.assert(self.page != null, "Session.removePage - page is null", .{});
|
||||||
|
|
||||||
std.debug.assert(self.page != null);
|
self.page.?.deinit(false);
|
||||||
|
|
||||||
self.page.?.deinit();
|
|
||||||
self.page = null;
|
self.page = null;
|
||||||
|
|
||||||
self.navigation.onRemovePage();
|
self.navigation.onRemovePage();
|
||||||
|
self.resetPageResources();
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.browser, "remove page", .{});
|
log.debug(.browser, "remove page", .{});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const GetArenaOpts = struct {
|
||||||
|
debug: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn getArena(self: *Session, opts: GetArenaOpts) !Allocator {
|
||||||
|
const allocator = try self.arena_pool.acquire();
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
// Use session's arena (not page_arena) since page_arena gets reset between pages
|
||||||
|
const gop = try self._arena_pool_leak_track.getOrPut(self.arena, @intFromPtr(allocator.ptr));
|
||||||
|
if (gop.found_existing and gop.value_ptr.count != 0) {
|
||||||
|
log.err(.bug, "ArenaPool Double Use", .{ .owner = gop.value_ptr.*.owner });
|
||||||
|
@panic("ArenaPool Double Use");
|
||||||
|
}
|
||||||
|
gop.value_ptr.* = .{ .owner = opts.debug, .count = 1 };
|
||||||
|
}
|
||||||
|
return allocator;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn releaseArena(self: *Session, allocator: Allocator) void {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?;
|
||||||
|
if (found.count != 1) {
|
||||||
|
log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count });
|
||||||
|
if (comptime builtin.is_test) {
|
||||||
|
@panic("ArenaPool Double Free");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
found.count = 0;
|
||||||
|
}
|
||||||
|
return self.arena_pool.release(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getOrCreateOrigin(self: *Session, key_: ?[]const u8) !*js.Origin {
|
||||||
|
const key = key_ orelse {
|
||||||
|
var opaque_origin: [36]u8 = undefined;
|
||||||
|
@import("../id.zig").uuidv4(&opaque_origin);
|
||||||
|
// Origin.init will dupe opaque_origin. It's fine that this doesn't
|
||||||
|
// get added to self.origins. In fact, it further isolates it. When the
|
||||||
|
// context is freed, it'll call session.releaseOrigin which will free it.
|
||||||
|
return js.Origin.init(self.browser.app, self.browser.env.isolate, &opaque_origin);
|
||||||
|
};
|
||||||
|
|
||||||
|
const gop = try self.origins.getOrPut(self.arena, key);
|
||||||
|
if (gop.found_existing) {
|
||||||
|
const origin = gop.value_ptr.*;
|
||||||
|
origin.rc += 1;
|
||||||
|
return origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
errdefer _ = self.origins.remove(key);
|
||||||
|
|
||||||
|
const origin = try js.Origin.init(self.browser.app, self.browser.env.isolate, key);
|
||||||
|
gop.key_ptr.* = origin.key;
|
||||||
|
gop.value_ptr.* = origin;
|
||||||
|
return origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn releaseOrigin(self: *Session, origin: *js.Origin) void {
|
||||||
|
const rc = origin.rc;
|
||||||
|
if (rc == 1) {
|
||||||
|
_ = self.origins.remove(origin.key);
|
||||||
|
origin.deinit(self.browser.app);
|
||||||
|
} else {
|
||||||
|
origin.rc = rc - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset page_arena and factory for a clean slate.
|
||||||
|
/// Called when root page is removed.
|
||||||
|
fn resetPageResources(self: *Session) void {
|
||||||
|
// Check for arena leaks before releasing
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
var it = self._arena_pool_leak_track.valueIterator();
|
||||||
|
while (it.next()) |value_ptr| {
|
||||||
|
if (value_ptr.count > 0) {
|
||||||
|
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self._arena_pool_leak_track.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
// All origins should have been released when contexts were destroyed
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(self.origins.count() == 0);
|
||||||
|
}
|
||||||
|
// Defensive cleanup in case origins leaked
|
||||||
|
{
|
||||||
|
const app = self.browser.app;
|
||||||
|
var it = self.origins.valueIterator();
|
||||||
|
while (it.next()) |value| {
|
||||||
|
value.*.deinit(app);
|
||||||
|
}
|
||||||
|
self.origins.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release old page_arena and acquire fresh one
|
||||||
|
self.frame_id_gen = 0;
|
||||||
|
self.arena_pool.reset(self.page_arena, 64 * 1024);
|
||||||
|
self.factory = Factory.init(self.page_arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replacePage(self: *Session) !*Page {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
log.debug(.browser, "replace page", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.assert(self.page != null, "Session.replacePage null page", .{});
|
||||||
|
lp.assert(self.page.?.parent == null, "Session.replacePage with parent", .{});
|
||||||
|
|
||||||
|
var current = self.page.?;
|
||||||
|
const frame_id = current._frame_id;
|
||||||
|
current.deinit(true);
|
||||||
|
|
||||||
|
self.resetPageResources();
|
||||||
|
self.browser.env.memoryPressureNotification(.moderate);
|
||||||
|
|
||||||
|
self.page = @as(Page, undefined);
|
||||||
|
const page = &self.page.?;
|
||||||
|
try Page.init(page, frame_id, self, null);
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const WaitResult = enum {
|
pub const WaitResult = enum {
|
||||||
done,
|
done,
|
||||||
no_page,
|
no_page,
|
||||||
cdp_socket,
|
cdp_socket,
|
||||||
navigate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub fn findPageByFrameId(self: *Session, frame_id: u32) ?*Page {
|
||||||
|
const page = self.currentPage() orelse return null;
|
||||||
|
return findPageBy(page, "_frame_id", frame_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn findPageById(self: *Session, id: u32) ?*Page {
|
||||||
|
const page = self.currentPage() orelse return null;
|
||||||
|
return findPageBy(page, "id", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findPageBy(page: *Page, comptime field: []const u8, id: u32) ?*Page {
|
||||||
|
if (@field(page, field) == id) return page;
|
||||||
|
for (page.frames.items) |f| {
|
||||||
|
if (findPageBy(f, field, id)) |found| {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 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) {
|
||||||
const page = self.page orelse return .no_page;
|
const wait_result = self._wait(page, wait_ms) catch |err| {
|
||||||
switch (page.wait(wait_ms)) {
|
switch (err) {
|
||||||
.navigate => self.processScheduledNavigation() catch return .done,
|
error.JsError => {}, // already logged (with hopefully more context)
|
||||||
|
else => log.err(.browser, "session wait", .{
|
||||||
|
.err = err,
|
||||||
|
.url = page.url,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
return .done;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (wait_result) {
|
||||||
|
.done => {
|
||||||
|
if (self.queued_navigation.items.len == 0) {
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
self.processQueuedNavigation() catch return .done;
|
||||||
|
page = &self.page.?; // might have changed
|
||||||
|
},
|
||||||
else => |result| return result,
|
else => |result| return result,
|
||||||
}
|
}
|
||||||
// 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 {
|
||||||
const qn = self.page.?._queued_navigation.?;
|
var timer = try std.time.Timer.start();
|
||||||
defer _ = self.browser.transfer_arena.reset(.{ .retain_with_limit = 8 * 1024 });
|
var ms_remaining = wait_ms;
|
||||||
|
|
||||||
// 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();
|
|
||||||
self.removePage();
|
|
||||||
|
|
||||||
const page = self.createPage() catch |err| {
|
// I'd like the page to know NOTHING about cdp_socket / CDP, but the
|
||||||
log.err(.browser, "queued navigation page error", .{
|
// fact is that the behavior of wait changes depending on whether or
|
||||||
.err = err,
|
// not we're using CDP.
|
||||||
.url = qn.url,
|
// If we aren't using CDP, as soon as we think there's nothing left
|
||||||
});
|
// to do, we can exit - we'de done.
|
||||||
return err;
|
// But if we are using CDP, we should wait for the whole `wait_ms`
|
||||||
|
// because the http_click.tick() also monitors the CDP socket. And while
|
||||||
|
// we could let CDP poll http (like it does for HTTP requests), the fact
|
||||||
|
// is that we know more about the timing of stuff (e.g. how long to
|
||||||
|
// poll/sleep) in the page.
|
||||||
|
const exit_when_done = http_client.cdp_client == null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
switch (page._parse_state) {
|
||||||
|
.pre, .raw, .text, .image => {
|
||||||
|
// The main page hasn't started/finished navigating.
|
||||||
|
// There's no JS to run, and no reason to run the scheduler.
|
||||||
|
if (http_client.active == 0 and exit_when_done) {
|
||||||
|
// haven't started navigating, I guess.
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
// Either we have active http connections, or we're in CDP
|
||||||
|
// mode with an extra socket. Either way, we're waiting
|
||||||
|
// for http traffic
|
||||||
|
if (try http_client.tick(@intCast(ms_remaining)) == .cdp_socket) {
|
||||||
|
// exit_when_done is explicitly set when there isn't
|
||||||
|
// an extra socket, so it should not be possibl to
|
||||||
|
// get an cdp_socket message when exit_when_done
|
||||||
|
// is true.
|
||||||
|
if (IS_DEBUG) {
|
||||||
|
std.debug.assert(exit_when_done == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// data on a socket we aren't handling, return to caller
|
||||||
|
return .cdp_socket;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.html, .complete => {
|
||||||
|
if (self.queued_navigation.items.len != 0) {
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The HTML page was parsed. We now either have JS scripts to
|
||||||
|
// download, or scheduled tasks to execute, or both.
|
||||||
|
|
||||||
|
// scheduler.run could trigger new http transfers, so do not
|
||||||
|
// store http_client.active BEFORE this call and then use
|
||||||
|
// it AFTER.
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
var 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.
|
||||||
|
if (!browser.hasBackgroundTasks()) {
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
// _we_ have nothing to run, but v8 is working on
|
||||||
|
// background tasks. We'll wait for them.
|
||||||
|
browser.waitForBackgroundTasks();
|
||||||
|
ms = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a task to run in the not-so-distant future.
|
||||||
|
// You might think we can just sleep until that task is
|
||||||
|
// ready, but we should continue to run lowPriority tasks
|
||||||
|
// in the meantime, and that could unblock things. So
|
||||||
|
// we'll just sleep for a bit, and then restart our wait
|
||||||
|
// loop to see if anything new can be processed.
|
||||||
|
std.Thread.sleep(std.time.ns_per_ms * @as(u64, @intCast(@min(ms, 20))));
|
||||||
|
} else {
|
||||||
|
// We're here because we either have active HTTP
|
||||||
|
// connections, or exit_when_done == false (aka, there's
|
||||||
|
// an cdp_socket registered with the http client).
|
||||||
|
// We should continue to run 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scheduleNavigation(self: *Session, page: *Page) !void {
|
||||||
|
const list = &self.queued_navigation;
|
||||||
|
|
||||||
|
// Check if page is already queued
|
||||||
|
for (list.items) |existing| {
|
||||||
|
if (existing == page) {
|
||||||
|
// Already queued
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.append(self.arena, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processQueuedNavigation(self: *Session) !void {
|
||||||
|
const navigations = &self.queued_navigation;
|
||||||
|
|
||||||
|
if (self.page.?._queued_navigation != null) {
|
||||||
|
// This is both an optimization and a simplification of sorts. If the
|
||||||
|
// root page is navigating, then we don't need to process any other
|
||||||
|
// navigation. Also, the navigation for the root page and for a frame
|
||||||
|
// is different enough that have two distinct code blocks is, imo,
|
||||||
|
// better. Yes, there will be duplication.
|
||||||
|
navigations.clearRetainingCapacity();
|
||||||
|
return self.processRootQueuedNavigation();
|
||||||
|
}
|
||||||
|
|
||||||
|
const about_blank_queue = &self.queued_queued_navigation;
|
||||||
|
defer about_blank_queue.clearRetainingCapacity();
|
||||||
|
|
||||||
|
// First pass: process async navigations (non-about:blank)
|
||||||
|
// These cannot cause re-entrant navigation scheduling
|
||||||
|
for (navigations.items) |page| {
|
||||||
|
const qn = page._queued_navigation.?;
|
||||||
|
|
||||||
|
if (qn.is_about_blank) {
|
||||||
|
// Defer about:blank to second pass
|
||||||
|
try about_blank_queue.append(self.arena, page);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.processFrameNavigation(page, qn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the queue after first pass
|
||||||
|
navigations.clearRetainingCapacity();
|
||||||
|
|
||||||
|
// Second pass: process synchronous navigations (about:blank)
|
||||||
|
// These may trigger new navigations which go into queued_navigation
|
||||||
|
for (about_blank_queue.items) |page| {
|
||||||
|
const qn = page._queued_navigation.?;
|
||||||
|
try self.processFrameNavigation(page, qn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety: Remove any about:blank navigations that were queued during the
|
||||||
|
// second pass to prevent infinite loops
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < navigations.items.len) {
|
||||||
|
const page = navigations.items[i];
|
||||||
|
if (page._queued_navigation) |qn| {
|
||||||
|
if (qn.is_about_blank) {
|
||||||
|
log.warn(.page, "recursive about blank", .{});
|
||||||
|
_ = navigations.swapRemove(i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processFrameNavigation(self: *Session, page: *Page, qn: *QueuedNavigation) !void {
|
||||||
|
lp.assert(page.parent != null, "root queued navigation", .{});
|
||||||
|
|
||||||
|
const iframe = page.iframe.?;
|
||||||
|
const parent = page.parent.?;
|
||||||
|
|
||||||
|
page._queued_navigation = null;
|
||||||
|
defer self.releaseArena(qn.arena);
|
||||||
|
|
||||||
|
errdefer iframe._window = null;
|
||||||
|
|
||||||
|
if (page._parent_notified) {
|
||||||
|
// we already notified the parent that we had loaded
|
||||||
|
parent._pending_loads += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frame_id = page._frame_id;
|
||||||
|
page.deinit(true);
|
||||||
|
page.* = undefined;
|
||||||
|
|
||||||
|
try Page.init(page, frame_id, self, parent);
|
||||||
|
errdefer page.deinit(true);
|
||||||
|
|
||||||
|
page.iframe = iframe;
|
||||||
|
iframe._window = page.window;
|
||||||
|
|
||||||
page.navigate(qn.url, qn.opts) catch |err| {
|
page.navigate(qn.url, qn.opts) catch |err| {
|
||||||
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
|
log.err(.browser, "queued frame navigation error", .{ .err = err });
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn processRootQueuedNavigation(self: *Session) !void {
|
||||||
|
const current_page = &self.page.?;
|
||||||
|
const frame_id = current_page._frame_id;
|
||||||
|
|
||||||
|
// create a copy before the page is cleared
|
||||||
|
const qn = current_page._queued_navigation.?;
|
||||||
|
current_page._queued_navigation = null;
|
||||||
|
|
||||||
|
defer self.arena_pool.release(qn.arena);
|
||||||
|
|
||||||
|
// HACK
|
||||||
|
// Mark as released in tracking BEFORE removePage clears the map.
|
||||||
|
// We can't call releaseArena() because that would also return the arena
|
||||||
|
// to the pool, making the memory invalid before we use qn.url/qn.opts.
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
if (self._arena_pool_leak_track.getPtr(@intFromPtr(qn.arena.ptr))) |found| {
|
||||||
|
found.count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.removePage();
|
||||||
|
|
||||||
|
self.page = @as(Page, undefined);
|
||||||
|
const new_page = &self.page.?;
|
||||||
|
try Page.init(new_page, frame_id, self, null);
|
||||||
|
|
||||||
|
// Creates a new NavigationEventTarget for this page.
|
||||||
|
try self.navigation.onNewPage(new_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, new_page);
|
||||||
|
|
||||||
|
new_page.navigate(qn.url, qn.opts) catch |err| {
|
||||||
|
log.err(.browser, "queued navigation error", .{ .err = err });
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nextFrameId(self: *Session) u32 {
|
||||||
|
const id = self.frame_id_gen +% 1;
|
||||||
|
self.frame_id_gen = id;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nextPageId(self: *Session) u32 {
|
||||||
|
const id = self.page_id_gen +% 1;
|
||||||
|
self.page_id_gen = id;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -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];
|
||||||
@@ -76,8 +94,9 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
|
|||||||
}
|
}
|
||||||
|
|
||||||
// trailing space so that we always have space to append the null terminator
|
// trailing space so that we always have space to append the null terminator
|
||||||
|
// and so that we can compare the next two characters without needing to length check
|
||||||
var out = try std.mem.join(allocator, "", &.{ normalized_base, "/", path, " " });
|
var out = try std.mem.join(allocator, "", &.{ normalized_base, "/", path, " " });
|
||||||
const end = out.len - 1;
|
const end = out.len - 2;
|
||||||
|
|
||||||
const path_marker = path_start + 1;
|
const path_marker = path_start + 1;
|
||||||
|
|
||||||
@@ -87,14 +106,14 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
|
|||||||
var in_i: usize = 0;
|
var in_i: usize = 0;
|
||||||
var out_i: usize = 0;
|
var out_i: usize = 0;
|
||||||
while (in_i < end) {
|
while (in_i < end) {
|
||||||
if (std.mem.startsWith(u8, out[in_i..], "./")) {
|
if (out[in_i] == '.' and (out_i == 0 or out[out_i - 1] == '/')) {
|
||||||
|
if (out[in_i + 1] == '/') { // always safe, because we added a whitespace
|
||||||
|
// /./
|
||||||
in_i += 2;
|
in_i += 2;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (out[in_i + 1] == '.' and out[in_i + 2] == '/') { // always safe, because we added two whitespaces
|
||||||
if (std.mem.startsWith(u8, out[in_i..], "../")) {
|
// /../
|
||||||
std.debug.assert(out[out_i - 1] == '/');
|
|
||||||
|
|
||||||
if (out_i > path_marker) {
|
if (out_i > path_marker) {
|
||||||
// go back before the /
|
// go back before the /
|
||||||
out_i -= 2;
|
out_i -= 2;
|
||||||
@@ -112,15 +131,136 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
|
|||||||
in_i += 3;
|
in_i += 3;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (in_i == end - 1) {
|
||||||
|
// ignore trailing dot
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
out[out_i] = out[in_i];
|
const c = out[in_i];
|
||||||
|
out[out_i] = c;
|
||||||
in_i += 1;
|
in_i += 1;
|
||||||
out_i += 1;
|
out_i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, .path);
|
||||||
|
|
||||||
|
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, .query);
|
||||||
|
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, .query);
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
const EncodeSet = enum { path, query, userinfo };
|
||||||
|
|
||||||
|
fn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime encode_set: EncodeSet) ![]const u8 {
|
||||||
|
// Check if encoding is needed
|
||||||
|
var needs_encoding = false;
|
||||||
|
for (segment) |c| {
|
||||||
|
if (shouldPercentEncode(c, encode_set)) {
|
||||||
|
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, encode_set)) {
|
||||||
|
try buf.writer(allocator).print("%{X:0>2}", .{c});
|
||||||
|
} else {
|
||||||
|
try buf.append(allocator, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shouldPercentEncode(c: u8, comptime encode_set: EncodeSet) bool {
|
||||||
|
return switch (c) {
|
||||||
|
// Unreserved characters (RFC 3986)
|
||||||
|
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => false,
|
||||||
|
// sub-delims allowed in path/query but some must be encoded in userinfo
|
||||||
|
'!', '$', '&', '\'', '(', ')', '*', '+', ',' => false,
|
||||||
|
';', '=' => encode_set == .userinfo,
|
||||||
|
// Separators: userinfo must encode these
|
||||||
|
'/', ':', '@' => encode_set == .userinfo,
|
||||||
|
// '?' is allowed in queries but not in paths or userinfo
|
||||||
|
'?' => encode_set != .query,
|
||||||
|
// Everything else needs encoding (including space)
|
||||||
|
else => true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isNullTerminated(comptime value: type) bool {
|
fn isNullTerminated(comptime value: type) bool {
|
||||||
@@ -137,6 +277,11 @@ pub fn isCompleteHTTPUrl(url: []const u8) bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// blob: and data: URLs are complete but don't follow scheme:// pattern
|
||||||
|
if (std.mem.startsWith(u8, url, "blob:") or std.mem.startsWith(u8, url, "data:")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if there's a scheme (protocol) ending with ://
|
// Check if there's a scheme (protocol) ending with ://
|
||||||
const colon_pos = std.mem.indexOfScalar(u8, url, ':') orelse return false;
|
const colon_pos = std.mem.indexOfScalar(u8, url, ':') orelse return false;
|
||||||
|
|
||||||
@@ -377,7 +522,7 @@ pub fn setHost(current: [:0]const u8, value: []const u8, allocator: Allocator) !
|
|||||||
const search = getSearch(current);
|
const search = getSearch(current);
|
||||||
const hash = getHash(current);
|
const hash = getHash(current);
|
||||||
|
|
||||||
// Check if the host includes a port
|
// Check if the new value includes a port
|
||||||
const colon_pos = std.mem.lastIndexOfScalar(u8, value, ':');
|
const colon_pos = std.mem.lastIndexOfScalar(u8, value, ':');
|
||||||
const clean_host = if (colon_pos) |pos| blk: {
|
const clean_host = if (colon_pos) |pos| blk: {
|
||||||
const port_str = value[pos + 1 ..];
|
const port_str = value[pos + 1 ..];
|
||||||
@@ -389,7 +534,14 @@ pub fn setHost(current: [:0]const u8, value: []const u8, allocator: Allocator) !
|
|||||||
break :blk value[0..pos];
|
break :blk value[0..pos];
|
||||||
}
|
}
|
||||||
break :blk value;
|
break :blk value;
|
||||||
} else value;
|
} else blk: {
|
||||||
|
// No port in new value - preserve existing port
|
||||||
|
const current_port = getPort(current);
|
||||||
|
if (current_port.len > 0) {
|
||||||
|
break :blk try std.fmt.allocPrint(allocator, "{s}:{s}", .{ value, current_port });
|
||||||
|
}
|
||||||
|
break :blk value;
|
||||||
|
};
|
||||||
|
|
||||||
return buildUrl(allocator, protocol, clean_host, pathname, search, hash);
|
return buildUrl(allocator, protocol, clean_host, pathname, search, hash);
|
||||||
}
|
}
|
||||||
@@ -407,6 +559,9 @@ pub fn setHostname(current: [:0]const u8, value: []const u8, allocator: Allocato
|
|||||||
pub fn setPort(current: [:0]const u8, value: ?[]const u8, allocator: Allocator) ![:0]const u8 {
|
pub fn setPort(current: [:0]const u8, value: ?[]const u8, allocator: Allocator) ![:0]const u8 {
|
||||||
const hostname = getHostname(current);
|
const hostname = getHostname(current);
|
||||||
const protocol = getProtocol(current);
|
const protocol = getProtocol(current);
|
||||||
|
const pathname = getPathname(current);
|
||||||
|
const search = getSearch(current);
|
||||||
|
const hash = getHash(current);
|
||||||
|
|
||||||
// Handle null or default ports
|
// Handle null or default ports
|
||||||
const new_host = if (value) |port_str| blk: {
|
const new_host = if (value) |port_str| blk: {
|
||||||
@@ -423,7 +578,7 @@ pub fn setPort(current: [:0]const u8, value: ?[]const u8, allocator: Allocator)
|
|||||||
break :blk try std.fmt.allocPrint(allocator, "{s}:{s}", .{ hostname, port_str });
|
break :blk try std.fmt.allocPrint(allocator, "{s}:{s}", .{ hostname, port_str });
|
||||||
} else hostname;
|
} else hostname;
|
||||||
|
|
||||||
return setHost(current, new_host, allocator);
|
return buildUrl(allocator, protocol, new_host, pathname, search, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setPathname(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {
|
pub fn setPathname(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {
|
||||||
@@ -471,6 +626,64 @@ pub fn setHash(current: [:0]const u8, value: []const u8, allocator: Allocator) !
|
|||||||
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn setUsername(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {
|
||||||
|
const protocol = getProtocol(current);
|
||||||
|
const host = getHost(current);
|
||||||
|
const pathname = getPathname(current);
|
||||||
|
const search = getSearch(current);
|
||||||
|
const hash = getHash(current);
|
||||||
|
const password = getPassword(current);
|
||||||
|
|
||||||
|
const encoded_username = try percentEncodeSegment(allocator, value, .userinfo);
|
||||||
|
return buildUrlWithUserInfo(allocator, protocol, encoded_username, password, host, pathname, search, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setPassword(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {
|
||||||
|
const protocol = getProtocol(current);
|
||||||
|
const host = getHost(current);
|
||||||
|
const pathname = getPathname(current);
|
||||||
|
const search = getSearch(current);
|
||||||
|
const hash = getHash(current);
|
||||||
|
const username = getUsername(current);
|
||||||
|
|
||||||
|
const encoded_password = try percentEncodeSegment(allocator, value, .userinfo);
|
||||||
|
return buildUrlWithUserInfo(allocator, protocol, username, encoded_password, host, pathname, search, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buildUrlWithUserInfo(
|
||||||
|
allocator: Allocator,
|
||||||
|
protocol: []const u8,
|
||||||
|
username: []const u8,
|
||||||
|
password: []const u8,
|
||||||
|
host: []const u8,
|
||||||
|
pathname: []const u8,
|
||||||
|
search: []const u8,
|
||||||
|
hash: []const u8,
|
||||||
|
) ![:0]const u8 {
|
||||||
|
if (username.len == 0 and password.len == 0) {
|
||||||
|
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
||||||
|
} else if (password.len == 0) {
|
||||||
|
return std.fmt.allocPrintSentinel(allocator, "{s}//{s}@{s}{s}{s}{s}", .{
|
||||||
|
protocol,
|
||||||
|
username,
|
||||||
|
host,
|
||||||
|
pathname,
|
||||||
|
search,
|
||||||
|
hash,
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
return std.fmt.allocPrintSentinel(allocator, "{s}//{s}:{s}@{s}{s}{s}{s}", .{
|
||||||
|
protocol,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
host,
|
||||||
|
pathname,
|
||||||
|
search,
|
||||||
|
hash,
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn concatQueryString(arena: Allocator, url: []const u8, query_string: []const u8) ![:0]const u8 {
|
pub fn concatQueryString(arena: Allocator, url: []const u8, query_string: []const u8) ![:0]const u8 {
|
||||||
if (query_string.len == 0) {
|
if (query_string.len == 0) {
|
||||||
return arena.dupeZ(u8, url);
|
return arena.dupeZ(u8, url);
|
||||||
@@ -495,6 +708,43 @@ pub fn concatQueryString(arena: Allocator, url: []const u8, query_string: []cons
|
|||||||
return buf.items[0 .. buf.items.len - 1 :0];
|
return buf.items[0 .. buf.items.len - 1 :0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getRobotsUrl(arena: Allocator, url: [:0]const u8) ![:0]const u8 {
|
||||||
|
const origin = try getOrigin(arena, url) orelse return error.NoOrigin;
|
||||||
|
return try std.fmt.allocPrintSentinel(
|
||||||
|
arena,
|
||||||
|
"{s}/robots.txt",
|
||||||
|
.{origin},
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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"));
|
||||||
@@ -541,6 +791,21 @@ test "URL: resolve" {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cases = [_]Case{
|
const cases = [_]Case{
|
||||||
|
.{
|
||||||
|
.base = "https://example/dir",
|
||||||
|
.path = "abc../test",
|
||||||
|
.expected = "https://example/abc../test",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example/dir",
|
||||||
|
.path = "abc.",
|
||||||
|
.expected = "https://example/abc.",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example/dir",
|
||||||
|
.path = "abc/.",
|
||||||
|
.expected = "https://example/abc/",
|
||||||
|
},
|
||||||
.{
|
.{
|
||||||
.base = "https://example/xyz/abc/123",
|
.base = "https://example/xyz/abc/123",
|
||||||
.path = "something.js",
|
.path = "something.js",
|
||||||
@@ -659,6 +924,297 @@ 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",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "about:blank",
|
||||||
|
.expected = "about:blank",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
{
|
{
|
||||||
@@ -756,3 +1312,105 @@ test "URL: concatQueryString" {
|
|||||||
try testing.expectEqual("https://www.lightpanda.io/index?1=2&a=b", url);
|
try testing.expectEqual("https://www.lightpanda.io/index?1=2&a=b", url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "URL: getRobotsUrl" {
|
||||||
|
defer testing.reset();
|
||||||
|
const arena = testing.arena_allocator;
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = try getRobotsUrl(arena, "https://www.lightpanda.io");
|
||||||
|
try testing.expectEqual("https://www.lightpanda.io/robots.txt", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = try getRobotsUrl(arena, "https://www.lightpanda.io/some/path");
|
||||||
|
try testing.expectString("https://www.lightpanda.io/robots.txt", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = try getRobotsUrl(arena, "https://www.lightpanda.io:8080/page");
|
||||||
|
try testing.expectString("https://www.lightpanda.io:8080/robots.txt", url);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const url = try getRobotsUrl(arena, "http://example.com/deep/nested/path?query=value#fragment");
|
||||||
|
try testing.expectString("http://example.com/robots.txt", url);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const url = try getRobotsUrl(arena, "https://user:pass@example.com/page");
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "URL: getHost" {
|
||||||
|
try testing.expectEqualSlices(u8, "example.com:8080", getHost("https://example.com:8080/path"));
|
||||||
|
try testing.expectEqualSlices(u8, "example.com", getHost("https://example.com/path"));
|
||||||
|
try testing.expectEqualSlices(u8, "example.com:443", getHost("https://example.com:443/"));
|
||||||
|
try testing.expectEqualSlices(u8, "example.com", getHost("https://user:pass@example.com/page"));
|
||||||
|
try testing.expectEqualSlices(u8, "example.com:8080", getHost("https://user:pass@example.com:8080/page"));
|
||||||
|
try testing.expectEqualSlices(u8, "", getHost("not-a-url"));
|
||||||
|
}
|
||||||
|
|||||||
298
src/browser/color.zig
Normal file
298
src/browser/color.zig
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Io = std.Io;
|
||||||
|
|
||||||
|
pub fn isHexColor(value: []const u8) bool {
|
||||||
|
if (value.len == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value[0] != '#') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hex_part = value[1..];
|
||||||
|
switch (hex_part.len) {
|
||||||
|
3, 4, 6, 8 => for (hex_part) |c| if (!std.ascii.isHex(c)) return false,
|
||||||
|
else => return false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const RGBA = packed struct(u32) {
|
||||||
|
r: u8,
|
||||||
|
g: u8,
|
||||||
|
b: u8,
|
||||||
|
/// Opaque by default.
|
||||||
|
a: u8 = std.math.maxInt(u8),
|
||||||
|
|
||||||
|
pub const Named = struct {
|
||||||
|
// Basic colors (CSS Level 1)
|
||||||
|
pub const black: RGBA = .init(0, 0, 0, 1);
|
||||||
|
pub const silver: RGBA = .init(192, 192, 192, 1);
|
||||||
|
pub const gray: RGBA = .init(128, 128, 128, 1);
|
||||||
|
pub const white: RGBA = .init(255, 255, 255, 1);
|
||||||
|
pub const maroon: RGBA = .init(128, 0, 0, 1);
|
||||||
|
pub const red: RGBA = .init(255, 0, 0, 1);
|
||||||
|
pub const purple: RGBA = .init(128, 0, 128, 1);
|
||||||
|
pub const fuchsia: RGBA = .init(255, 0, 255, 1);
|
||||||
|
pub const green: RGBA = .init(0, 128, 0, 1);
|
||||||
|
pub const lime: RGBA = .init(0, 255, 0, 1);
|
||||||
|
pub const olive: RGBA = .init(128, 128, 0, 1);
|
||||||
|
pub const yellow: RGBA = .init(255, 255, 0, 1);
|
||||||
|
pub const navy: RGBA = .init(0, 0, 128, 1);
|
||||||
|
pub const blue: RGBA = .init(0, 0, 255, 1);
|
||||||
|
pub const teal: RGBA = .init(0, 128, 128, 1);
|
||||||
|
pub const aqua: RGBA = .init(0, 255, 255, 1);
|
||||||
|
|
||||||
|
// Extended colors (CSS Level 2+)
|
||||||
|
pub const aliceblue: RGBA = .init(240, 248, 255, 1);
|
||||||
|
pub const antiquewhite: RGBA = .init(250, 235, 215, 1);
|
||||||
|
pub const aquamarine: RGBA = .init(127, 255, 212, 1);
|
||||||
|
pub const azure: RGBA = .init(240, 255, 255, 1);
|
||||||
|
pub const beige: RGBA = .init(245, 245, 220, 1);
|
||||||
|
pub const bisque: RGBA = .init(255, 228, 196, 1);
|
||||||
|
pub const blanchedalmond: RGBA = .init(255, 235, 205, 1);
|
||||||
|
pub const blueviolet: RGBA = .init(138, 43, 226, 1);
|
||||||
|
pub const brown: RGBA = .init(165, 42, 42, 1);
|
||||||
|
pub const burlywood: RGBA = .init(222, 184, 135, 1);
|
||||||
|
pub const cadetblue: RGBA = .init(95, 158, 160, 1);
|
||||||
|
pub const chartreuse: RGBA = .init(127, 255, 0, 1);
|
||||||
|
pub const chocolate: RGBA = .init(210, 105, 30, 1);
|
||||||
|
pub const coral: RGBA = .init(255, 127, 80, 1);
|
||||||
|
pub const cornflowerblue: RGBA = .init(100, 149, 237, 1);
|
||||||
|
pub const cornsilk: RGBA = .init(255, 248, 220, 1);
|
||||||
|
pub const crimson: RGBA = .init(220, 20, 60, 1);
|
||||||
|
pub const cyan: RGBA = .init(0, 255, 255, 1); // Synonym of aqua
|
||||||
|
pub const darkblue: RGBA = .init(0, 0, 139, 1);
|
||||||
|
pub const darkcyan: RGBA = .init(0, 139, 139, 1);
|
||||||
|
pub const darkgoldenrod: RGBA = .init(184, 134, 11, 1);
|
||||||
|
pub const darkgray: RGBA = .init(169, 169, 169, 1);
|
||||||
|
pub const darkgreen: RGBA = .init(0, 100, 0, 1);
|
||||||
|
pub const darkgrey: RGBA = .init(169, 169, 169, 1); // Synonym of darkgray
|
||||||
|
pub const darkkhaki: RGBA = .init(189, 183, 107, 1);
|
||||||
|
pub const darkmagenta: RGBA = .init(139, 0, 139, 1);
|
||||||
|
pub const darkolivegreen: RGBA = .init(85, 107, 47, 1);
|
||||||
|
pub const darkorange: RGBA = .init(255, 140, 0, 1);
|
||||||
|
pub const darkorchid: RGBA = .init(153, 50, 204, 1);
|
||||||
|
pub const darkred: RGBA = .init(139, 0, 0, 1);
|
||||||
|
pub const darksalmon: RGBA = .init(233, 150, 122, 1);
|
||||||
|
pub const darkseagreen: RGBA = .init(143, 188, 143, 1);
|
||||||
|
pub const darkslateblue: RGBA = .init(72, 61, 139, 1);
|
||||||
|
pub const darkslategray: RGBA = .init(47, 79, 79, 1);
|
||||||
|
pub const darkslategrey: RGBA = .init(47, 79, 79, 1); // Synonym of darkslategray
|
||||||
|
pub const darkturquoise: RGBA = .init(0, 206, 209, 1);
|
||||||
|
pub const darkviolet: RGBA = .init(148, 0, 211, 1);
|
||||||
|
pub const deeppink: RGBA = .init(255, 20, 147, 1);
|
||||||
|
pub const deepskyblue: RGBA = .init(0, 191, 255, 1);
|
||||||
|
pub const dimgray: RGBA = .init(105, 105, 105, 1);
|
||||||
|
pub const dimgrey: RGBA = .init(105, 105, 105, 1); // Synonym of dimgray
|
||||||
|
pub const dodgerblue: RGBA = .init(30, 144, 255, 1);
|
||||||
|
pub const firebrick: RGBA = .init(178, 34, 34, 1);
|
||||||
|
pub const floralwhite: RGBA = .init(255, 250, 240, 1);
|
||||||
|
pub const forestgreen: RGBA = .init(34, 139, 34, 1);
|
||||||
|
pub const gainsboro: RGBA = .init(220, 220, 220, 1);
|
||||||
|
pub const ghostwhite: RGBA = .init(248, 248, 255, 1);
|
||||||
|
pub const gold: RGBA = .init(255, 215, 0, 1);
|
||||||
|
pub const goldenrod: RGBA = .init(218, 165, 32, 1);
|
||||||
|
pub const greenyellow: RGBA = .init(173, 255, 47, 1);
|
||||||
|
pub const grey: RGBA = .init(128, 128, 128, 1); // Synonym of gray
|
||||||
|
pub const honeydew: RGBA = .init(240, 255, 240, 1);
|
||||||
|
pub const hotpink: RGBA = .init(255, 105, 180, 1);
|
||||||
|
pub const indianred: RGBA = .init(205, 92, 92, 1);
|
||||||
|
pub const indigo: RGBA = .init(75, 0, 130, 1);
|
||||||
|
pub const ivory: RGBA = .init(255, 255, 240, 1);
|
||||||
|
pub const khaki: RGBA = .init(240, 230, 140, 1);
|
||||||
|
pub const lavender: RGBA = .init(230, 230, 250, 1);
|
||||||
|
pub const lavenderblush: RGBA = .init(255, 240, 245, 1);
|
||||||
|
pub const lawngreen: RGBA = .init(124, 252, 0, 1);
|
||||||
|
pub const lemonchiffon: RGBA = .init(255, 250, 205, 1);
|
||||||
|
pub const lightblue: RGBA = .init(173, 216, 230, 1);
|
||||||
|
pub const lightcoral: RGBA = .init(240, 128, 128, 1);
|
||||||
|
pub const lightcyan: RGBA = .init(224, 255, 255, 1);
|
||||||
|
pub const lightgoldenrodyellow: RGBA = .init(250, 250, 210, 1);
|
||||||
|
pub const lightgray: RGBA = .init(211, 211, 211, 1);
|
||||||
|
pub const lightgreen: RGBA = .init(144, 238, 144, 1);
|
||||||
|
pub const lightgrey: RGBA = .init(211, 211, 211, 1); // Synonym of lightgray
|
||||||
|
pub const lightpink: RGBA = .init(255, 182, 193, 1);
|
||||||
|
pub const lightsalmon: RGBA = .init(255, 160, 122, 1);
|
||||||
|
pub const lightseagreen: RGBA = .init(32, 178, 170, 1);
|
||||||
|
pub const lightskyblue: RGBA = .init(135, 206, 250, 1);
|
||||||
|
pub const lightslategray: RGBA = .init(119, 136, 153, 1);
|
||||||
|
pub const lightslategrey: RGBA = .init(119, 136, 153, 1); // Synonym of lightslategray
|
||||||
|
pub const lightsteelblue: RGBA = .init(176, 196, 222, 1);
|
||||||
|
pub const lightyellow: RGBA = .init(255, 255, 224, 1);
|
||||||
|
pub const limegreen: RGBA = .init(50, 205, 50, 1);
|
||||||
|
pub const linen: RGBA = .init(250, 240, 230, 1);
|
||||||
|
pub const magenta: RGBA = .init(255, 0, 255, 1); // Synonym of fuchsia
|
||||||
|
pub const mediumaquamarine: RGBA = .init(102, 205, 170, 1);
|
||||||
|
pub const mediumblue: RGBA = .init(0, 0, 205, 1);
|
||||||
|
pub const mediumorchid: RGBA = .init(186, 85, 211, 1);
|
||||||
|
pub const mediumpurple: RGBA = .init(147, 112, 219, 1);
|
||||||
|
pub const mediumseagreen: RGBA = .init(60, 179, 113, 1);
|
||||||
|
pub const mediumslateblue: RGBA = .init(123, 104, 238, 1);
|
||||||
|
pub const mediumspringgreen: RGBA = .init(0, 250, 154, 1);
|
||||||
|
pub const mediumturquoise: RGBA = .init(72, 209, 204, 1);
|
||||||
|
pub const mediumvioletred: RGBA = .init(199, 21, 133, 1);
|
||||||
|
pub const midnightblue: RGBA = .init(25, 25, 112, 1);
|
||||||
|
pub const mintcream: RGBA = .init(245, 255, 250, 1);
|
||||||
|
pub const mistyrose: RGBA = .init(255, 228, 225, 1);
|
||||||
|
pub const moccasin: RGBA = .init(255, 228, 181, 1);
|
||||||
|
pub const navajowhite: RGBA = .init(255, 222, 173, 1);
|
||||||
|
pub const oldlace: RGBA = .init(253, 245, 230, 1);
|
||||||
|
pub const olivedrab: RGBA = .init(107, 142, 35, 1);
|
||||||
|
pub const orange: RGBA = .init(255, 165, 0, 1);
|
||||||
|
pub const orangered: RGBA = .init(255, 69, 0, 1);
|
||||||
|
pub const orchid: RGBA = .init(218, 112, 214, 1);
|
||||||
|
pub const palegoldenrod: RGBA = .init(238, 232, 170, 1);
|
||||||
|
pub const palegreen: RGBA = .init(152, 251, 152, 1);
|
||||||
|
pub const paleturquoise: RGBA = .init(175, 238, 238, 1);
|
||||||
|
pub const palevioletred: RGBA = .init(219, 112, 147, 1);
|
||||||
|
pub const papayawhip: RGBA = .init(255, 239, 213, 1);
|
||||||
|
pub const peachpuff: RGBA = .init(255, 218, 185, 1);
|
||||||
|
pub const peru: RGBA = .init(205, 133, 63, 1);
|
||||||
|
pub const pink: RGBA = .init(255, 192, 203, 1);
|
||||||
|
pub const plum: RGBA = .init(221, 160, 221, 1);
|
||||||
|
pub const powderblue: RGBA = .init(176, 224, 230, 1);
|
||||||
|
pub const rebeccapurple: RGBA = .init(102, 51, 153, 1);
|
||||||
|
pub const rosybrown: RGBA = .init(188, 143, 143, 1);
|
||||||
|
pub const royalblue: RGBA = .init(65, 105, 225, 1);
|
||||||
|
pub const saddlebrown: RGBA = .init(139, 69, 19, 1);
|
||||||
|
pub const salmon: RGBA = .init(250, 128, 114, 1);
|
||||||
|
pub const sandybrown: RGBA = .init(244, 164, 96, 1);
|
||||||
|
pub const seagreen: RGBA = .init(46, 139, 87, 1);
|
||||||
|
pub const seashell: RGBA = .init(255, 245, 238, 1);
|
||||||
|
pub const sienna: RGBA = .init(160, 82, 45, 1);
|
||||||
|
pub const skyblue: RGBA = .init(135, 206, 235, 1);
|
||||||
|
pub const slateblue: RGBA = .init(106, 90, 205, 1);
|
||||||
|
pub const slategray: RGBA = .init(112, 128, 144, 1);
|
||||||
|
pub const slategrey: RGBA = .init(112, 128, 144, 1); // Synonym of slategray
|
||||||
|
pub const snow: RGBA = .init(255, 250, 250, 1);
|
||||||
|
pub const springgreen: RGBA = .init(0, 255, 127, 1);
|
||||||
|
pub const steelblue: RGBA = .init(70, 130, 180, 1);
|
||||||
|
pub const tan: RGBA = .init(210, 180, 140, 1);
|
||||||
|
pub const thistle: RGBA = .init(216, 191, 216, 1);
|
||||||
|
pub const tomato: RGBA = .init(255, 99, 71, 1);
|
||||||
|
pub const transparent: RGBA = .init(0, 0, 0, 0);
|
||||||
|
pub const turquoise: RGBA = .init(64, 224, 208, 1);
|
||||||
|
pub const violet: RGBA = .init(238, 130, 238, 1);
|
||||||
|
pub const wheat: RGBA = .init(245, 222, 179, 1);
|
||||||
|
pub const whitesmoke: RGBA = .init(245, 245, 245, 1);
|
||||||
|
pub const yellowgreen: RGBA = .init(154, 205, 50, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(r: u8, g: u8, b: u8, a: f32) RGBA {
|
||||||
|
const clamped = std.math.clamp(a, 0, 1);
|
||||||
|
return .{ .r = r, .g = g, .b = b, .a = @intFromFloat(clamped * 255) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds a color by its name.
|
||||||
|
pub fn find(name: []const u8) ?RGBA {
|
||||||
|
const match = std.meta.stringToEnum(std.meta.DeclEnum(Named), name) orelse return null;
|
||||||
|
|
||||||
|
return switch (match) {
|
||||||
|
inline else => |comptime_enum| @field(Named, @tagName(comptime_enum)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the given color.
|
||||||
|
/// Currently we only parse hex colors and named colors; other variants
|
||||||
|
/// require CSS evaluation.
|
||||||
|
pub fn parse(input: []const u8) !RGBA {
|
||||||
|
if (!isHexColor(input)) {
|
||||||
|
// Try named colors.
|
||||||
|
return find(input) orelse return error.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slice = input[1..];
|
||||||
|
switch (slice.len) {
|
||||||
|
// This means the digit for a color is repeated.
|
||||||
|
// Given HEX is #f0c, its interpreted the same as #FF00CC.
|
||||||
|
3 => {
|
||||||
|
const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16);
|
||||||
|
const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16);
|
||||||
|
const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16);
|
||||||
|
return .{ .r = r, .g = g, .b = b, .a = 255 };
|
||||||
|
},
|
||||||
|
4 => {
|
||||||
|
const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16);
|
||||||
|
const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16);
|
||||||
|
const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16);
|
||||||
|
const a = try std.fmt.parseInt(u8, &.{ slice[3], slice[3] }, 16);
|
||||||
|
return .{ .r = r, .g = g, .b = b, .a = a };
|
||||||
|
},
|
||||||
|
// Regular HEX format.
|
||||||
|
6 => {
|
||||||
|
const r = try std.fmt.parseInt(u8, slice[0..2], 16);
|
||||||
|
const g = try std.fmt.parseInt(u8, slice[2..4], 16);
|
||||||
|
const b = try std.fmt.parseInt(u8, slice[4..6], 16);
|
||||||
|
return .{ .r = r, .g = g, .b = b, .a = 255 };
|
||||||
|
},
|
||||||
|
8 => {
|
||||||
|
const r = try std.fmt.parseInt(u8, slice[0..2], 16);
|
||||||
|
const g = try std.fmt.parseInt(u8, slice[2..4], 16);
|
||||||
|
const b = try std.fmt.parseInt(u8, slice[4..6], 16);
|
||||||
|
const a = try std.fmt.parseInt(u8, slice[6..8], 16);
|
||||||
|
return .{ .r = r, .g = g, .b = b, .a = a };
|
||||||
|
},
|
||||||
|
else => return error.Invalid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// By default, browsers prefer lowercase formatting.
|
||||||
|
const format_upper = false;
|
||||||
|
|
||||||
|
/// Formats the `Color` according to web expectations.
|
||||||
|
/// If color is opaque, HEX is preferred; RGBA otherwise.
|
||||||
|
pub fn format(self: *const RGBA, writer: *Io.Writer) Io.Writer.Error!void {
|
||||||
|
if (self.isOpaque()) {
|
||||||
|
// Convert RGB to HEX.
|
||||||
|
// https://gristle.tripod.com/hexconv.html
|
||||||
|
// Hexadecimal characters up to 15.
|
||||||
|
const char: []const u8 = "0123456789" ++ if (format_upper) "ABCDEF" else "abcdef";
|
||||||
|
// This variant always prefers 6 digit format, +1 is for hash char.
|
||||||
|
const buffer = [7]u8{
|
||||||
|
'#',
|
||||||
|
char[self.r >> 4],
|
||||||
|
char[self.r & 15],
|
||||||
|
char[self.g >> 4],
|
||||||
|
char[self.g & 15],
|
||||||
|
char[self.b >> 4],
|
||||||
|
char[self.b & 15],
|
||||||
|
};
|
||||||
|
|
||||||
|
return writer.writeAll(&buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer RGBA format for everything else.
|
||||||
|
return writer.print("rgba({d}, {d}, {d}, {d:.2})", .{ self.r, self.g, self.b, self.normalizedAlpha() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if `Color` is opaque.
|
||||||
|
pub inline fn isOpaque(self: *const RGBA) bool {
|
||||||
|
return self.a == std.math.maxInt(u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the normalized alpha value.
|
||||||
|
pub inline fn normalizedAlpha(self: *const RGBA) f32 {
|
||||||
|
return @as(f32, @floatFromInt(self.a)) / 255;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -480,10 +480,11 @@ fn consumeName(self: *Tokenizer) []const u8 {
|
|||||||
self.consumeEscape();
|
self.consumeEscape();
|
||||||
},
|
},
|
||||||
0x0 => self.advance(1),
|
0x0 => self.advance(1),
|
||||||
'\x80'...'\xBF', '\xC0'...'\xEF', '\xF0'...'\xFF' => {
|
'\x80'...'\xFF' => {
|
||||||
// This byte *is* part of a multi-byte code point,
|
// Non-ASCII: advance over the complete UTF-8 code point in one step.
|
||||||
// we’ll end up copying the whole code point before this loop does something else.
|
// Using consumeChar() instead of advance(1) ensures we never land on
|
||||||
self.advance(1);
|
// a continuation byte, which advance() asserts against.
|
||||||
|
self.consumeChar();
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
if (self.hasNonAsciiAt(0)) {
|
if (self.hasNonAsciiAt(0)) {
|
||||||
@@ -583,7 +584,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,18 +48,29 @@ 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: {
|
||||||
|
// Ideally we just render the doctype which is part of the document
|
||||||
|
if (doc.asNode().firstChild()) |first| {
|
||||||
|
if (first._type == .document_type) {
|
||||||
|
break :blk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// But if the doc has no child, or the first child isn't a doctype
|
||||||
|
// well force it.
|
||||||
try writer.writeAll("<!DOCTYPE html>");
|
try writer.writeAll("<!DOCTYPE html>");
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.with_base) {
|
if (opts.with_base) {
|
||||||
const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode();
|
const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode();
|
||||||
const base = try doc.createElement("base", null, page);
|
const base = try doc.createElement("base", null, page);
|
||||||
try base.setAttributeSafe("base", page.base(), page);
|
try base.setAttributeSafe(comptime .wrap("base"), .wrap(page.base()), page);
|
||||||
_ = try parent.insertBefore(base.asNode(), parent.firstChild(), page);
|
_ = try parent.insertBefore(base.asNode(), parent.firstChild(), 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 {
|
||||||
@@ -72,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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -99,7 +109,7 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
|
|||||||
// to render that "active" content, so when we're trying to render
|
// to render that "active" content, so when we're trying to render
|
||||||
// it, we don't want to skip it.
|
// it, we don't want to skip it.
|
||||||
if ((comptime force_slot == false) and opts.shadow == .rendered) {
|
if ((comptime force_slot == false) and opts.shadow == .rendered) {
|
||||||
if (el.getAttributeSafe("slot")) |_| {
|
if (el.getAttributeSafe(comptime .wrap("slot"))) |_| {
|
||||||
// Skip - will be rendered by the Slot if it's the active container
|
// Skip - will be rendered by the Slot if it's the active container
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -129,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());
|
||||||
@@ -161,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("");
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,12 +273,12 @@ fn shouldStripElement(el: *const Node.Element, opts: Opts) bool {
|
|||||||
if (std.mem.eql(u8, tag_name, "noscript")) return true;
|
if (std.mem.eql(u8, tag_name, "noscript")) return true;
|
||||||
|
|
||||||
if (std.mem.eql(u8, tag_name, "link")) {
|
if (std.mem.eql(u8, tag_name, "link")) {
|
||||||
if (el.getAttributeSafe("as")) |as| {
|
if (el.getAttributeSafe(comptime .wrap("as"))) |as| {
|
||||||
if (std.mem.eql(u8, as, "script")) return true;
|
if (std.mem.eql(u8, as, "script")) return true;
|
||||||
}
|
}
|
||||||
if (el.getAttributeSafe("rel")) |rel| {
|
if (el.getAttributeSafe(comptime .wrap("rel"))) |rel| {
|
||||||
if (std.mem.eql(u8, rel, "modulepreload") or std.mem.eql(u8, rel, "preload")) {
|
if (std.mem.eql(u8, rel, "modulepreload") or std.mem.eql(u8, rel, "preload")) {
|
||||||
if (el.getAttributeSafe("as")) |as| {
|
if (el.getAttributeSafe(comptime .wrap("as"))) |as| {
|
||||||
if (std.mem.eql(u8, as, "script")) return true;
|
if (std.mem.eql(u8, as, "script")) return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,7 +290,7 @@ fn shouldStripElement(el: *const Node.Element, opts: Opts) bool {
|
|||||||
if (std.mem.eql(u8, tag_name, "style")) return true;
|
if (std.mem.eql(u8, tag_name, "style")) return true;
|
||||||
|
|
||||||
if (std.mem.eql(u8, tag_name, "link")) {
|
if (std.mem.eql(u8, tag_name, "link")) {
|
||||||
if (el.getAttributeSafe("rel")) |rel| {
|
if (el.getAttributeSafe(comptime .wrap("rel"))) |rel| {
|
||||||
if (std.mem.eql(u8, rel, "stylesheet")) return true;
|
if (std.mem.eql(u8, rel, "stylesheet")) return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,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 {
|
||||||
|
|||||||
546
src/browser/interactive.zig
Normal file
546
src/browser/interactive.zig
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
// 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 URL = @import("URL.zig");
|
||||||
|
const TreeWalker = @import("webapi/TreeWalker.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
const EventTarget = @import("webapi/EventTarget.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
pub const InteractivityType = enum {
|
||||||
|
native,
|
||||||
|
aria,
|
||||||
|
contenteditable,
|
||||||
|
listener,
|
||||||
|
focusable,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const InteractiveElement = struct {
|
||||||
|
node: *Node,
|
||||||
|
tag_name: []const u8,
|
||||||
|
role: ?[]const u8,
|
||||||
|
name: ?[]const u8,
|
||||||
|
interactivity_type: InteractivityType,
|
||||||
|
listener_types: []const []const u8,
|
||||||
|
disabled: bool,
|
||||||
|
tab_index: i32,
|
||||||
|
id: ?[]const u8,
|
||||||
|
class: ?[]const u8,
|
||||||
|
href: ?[]const u8,
|
||||||
|
input_type: ?[]const u8,
|
||||||
|
value: ?[]const u8,
|
||||||
|
element_name: ?[]const u8,
|
||||||
|
placeholder: ?[]const u8,
|
||||||
|
|
||||||
|
pub fn jsonStringify(self: *const InteractiveElement, jw: anytype) !void {
|
||||||
|
try jw.beginObject();
|
||||||
|
|
||||||
|
try jw.objectField("tagName");
|
||||||
|
try jw.write(self.tag_name);
|
||||||
|
|
||||||
|
try jw.objectField("role");
|
||||||
|
try jw.write(self.role);
|
||||||
|
|
||||||
|
try jw.objectField("name");
|
||||||
|
try jw.write(self.name);
|
||||||
|
|
||||||
|
try jw.objectField("type");
|
||||||
|
try jw.write(@tagName(self.interactivity_type));
|
||||||
|
|
||||||
|
if (self.listener_types.len > 0) {
|
||||||
|
try jw.objectField("listeners");
|
||||||
|
try jw.beginArray();
|
||||||
|
for (self.listener_types) |lt| {
|
||||||
|
try jw.write(lt);
|
||||||
|
}
|
||||||
|
try jw.endArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.disabled) {
|
||||||
|
try jw.objectField("disabled");
|
||||||
|
try jw.write(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.objectField("tabIndex");
|
||||||
|
try jw.write(self.tab_index);
|
||||||
|
|
||||||
|
if (self.id) |v| {
|
||||||
|
try jw.objectField("id");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.class) |v| {
|
||||||
|
try jw.objectField("class");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.href) |v| {
|
||||||
|
try jw.objectField("href");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.input_type) |v| {
|
||||||
|
try jw.objectField("inputType");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.value) |v| {
|
||||||
|
try jw.objectField("value");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.element_name) |v| {
|
||||||
|
try jw.objectField("elementName");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.placeholder) |v| {
|
||||||
|
try jw.objectField("placeholder");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Collect all interactive elements under `root`.
|
||||||
|
pub fn collectInteractiveElements(
|
||||||
|
root: *Node,
|
||||||
|
arena: Allocator,
|
||||||
|
page: *Page,
|
||||||
|
) ![]InteractiveElement {
|
||||||
|
// Pre-build a map of event_target pointer → event type names,
|
||||||
|
// so classify and getListenerTypes are both O(1) per element.
|
||||||
|
const listener_targets = try buildListenerTargetMap(page, arena);
|
||||||
|
|
||||||
|
var results: std.ArrayList(InteractiveElement) = .empty;
|
||||||
|
|
||||||
|
var tw = TreeWalker.Full.init(root, .{});
|
||||||
|
while (tw.next()) |node| {
|
||||||
|
const el = node.is(Element) orelse continue;
|
||||||
|
const html_el = el.is(Element.Html) orelse continue;
|
||||||
|
|
||||||
|
// Skip non-visual elements that are never user-interactive.
|
||||||
|
switch (el.getTag()) {
|
||||||
|
.script, .style, .link, .meta, .head, .noscript, .template => continue,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const itype = classifyInteractivity(el, html_el, listener_targets) orelse continue;
|
||||||
|
|
||||||
|
const listener_types = getListenerTypes(
|
||||||
|
el.asEventTarget(),
|
||||||
|
listener_targets,
|
||||||
|
);
|
||||||
|
|
||||||
|
try results.append(arena, .{
|
||||||
|
.node = node,
|
||||||
|
.tag_name = el.getTagNameLower(),
|
||||||
|
.role = getRole(el),
|
||||||
|
.name = try getAccessibleName(el, arena),
|
||||||
|
.interactivity_type = itype,
|
||||||
|
.listener_types = listener_types,
|
||||||
|
.disabled = isDisabled(el),
|
||||||
|
.tab_index = html_el.getTabIndex(),
|
||||||
|
.id = el.getAttributeSafe(comptime .wrap("id")),
|
||||||
|
.class = el.getAttributeSafe(comptime .wrap("class")),
|
||||||
|
.href = if (el.getAttributeSafe(comptime .wrap("href"))) |href|
|
||||||
|
URL.resolve(arena, page.base(), href, .{ .encode = true }) catch href
|
||||||
|
else
|
||||||
|
null,
|
||||||
|
.input_type = getInputType(el),
|
||||||
|
.value = getInputValue(el),
|
||||||
|
.element_name = el.getAttributeSafe(comptime .wrap("name")),
|
||||||
|
.placeholder = el.getAttributeSafe(comptime .wrap("placeholder")),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const ListenerTargetMap = std.AutoHashMapUnmanaged(usize, std.ArrayList([]const u8));
|
||||||
|
|
||||||
|
/// Pre-build a map from event_target pointer → list of event type names.
|
||||||
|
/// This lets both classifyInteractivity (O(1) "has any?") and
|
||||||
|
/// getListenerTypes (O(1) "which ones?") avoid re-iterating per element.
|
||||||
|
pub fn buildListenerTargetMap(page: *Page, arena: Allocator) !ListenerTargetMap {
|
||||||
|
var map = ListenerTargetMap{};
|
||||||
|
|
||||||
|
// addEventListener registrations
|
||||||
|
var it = page._event_manager.lookup.iterator();
|
||||||
|
while (it.next()) |entry| {
|
||||||
|
const list = entry.value_ptr.*;
|
||||||
|
if (list.first != null) {
|
||||||
|
const gop = try map.getOrPut(arena, entry.key_ptr.event_target);
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .empty;
|
||||||
|
try gop.value_ptr.append(arena, entry.key_ptr.type_string.str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline handlers (onclick, onmousedown, etc.)
|
||||||
|
var attr_it = page._event_target_attr_listeners.iterator();
|
||||||
|
while (attr_it.next()) |entry| {
|
||||||
|
const gop = try map.getOrPut(arena, @intFromPtr(entry.key_ptr.target));
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .empty;
|
||||||
|
// Strip "on" prefix to get the event type name.
|
||||||
|
try gop.value_ptr.append(arena, @tagName(entry.key_ptr.handler)[2..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn classifyInteractivity(
|
||||||
|
el: *Element,
|
||||||
|
html_el: *Element.Html,
|
||||||
|
listener_targets: ListenerTargetMap,
|
||||||
|
) ?InteractivityType {
|
||||||
|
// 1. Native interactive by tag
|
||||||
|
switch (el.getTag()) {
|
||||||
|
.button, .summary, .details, .select, .textarea => return .native,
|
||||||
|
.anchor, .area => {
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("href")) != null) return .native;
|
||||||
|
},
|
||||||
|
.input => {
|
||||||
|
if (el.is(Element.Html.Input)) |input| {
|
||||||
|
if (input._input_type != .hidden) return .native;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. ARIA interactive role
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("role"))) |role| {
|
||||||
|
if (isInteractiveRole(role)) return .aria;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. contenteditable (15 bytes, exceeds SSO limit for comptime)
|
||||||
|
if (el.getAttributeSafe(.wrap("contenteditable"))) |ce| {
|
||||||
|
if (ce.len == 0 or std.ascii.eqlIgnoreCase(ce, "true")) return .contenteditable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Event listeners (addEventListener or inline handlers)
|
||||||
|
const et_ptr = @intFromPtr(html_el.asEventTarget());
|
||||||
|
if (listener_targets.get(et_ptr) != null) return .listener;
|
||||||
|
|
||||||
|
// 5. Explicitly focusable via tabindex.
|
||||||
|
// Only count elements with an EXPLICIT tabindex attribute,
|
||||||
|
// since getTabIndex() returns 0 for all interactive tags by default
|
||||||
|
// (including anchors without href and hidden inputs).
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("tabindex"))) |_| {
|
||||||
|
if (html_el.getTabIndex() >= 0) return .focusable;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isInteractiveRole(role: []const u8) bool {
|
||||||
|
const interactive_roles = [_][]const u8{
|
||||||
|
"button", "link", "tab", "menuitem",
|
||||||
|
"menuitemcheckbox", "menuitemradio", "switch", "checkbox",
|
||||||
|
"radio", "slider", "spinbutton", "searchbox",
|
||||||
|
"combobox", "option", "treeitem",
|
||||||
|
};
|
||||||
|
for (interactive_roles) |r| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(role, r)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getRole(el: *Element) ?[]const u8 {
|
||||||
|
// Explicit role attribute takes precedence
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("role"))) |role| return role;
|
||||||
|
|
||||||
|
// Implicit role from tag
|
||||||
|
return switch (el.getTag()) {
|
||||||
|
.button, .summary => "button",
|
||||||
|
.anchor, .area => if (el.getAttributeSafe(comptime .wrap("href")) != null) "link" else null,
|
||||||
|
.input => blk: {
|
||||||
|
if (el.is(Element.Html.Input)) |input| {
|
||||||
|
break :blk switch (input._input_type) {
|
||||||
|
.text, .tel, .url, .email => "textbox",
|
||||||
|
.checkbox => "checkbox",
|
||||||
|
.radio => "radio",
|
||||||
|
.button, .submit, .reset, .image => "button",
|
||||||
|
.range => "slider",
|
||||||
|
.number => "spinbutton",
|
||||||
|
.search => "searchbox",
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break :blk null;
|
||||||
|
},
|
||||||
|
.select => "combobox",
|
||||||
|
.textarea => "textbox",
|
||||||
|
.details => "group",
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getAccessibleName(el: *Element, arena: Allocator) !?[]const u8 {
|
||||||
|
// aria-label
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("aria-label"))) |v| {
|
||||||
|
if (v.len > 0) return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// alt (for img, input[type=image])
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("alt"))) |v| {
|
||||||
|
if (v.len > 0) return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// title
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("title"))) |v| {
|
||||||
|
if (v.len > 0) return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// placeholder
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("placeholder"))) |v| {
|
||||||
|
if (v.len > 0) return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// value (for buttons)
|
||||||
|
if (el.getTag() == .input) {
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("value"))) |v| {
|
||||||
|
if (v.len > 0) return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text content (first non-empty text node, trimmed)
|
||||||
|
return try getTextContent(el.asNode(), arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getTextContent(node: *Node, arena: Allocator) !?[]const u8 {
|
||||||
|
var tw: TreeWalker.FullExcludeSelf = .init(node, .{});
|
||||||
|
|
||||||
|
var arr: std.ArrayList(u8) = .empty;
|
||||||
|
var single_chunk: ?[]const u8 = null;
|
||||||
|
|
||||||
|
while (tw.next()) |child| {
|
||||||
|
// Skip text inside script/style elements.
|
||||||
|
if (child.is(Element)) |el| {
|
||||||
|
switch (el.getTag()) {
|
||||||
|
.script, .style => {
|
||||||
|
tw.skipChildren();
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (child.is(Node.CData)) |cdata| {
|
||||||
|
if (cdata.is(Node.CData.Text)) |text| {
|
||||||
|
const content = std.mem.trim(u8, text.getWholeText(), &std.ascii.whitespace);
|
||||||
|
if (content.len > 0) {
|
||||||
|
if (single_chunk == null and arr.items.len == 0) {
|
||||||
|
single_chunk = content;
|
||||||
|
} else {
|
||||||
|
if (single_chunk) |sc| {
|
||||||
|
try arr.appendSlice(arena, sc);
|
||||||
|
try arr.append(arena, ' ');
|
||||||
|
single_chunk = null;
|
||||||
|
}
|
||||||
|
try arr.appendSlice(arena, content);
|
||||||
|
try arr.append(arena, ' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (single_chunk) |sc| return sc;
|
||||||
|
if (arr.items.len == 0) return null;
|
||||||
|
|
||||||
|
// strip out trailing space
|
||||||
|
return arr.items[0 .. arr.items.len - 1];
|
||||||
|
}
|
||||||
|
fn isDisabled(el: *Element) bool {
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("disabled")) != null) return true;
|
||||||
|
return isDisabledByFieldset(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if an element is disabled by an ancestor <fieldset disabled>.
|
||||||
|
/// Per spec, elements inside the first <legend> child of a disabled fieldset
|
||||||
|
/// are NOT disabled by that fieldset.
|
||||||
|
fn isDisabledByFieldset(el: *Element) bool {
|
||||||
|
const element_node = el.asNode();
|
||||||
|
var current: ?*Node = element_node._parent;
|
||||||
|
while (current) |node| {
|
||||||
|
current = node._parent;
|
||||||
|
const ancestor = node.is(Element) orelse continue;
|
||||||
|
|
||||||
|
if (ancestor.getTag() == .fieldset and ancestor.getAttributeSafe(comptime .wrap("disabled")) != null) {
|
||||||
|
// Check if element is inside the first <legend> child of this fieldset
|
||||||
|
var child = ancestor.firstElementChild();
|
||||||
|
while (child) |c| {
|
||||||
|
if (c.getTag() == .legend) {
|
||||||
|
if (c.asNode().contains(element_node)) return false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
child = c.nextElementSibling();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getInputType(el: *Element) ?[]const u8 {
|
||||||
|
if (el.is(Element.Html.Input)) |input| {
|
||||||
|
return input._input_type.toString();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getInputValue(el: *Element) ?[]const u8 {
|
||||||
|
if (el.is(Element.Html.Input)) |input| {
|
||||||
|
return input.getValue();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all event listener types registered on this target.
|
||||||
|
fn getListenerTypes(target: *EventTarget, listener_targets: ListenerTargetMap) []const []const u8 {
|
||||||
|
if (listener_targets.get(@intFromPtr(target))) |types| return types.items;
|
||||||
|
return &.{};
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
|
||||||
|
fn testInteractive(html: []const u8) ![]InteractiveElement {
|
||||||
|
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);
|
||||||
|
|
||||||
|
return collectInteractiveElements(div.asNode(), page.call_arena, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: button" {
|
||||||
|
const elements = try testInteractive("<button>Click me</button>");
|
||||||
|
try testing.expectEqual(1, elements.len);
|
||||||
|
try testing.expectEqual("button", elements[0].tag_name);
|
||||||
|
try testing.expectEqual("button", elements[0].role.?);
|
||||||
|
try testing.expectEqual("Click me", elements[0].name.?);
|
||||||
|
try testing.expectEqual(InteractivityType.native, elements[0].interactivity_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: anchor with href" {
|
||||||
|
const elements = try testInteractive("<a href=\"/page\">Link</a>");
|
||||||
|
try testing.expectEqual(1, elements.len);
|
||||||
|
try testing.expectEqual("a", elements[0].tag_name);
|
||||||
|
try testing.expectEqual("link", elements[0].role.?);
|
||||||
|
try testing.expectEqual("Link", elements[0].name.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: anchor without href" {
|
||||||
|
const elements = try testInteractive("<a>Not a link</a>");
|
||||||
|
try testing.expectEqual(0, elements.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: input types" {
|
||||||
|
const elements = try testInteractive(
|
||||||
|
\\<input type="text" placeholder="Search">
|
||||||
|
\\<input type="hidden" name="csrf">
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, elements.len);
|
||||||
|
try testing.expectEqual("input", elements[0].tag_name);
|
||||||
|
try testing.expectEqual("text", elements[0].input_type.?);
|
||||||
|
try testing.expectEqual("Search", elements[0].placeholder.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: select and textarea" {
|
||||||
|
const elements = try testInteractive(
|
||||||
|
\\<select name="color"><option>Red</option></select>
|
||||||
|
\\<textarea name="msg"></textarea>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(2, elements.len);
|
||||||
|
try testing.expectEqual("select", elements[0].tag_name);
|
||||||
|
try testing.expectEqual("textarea", elements[1].tag_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: aria role" {
|
||||||
|
const elements = try testInteractive("<div role=\"button\">Custom</div>");
|
||||||
|
try testing.expectEqual(1, elements.len);
|
||||||
|
try testing.expectEqual("div", elements[0].tag_name);
|
||||||
|
try testing.expectEqual("button", elements[0].role.?);
|
||||||
|
try testing.expectEqual(InteractivityType.aria, elements[0].interactivity_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: contenteditable" {
|
||||||
|
const elements = try testInteractive("<div contenteditable=\"true\">Edit me</div>");
|
||||||
|
try testing.expectEqual(1, elements.len);
|
||||||
|
try testing.expectEqual(InteractivityType.contenteditable, elements[0].interactivity_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: tabindex" {
|
||||||
|
const elements = try testInteractive("<div tabindex=\"0\">Focusable</div>");
|
||||||
|
try testing.expectEqual(1, elements.len);
|
||||||
|
try testing.expectEqual(InteractivityType.focusable, elements[0].interactivity_type);
|
||||||
|
try testing.expectEqual(@as(i32, 0), elements[0].tab_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: disabled" {
|
||||||
|
const elements = try testInteractive("<button disabled>Off</button>");
|
||||||
|
try testing.expectEqual(1, elements.len);
|
||||||
|
try testing.expect(elements[0].disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: disabled by fieldset" {
|
||||||
|
const elements = try testInteractive(
|
||||||
|
\\<fieldset disabled>
|
||||||
|
\\ <button>Disabled</button>
|
||||||
|
\\ <legend><button>In legend</button></legend>
|
||||||
|
\\</fieldset>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(2, elements.len);
|
||||||
|
// Button outside legend is disabled by fieldset
|
||||||
|
try testing.expect(elements[0].disabled);
|
||||||
|
// Button inside first legend is NOT disabled
|
||||||
|
try testing.expect(!elements[1].disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: non-interactive div" {
|
||||||
|
const elements = try testInteractive("<div>Just text</div>");
|
||||||
|
try testing.expectEqual(0, elements.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: details and summary" {
|
||||||
|
const elements = try testInteractive("<details><summary>More</summary><p>Content</p></details>");
|
||||||
|
try testing.expectEqual(2, elements.len);
|
||||||
|
try testing.expectEqual("details", elements[0].tag_name);
|
||||||
|
try testing.expectEqual("summary", elements[1].tag_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: mixed elements" {
|
||||||
|
const elements = try testInteractive(
|
||||||
|
\\<div>
|
||||||
|
\\ <a href="/home">Home</a>
|
||||||
|
\\ <p>Some text</p>
|
||||||
|
\\ <button id="btn1">Submit</button>
|
||||||
|
\\ <input type="email" placeholder="Email">
|
||||||
|
\\ <div>Not interactive</div>
|
||||||
|
\\ <div role="tab">Tab</div>
|
||||||
|
\\</div>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(4, elements.len);
|
||||||
|
}
|
||||||
@@ -21,18 +21,46 @@ const js = @import("js.zig");
|
|||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const Array = @This();
|
const Array = @This();
|
||||||
js_arr: v8.Array,
|
|
||||||
context: *js.Context,
|
local: *const js.Local,
|
||||||
|
handle: *const v8.Array,
|
||||||
|
|
||||||
pub fn len(self: Array) usize {
|
pub fn len(self: Array) usize {
|
||||||
return @intCast(self.js_arr.length());
|
return v8.v8__Array__Length(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(self: Array, index: usize) !js.Value {
|
pub fn get(self: Array, index: u32) !js.Value {
|
||||||
const idx_key = v8.Integer.initU32(self.context.isolate, @intCast(index));
|
const ctx = self.local.ctx;
|
||||||
const js_obj = self.js_arr.castTo(v8.Object);
|
|
||||||
|
const idx = js.Integer.init(ctx.isolate.handle, index);
|
||||||
|
const handle = v8.v8__Object__Get(@ptrCast(self.handle), self.local.handle, idx.handle) orelse {
|
||||||
|
return error.JsException;
|
||||||
|
};
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.context = self.context,
|
.local = self.local,
|
||||||
.js_val = try js_obj.getValue(self.context.v8_context, idx_key.toValue()),
|
.handle = handle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(self: Array, index: u32, value: anytype, comptime opts: js.Caller.CallOpts) !bool {
|
||||||
|
const js_value = try self.local.zigValueToJs(value, opts);
|
||||||
|
|
||||||
|
var out: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Object__SetAtIndex(@ptrCast(self.handle), self.local.handle, index, js_value.handle, &out);
|
||||||
|
return out.has_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toObject(self: Array) js.Object {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = @ptrCast(self.handle),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toValue(self: Array) js.Value {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = @ptrCast(self.handle),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
41
src/browser/js/BigInt.zig
Normal file
41
src/browser/js/BigInt.zig
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const BigInt = @This();
|
||||||
|
|
||||||
|
handle: *const v8.Integer,
|
||||||
|
|
||||||
|
pub fn init(isolate: *v8.Isolate, val: anytype) BigInt {
|
||||||
|
const handle = switch (@TypeOf(val)) {
|
||||||
|
i8, i16, i32, i64, isize => v8.v8__BigInt__New(isolate, val).?,
|
||||||
|
u8, u16, u32, u64, usize => v8.v8__BigInt__NewFromUnsigned(isolate, val).?,
|
||||||
|
else => |T| @compileError("cannot create v8::BigInt from: " ++ @typeName(T)),
|
||||||
|
};
|
||||||
|
return .{ .handle = handle };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getInt64(self: BigInt) i64 {
|
||||||
|
return v8.v8__BigInt__Int64Value(self.handle, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getUint64(self: BigInt) u64 {
|
||||||
|
return v8.v8__BigInt__Uint64Value(self.handle, null);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -18,20 +18,35 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const App = @import("../../App.zig");
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
const bridge = @import("bridge.zig");
|
const bridge = @import("bridge.zig");
|
||||||
|
const Origin = @import("Origin.zig");
|
||||||
const Context = @import("Context.zig");
|
const Context = @import("Context.zig");
|
||||||
|
const Isolate = @import("Isolate.zig");
|
||||||
const Platform = @import("Platform.zig");
|
const Platform = @import("Platform.zig");
|
||||||
const Snapshot = @import("Snapshot.zig");
|
const Snapshot = @import("Snapshot.zig");
|
||||||
const Inspector = @import("Inspector.zig");
|
const Inspector = @import("Inspector.zig");
|
||||||
const ExecutionWorld = @import("ExecutionWorld.zig");
|
|
||||||
|
const Page = @import("../Page.zig");
|
||||||
|
const Window = @import("../webapi/Window.zig");
|
||||||
|
|
||||||
const JsApis = bridge.JsApis;
|
const JsApis = bridge.JsApis;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
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,
|
||||||
@@ -41,118 +56,416 @@ const ArenaAllocator = std.heap.ArenaAllocator;
|
|||||||
// The `types` parameter is a tuple of Zig structures we want to bind to V8.
|
// The `types` parameter is a tuple of Zig structures we want to bind to V8.
|
||||||
const Env = @This();
|
const Env = @This();
|
||||||
|
|
||||||
|
app: *App,
|
||||||
|
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
|
|
||||||
platform: *const Platform,
|
platform: *const Platform,
|
||||||
|
|
||||||
// the global isolate
|
// the global isolate
|
||||||
isolate: v8.Isolate,
|
isolate: js.Isolate,
|
||||||
|
|
||||||
|
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,
|
||||||
|
|
||||||
context_id: usize,
|
context_id: usize,
|
||||||
|
|
||||||
// Dynamic slice to avoid circular dependency on JsApis.len at comptime
|
// Maps origin -> shared Origin contains, for v8 values shared across
|
||||||
templates: []v8.FunctionTemplate,
|
// same-origin Contexts. There's a mismatch here between our JS model and our
|
||||||
|
// Browser model. Origins only live as long as the root page of a session exists.
|
||||||
|
// It would be wrong/dangerous to re-use an Origin across root page navigations.
|
||||||
|
|
||||||
|
// Global handles that need to be freed on deinit
|
||||||
|
eternal_function_templates: []v8.Eternal,
|
||||||
|
|
||||||
|
// Dynamic slice to avoid circular dependency on JsApis.len at comptime
|
||||||
|
templates: []*const v8.FunctionTemplate,
|
||||||
|
|
||||||
|
// Global template created once per isolate and reused across all contexts
|
||||||
|
global_template: v8.Eternal,
|
||||||
|
|
||||||
|
// Inspector associated with the Isolate. Exists when CDP is being used.
|
||||||
|
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 {
|
||||||
|
with_inspector: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 snapshot = &app.snapshot;
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot) !Env {
|
|
||||||
var params = try allocator.create(v8.CreateParams);
|
var params = try allocator.create(v8.CreateParams);
|
||||||
errdefer allocator.destroy(params);
|
errdefer allocator.destroy(params);
|
||||||
v8.c.v8__Isolate__CreateParams__CONSTRUCT(params);
|
v8.v8__Isolate__CreateParams__CONSTRUCT(params);
|
||||||
params.snapshot_blob = @ptrCast(&snapshot.startup_data);
|
params.snapshot_blob = @ptrCast(&snapshot.startup_data);
|
||||||
|
|
||||||
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
|
params.array_buffer_allocator = v8.v8__ArrayBuffer__Allocator__NewDefaultAllocator().?;
|
||||||
errdefer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
|
errdefer v8.v8__ArrayBuffer__Allocator__DELETE(params.array_buffer_allocator.?);
|
||||||
|
|
||||||
params.external_references = &snapshot.external_references;
|
params.external_references = &snapshot.external_references;
|
||||||
|
|
||||||
var isolate = v8.Isolate.init(params);
|
var isolate = js.Isolate.init(params);
|
||||||
errdefer isolate.deinit();
|
errdefer isolate.deinit();
|
||||||
|
const isolate_handle = isolate.handle;
|
||||||
|
|
||||||
// This is the callback that runs whenever a module is dynamically imported.
|
v8.v8__Isolate__SetHostImportModuleDynamicallyCallback(isolate_handle, Context.dynamicModuleCallback);
|
||||||
isolate.setHostImportModuleDynamicallyCallback(Context.dynamicModuleCallback);
|
v8.v8__Isolate__SetPromiseRejectCallback(isolate_handle, promiseRejectCallback);
|
||||||
isolate.setPromiseRejectCallback(promiseRejectCallback);
|
v8.v8__Isolate__SetMicrotasksPolicy(isolate_handle, v8.kExplicit);
|
||||||
isolate.setMicrotasksPolicy(v8.c.kExplicit);
|
v8.v8__Isolate__SetFatalErrorHandler(isolate_handle, fatalCallback);
|
||||||
|
v8.v8__Isolate__SetOOMErrorHandler(isolate_handle, oomCallback);
|
||||||
|
|
||||||
isolate.enter();
|
isolate.enter();
|
||||||
errdefer isolate.exit();
|
errdefer isolate.exit();
|
||||||
|
|
||||||
isolate.setHostInitializeImportMetaObjectCallback(Context.metaObjectCallback);
|
v8.v8__Isolate__SetHostInitializeImportMetaObjectCallback(isolate_handle, Context.metaObjectCallback);
|
||||||
|
|
||||||
// Allocate templates array dynamically to avoid comptime dependency on JsApis.len
|
// Allocate arrays dynamically to avoid comptime dependency on JsApis.len
|
||||||
const templates = try allocator.alloc(v8.FunctionTemplate, JsApis.len);
|
const eternal_function_templates = try allocator.alloc(v8.Eternal, JsApis.len);
|
||||||
|
errdefer allocator.free(eternal_function_templates);
|
||||||
|
|
||||||
|
const templates = try allocator.alloc(*const v8.FunctionTemplate, JsApis.len);
|
||||||
errdefer allocator.free(templates);
|
errdefer allocator.free(templates);
|
||||||
|
|
||||||
|
var global_eternal: v8.Eternal = undefined;
|
||||||
|
var private_symbols: PrivateSymbols = undefined;
|
||||||
{
|
{
|
||||||
var temp_scope: v8.HandleScope = undefined;
|
var temp_scope: js.HandleScope = undefined;
|
||||||
v8.HandleScope.init(&temp_scope, isolate);
|
temp_scope.init(isolate);
|
||||||
defer temp_scope.deinit();
|
defer temp_scope.deinit();
|
||||||
const context = v8.Context.init(isolate, null, null);
|
|
||||||
|
|
||||||
context.enter();
|
inline for (JsApis, 0..) |_, i| {
|
||||||
defer context.exit();
|
const data = v8.v8__Isolate__GetDataFromSnapshotOnce(isolate_handle, snapshot.data_start + i);
|
||||||
|
const function_handle: *const v8.FunctionTemplate = @ptrCast(data);
|
||||||
|
// Make function template eternal
|
||||||
|
v8.v8__Eternal__New(isolate_handle, @ptrCast(function_handle), &eternal_function_templates[i]);
|
||||||
|
|
||||||
inline for (JsApis, 0..) |JsApi, i| {
|
// Extract the local handle from the global for easy access
|
||||||
JsApi.Meta.class_id = i;
|
const eternal_ptr = v8.v8__Eternal__Get(&eternal_function_templates[i], isolate_handle);
|
||||||
const data = context.getDataFromSnapshotOnce(snapshot.data_start + i);
|
templates[i] = @ptrCast(@alignCast(eternal_ptr.?));
|
||||||
const function = v8.FunctionTemplate{ .handle = @ptrCast(data) };
|
|
||||||
templates[i] = v8.Persistent(v8.FunctionTemplate).init(isolate, function).castToFunctionTemplate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create global template once per isolate
|
||||||
|
const js_global = v8.v8__FunctionTemplate__New__DEFAULT(isolate_handle);
|
||||||
|
const window_name = v8.v8__String__NewFromUtf8(isolate_handle, "Window", v8.kNormal, 6);
|
||||||
|
v8.v8__FunctionTemplate__SetClassName(js_global, window_name);
|
||||||
|
|
||||||
|
// Find Window in JsApis by name (avoids circular import)
|
||||||
|
const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi);
|
||||||
|
v8.v8__FunctionTemplate__Inherit(js_global, templates[window_index]);
|
||||||
|
|
||||||
|
const global_template_local = v8.v8__FunctionTemplate__InstanceTemplate(js_global).?;
|
||||||
|
v8.v8__ObjectTemplate__SetNamedHandler(global_template_local, &.{
|
||||||
|
.getter = bridge.unknownWindowPropertyCallback,
|
||||||
|
.setter = null,
|
||||||
|
.query = null,
|
||||||
|
.deleter = null,
|
||||||
|
.enumerator = null,
|
||||||
|
.definer = null,
|
||||||
|
.descriptor = null,
|
||||||
|
.data = null,
|
||||||
|
.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);
|
||||||
|
private_symbols = PrivateSymbols.init(isolate_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
var inspector: ?*js.Inspector = null;
|
||||||
|
if (opts.with_inspector) {
|
||||||
|
inspector = try Inspector.init(allocator, isolate_handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
|
.app = app,
|
||||||
.context_id = 0,
|
.context_id = 0,
|
||||||
.isolate = isolate,
|
|
||||||
.platform = platform,
|
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
|
.contexts = undefined,
|
||||||
|
.context_count = 0,
|
||||||
|
.isolate = isolate,
|
||||||
|
.platform = &app.platform,
|
||||||
.templates = templates,
|
.templates = templates,
|
||||||
.isolate_params = params,
|
.isolate_params = params,
|
||||||
|
.inspector = inspector,
|
||||||
|
.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) {
|
||||||
|
std.debug.assert(self.context_count == 0);
|
||||||
|
}
|
||||||
|
for (self.contexts[0..self.context_count]) |ctx| {
|
||||||
|
ctx.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = self.app;
|
||||||
|
const allocator = app.allocator;
|
||||||
|
|
||||||
|
if (self.inspector) |i| {
|
||||||
|
i.deinit(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
allocator.free(self.templates);
|
||||||
|
allocator.free(self.eternal_function_templates);
|
||||||
|
self.private_symbols.deinit();
|
||||||
|
|
||||||
self.isolate.exit();
|
self.isolate.exit();
|
||||||
self.isolate.deinit();
|
self.isolate.deinit();
|
||||||
v8.destroyArrayBufferAllocator(self.isolate_params.array_buffer_allocator.?);
|
v8.v8__ArrayBuffer__Allocator__DELETE(self.isolate_params.array_buffer_allocator.?);
|
||||||
self.allocator.destroy(self.isolate_params);
|
allocator.destroy(self.isolate_params);
|
||||||
self.allocator.free(self.templates);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !Inspector {
|
pub fn createContext(self: *Env, page: *Page) !*Context {
|
||||||
return Inspector.init(arena, self.isolate, ctx);
|
const context_arena = try self.app.arena_pool.acquire();
|
||||||
|
errdefer self.app.arena_pool.release(context_arena);
|
||||||
|
|
||||||
|
const isolate = self.isolate;
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(isolate);
|
||||||
|
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
|
||||||
|
const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&self.global_template, isolate.handle).?));
|
||||||
|
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
|
||||||
|
var context_global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(isolate.handle, v8_context, &context_global);
|
||||||
|
|
||||||
|
// get the global object for the context, this maps to our Window
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMicrotasks(self: *const Env) void {
|
// our window wrapped in a v8::Global
|
||||||
self.isolate.performMicrotasksCheckpoint();
|
var global_global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
|
||||||
|
|
||||||
|
const context_id = self.context_id;
|
||||||
|
self.context_id = context_id + 1;
|
||||||
|
|
||||||
|
const origin = try page._session.getOrCreateOrigin(null);
|
||||||
|
errdefer page._session.releaseOrigin(origin);
|
||||||
|
|
||||||
|
const context = try context_arena.create(Context);
|
||||||
|
context.* = .{
|
||||||
|
.env = self,
|
||||||
|
.page = page,
|
||||||
|
.session = page._session,
|
||||||
|
.origin = origin,
|
||||||
|
.id = context_id,
|
||||||
|
.isolate = isolate,
|
||||||
|
.arena = context_arena,
|
||||||
|
.handle = context_global,
|
||||||
|
.templates = self.templates,
|
||||||
|
.call_arena = page.call_arena,
|
||||||
|
.microtask_queue = microtask_queue,
|
||||||
|
.script_manager = &page._script_manager,
|
||||||
|
.scheduler = .init(context_arena),
|
||||||
|
};
|
||||||
|
try context.origin.identity_map.putNoClobber(origin.arena, @intFromPtr(page.window), global_global);
|
||||||
|
|
||||||
|
// Store a pointer to our context inside the v8 context so that, given
|
||||||
|
// a v8 context, we can get our context out
|
||||||
|
v8.v8__Context__SetAlignedPointerInEmbedderData(v8_context, 1, @ptrCast(context));
|
||||||
|
|
||||||
|
const count = self.context_count;
|
||||||
|
if (count >= self.contexts.len) {
|
||||||
|
return error.TooManyContexts;
|
||||||
|
}
|
||||||
|
self.contexts[count] = context;
|
||||||
|
self.context_count = count + 1;
|
||||||
|
|
||||||
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pumpMessageLoop(self: *const Env) bool {
|
pub fn destroyContext(self: *Env, context: *Context) void {
|
||||||
return self.platform.inner.pumpMessageLoop(self.isolate, false);
|
for (self.contexts[0..self.context_count], 0..) |ctx, i| {
|
||||||
|
if (ctx == context) {
|
||||||
|
// Swap with last element and decrement count
|
||||||
|
self.context_count -= 1;
|
||||||
|
self.contexts[i] = self.contexts[self.context_count];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
@panic("Tried to remove unknown context");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isolate = self.isolate;
|
||||||
|
if (self.inspector) |inspector| {
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
inspector.contextDestroyed(@ptrCast(v8.v8__Global__Get(&context.handle, isolate.handle)));
|
||||||
|
}
|
||||||
|
|
||||||
|
context.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runMicrotasks(self: *Env) void {
|
||||||
|
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 {
|
||||||
|
var ms_to_next_task: ?u64 = null;
|
||||||
|
for (self.contexts[0..self.context_count]) |ctx| {
|
||||||
|
if (comptime builtin.is_test == false) {
|
||||||
|
// 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
|
||||||
|
// underterministic whether a timer will or won't run before the
|
||||||
|
// page shutsdown. But for tests, we need to run them to their end.
|
||||||
|
if (ctx.scheduler.hasReadyTasks() == false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
const entered = ctx.enter(&hs);
|
||||||
|
defer entered.exit();
|
||||||
|
|
||||||
|
const ms = (try ctx.scheduler.run()) orelse continue;
|
||||||
|
if (ms_to_next_task == null or ms < ms_to_next_task.?) {
|
||||||
|
ms_to_next_task = ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ms_to_next_task;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pumpMessageLoop(self: *const 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__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 {
|
||||||
return self.platform.inner.runIdleTasks(self.isolate, 1);
|
v8.v8__Platform__RunIdleTasks(self.platform.handle, self.isolate.handle, 1);
|
||||||
}
|
|
||||||
pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
|
|
||||||
return .{
|
|
||||||
.env = self,
|
|
||||||
.context = null,
|
|
||||||
.context_arena = ArenaAllocator.init(self.allocator),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// V8 doesn't immediately free memory associated with
|
// V8 doesn't immediately free memory associated with
|
||||||
// a Context, it's managed by the garbage collector. We use the
|
// a Context, it's managed by the garbage collector. We use the
|
||||||
// `lowMemoryNotification` call on the isolate to encourage v8 to free
|
// `lowMemoryNotification` call on the isolate to encourage v8 to free
|
||||||
// any contexts which have been freed.
|
// any contexts which have been freed.
|
||||||
|
// This GC is very aggressive. Use memoryPressureNotification for less
|
||||||
|
// aggressive GC passes.
|
||||||
pub fn lowMemoryNotification(self: *Env) void {
|
pub fn lowMemoryNotification(self: *Env) void {
|
||||||
var handle_scope: v8.HandleScope = undefined;
|
var handle_scope: js.HandleScope = undefined;
|
||||||
v8.HandleScope.init(&handle_scope, self.isolate);
|
handle_scope.init(self.isolate);
|
||||||
defer handle_scope.deinit();
|
defer handle_scope.deinit();
|
||||||
self.isolate.lowMemoryNotification();
|
self.isolate.lowMemoryNotification();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// V8 doesn't immediately free memory associated with
|
||||||
|
// a Context, it's managed by the garbage collector. We use the
|
||||||
|
// `memoryPressureNotification` call on the isolate to encourage v8 to free
|
||||||
|
// any contexts which have been freed.
|
||||||
|
// The level indicates the aggressivity of the GC required:
|
||||||
|
// moderate speeds up incremental GC
|
||||||
|
// critical runs one full GC
|
||||||
|
// For a more aggressive GC, use lowMemoryNotification.
|
||||||
|
pub fn memoryPressureNotification(self: *Env, level: Isolate.MemoryPressureLevel) void {
|
||||||
|
var handle_scope: js.HandleScope = undefined;
|
||||||
|
handle_scope.init(self.isolate);
|
||||||
|
defer handle_scope.deinit();
|
||||||
|
self.isolate.memoryPressureNotification(level);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn dumpMemoryStats(self: *Env) void {
|
pub fn dumpMemoryStats(self: *Env) void {
|
||||||
const stats = self.isolate.getHeapStatistics();
|
const stats = self.isolate.getHeapStatistics();
|
||||||
std.debug.print(
|
std.debug.print(
|
||||||
@@ -174,20 +487,58 @@ pub fn dumpMemoryStats(self: *Env) void {
|
|||||||
, .{ stats.total_heap_size, stats.total_heap_size_executable, stats.total_physical_size, stats.total_available_size, stats.used_heap_size, stats.heap_size_limit, stats.malloced_memory, stats.external_memory, stats.peak_malloced_memory, stats.number_of_native_contexts, stats.number_of_detached_contexts, stats.total_global_handles_size, stats.used_global_handles_size, stats.does_zap_garbage });
|
, .{ stats.total_heap_size, stats.total_heap_size_executable, stats.total_physical_size, stats.total_available_size, stats.used_heap_size, stats.heap_size_limit, stats.malloced_memory, stats.external_memory, stats.peak_malloced_memory, stats.number_of_native_contexts, stats.number_of_detached_contexts, stats.total_global_handles_size, stats.used_global_handles_size, stats.does_zap_garbage });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn promiseRejectCallback(v8_msg: v8.C_PromiseRejectMessage) callconv(.c) void {
|
pub fn terminate(self: *const Env) void {
|
||||||
const msg = v8.PromiseRejectMessage.initFromC(v8_msg);
|
v8.v8__Isolate__TerminateExecution(self.isolate.handle);
|
||||||
const isolate = msg.getPromise().toObject().getIsolate();
|
|
||||||
const context = Context.fromIsolate(isolate);
|
|
||||||
|
|
||||||
const value =
|
|
||||||
if (msg.getValue()) |v8_value|
|
|
||||||
context.valueToString(v8_value, .{}) catch |err| @errorName(err)
|
|
||||||
else
|
|
||||||
"no value";
|
|
||||||
|
|
||||||
log.debug(.js, "unhandled rejection", .{
|
|
||||||
.value = value,
|
|
||||||
.stack = context.stackTrace() catch |err| @errorName(err) orelse "???",
|
|
||||||
.note = "This should be updated to call window.unhandledrejection",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
|
||||||
|
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
|
||||||
|
const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
|
||||||
|
const js_isolate = js.Isolate{ .handle = v8_isolate };
|
||||||
|
const ctx = Context.fromIsolate(js_isolate);
|
||||||
|
|
||||||
|
const local = js.Local{
|
||||||
|
.ctx = ctx,
|
||||||
|
.isolate = js_isolate,
|
||||||
|
.handle = v8.v8__Isolate__GetCurrentContext(v8_isolate).?,
|
||||||
|
.call_arena = ctx.call_arena,
|
||||||
|
};
|
||||||
|
|
||||||
|
const page = ctx.page;
|
||||||
|
page.window.unhandledPromiseRejection(.{
|
||||||
|
.local = &local,
|
||||||
|
.handle = &message_handle,
|
||||||
|
}, page) catch |err| {
|
||||||
|
log.warn(.browser, "unhandled rejection handler", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fatalCallback(c_location: [*c]const u8, c_message: [*c]const u8) callconv(.c) void {
|
||||||
|
const location = std.mem.span(c_location);
|
||||||
|
const message = std.mem.span(c_message);
|
||||||
|
log.fatal(.app, "V8 fatal callback", .{ .location = location, .message = message });
|
||||||
|
@import("../../crash_handler.zig").crash("Fatal V8 Error", .{ .location = location, .message = message }, @returnAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oomCallback(c_location: [*c]const u8, details: ?*const v8.OOMDetails) callconv(.c) void {
|
||||||
|
const location = std.mem.span(c_location);
|
||||||
|
const detail = if (details) |d| std.mem.span(d.detail) else "";
|
||||||
|
log.fatal(.app, "V8 OOM", .{ .location = location, .detail = detail });
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
|
|
||||||
const js = @import("js.zig");
|
|
||||||
const v8 = js.v8;
|
|
||||||
|
|
||||||
const Env = @import("Env.zig");
|
|
||||||
const Context = @import("Context.zig");
|
|
||||||
|
|
||||||
const Page = @import("../Page.zig");
|
|
||||||
|
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
|
||||||
|
|
||||||
const CONTEXT_ARENA_RETAIN = 1024 * 64;
|
|
||||||
|
|
||||||
// ExecutionWorld closely models a JS World.
|
|
||||||
// https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#World
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld
|
|
||||||
const ExecutionWorld = @This();
|
|
||||||
|
|
||||||
env: *Env,
|
|
||||||
|
|
||||||
// Arena whose lifetime is for a single page load. Where
|
|
||||||
// the call_arena lives for a single function call, the context_arena
|
|
||||||
// lives for the lifetime of the entire page. The allocator will be
|
|
||||||
// owned by the Context, but the arena itself is owned by the ExecutionWorld
|
|
||||||
// so that we can re-use it from context to context.
|
|
||||||
context_arena: ArenaAllocator,
|
|
||||||
|
|
||||||
// Currently a context maps to a Browser's Page. Here though, it's only a
|
|
||||||
// mechanism to organization page-specific memory. The ExecutionWorld
|
|
||||||
// does all the work, but having all page-specific data structures
|
|
||||||
// grouped together helps keep things clean.
|
|
||||||
context: ?Context = null,
|
|
||||||
|
|
||||||
// no init, must be initialized via env.newExecutionWorld()
|
|
||||||
|
|
||||||
pub fn deinit(self: *ExecutionWorld) void {
|
|
||||||
if (self.context != null) {
|
|
||||||
self.removeContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.context_arena.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only the top Context in the Main ExecutionWorld should hold a handle_scope.
|
|
||||||
// A v8.HandleScope is like an arena. Once created, any "Local" that
|
|
||||||
// v8 creates will be released (or at least, releasable by the v8 GC)
|
|
||||||
// when the handle_scope is freed.
|
|
||||||
// We also maintain our own "context_arena" which allows us to have
|
|
||||||
// all page related memory easily managed.
|
|
||||||
pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context {
|
|
||||||
std.debug.assert(self.context == null);
|
|
||||||
|
|
||||||
const env = self.env;
|
|
||||||
const isolate = env.isolate;
|
|
||||||
const arena = self.context_arena.allocator();
|
|
||||||
|
|
||||||
var v8_context: v8.Context = blk: {
|
|
||||||
var temp_scope: v8.HandleScope = undefined;
|
|
||||||
v8.HandleScope.init(&temp_scope, isolate);
|
|
||||||
defer temp_scope.deinit();
|
|
||||||
|
|
||||||
// Creates a global template that inherits from Window.
|
|
||||||
const global_template = @import("Snapshot.zig").createGlobalTemplate(isolate, env.templates);
|
|
||||||
|
|
||||||
// Add the named property handler
|
|
||||||
global_template.setNamedProperty(v8.NamedPropertyHandlerConfiguration{
|
|
||||||
.getter = unknownPropertyCallback,
|
|
||||||
.flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings,
|
|
||||||
}, null);
|
|
||||||
|
|
||||||
const context_local = v8.Context.init(isolate, global_template, null);
|
|
||||||
const v8_context = v8.Persistent(v8.Context).init(isolate, context_local).castToContext();
|
|
||||||
break :blk v8_context;
|
|
||||||
};
|
|
||||||
|
|
||||||
// For a Page we only create one HandleScope, it is stored in the main World (enter==true). A page can have multple contexts, 1 for each World.
|
|
||||||
// The main Context that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
|
|
||||||
// like isolated Worlds, will thereby place their objects on the main page's HandleScope. Note: In the furure the number of context will multiply multiple frames support
|
|
||||||
var handle_scope: ?v8.HandleScope = null;
|
|
||||||
if (enter) {
|
|
||||||
handle_scope = @as(v8.HandleScope, undefined);
|
|
||||||
v8.HandleScope.init(&handle_scope.?, isolate);
|
|
||||||
v8_context.enter();
|
|
||||||
}
|
|
||||||
errdefer if (enter) {
|
|
||||||
v8_context.exit();
|
|
||||||
handle_scope.?.deinit();
|
|
||||||
};
|
|
||||||
|
|
||||||
const context_id = env.context_id;
|
|
||||||
env.context_id = context_id + 1;
|
|
||||||
|
|
||||||
self.context = Context{
|
|
||||||
.page = page,
|
|
||||||
.id = context_id,
|
|
||||||
.isolate = isolate,
|
|
||||||
.v8_context = v8_context,
|
|
||||||
.templates = env.templates,
|
|
||||||
.handle_scope = handle_scope,
|
|
||||||
.script_manager = &page._script_manager,
|
|
||||||
.call_arena = page.call_arena,
|
|
||||||
.arena = arena,
|
|
||||||
};
|
|
||||||
|
|
||||||
var context = &self.context.?;
|
|
||||||
// Store a pointer to our context inside the v8 context so that, given
|
|
||||||
// a v8 context, we can get our context out
|
|
||||||
const data = isolate.initBigIntU64(@intCast(@intFromPtr(context)));
|
|
||||||
v8_context.setEmbedderData(1, data);
|
|
||||||
|
|
||||||
try context.setupGlobal();
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn removeContext(self: *ExecutionWorld) void {
|
|
||||||
// Force running the micro task to drain the queue before reseting the
|
|
||||||
// context arena.
|
|
||||||
// Tasks in the queue are relying to the arena memory could be present in
|
|
||||||
// the queue. Running them later could lead to invalid memory accesses.
|
|
||||||
self.env.runMicrotasks();
|
|
||||||
|
|
||||||
self.context.?.deinit();
|
|
||||||
self.context = null;
|
|
||||||
_ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn terminateExecution(self: *const ExecutionWorld) void {
|
|
||||||
self.env.isolate.terminateExecution();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resumeExecution(self: *const ExecutionWorld) void {
|
|
||||||
self.env.isolate.cancelTerminateExecution();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn unknownPropertyCallback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
|
||||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
|
||||||
|
|
||||||
const context = Context.fromIsolate(info.getIsolate());
|
|
||||||
const maybe_property: ?[]u8 = context.valueToString(.{ .handle = c_name.? }, .{}) catch null;
|
|
||||||
|
|
||||||
const ignored = std.StaticStringMap(void).initComptime(.{
|
|
||||||
.{ "process", {} },
|
|
||||||
.{ "ShadyDOM", {} },
|
|
||||||
.{ "ShadyCSS", {} },
|
|
||||||
|
|
||||||
.{ "litNonce", {} },
|
|
||||||
.{ "litHtmlVersions", {} },
|
|
||||||
.{ "litElementVersions", {} },
|
|
||||||
.{ "litHtmlPolyfillSupport", {} },
|
|
||||||
.{ "litElementHydrateSupport", {} },
|
|
||||||
.{ "litElementPolyfillSupport", {} },
|
|
||||||
.{ "reactiveElementVersions", {} },
|
|
||||||
|
|
||||||
.{ "recaptcha", {} },
|
|
||||||
.{ "grecaptcha", {} },
|
|
||||||
.{ "___grecaptcha_cfg", {} },
|
|
||||||
.{ "__recaptcha_api", {} },
|
|
||||||
.{ "__google_recaptcha_client", {} },
|
|
||||||
|
|
||||||
.{ "CLOSURE_FLAGS", {} },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (maybe_property) |prop| {
|
|
||||||
if (!ignored.has(prop)) {
|
|
||||||
const page = context.page;
|
|
||||||
const document = page.document;
|
|
||||||
|
|
||||||
if (document.getElementById(prop, page)) |el| {
|
|
||||||
const js_value = context.zigValueToJs(el, .{}) catch {
|
|
||||||
return v8.Intercepted.No;
|
|
||||||
};
|
|
||||||
|
|
||||||
info.getReturnValue().set(js_value);
|
|
||||||
return v8.Intercepted.Yes;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug(.unknown_prop, "unknown global property", .{
|
|
||||||
.info = "but the property can exist in pure JS",
|
|
||||||
.stack = context.stackTrace() catch "???",
|
|
||||||
.property = prop,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return v8.Intercepted.No;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -20,101 +20,91 @@ const std = @import("std");
|
|||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const PersistentFunction = v8.Persistent(v8.Function);
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const Function = @This();
|
const Function = @This();
|
||||||
|
|
||||||
id: usize,
|
local: *const js.Local,
|
||||||
context: *js.Context,
|
this: ?*const v8.Object = null,
|
||||||
this: ?v8.Object = null,
|
handle: *const v8.Function,
|
||||||
func: PersistentFunction,
|
|
||||||
|
|
||||||
pub const Result = struct {
|
pub const Result = struct {
|
||||||
stack: ?[]const u8,
|
stack: ?[]const u8,
|
||||||
exception: []const u8,
|
exception: []const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn getName(self: *const Function, allocator: Allocator) ![]const u8 {
|
|
||||||
const name = self.func.castToFunction().getName();
|
|
||||||
return self.context.valueToString(name, .{ .allocator = allocator });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setName(self: *const Function, name: []const u8) void {
|
|
||||||
const v8_name = v8.String.initUtf8(self.context.isolate, name);
|
|
||||||
self.func.castToFunction().setName(v8_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn withThis(self: *const Function, value: anytype) !Function {
|
pub fn withThis(self: *const Function, value: anytype) !Function {
|
||||||
|
const local = self.local;
|
||||||
const this_obj = if (@TypeOf(value) == js.Object)
|
const this_obj = if (@TypeOf(value) == js.Object)
|
||||||
value.js_obj
|
value.handle
|
||||||
else
|
else
|
||||||
(try self.context.zigValueToJs(value, .{})).castTo(v8.Object);
|
(try local.zigValueToJs(value, .{})).handle;
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.id = self.id,
|
.local = local,
|
||||||
.this = this_obj,
|
.this = this_obj,
|
||||||
.func = self.func,
|
.handle = self.handle,
|
||||||
.context = self.context,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn newInstance(self: *const Function, result: *Result) !js.Object {
|
pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Object {
|
||||||
const context = self.context;
|
const local = self.local;
|
||||||
|
|
||||||
var try_catch: js.TryCatch = undefined;
|
var try_catch: js.TryCatch = undefined;
|
||||||
try_catch.init(context);
|
try_catch.init(local);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
// This creates a new instance using this Function as a constructor.
|
// This creates a new instance using this Function as a constructor.
|
||||||
// This returns a generic Object
|
// const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{}));
|
||||||
const js_obj = self.func.castToFunction().initInstance(context.v8_context, &.{}) orelse {
|
const handle = v8.v8__Function__NewInstance(self.handle, local.handle, 0, null) orelse {
|
||||||
if (try_catch.hasCaught()) {
|
caught.* = try_catch.caughtOrError(local.call_arena, error.Unknown);
|
||||||
const allocator = context.call_arena;
|
|
||||||
result.stack = try_catch.stack(allocator) catch null;
|
|
||||||
result.exception = (try_catch.exception(allocator) catch "???") orelse "???";
|
|
||||||
} else {
|
|
||||||
result.stack = null;
|
|
||||||
result.exception = "???";
|
|
||||||
}
|
|
||||||
return error.JsConstructorFailed;
|
return error.JsConstructorFailed;
|
||||||
};
|
};
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.context = context,
|
.local = local,
|
||||||
.js_obj = js_obj,
|
.handle = handle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
|
pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
|
||||||
return self.callWithThis(T, self.getThis(), args);
|
var caught: js.TryCatch.Caught = undefined;
|
||||||
|
return self._tryCallWithThis(T, self.getThis(), args, &caught, .{}) catch |err| {
|
||||||
|
log.warn(.js, "call caught", .{ .err = err, .caught = caught });
|
||||||
|
return err;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, result: *Result) !T {
|
pub fn callRethrow(self: *const Function, comptime T: type, args: anytype) !T {
|
||||||
return self.tryCallWithThis(T, self.getThis(), args, result);
|
var caught: js.TryCatch.Caught = undefined;
|
||||||
}
|
return self._tryCallWithThis(T, self.getThis(), args, &caught, .{ .rethrow = true }) catch |err| {
|
||||||
|
log.warn(.js, "call caught", .{ .err = err, .caught = caught });
|
||||||
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, result: *Result) !T {
|
|
||||||
var try_catch: js.TryCatch = undefined;
|
|
||||||
try_catch.init(self.context);
|
|
||||||
defer try_catch.deinit();
|
|
||||||
|
|
||||||
return self.callWithThis(T, this, args) catch |err| {
|
|
||||||
if (try_catch.hasCaught()) {
|
|
||||||
const allocator = self.context.call_arena;
|
|
||||||
result.stack = try_catch.stack(allocator) catch null;
|
|
||||||
result.exception = (try_catch.exception(allocator) catch @errorName(err)) orelse @errorName(err);
|
|
||||||
} else {
|
|
||||||
result.stack = null;
|
|
||||||
result.exception = @errorName(err);
|
|
||||||
}
|
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
|
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
|
||||||
const context = self.context;
|
var caught: js.TryCatch.Caught = undefined;
|
||||||
|
return self._tryCallWithThis(T, this, args, &caught, .{}) catch |err| {
|
||||||
|
log.warn(.js, "callWithThis caught", .{ .err = err, .caught = caught });
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, caught: *js.TryCatch.Caught) !T {
|
||||||
|
return self._tryCallWithThis(T, self.getThis(), args, caught, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T {
|
||||||
|
return self._tryCallWithThis(T, this, args, caught, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
const CallOpts = struct {
|
||||||
|
rethrow: bool = false,
|
||||||
|
};
|
||||||
|
fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught, comptime opts: CallOpts) !T {
|
||||||
|
caught.* = .{};
|
||||||
|
const local = self.local;
|
||||||
|
|
||||||
// When we're calling a function from within JavaScript itself, this isn't
|
// When we're calling a function from within JavaScript itself, this isn't
|
||||||
// necessary. We're within a Caller instantiation, which will already have
|
// necessary. We're within a Caller instantiation, which will already have
|
||||||
@@ -125,65 +115,149 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
|
|||||||
// need to increase the call_depth so that the call_arena remains valid for
|
// need to increase the call_depth so that the call_arena remains valid for
|
||||||
// the duration of the function call. If we don't do this, the call_arena
|
// the duration of the function call. If we don't do this, the call_arena
|
||||||
// will be reset after each statement of the function which executes Zig code.
|
// will be reset after each statement of the function which executes Zig code.
|
||||||
const call_depth = context.call_depth;
|
const ctx = local.ctx;
|
||||||
context.call_depth = call_depth + 1;
|
const call_depth = ctx.call_depth;
|
||||||
defer context.call_depth = call_depth;
|
ctx.call_depth = call_depth + 1;
|
||||||
|
defer ctx.call_depth = call_depth;
|
||||||
|
|
||||||
const js_this = blk: {
|
const js_this = blk: {
|
||||||
if (@TypeOf(this) == v8.Object) {
|
if (@TypeOf(this) == js.Object) {
|
||||||
break :blk this;
|
break :blk this;
|
||||||
}
|
}
|
||||||
|
break :blk try local.zigValueToJs(this, .{});
|
||||||
if (@TypeOf(this) == js.Object) {
|
|
||||||
break :blk this.js_obj;
|
|
||||||
}
|
|
||||||
break :blk try context.zigValueToJs(this, .{});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
|
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
|
||||||
|
|
||||||
const js_args: []const v8.Value = switch (@typeInfo(@TypeOf(aargs))) {
|
const js_args: []const *const v8.Value = switch (@typeInfo(@TypeOf(aargs))) {
|
||||||
.@"struct" => |s| blk: {
|
.@"struct" => |s| blk: {
|
||||||
const fields = s.fields;
|
const fields = s.fields;
|
||||||
var js_args: [fields.len]v8.Value = undefined;
|
var js_args: [fields.len]*const v8.Value = undefined;
|
||||||
inline for (fields, 0..) |f, i| {
|
inline for (fields, 0..) |f, i| {
|
||||||
js_args[i] = try context.zigValueToJs(@field(aargs, f.name), .{});
|
js_args[i] = (try local.zigValueToJs(@field(aargs, f.name), .{})).handle;
|
||||||
}
|
}
|
||||||
const cargs: [fields.len]v8.Value = js_args;
|
const cargs: [fields.len]*const v8.Value = js_args;
|
||||||
break :blk &cargs;
|
break :blk &cargs;
|
||||||
},
|
},
|
||||||
.pointer => blk: {
|
.pointer => blk: {
|
||||||
var values = try context.call_arena.alloc(v8.Value, args.len);
|
var values = try local.call_arena.alloc(*const v8.Value, args.len);
|
||||||
for (args, 0..) |a, i| {
|
for (args, 0..) |a, i| {
|
||||||
values[i] = try context.zigValueToJs(a, .{});
|
values[i] = (try local.zigValueToJs(a, .{})).handle;
|
||||||
}
|
}
|
||||||
break :blk values;
|
break :blk values;
|
||||||
},
|
},
|
||||||
else => @compileError("JS Function called with invalid paremter type"),
|
else => @compileError("JS Function called with invalid paremter type"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = self.func.castToFunction().call(context.v8_context, js_this, js_args);
|
const c_args = @as(?[*]const ?*v8.Value, @ptrCast(js_args.ptr));
|
||||||
if (result == null) {
|
|
||||||
// std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"});
|
var try_catch: js.TryCatch = undefined;
|
||||||
return error.JSExecCallback;
|
try_catch.init(local);
|
||||||
|
defer try_catch.deinit();
|
||||||
|
|
||||||
|
const handle = v8.v8__Function__Call(self.handle, local.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse {
|
||||||
|
if ((comptime opts.rethrow) and try_catch.hasCaught()) {
|
||||||
|
try_catch.rethrow();
|
||||||
|
return error.TryCatchRethrow;
|
||||||
|
}
|
||||||
|
caught.* = try_catch.caughtOrError(local.call_arena, error.JsException);
|
||||||
|
return error.JsException;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (@typeInfo(T) == .void) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return local.jsValueToZig(T, .{ .local = local, .handle = handle });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (@typeInfo(T) == .void) return {};
|
fn getThis(self: *const Function) js.Object {
|
||||||
return context.jsValueToZig(T, result.?);
|
const handle = if (self.this) |t| t else v8.v8__Context__Global(self.local.handle).?;
|
||||||
}
|
return .{
|
||||||
|
.local = self.local,
|
||||||
fn getThis(self: *const Function) v8.Object {
|
.handle = handle,
|
||||||
return self.this orelse self.context.v8_context.getGlobal();
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn src(self: *const Function) ![]const u8 {
|
pub fn src(self: *const Function) ![]const u8 {
|
||||||
const value = self.func.castToFunction().toValue();
|
return self.local.valueToString(.{ .local = self.local, .handle = @ptrCast(self.handle) }, .{});
|
||||||
return self.context.valueToString(value, .{});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value {
|
pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value {
|
||||||
const func_obj = self.func.castToFunction().toObject();
|
const local = self.local;
|
||||||
const key = v8.String.initUtf8(self.context.isolate, name);
|
const key = local.isolate.initStringHandle(name);
|
||||||
const value = func_obj.getValue(self.context.v8_context, key) catch return null;
|
const handle = v8.v8__Object__Get(self.handle, self.local.handle, key) orelse {
|
||||||
return self.context.createValue(value);
|
return error.JsException;
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.local = local,
|
||||||
|
.handle = handle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn persist(self: *const Function) !Global {
|
||||||
|
return self._persist(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn temp(self: *const Function) !Temp {
|
||||||
|
return self._persist(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Global else Temp) {
|
||||||
|
var ctx = self.local.ctx;
|
||||||
|
|
||||||
|
var global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
|
if (comptime is_global) {
|
||||||
|
try ctx.trackGlobal(global);
|
||||||
|
return .{ .handle = global, .origin = {} };
|
||||||
|
}
|
||||||
|
try ctx.trackTemp(global);
|
||||||
|
return .{ .handle = global, .origin = ctx.origin };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tempWithThis(self: *const Function, value: anytype) !Temp {
|
||||||
|
const with_this = try self.withThis(value);
|
||||||
|
return with_this.temp();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn persistWithThis(self: *const Function, value: anytype) !Global {
|
||||||
|
const with_this = try self.withThis(value);
|
||||||
|
return with_this.persist();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Temp = G(.temp);
|
||||||
|
pub const Global = G(.global);
|
||||||
|
|
||||||
|
const GlobalType = enum(u8) {
|
||||||
|
temp,
|
||||||
|
global,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn G(comptime global_type: GlobalType) type {
|
||||||
|
return struct {
|
||||||
|
handle: v8.Global,
|
||||||
|
origin: if (global_type == .temp) *js.Origin else void,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
v8.v8__Global__Reset(&self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local(self: *const Self, l: *const js.Local) Function {
|
||||||
|
return .{
|
||||||
|
.local = l,
|
||||||
|
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isEqual(self: *const Self, other: Function) bool {
|
||||||
|
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn release(self: *const Self) void {
|
||||||
|
self.origin.releaseTemp(self.handle);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
40
src/browser/js/HandleScope.zig
Normal file
40
src/browser/js/HandleScope.zig
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const HandleScope = @This();
|
||||||
|
|
||||||
|
handle: v8.HandleScope,
|
||||||
|
|
||||||
|
// V8 takes an address of the value that's passed in, so it needs to be stable.
|
||||||
|
// We can't create the v8.HandleScope here, pass it to v8 and then return the
|
||||||
|
// value, as v8 will then have taken the address of the function-scopped (and no
|
||||||
|
// longer valid) local.
|
||||||
|
pub fn init(self: *HandleScope, isolate: js.Isolate) void {
|
||||||
|
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 {
|
||||||
|
v8.v8__HandleScope__DESTRUCT(&self.handle);
|
||||||
|
}
|
||||||
@@ -20,63 +20,79 @@ const std = @import("std");
|
|||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const Context = @import("Context.zig");
|
const TaggedOpaque = @import("TaggedOpaque.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const CONTEXT_GROUP_ID = 1;
|
||||||
|
const CLIENT_TRUST_LEVEL = 1;
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
|
// Inspector exists for the lifetime of the Isolate/Env. 1 Isolate = 1 Inspector.
|
||||||
|
// It combines the v8.Inspector and the v8.InspectorClientImpl. The v8.InspectorClientImpl
|
||||||
|
// is our own implementation that fulfills the InspectorClient API, i.e. it's the
|
||||||
|
// mechanism v8 provides to let us tweak how the inspector works. For example, it
|
||||||
|
// Below, you'll find a few pub export fn v8_inspector__Client__IMPL__XYZ functions
|
||||||
|
// which is our implementation of what the v8::Inspector requires of our Client
|
||||||
|
// (not much at all)
|
||||||
const Inspector = @This();
|
const Inspector = @This();
|
||||||
|
|
||||||
pub const RemoteObject = v8.RemoteObject;
|
unique_id: i64,
|
||||||
|
isolate: *v8.Isolate,
|
||||||
|
handle: *v8.Inspector,
|
||||||
|
client: *v8.InspectorClientImpl,
|
||||||
|
default_context: ?v8.Global,
|
||||||
|
session: ?Session,
|
||||||
|
|
||||||
isolate: v8.Isolate,
|
pub fn init(allocator: Allocator, isolate: *v8.Isolate) !*Inspector {
|
||||||
inner: *v8.Inspector,
|
const self = try allocator.create(Inspector);
|
||||||
session: v8.InspectorSession,
|
errdefer allocator.destroy(self);
|
||||||
|
|
||||||
// We expect allocator to be an arena
|
self.* = .{
|
||||||
pub fn init(allocator: Allocator, isolate: v8.Isolate, ctx: anytype) !Inspector {
|
.unique_id = 1,
|
||||||
const ContextT = @TypeOf(ctx);
|
.session = null,
|
||||||
|
.isolate = isolate,
|
||||||
const InspectorContainer = switch (@typeInfo(ContextT)) {
|
.client = undefined,
|
||||||
.@"struct" => ContextT,
|
.handle = undefined,
|
||||||
.pointer => |ptr| ptr.child,
|
.default_context = null,
|
||||||
.void => NoopInspector,
|
|
||||||
else => @compileError("invalid context type"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// If necessary, turn a void context into something we can safely ptrCast
|
self.client = v8.v8_inspector__Client__IMPL__CREATE();
|
||||||
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
|
errdefer v8.v8_inspector__Client__IMPL__DELETE(self.client);
|
||||||
|
v8.v8_inspector__Client__IMPL__SET_DATA(self.client, self);
|
||||||
|
|
||||||
const channel = v8.InspectorChannel.init(
|
self.handle = v8.v8_inspector__Inspector__Create(isolate, self.client).?;
|
||||||
safe_context,
|
errdefer v8.v8_inspector__Inspector__DELETE(self.handle);
|
||||||
InspectorContainer.onInspectorResponse,
|
|
||||||
InspectorContainer.onInspectorEvent,
|
|
||||||
InspectorContainer.onRunMessageLoopOnPause,
|
|
||||||
InspectorContainer.onQuitMessageLoopOnPause,
|
|
||||||
isolate,
|
|
||||||
);
|
|
||||||
|
|
||||||
const client = v8.InspectorClient.init();
|
return self;
|
||||||
|
|
||||||
const inner = try allocator.create(v8.Inspector);
|
|
||||||
v8.Inspector.init(inner, client, channel, isolate);
|
|
||||||
return .{ .inner = inner, .isolate = isolate, .session = inner.connect() };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *const Inspector) void {
|
pub fn deinit(self: *const Inspector, allocator: Allocator) void {
|
||||||
self.session.deinit();
|
var hs: v8.HandleScope = undefined;
|
||||||
self.inner.deinit();
|
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate);
|
||||||
|
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||||
|
|
||||||
|
if (self.session) |*s| {
|
||||||
|
s.deinit();
|
||||||
|
}
|
||||||
|
v8.v8_inspector__Client__IMPL__DELETE(self.client);
|
||||||
|
v8.v8_inspector__Inspector__DELETE(self.handle);
|
||||||
|
allocator.destroy(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(self: *const Inspector, msg: []const u8) void {
|
pub fn startSession(self: *Inspector, ctx: anytype) *Session {
|
||||||
// Can't assume the main Context exists (with its HandleScope)
|
if (comptime IS_DEBUG) {
|
||||||
// available when doing this. Pages (and thus the HandleScope)
|
std.debug.assert(self.session == null);
|
||||||
// comes and goes, but CDP can keep sending messages.
|
}
|
||||||
const isolate = self.isolate;
|
|
||||||
var temp_scope: v8.HandleScope = undefined;
|
|
||||||
v8.HandleScope.init(&temp_scope, isolate);
|
|
||||||
defer temp_scope.deinit();
|
|
||||||
|
|
||||||
self.session.dispatchProtocolMessage(isolate, msg);
|
self.session = @as(Session, undefined);
|
||||||
|
Session.init(&self.session.?, self, ctx);
|
||||||
|
return &self.session.?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stopSession(self: *Inspector) void {
|
||||||
|
self.session.?.deinit();
|
||||||
|
self.session = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// From CDP docs
|
// From CDP docs
|
||||||
@@ -88,75 +104,359 @@ pub fn send(self: *const Inspector, msg: []const u8) void {
|
|||||||
// {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
|
// {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
|
||||||
// - is_default_context: Whether the execution context is default, should match the auxData
|
// - is_default_context: Whether the execution context is default, should match the auxData
|
||||||
pub fn contextCreated(
|
pub fn contextCreated(
|
||||||
self: *const Inspector,
|
self: *Inspector,
|
||||||
context: *const Context,
|
local: *const js.Local,
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
origin: []const u8,
|
origin: []const u8,
|
||||||
aux_data: ?[]const u8,
|
aux_data: []const u8,
|
||||||
is_default_context: bool,
|
is_default_context: bool,
|
||||||
) void {
|
) void {
|
||||||
self.inner.contextCreated(context.v8_context, name, origin, aux_data, is_default_context);
|
v8.v8_inspector__Inspector__ContextCreated(
|
||||||
|
self.handle,
|
||||||
|
name.ptr,
|
||||||
|
name.len,
|
||||||
|
origin.ptr,
|
||||||
|
origin.len,
|
||||||
|
aux_data.ptr,
|
||||||
|
aux_data.len,
|
||||||
|
CONTEXT_GROUP_ID,
|
||||||
|
local.handle,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (is_default_context) {
|
||||||
|
self.default_context = local.ctx.handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contextDestroyed(self: *Inspector, context: *const v8.Context) void {
|
||||||
|
v8.v8_inspector__Inspector__ContextDestroyed(self.handle, context);
|
||||||
|
|
||||||
|
if (self.default_context) |*dc| {
|
||||||
|
if (v8.v8__Global__IsEqual(dc, context)) {
|
||||||
|
self.default_context = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resetContextGroup(self: *const Inspector) void {
|
||||||
|
var hs: v8.HandleScope = undefined;
|
||||||
|
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate);
|
||||||
|
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||||
|
|
||||||
|
v8.v8_inspector__Inspector__ResetContextGroup(self.handle, CONTEXT_GROUP_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const RemoteObject = struct {
|
||||||
|
handle: *v8.RemoteObject,
|
||||||
|
|
||||||
|
pub fn deinit(self: RemoteObject) void {
|
||||||
|
v8.v8_inspector__RemoteObject__DELETE(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getType(self: RemoteObject, allocator: Allocator) ![]const u8 {
|
||||||
|
var ctype_: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||||
|
if (!v8.v8_inspector__RemoteObject__getType(self.handle, &allocator, &ctype_)) return error.V8AllocFailed;
|
||||||
|
return cZigStringToString(ctype_) orelse return error.InvalidType;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getSubtype(self: RemoteObject, allocator: Allocator) !?[]const u8 {
|
||||||
|
if (!v8.v8_inspector__RemoteObject__hasSubtype(self.handle)) return null;
|
||||||
|
|
||||||
|
var csubtype: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||||
|
if (!v8.v8_inspector__RemoteObject__getSubtype(self.handle, &allocator, &csubtype)) return error.V8AllocFailed;
|
||||||
|
return cZigStringToString(csubtype);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getClassName(self: RemoteObject, allocator: Allocator) !?[]const u8 {
|
||||||
|
if (!v8.v8_inspector__RemoteObject__hasClassName(self.handle)) return null;
|
||||||
|
|
||||||
|
var cclass_name: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||||
|
if (!v8.v8_inspector__RemoteObject__getClassName(self.handle, &allocator, &cclass_name)) return error.V8AllocFailed;
|
||||||
|
return cZigStringToString(cclass_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getDescription(self: RemoteObject, allocator: Allocator) !?[]const u8 {
|
||||||
|
if (!v8.v8_inspector__RemoteObject__hasDescription(self.handle)) return null;
|
||||||
|
|
||||||
|
var description: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||||
|
if (!v8.v8_inspector__RemoteObject__getDescription(self.handle, &allocator, &description)) return error.V8AllocFailed;
|
||||||
|
return cZigStringToString(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getObjectId(self: RemoteObject, allocator: Allocator) !?[]const u8 {
|
||||||
|
if (!v8.v8_inspector__RemoteObject__hasObjectId(self.handle)) return null;
|
||||||
|
|
||||||
|
var cobject_id: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||||
|
if (!v8.v8_inspector__RemoteObject__getObjectId(self.handle, &allocator, &cobject_id)) return error.V8AllocFailed;
|
||||||
|
return cZigStringToString(cobject_id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Combines a v8::InspectorSession and a v8::InspectorChannelImpl. The
|
||||||
|
// InspectorSession is for zig -> v8 (sending messages to the inspector). The
|
||||||
|
// Channel is for v8 -> zig, getting events from the Inspector (that we'll pass
|
||||||
|
// back ot some opaque context, i.e the CDP BrowserContext).
|
||||||
|
// The channel callbacks are defined below, as:
|
||||||
|
// pub export fn v8_inspector__Channel__IMPL__XYZ
|
||||||
|
pub const Session = struct {
|
||||||
|
inspector: *Inspector,
|
||||||
|
handle: *v8.InspectorSession,
|
||||||
|
channel: *v8.InspectorChannelImpl,
|
||||||
|
|
||||||
|
// callbacks
|
||||||
|
ctx: *anyopaque,
|
||||||
|
onNotif: *const fn (ctx: *anyopaque, msg: []const u8) void,
|
||||||
|
onResp: *const fn (ctx: *anyopaque, call_id: u32, msg: []const u8) void,
|
||||||
|
|
||||||
|
fn init(self: *Session, inspector: *Inspector, ctx: anytype) void {
|
||||||
|
const Container = @typeInfo(@TypeOf(ctx)).pointer.child;
|
||||||
|
|
||||||
|
const channel = v8.v8_inspector__Channel__IMPL__CREATE(inspector.isolate);
|
||||||
|
const handle = v8.v8_inspector__Inspector__Connect(
|
||||||
|
inspector.handle,
|
||||||
|
CONTEXT_GROUP_ID,
|
||||||
|
channel,
|
||||||
|
CLIENT_TRUST_LEVEL,
|
||||||
|
).?;
|
||||||
|
v8.v8_inspector__Channel__IMPL__SET_DATA(channel, self);
|
||||||
|
|
||||||
|
self.* = .{
|
||||||
|
.ctx = ctx,
|
||||||
|
.handle = handle,
|
||||||
|
.channel = channel,
|
||||||
|
.inspector = inspector,
|
||||||
|
.onResp = Container.onInspectorResponse,
|
||||||
|
.onNotif = Container.onInspectorEvent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(self: *const Session) void {
|
||||||
|
v8.v8_inspector__Session__DELETE(self.handle);
|
||||||
|
v8.v8_inspector__Channel__IMPL__DELETE(self.channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(self: *const Session, msg: []const u8) void {
|
||||||
|
const isolate = self.inspector.isolate;
|
||||||
|
var hs: v8.HandleScope = undefined;
|
||||||
|
v8.v8__HandleScope__CONSTRUCT(&hs, isolate);
|
||||||
|
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||||
|
|
||||||
|
v8.v8_inspector__Session__dispatchProtocolMessage(
|
||||||
|
self.handle,
|
||||||
|
isolate,
|
||||||
|
msg.ptr,
|
||||||
|
msg.len,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets a value by object ID regardless of which context it is in.
|
||||||
|
// Our TaggedOpaque stores the "resolved" ptr value (the most specific _type,
|
||||||
|
// e.g. we store the ptr to the Div not the EventTarget). But, this is asking for
|
||||||
|
// the pointer to the Node, so we need to use the same resolution mechanism which
|
||||||
|
// is used when we're calling a function to turn the Div into a Node, which is
|
||||||
|
// what TaggedOpaque.fromJS does.
|
||||||
|
pub fn getNodePtr(self: *const Session, allocator: Allocator, object_id: []const u8, local: *js.Local) !*anyopaque {
|
||||||
|
// just to indicate that the caller is responsible for ensuring there's a local environment
|
||||||
|
_ = local;
|
||||||
|
|
||||||
|
const unwrapped = try self.unwrapObject(allocator, object_id);
|
||||||
|
// The values context and groupId are not used here
|
||||||
|
const js_val = unwrapped.value;
|
||||||
|
if (!v8.v8__Value__IsObject(js_val)) {
|
||||||
|
return error.ObjectIdIsNotANode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Node = @import("../webapi/Node.zig");
|
||||||
|
// Cast to *const v8.Object for typeTaggedAnyOpaque
|
||||||
|
return TaggedOpaque.fromJS(*Node, @ptrCast(js_val)) catch return error.ObjectIdIsNotANode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieves the RemoteObject for a given value.
|
// Retrieves the RemoteObject for a given value.
|
||||||
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
|
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
|
||||||
// just like a method return value. Therefore, if we've mapped this
|
// just like a method return value. Therefore, if we've mapped this
|
||||||
// value before, we'll get the existing JS PersistedObject and if not
|
// value before, we'll get the existing js.Global(js.Object) and if not
|
||||||
// we'll create it and track it for cleanup when the context ends.
|
// we'll create it and track it for cleanup when the context ends.
|
||||||
pub fn getRemoteObject(
|
pub fn getRemoteObject(
|
||||||
self: *const Inspector,
|
self: *const Session,
|
||||||
context: *Context,
|
local: *const js.Local,
|
||||||
group: []const u8,
|
group: []const u8,
|
||||||
value: anytype,
|
value: anytype,
|
||||||
) !RemoteObject {
|
) !RemoteObject {
|
||||||
const js_value = try context.zigValueToJs(value, .{});
|
const js_val = try local.zigValueToJs(value, .{});
|
||||||
|
|
||||||
// We do not want to expose this as a parameter for now
|
// We do not want to expose this as a parameter for now
|
||||||
const generate_preview = false;
|
const generate_preview = false;
|
||||||
return self.session.wrapObject(
|
return self.wrapObject(
|
||||||
context.isolate,
|
local.isolate.handle,
|
||||||
context.v8_context,
|
local.handle,
|
||||||
js_value,
|
js_val.handle,
|
||||||
group,
|
group,
|
||||||
generate_preview,
|
generate_preview,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets a value by object ID regardless of which context it is in.
|
fn wrapObject(
|
||||||
// Our TaggedAnyOpaque stores the "resolved" ptr value (the most specific _type,
|
self: Session,
|
||||||
// e.g. we store the ptr to the Div not the EventTarget). But, this is asking for
|
isolate: *v8.Isolate,
|
||||||
// the pointer to the Node, so we need to use the same resolution mechanism which
|
ctx: *const v8.Context,
|
||||||
// is used when we're calling a function to turn the Div into a Node, which is
|
val: *const v8.Value,
|
||||||
// what Context.typeTaggedAnyOpaque does.
|
grpname: []const u8,
|
||||||
pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8) !*anyopaque {
|
generatepreview: bool,
|
||||||
const unwrapped = try self.session.unwrapObject(allocator, object_id);
|
) !RemoteObject {
|
||||||
// The values context and groupId are not used here
|
const remote_object = v8.v8_inspector__Session__wrapObject(
|
||||||
const js_val = unwrapped.value;
|
self.handle,
|
||||||
if (js_val.isObject() == false) {
|
isolate,
|
||||||
return error.ObjectIdIsNotANode;
|
ctx,
|
||||||
}
|
val,
|
||||||
const Node = @import("../webapi/Node.zig");
|
grpname.ptr,
|
||||||
return Context.typeTaggedAnyOpaque(*Node, js_val.castTo(v8.Object)) catch {
|
grpname.len,
|
||||||
return error.ObjectIdIsNotANode;
|
generatepreview,
|
||||||
};
|
).?;
|
||||||
|
return .{ .handle = remote_object };
|
||||||
}
|
}
|
||||||
|
|
||||||
const NoopInspector = struct {
|
fn unwrapObject(
|
||||||
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
|
self: Session,
|
||||||
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
|
allocator: Allocator,
|
||||||
pub fn onRunMessageLoopOnPause(_: *anyopaque, _: u32) void {}
|
object_id: []const u8,
|
||||||
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {}
|
) !UnwrappedObject {
|
||||||
|
const in_object_id = v8.CZigString{
|
||||||
|
.ptr = object_id.ptr,
|
||||||
|
.len = object_id.len,
|
||||||
|
};
|
||||||
|
var out_error: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||||
|
var out_value_handle: ?*v8.Value = null;
|
||||||
|
var out_context_handle: ?*v8.Context = null;
|
||||||
|
var out_object_group: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||||
|
|
||||||
|
const result = v8.v8_inspector__Session__unwrapObject(
|
||||||
|
self.handle,
|
||||||
|
&allocator,
|
||||||
|
&out_error,
|
||||||
|
in_object_id,
|
||||||
|
&out_value_handle,
|
||||||
|
&out_context_handle,
|
||||||
|
&out_object_group,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
const error_str = cZigStringToString(out_error) orelse return error.UnwrapFailed;
|
||||||
|
std.log.err("unwrapObject failed: {s}", .{error_str});
|
||||||
|
return error.UnwrapFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.value = out_value_handle.?,
|
||||||
|
.context = out_context_handle.?,
|
||||||
|
.object_group = cZigStringToString(out_object_group),
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque {
|
const UnwrappedObject = struct {
|
||||||
if (value.isObject() == false) {
|
value: *const v8.Value,
|
||||||
|
context: *const v8.Context,
|
||||||
|
object_group: ?[]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn getTaggedOpaque(value: *const v8.Value) ?*TaggedOpaque {
|
||||||
|
if (!v8.v8__Value__IsObject(value)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const obj = value.castTo(v8.Object);
|
const internal_field_count = v8.v8__Object__InternalFieldCount(value);
|
||||||
if (obj.internalFieldCount() == 0) {
|
if (internal_field_count == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const external_data = obj.getInternalField(0).castTo(v8.External).get().?;
|
const tao_ptr = v8.v8__Object__GetAlignedPointerFromInternalField(value, 0).?;
|
||||||
return @ptrCast(@alignCast(external_data));
|
return @ptrCast(@alignCast(tao_ptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cZigStringToString(s: v8.CZigString) ?[]const u8 {
|
||||||
|
if (s.ptr == null) return null;
|
||||||
|
return s.ptr[0..s.len];
|
||||||
|
}
|
||||||
|
|
||||||
|
// C export functions for Inspector callbacks
|
||||||
|
pub export fn v8_inspector__Client__IMPL__generateUniqueId(
|
||||||
|
_: *v8.InspectorClientImpl,
|
||||||
|
data: *anyopaque,
|
||||||
|
) callconv(.c) i64 {
|
||||||
|
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
||||||
|
const unique_id = inspector.unique_id + 1;
|
||||||
|
inspector.unique_id = unique_id;
|
||||||
|
return unique_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub export fn v8_inspector__Client__IMPL__runMessageLoopOnPause(
|
||||||
|
_: *v8.InspectorClientImpl,
|
||||||
|
data: *anyopaque,
|
||||||
|
context_group_id: c_int,
|
||||||
|
) callconv(.c) void {
|
||||||
|
_ = data;
|
||||||
|
_ = context_group_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub export fn v8_inspector__Client__IMPL__quitMessageLoopOnPause(
|
||||||
|
_: *v8.InspectorClientImpl,
|
||||||
|
data: *anyopaque,
|
||||||
|
) callconv(.c) void {
|
||||||
|
_ = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub export fn v8_inspector__Client__IMPL__runIfWaitingForDebugger(
|
||||||
|
_: *v8.InspectorClientImpl,
|
||||||
|
_: *anyopaque,
|
||||||
|
_: c_int,
|
||||||
|
) callconv(.c) void {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
pub export fn v8_inspector__Client__IMPL__consoleAPIMessage(
|
||||||
|
_: *v8.InspectorClientImpl,
|
||||||
|
_: *anyopaque,
|
||||||
|
_: c_int,
|
||||||
|
_: v8.MessageErrorLevel,
|
||||||
|
_: *v8.StringView,
|
||||||
|
_: *v8.StringView,
|
||||||
|
_: c_uint,
|
||||||
|
_: c_uint,
|
||||||
|
_: *v8.StackTrace,
|
||||||
|
) callconv(.c) void {}
|
||||||
|
|
||||||
|
pub export fn v8_inspector__Client__IMPL__ensureDefaultContextInGroup(
|
||||||
|
_: *v8.InspectorClientImpl,
|
||||||
|
data: *anyopaque,
|
||||||
|
) callconv(.c) ?*const v8.Context {
|
||||||
|
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
||||||
|
const global_handle = inspector.default_context orelse return null;
|
||||||
|
return v8.v8__Global__Get(&global_handle, inspector.isolate);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub export fn v8_inspector__Channel__IMPL__sendResponse(
|
||||||
|
_: *v8.InspectorChannelImpl,
|
||||||
|
data: *anyopaque,
|
||||||
|
call_id: c_int,
|
||||||
|
msg: [*c]u8,
|
||||||
|
length: usize,
|
||||||
|
) callconv(.c) void {
|
||||||
|
const session: *Session = @ptrCast(@alignCast(data));
|
||||||
|
session.onResp(session.ctx, @intCast(call_id), msg[0..length]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub export fn v8_inspector__Channel__IMPL__sendNotification(
|
||||||
|
_: *v8.InspectorChannelImpl,
|
||||||
|
data: *anyopaque,
|
||||||
|
msg: [*c]u8,
|
||||||
|
length: usize,
|
||||||
|
) callconv(.c) void {
|
||||||
|
const session: *Session = @ptrCast(@alignCast(data));
|
||||||
|
session.onNotif(session.ctx, msg[0..length]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub export fn v8_inspector__Channel__IMPL__flushProtocolNotifications(
|
||||||
|
_: *v8.InspectorChannelImpl,
|
||||||
|
_: *anyopaque,
|
||||||
|
) callconv(.c) void {
|
||||||
|
// TODO
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,22 +19,17 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
|
|
||||||
// This only exists so that we know whether a function wants the opaque
|
const v8 = js.v8;
|
||||||
// JS argument (js.Object), or if it wants the receiver as an opaque
|
|
||||||
// value.
|
|
||||||
// js.Object is normally used when a method wants an opaque JS object
|
|
||||||
// that it'll pass into a callback.
|
|
||||||
// This is used when the function wants to do advanced manipulation
|
|
||||||
// of the v8.Object bound to the instance. For example, postAttach is an
|
|
||||||
// example of using This.
|
|
||||||
|
|
||||||
const This = @This();
|
const Integer = @This();
|
||||||
obj: js.Object,
|
|
||||||
|
|
||||||
pub fn setIndex(self: This, index: u32, value: anytype, opts: js.Object.SetOpts) !void {
|
handle: *const v8.Integer,
|
||||||
return self.obj.setIndex(index, value, opts);
|
|
||||||
}
|
pub fn init(isolate: *v8.Isolate, value: anytype) Integer {
|
||||||
|
const handle = switch (@TypeOf(value)) {
|
||||||
pub fn set(self: This, key: []const u8, value: anytype, opts: js.Object.SetOpts) !void {
|
i8, i16, i32 => v8.v8__Integer__New(isolate, value).?,
|
||||||
return self.obj.set(key, value, opts);
|
u8, u16, u32 => v8.v8__Integer__NewFromUnsigned(isolate, value).?,
|
||||||
|
else => |T| @compileError("cannot create v8::Integer from: " ++ @typeName(T)),
|
||||||
|
};
|
||||||
|
return .{ .handle = handle };
|
||||||
}
|
}
|
||||||
116
src/browser/js/Isolate.zig
Normal file
116
src/browser/js/Isolate.zig
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Isolate = @This();
|
||||||
|
|
||||||
|
handle: *v8.Isolate,
|
||||||
|
|
||||||
|
pub fn init(params: *v8.CreateParams) Isolate {
|
||||||
|
return .{
|
||||||
|
.handle = v8.v8__Isolate__New(params).?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: Isolate) void {
|
||||||
|
v8.v8__Isolate__Dispose(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enter(self: Isolate) void {
|
||||||
|
v8.v8__Isolate__Enter(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exit(self: Isolate) void {
|
||||||
|
v8.v8__Isolate__Exit(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lowMemoryNotification(self: Isolate) void {
|
||||||
|
v8.v8__Isolate__LowMemoryNotification(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const MemoryPressureLevel = enum(u32) {
|
||||||
|
none = v8.kNone,
|
||||||
|
moderate = v8.kModerate,
|
||||||
|
critical = v8.kCritical,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn memoryPressureNotification(self: Isolate, level: MemoryPressureLevel) void {
|
||||||
|
v8.v8__Isolate__MemoryPressureNotification(self.handle, @intFromEnum(level));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notifyContextDisposed(self: Isolate) void {
|
||||||
|
_ = v8.v8__Isolate__ContextDisposedNotification(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getHeapStatistics(self: Isolate) v8.HeapStatistics {
|
||||||
|
var res: v8.HeapStatistics = undefined;
|
||||||
|
v8.v8__Isolate__GetHeapStatistics(self.handle, &res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn throwException(self: Isolate, value: *const v8.Value) *const v8.Value {
|
||||||
|
return v8.v8__Isolate__ThrowException(self.handle, value).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initStringHandle(self: Isolate, str: []const u8) *const v8.String {
|
||||||
|
return v8.v8__String__NewFromUtf8(self.handle, str.ptr, v8.kNormal, @as(c_int, @intCast(str.len))).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
|
const message = self.initStringHandle(msg);
|
||||||
|
return v8.v8__Exception__Error(message).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createTypeError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
|
const message = self.initStringHandle(msg);
|
||||||
|
return v8.v8__Exception__TypeError(message).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initNull(self: Isolate) *const v8.Value {
|
||||||
|
return v8.v8__Null(self.handle).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initUndefined(self: Isolate) *const v8.Value {
|
||||||
|
return v8.v8__Undefined(self.handle).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initFalse(self: Isolate) *const v8.Value {
|
||||||
|
return v8.v8__False(self.handle).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initTrue(self: Isolate) *const v8.Value {
|
||||||
|
return v8.v8__True(self.handle).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initInteger(self: Isolate, val: anytype) js.Integer {
|
||||||
|
return js.Integer.init(self.handle, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initBigInt(self: Isolate, val: anytype) js.BigInt {
|
||||||
|
return js.BigInt.init(self.handle, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initNumber(self: Isolate, val: anytype) js.Number {
|
||||||
|
return js.Number.init(self.handle, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createExternal(self: Isolate, val: *anyopaque) *const v8.External {
|
||||||
|
return v8.v8__External__New(self.handle, val).?;
|
||||||
|
}
|
||||||
1417
src/browser/js/Local.zig
Normal file
1417
src/browser/js/Local.zig
Normal file
File diff suppressed because it is too large
Load Diff
137
src/browser/js/Module.zig
Normal file
137
src/browser/js/Module.zig
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Module = @This();
|
||||||
|
|
||||||
|
local: *const js.Local,
|
||||||
|
handle: *const v8.Module,
|
||||||
|
|
||||||
|
pub const Status = enum(u32) {
|
||||||
|
kUninstantiated = v8.kUninstantiated,
|
||||||
|
kInstantiating = v8.kInstantiating,
|
||||||
|
kInstantiated = v8.kInstantiated,
|
||||||
|
kEvaluating = v8.kEvaluating,
|
||||||
|
kEvaluated = v8.kEvaluated,
|
||||||
|
kErrored = v8.kErrored,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn getStatus(self: Module) Status {
|
||||||
|
return @enumFromInt(v8.v8__Module__GetStatus(self.handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getException(self: Module) js.Value {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = v8.v8__Module__GetException(self.handle).?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getModuleRequests(self: Module) Requests {
|
||||||
|
return .{
|
||||||
|
.context_handle = self.local.handle,
|
||||||
|
.handle = v8.v8__Module__GetModuleRequests(self.handle).?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn instantiate(self: Module, cb: v8.ResolveModuleCallback) !bool {
|
||||||
|
var out: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Module__InstantiateModule(self.handle, self.local.handle, cb, &out);
|
||||||
|
if (out.has_value) {
|
||||||
|
return out.value;
|
||||||
|
}
|
||||||
|
return error.JsException;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn evaluate(self: Module) !js.Value {
|
||||||
|
const res = v8.v8__Module__Evaluate(self.handle, self.local.handle) orelse return error.JsException;
|
||||||
|
|
||||||
|
if (self.getStatus() == .kErrored) {
|
||||||
|
return error.JsException;
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = res,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getIdentityHash(self: Module) u32 {
|
||||||
|
return @bitCast(v8.v8__Module__GetIdentityHash(self.handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getModuleNamespace(self: Module) js.Value {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = v8.v8__Module__GetModuleNamespace(self.handle).?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getScriptId(self: Module) u32 {
|
||||||
|
return @intCast(v8.v8__Module__ScriptId(self.handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn persist(self: Module) !Global {
|
||||||
|
var ctx = self.local.ctx;
|
||||||
|
var global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
|
try ctx.global_modules.append(ctx.arena, global);
|
||||||
|
return .{ .handle = global };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 js.Local) Module {
|
||||||
|
return .{
|
||||||
|
.local = l,
|
||||||
|
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isEqual(self: *const Global, other: Module) bool {
|
||||||
|
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Requests = struct {
|
||||||
|
handle: *const v8.FixedArray,
|
||||||
|
context_handle: *const v8.Context,
|
||||||
|
|
||||||
|
pub fn len(self: Requests) usize {
|
||||||
|
return @intCast(v8.v8__FixedArray__Length(self.handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: Requests, idx: usize) Request {
|
||||||
|
return .{ .handle = v8.v8__FixedArray__Get(self.handle, self.context_handle, @intCast(idx)).? };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Request = struct {
|
||||||
|
handle: *const v8.ModuleRequest,
|
||||||
|
|
||||||
|
pub fn specifier(self: Request, local: *const js.Local) js.String {
|
||||||
|
return .{ .local = local, .handle = v8.v8__ModuleRequest__GetSpecifier(self.handle).? };
|
||||||
|
}
|
||||||
|
};
|
||||||
31
src/browser/js/Number.zig
Normal file
31
src/browser/js/Number.zig
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Number = @This();
|
||||||
|
|
||||||
|
handle: *const v8.Number,
|
||||||
|
|
||||||
|
pub fn init(isolate: *v8.Isolate, value: anytype) Number {
|
||||||
|
const handle = v8.v8__Number__New(isolate, value).?;
|
||||||
|
return .{ .handle = handle };
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -22,103 +22,102 @@ const v8 = js.v8;
|
|||||||
|
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
const Context = @import("Context.zig");
|
|
||||||
const PersistentObject = v8.Persistent(v8.Object);
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const Object = @This();
|
const Object = @This();
|
||||||
js_obj: v8.Object,
|
|
||||||
context: *js.Context,
|
|
||||||
|
|
||||||
pub fn getId(self: Object) u32 {
|
local: *const js.Local,
|
||||||
return self.js_obj.getIdentityHash();
|
handle: *const v8.Object,
|
||||||
|
|
||||||
|
pub fn has(self: Object, key: anytype) bool {
|
||||||
|
const ctx = self.local.ctx;
|
||||||
|
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
|
||||||
|
|
||||||
|
var out: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Object__Has(self.handle, self.local.handle, key_handle, &out);
|
||||||
|
if (out.has_value) {
|
||||||
|
return out.value;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const SetOpts = packed struct(u32) {
|
pub fn get(self: Object, key: anytype) !js.Value {
|
||||||
READ_ONLY: bool = false,
|
const ctx = self.local.ctx;
|
||||||
DONT_ENUM: bool = false,
|
|
||||||
DONT_DELETE: bool = false,
|
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
|
||||||
_: u29 = 0,
|
const js_val_handle = v8.v8__Object__Get(self.handle, self.local.handle, key_handle) orelse return error.JsException;
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = js_val_handle,
|
||||||
};
|
};
|
||||||
pub fn setIndex(self: Object, index: u32, value: anytype, opts: SetOpts) !void {
|
}
|
||||||
@setEvalBranchQuota(10000);
|
|
||||||
const key = switch (index) {
|
pub fn set(self: Object, key: anytype, value: anytype, comptime opts: js.Caller.CallOpts) !bool {
|
||||||
inline 0...20 => |i| std.fmt.comptimePrint("{d}", .{i}),
|
const ctx = self.local.ctx;
|
||||||
else => try std.fmt.allocPrint(self.context.arena, "{d}", .{index}),
|
|
||||||
|
const js_value = try self.local.zigValueToJs(value, opts);
|
||||||
|
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
|
||||||
|
|
||||||
|
var out: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Object__Set(self.handle, self.local.handle, key_handle, js_value.handle, &out);
|
||||||
|
return out.has_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn defineOwnProperty(self: Object, name: []const u8, value: js.Value, attr: v8.PropertyAttribute) ?bool {
|
||||||
|
const ctx = self.local.ctx;
|
||||||
|
const name_handle = ctx.isolate.initStringHandle(name);
|
||||||
|
|
||||||
|
var out: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Object__DefineOwnProperty(self.handle, self.local.handle, @ptrCast(name_handle), value.handle, attr, &out);
|
||||||
|
|
||||||
|
if (out.has_value) {
|
||||||
|
return out.value;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toValue(self: Object) js.Value {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = @ptrCast(self.handle),
|
||||||
};
|
};
|
||||||
return self.set(key, value, opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set(self: Object, key: []const u8, value: anytype, opts: SetOpts) error{ FailedToSet, OutOfMemory }!void {
|
|
||||||
const context = self.context;
|
|
||||||
|
|
||||||
const js_key = v8.String.initUtf8(context.isolate, key);
|
|
||||||
const js_value = try context.zigValueToJs(value, .{});
|
|
||||||
|
|
||||||
const res = self.js_obj.defineOwnProperty(context.v8_context, js_key.toName(), js_value, @bitCast(opts)) orelse false;
|
|
||||||
if (!res) {
|
|
||||||
return error.FailedToSet;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(self: Object, key: []const u8) !js.Value {
|
|
||||||
const context = self.context;
|
|
||||||
const js_key = v8.String.initUtf8(context.isolate, key);
|
|
||||||
const js_val = try self.js_obj.getValue(context.v8_context, js_key);
|
|
||||||
return context.createValue(js_val);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isTruthy(self: Object) bool {
|
|
||||||
const js_value = self.js_obj.toValue();
|
|
||||||
return js_value.toBool(self.context.isolate);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toString(self: Object) ![]const u8 {
|
|
||||||
const js_value = self.js_obj.toValue();
|
|
||||||
return self.context.valueToString(js_value, .{});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format(self: Object, writer: *std.Io.Writer) !void {
|
pub fn format(self: Object, writer: *std.Io.Writer) !void {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
return self.context.debugValue(self.js_obj.toValue(), writer);
|
return self.local.ctx.debugValue(self.toValue(), writer);
|
||||||
}
|
}
|
||||||
const str = self.toString() catch return error.WriteFailed;
|
const str = self.toString() catch return error.WriteFailed;
|
||||||
return writer.writeAll(str);
|
return writer.writeAll(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toJson(self: Object, allocator: Allocator) ![]u8 {
|
pub fn persist(self: Object) !Global {
|
||||||
const json_string = try v8.Json.stringify(self.context.v8_context, self.js_obj.toValue(), null);
|
var ctx = self.local.ctx;
|
||||||
const str = try self.context.jsStringToZig(json_string, .{ .allocator = allocator });
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn persist(self: Object) !Object {
|
var global: v8.Global = undefined;
|
||||||
var context = self.context;
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
const js_obj = self.js_obj;
|
|
||||||
|
|
||||||
const persisted = PersistentObject.init(context.isolate, js_obj);
|
try ctx.trackGlobal(global);
|
||||||
try context.js_object_list.append(context.arena, persisted);
|
|
||||||
|
|
||||||
return .{
|
return .{ .handle = global };
|
||||||
.context = context,
|
|
||||||
.js_obj = persisted.castToObject(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getFunction(self: Object, name: []const u8) !?js.Function {
|
pub fn getFunction(self: Object, name: []const u8) !?js.Function {
|
||||||
if (self.isNullOrUndefined()) {
|
if (self.isNullOrUndefined()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const context = self.context;
|
const local = self.local;
|
||||||
|
|
||||||
const js_name = v8.String.initUtf8(context.isolate, name);
|
const js_name = local.isolate.initStringHandle(name);
|
||||||
|
const js_val_handle = v8.v8__Object__Get(self.handle, local.handle, js_name) orelse return error.JsException;
|
||||||
|
|
||||||
const js_value = try self.js_obj.getValue(context.v8_context, js_name.toName());
|
if (v8.v8__Value__IsFunction(js_val_handle) == false) {
|
||||||
if (!js_value.isFunction()) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return try context.createFunction(js_value);
|
return .{
|
||||||
|
.local = local,
|
||||||
|
.handle = @ptrCast(js_val_handle),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn callMethod(self: Object, comptime T: type, method_name: []const u8, args: anytype) !T {
|
pub fn callMethod(self: Object, comptime T: type, method_name: []const u8, args: anytype) !T {
|
||||||
@@ -126,41 +125,75 @@ pub fn callMethod(self: Object, comptime T: type, method_name: []const u8, args:
|
|||||||
return func.callWithThis(T, self, args);
|
return func.callWithThis(T, self, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isNull(self: Object) bool {
|
|
||||||
return self.js_obj.toValue().isNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isUndefined(self: Object) bool {
|
|
||||||
return self.js_obj.toValue().isUndefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isNullOrUndefined(self: Object) bool {
|
pub fn isNullOrUndefined(self: Object) bool {
|
||||||
return self.js_obj.toValue().isNullOrUndefined();
|
return v8.v8__Value__IsNullOrUndefined(@ptrCast(self.handle));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nameIterator(self: Object) NameIterator {
|
pub fn getOwnPropertyNames(self: Object) !js.Array {
|
||||||
const context = self.context;
|
const handle = v8.v8__Object__GetOwnPropertyNames(self.handle, self.local.handle) orelse {
|
||||||
const js_obj = self.js_obj;
|
// 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
|
||||||
const array = js_obj.getPropertyNames(context.v8_context);
|
// the object (like some WPT tests do).
|
||||||
const count = array.length();
|
return error.TypeError;
|
||||||
|
};
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = handle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getPropertyNames(self: Object) js.Array {
|
||||||
|
const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle).?;
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = handle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nameIterator(self: Object) !NameIterator {
|
||||||
|
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);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = handle,
|
||||||
.count = count,
|
.count = count,
|
||||||
.context = context,
|
|
||||||
.js_obj = array.castTo(v8.Object),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toZig(self: Object, comptime T: type) !T {
|
pub fn toZig(self: Object, comptime T: type) !T {
|
||||||
return self.context.jsValueToZig(T, self.js_obj.toValue());
|
const js_value = js.Value{ .local = self.local, .handle = @ptrCast(self.handle) };
|
||||||
|
return self.local.jsValueToZig(T, js_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 js.Local) Object {
|
||||||
|
return .{
|
||||||
|
.local = l,
|
||||||
|
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isEqual(self: *const Global, other: Object) bool {
|
||||||
|
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub const NameIterator = struct {
|
pub const NameIterator = struct {
|
||||||
count: u32,
|
count: u32,
|
||||||
idx: u32 = 0,
|
idx: u32 = 0,
|
||||||
js_obj: v8.Object,
|
local: *const js.Local,
|
||||||
context: *const Context,
|
handle: *const v8.Array,
|
||||||
|
|
||||||
pub fn next(self: *NameIterator) !?[]const u8 {
|
pub fn next(self: *NameIterator) !?[]const u8 {
|
||||||
const idx = self.idx;
|
const idx = self.idx;
|
||||||
@@ -169,8 +202,8 @@ pub const NameIterator = struct {
|
|||||||
}
|
}
|
||||||
self.idx += 1;
|
self.idx += 1;
|
||||||
|
|
||||||
const context = self.context;
|
const local = self.local;
|
||||||
const js_val = try self.js_obj.getAtIndex(context.v8_context, idx);
|
const js_val_handle = v8.v8__Object__GetIndex(@ptrCast(self.handle), local.handle, idx) orelse return error.JsException;
|
||||||
return try context.valueToString(js_val, .{});
|
return try js.Value.toStringSlice(.{ .local = local, .handle = js_val_handle });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
253
src/browser/js/Origin.zig
Normal file
253
src/browser/js/Origin.zig
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// Origin represents the shared Zig<->JS bridge state for all contexts within
|
||||||
|
// the same origin. Multiple contexts (frames) from the same origin share a
|
||||||
|
// single Origin, ensuring that JS objects maintain their identity across frames.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
|
||||||
|
const App = @import("../../App.zig");
|
||||||
|
const Session = @import("../Session.zig");
|
||||||
|
|
||||||
|
const v8 = js.v8;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
|
const Origin = @This();
|
||||||
|
|
||||||
|
rc: usize = 1,
|
||||||
|
arena: Allocator,
|
||||||
|
|
||||||
|
// The key, e.g. lightpanda.io:443
|
||||||
|
key: []const u8,
|
||||||
|
|
||||||
|
// Security token - all contexts in this realm must use the same v8::Value instance
|
||||||
|
// as their security token for V8 to allow cross-context access
|
||||||
|
security_token: v8.Global,
|
||||||
|
|
||||||
|
// Serves two purposes. Like `global_objects`, this is used to free
|
||||||
|
// every Global(Object) we've created during the lifetime of the realm.
|
||||||
|
// More importantly, it serves as an identity map - for a given Zig
|
||||||
|
// instance, we map it to the same Global(Object).
|
||||||
|
// The key is the @intFromPtr of the Zig value
|
||||||
|
identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
||||||
|
|
||||||
|
// Some web APIs have to manage opaque values. Ideally, they use an
|
||||||
|
// js.Object, but the js.Object has no lifetime guarantee beyond the
|
||||||
|
// current call. They can call .persist() on their js.Object to get
|
||||||
|
// a `Global(Object)`. We need to track these to free them.
|
||||||
|
// This used to be a map and acted like identity_map; the key was
|
||||||
|
// the @intFromPtr(js_obj.handle). But v8 can re-use address. Without
|
||||||
|
// a reliable way to know if an object has already been persisted,
|
||||||
|
// we now simply persist every time persist() is called.
|
||||||
|
globals: std.ArrayList(v8.Global) = .empty,
|
||||||
|
|
||||||
|
// Temp variants stored in HashMaps for O(1) early cleanup.
|
||||||
|
// Key is global.data_ptr.
|
||||||
|
temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
||||||
|
|
||||||
|
// Any type that is stored in the identity_map which has a finalizer declared
|
||||||
|
// will have its finalizer stored here. This is only used when shutting down
|
||||||
|
// if v8 hasn't called the finalizer directly itself.
|
||||||
|
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
|
||||||
|
|
||||||
|
pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {
|
||||||
|
const arena = try app.arena_pool.acquire();
|
||||||
|
errdefer app.arena_pool.release(arena);
|
||||||
|
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
|
||||||
|
const owned_key = try arena.dupe(u8, key);
|
||||||
|
const token_local = isolate.initStringHandle(owned_key);
|
||||||
|
var token_global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(isolate.handle, token_local, &token_global);
|
||||||
|
|
||||||
|
const self = try arena.create(Origin);
|
||||||
|
self.* = .{
|
||||||
|
.rc = 1,
|
||||||
|
.arena = arena,
|
||||||
|
.key = owned_key,
|
||||||
|
.globals = .empty,
|
||||||
|
.temps = .empty,
|
||||||
|
.security_token = token_global,
|
||||||
|
};
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Origin, app: *App) void {
|
||||||
|
// Call finalizers before releasing anything
|
||||||
|
{
|
||||||
|
var it = self.finalizer_callbacks.valueIterator();
|
||||||
|
while (it.next()) |finalizer| {
|
||||||
|
finalizer.*.deinit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v8.v8__Global__Reset(&self.security_token);
|
||||||
|
|
||||||
|
{
|
||||||
|
var it = self.identity_map.valueIterator();
|
||||||
|
while (it.next()) |global| {
|
||||||
|
v8.v8__Global__Reset(global);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (self.globals.items) |*global| {
|
||||||
|
v8.v8__Global__Reset(global);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var it = self.temps.valueIterator();
|
||||||
|
while (it.next()) |global| {
|
||||||
|
v8.v8__Global__Reset(global);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.arena_pool.release(self.arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trackGlobal(self: *Origin, global: v8.Global) !void {
|
||||||
|
return self.globals.append(self.arena, global);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const IdentityResult = struct {
|
||||||
|
value_ptr: *v8.Global,
|
||||||
|
found_existing: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn addIdentity(self: *Origin, ptr: usize) !IdentityResult {
|
||||||
|
const gop = try self.identity_map.getOrPut(self.arena, ptr);
|
||||||
|
return .{
|
||||||
|
.value_ptr = gop.value_ptr,
|
||||||
|
.found_existing = gop.found_existing,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trackTemp(self: *Origin, global: v8.Global) !void {
|
||||||
|
return self.temps.put(self.arena, global.data_ptr, global);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn releaseTemp(self: *Origin, global: v8.Global) void {
|
||||||
|
if (self.temps.fetchRemove(global.data_ptr)) |kv| {
|
||||||
|
var g = kv.value;
|
||||||
|
v8.v8__Global__Reset(&g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release an item from the identity_map (called after finalizer runs from V8)
|
||||||
|
pub fn release(self: *Origin, item: *anyopaque) void {
|
||||||
|
var global = self.identity_map.fetchRemove(@intFromPtr(item)) orelse {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
v8.v8__Global__Reset(&global.value);
|
||||||
|
|
||||||
|
// The item has been finalized, remove it from the finalizer callback so that
|
||||||
|
// we don't try to call it again on shutdown.
|
||||||
|
const kv = self.finalizer_callbacks.fetchRemove(@intFromPtr(item)) orelse {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const fc = kv.value;
|
||||||
|
fc.session.releaseArena(fc.arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createFinalizerCallback(
|
||||||
|
self: *Origin,
|
||||||
|
session: *Session,
|
||||||
|
global: v8.Global,
|
||||||
|
ptr: *anyopaque,
|
||||||
|
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
|
||||||
|
) !*FinalizerCallback {
|
||||||
|
const arena = try session.getArena(.{ .debug = "FinalizerCallback" });
|
||||||
|
errdefer session.releaseArena(arena);
|
||||||
|
const fc = try arena.create(FinalizerCallback);
|
||||||
|
fc.* = .{
|
||||||
|
.arena = arena,
|
||||||
|
.origin = self,
|
||||||
|
.session = session,
|
||||||
|
.ptr = ptr,
|
||||||
|
.global = global,
|
||||||
|
.zig_finalizer = zig_finalizer,
|
||||||
|
};
|
||||||
|
return fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transferTo(self: *Origin, dest: *Origin) !void {
|
||||||
|
const arena = dest.arena;
|
||||||
|
|
||||||
|
try dest.globals.ensureUnusedCapacity(arena, self.globals.items.len);
|
||||||
|
for (self.globals.items) |obj| {
|
||||||
|
dest.globals.appendAssumeCapacity(obj);
|
||||||
|
}
|
||||||
|
self.globals.clearRetainingCapacity();
|
||||||
|
|
||||||
|
{
|
||||||
|
try dest.temps.ensureUnusedCapacity(arena, self.temps.count());
|
||||||
|
var it = self.temps.iterator();
|
||||||
|
while (it.next()) |kv| {
|
||||||
|
try dest.temps.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
||||||
|
}
|
||||||
|
self.temps.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
try dest.finalizer_callbacks.ensureUnusedCapacity(arena, self.finalizer_callbacks.count());
|
||||||
|
var it = self.finalizer_callbacks.iterator();
|
||||||
|
while (it.next()) |kv| {
|
||||||
|
kv.value_ptr.*.origin = dest;
|
||||||
|
try dest.finalizer_callbacks.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
||||||
|
}
|
||||||
|
self.finalizer_callbacks.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
try dest.identity_map.ensureUnusedCapacity(arena, self.identity_map.count());
|
||||||
|
var it = self.identity_map.iterator();
|
||||||
|
while (it.next()) |kv| {
|
||||||
|
try dest.identity_map.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
||||||
|
}
|
||||||
|
self.identity_map.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A type that has a finalizer can have its finalizer called one of two ways.
|
||||||
|
// The first is from V8 via the WeakCallback we give to weakRef. But that isn't
|
||||||
|
// guaranteed to fire, so we track this in finalizer_callbacks and call them on
|
||||||
|
// origin shutdown.
|
||||||
|
pub const FinalizerCallback = struct {
|
||||||
|
arena: Allocator,
|
||||||
|
origin: *Origin,
|
||||||
|
session: *Session,
|
||||||
|
ptr: *anyopaque,
|
||||||
|
global: v8.Global,
|
||||||
|
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
|
||||||
|
|
||||||
|
pub fn deinit(self: *FinalizerCallback) void {
|
||||||
|
self.zig_finalizer(self.ptr, self.session);
|
||||||
|
self.session.releaseArena(self.arena);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -20,20 +20,22 @@ const js = @import("js.zig");
|
|||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const Platform = @This();
|
const Platform = @This();
|
||||||
inner: v8.Platform,
|
handle: *v8.Platform,
|
||||||
|
|
||||||
pub fn init() !Platform {
|
pub fn init() !Platform {
|
||||||
if (v8.initV8ICU() == false) {
|
if (v8.v8__V8__InitializeICU() == false) {
|
||||||
return error.FailedToInitializeICU;
|
return error.FailedToInitializeICU;
|
||||||
}
|
}
|
||||||
const platform = v8.Platform.initDefault(0, true);
|
// 0 - threadpool size, 0 == let v8 decide
|
||||||
v8.initV8Platform(platform);
|
// 1 - idle_task_support, 1 == enabled
|
||||||
v8.initV8();
|
const handle = v8.v8__Platform__NewDefaultPlatform(0, 1).?;
|
||||||
return .{ .inner = platform };
|
v8.v8__V8__InitializePlatform(handle);
|
||||||
|
v8.v8__V8__Initialize();
|
||||||
|
return .{ .handle = handle };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: Platform) void {
|
pub fn deinit(self: Platform) void {
|
||||||
_ = v8.deinitV8();
|
_ = v8.v8__V8__Dispose();
|
||||||
v8.deinitV8Platform();
|
v8.v8__V8__DisposePlatform();
|
||||||
self.inner.deinit();
|
v8.v8__Platform__DELETE(self.handle);
|
||||||
}
|
}
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
102
src/browser/js/Promise.zig
Normal file
102
src/browser/js/Promise.zig
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Promise = @This();
|
||||||
|
|
||||||
|
local: *const js.Local,
|
||||||
|
handle: *const v8.Promise,
|
||||||
|
|
||||||
|
pub fn toObject(self: Promise) js.Object {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = @ptrCast(self.handle),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toValue(self: Promise) js.Value {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = @ptrCast(self.handle),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn thenAndCatch(self: Promise, on_fulfilled: js.Function, on_rejected: js.Function) !Promise {
|
||||||
|
if (v8.v8__Promise__Then2(self.handle, self.local.handle, on_fulfilled.handle, on_rejected.handle)) |handle| {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = handle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return error.PromiseChainFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn persist(self: Promise) !Global {
|
||||||
|
return self._persist(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn temp(self: Promise) !Temp {
|
||||||
|
return self._persist(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Global else Temp) {
|
||||||
|
var ctx = self.local.ctx;
|
||||||
|
|
||||||
|
var global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
|
if (comptime is_global) {
|
||||||
|
try ctx.trackGlobal(global);
|
||||||
|
return .{ .handle = global, .origin = {} };
|
||||||
|
}
|
||||||
|
try ctx.trackTemp(global);
|
||||||
|
return .{ .handle = global, .origin = ctx.origin };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Temp = G(.temp);
|
||||||
|
pub const Global = G(.global);
|
||||||
|
|
||||||
|
const GlobalType = enum(u8) {
|
||||||
|
temp,
|
||||||
|
global,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn G(comptime global_type: GlobalType) type {
|
||||||
|
return struct {
|
||||||
|
handle: v8.Global,
|
||||||
|
origin: if (global_type == .temp) *js.Origin else void,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
v8.v8__Global__Reset(&self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local(self: *const Self, l: *const js.Local) Promise {
|
||||||
|
return .{
|
||||||
|
.local = l,
|
||||||
|
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn release(self: *const Self) void {
|
||||||
|
self.origin.releaseTemp(self.handle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
41
src/browser/js/PromiseRejection.zig
Normal file
41
src/browser/js/PromiseRejection.zig
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// 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 PromiseRejection = @This();
|
||||||
|
|
||||||
|
local: *const js.Local,
|
||||||
|
handle: *const v8.PromiseRejectMessage,
|
||||||
|
|
||||||
|
pub fn promise(self: PromiseRejection) js.Promise {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = v8.v8__PromiseRejectMessage__GetPromise(self.handle).?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reason(self: PromiseRejection) ?js.Value {
|
||||||
|
const value_handle = v8.v8__PromiseRejectMessage__GetValue(self.handle) orelse return null;
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = value_handle,
|
||||||
|
};
|
||||||
|
}
|
||||||
99
src/browser/js/PromiseResolver.zig
Normal file
99
src/browser/js/PromiseResolver.zig
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
|
const PromiseResolver = @This();
|
||||||
|
|
||||||
|
local: *const js.Local,
|
||||||
|
handle: *const v8.PromiseResolver,
|
||||||
|
|
||||||
|
pub fn init(local: *const js.Local) PromiseResolver {
|
||||||
|
return .{
|
||||||
|
.local = local,
|
||||||
|
.handle = v8.v8__Promise__Resolver__New(local.handle).?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn promise(self: PromiseResolver) js.Promise {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = v8.v8__Promise__Resolver__GetPromise(self.handle).?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
|
||||||
|
self._resolve(value) catch |err| {
|
||||||
|
log.err(.bug, "resolve", .{ .source = source, .err = err, .persistent = false });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _resolve(self: PromiseResolver, value: anytype) !void {
|
||||||
|
const local = self.local;
|
||||||
|
const js_val = try local.zigValueToJs(value, .{});
|
||||||
|
|
||||||
|
var out: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Promise__Resolver__Resolve(self.handle, self.local.handle, js_val.handle, &out);
|
||||||
|
if (!out.has_value or !out.value) {
|
||||||
|
return error.FailedToResolvePromise;
|
||||||
|
}
|
||||||
|
local.runMicrotasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
|
||||||
|
self._reject(value) catch |err| {
|
||||||
|
log.err(.bug, "reject", .{ .source = source, .err = err, .persistent = false });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _reject(self: PromiseResolver, value: anytype) !void {
|
||||||
|
const local = self.local;
|
||||||
|
const js_val = try local.zigValueToJs(value, .{});
|
||||||
|
|
||||||
|
var out: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Promise__Resolver__Reject(self.handle, local.handle, js_val.handle, &out);
|
||||||
|
if (!out.has_value or !out.value) {
|
||||||
|
return error.FailedToRejectPromise;
|
||||||
|
}
|
||||||
|
local.runMicrotasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn persist(self: PromiseResolver) !Global {
|
||||||
|
var ctx = self.local.ctx;
|
||||||
|
var global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
|
try ctx.trackGlobal(global);
|
||||||
|
return .{ .handle = global };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 js.Local) PromiseResolver {
|
||||||
|
return .{
|
||||||
|
.local = l,
|
||||||
|
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const log = @import("../log.zig");
|
const log = @import("../../log.zig");
|
||||||
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
|
||||||
|
|
||||||
const IS_DEBUG = builtin.mode == .Debug;
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
@@ -47,9 +47,15 @@ pub fn init(allocator: std.mem.Allocator) Scheduler {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Scheduler) void {
|
||||||
|
finalizeTasks(&self.low_priority);
|
||||||
|
finalizeTasks(&self.high_priority);
|
||||||
|
}
|
||||||
|
|
||||||
const AddOpts = struct {
|
const AddOpts = struct {
|
||||||
name: []const u8 = "",
|
name: []const u8 = "",
|
||||||
low_priority: bool = false,
|
low_priority: bool = false,
|
||||||
|
finalizer: ?Finalizer = null,
|
||||||
};
|
};
|
||||||
pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void {
|
pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
@@ -63,6 +69,7 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts
|
|||||||
.callback = cb,
|
.callback = cb,
|
||||||
.sequence = seq,
|
.sequence = seq,
|
||||||
.name = opts.name,
|
.name = opts.name,
|
||||||
|
.finalizer = opts.finalizer,
|
||||||
.run_at = milliTimestamp(.monotonic) + run_in_ms,
|
.run_at = milliTimestamp(.monotonic) + run_in_ms,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -72,6 +79,11 @@ pub fn run(self: *Scheduler) !?u64 {
|
|||||||
return self.runQueue(&self.high_priority);
|
return self.runQueue(&self.high_priority);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hasReadyTasks(self: *Scheduler) bool {
|
||||||
|
const now = milliTimestamp(.monotonic);
|
||||||
|
return queueuHasReadyTask(&self.low_priority, now) or queueuHasReadyTask(&self.high_priority, now);
|
||||||
|
}
|
||||||
|
|
||||||
fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
||||||
if (queue.count() == 0) {
|
if (queue.count() == 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -95,7 +107,9 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
|||||||
|
|
||||||
if (repeat_in_ms) |ms| {
|
if (repeat_in_ms) |ms| {
|
||||||
// Task cannot be repeated immediately, and they should know that
|
// Task cannot be repeated immediately, and they should know that
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
std.debug.assert(ms != 0);
|
std.debug.assert(ms != 0);
|
||||||
|
}
|
||||||
task.run_at = now + ms;
|
task.run_at = now + ms;
|
||||||
try self.low_priority.add(task);
|
try self.low_priority.add(task);
|
||||||
}
|
}
|
||||||
@@ -103,12 +117,28 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn queueuHasReadyTask(queue: *Queue, now: u64) bool {
|
||||||
|
const task = queue.peek() orelse return false;
|
||||||
|
return task.run_at <= now;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finalizeTasks(queue: *Queue) void {
|
||||||
|
var it = queue.iterator();
|
||||||
|
while (it.next()) |t| {
|
||||||
|
if (t.finalizer) |func| {
|
||||||
|
func(t.ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const Task = struct {
|
const Task = struct {
|
||||||
run_at: u64,
|
run_at: u64,
|
||||||
sequence: u64,
|
sequence: u64,
|
||||||
ctx: *anyopaque,
|
ctx: *anyopaque,
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
callback: Callback,
|
callback: Callback,
|
||||||
|
finalizer: ?Finalizer,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Callback = *const fn (ctx: *anyopaque) anyerror!?u32;
|
const Callback = *const fn (ctx: *anyopaque) anyerror!?u32;
|
||||||
|
const Finalizer = *const fn (ctx: *anyopaque) void;
|
||||||
@@ -22,7 +22,6 @@ const bridge = @import("bridge.zig");
|
|||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
const Window = @import("../webapi/Window.zig");
|
|
||||||
|
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
const JsApis = bridge.JsApis;
|
const JsApis = bridge.JsApis;
|
||||||
@@ -53,14 +52,14 @@ startup_data: v8.StartupData,
|
|||||||
external_references: [countExternalReferences()]isize,
|
external_references: [countExternalReferences()]isize,
|
||||||
|
|
||||||
// Track whether this snapshot owns its data (was created in-process)
|
// Track whether this snapshot owns its data (was created in-process)
|
||||||
// If false, the data points into embedded_snapshot_blob and should not be freed
|
// If false, the data points into embedded_snapshot_blob and will not be freed
|
||||||
owns_data: bool = false,
|
owns_data: bool = false,
|
||||||
|
|
||||||
pub fn load(allocator: Allocator) !Snapshot {
|
pub fn load() !Snapshot {
|
||||||
if (loadEmbedded()) |snapshot| {
|
if (loadEmbedded()) |snapshot| {
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
return create(allocator);
|
return create();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn loadEmbedded() ?Snapshot {
|
fn loadEmbedded() ?Snapshot {
|
||||||
@@ -75,7 +74,7 @@ fn loadEmbedded() ?Snapshot {
|
|||||||
const blob = embedded_snapshot_blob[@sizeOf(usize)..];
|
const blob = embedded_snapshot_blob[@sizeOf(usize)..];
|
||||||
|
|
||||||
const startup_data = v8.StartupData{ .data = blob.ptr, .raw_size = @intCast(blob.len) };
|
const startup_data = v8.StartupData{ .data = blob.ptr, .raw_size = @intCast(blob.len) };
|
||||||
if (!v8.SnapshotCreator.startupDataIsValid(startup_data)) {
|
if (!v8.v8__StartupData__IsValid(startup_data)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,10 +86,11 @@ fn loadEmbedded() ?Snapshot {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: Snapshot, allocator: Allocator) void {
|
pub fn deinit(self: Snapshot) void {
|
||||||
// Only free if we own the data (was created in-process)
|
// Only free if we own the data (was created in-process)
|
||||||
if (self.owns_data) {
|
if (self.owns_data) {
|
||||||
allocator.free(self.startup_data.data[0..@intCast(self.startup_data.raw_size)]);
|
// V8 allocated this with `new char[]`, so we need to use the C++ delete[] operator
|
||||||
|
v8.v8__StartupData__DELETE(self.startup_data.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,50 +105,39 @@ pub fn write(self: Snapshot, writer: *std.Io.Writer) !void {
|
|||||||
|
|
||||||
pub fn fromEmbedded(self: Snapshot) bool {
|
pub fn fromEmbedded(self: Snapshot) bool {
|
||||||
// if the snapshot comes from the embedFile, then it'll be flagged as not
|
// if the snapshot comes from the embedFile, then it'll be flagged as not
|
||||||
// owneing (aka, not needing to free) the data.
|
// owning (aka, not needing to free) the data.
|
||||||
return self.owns_data == false;
|
return self.owns_data == false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isValid(self: Snapshot) bool {
|
fn isValid(self: Snapshot) bool {
|
||||||
return v8.SnapshotCreator.startupDataIsValid(self.startup_data);
|
return v8.v8__StartupData__IsValid(self.startup_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn createGlobalTemplate(isolate: v8.Isolate, templates: []const v8.FunctionTemplate) v8.ObjectTemplate {
|
pub fn create() !Snapshot {
|
||||||
// Set up the global template to inherit from Window's template
|
|
||||||
// This way the global object gets all Window properties through inheritance
|
|
||||||
const js_global = v8.FunctionTemplate.initDefault(isolate);
|
|
||||||
js_global.setClassName(v8.String.initUtf8(isolate, "Window"));
|
|
||||||
// Find Window in JsApis by name (avoids circular import)
|
|
||||||
const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi);
|
|
||||||
js_global.inherit(templates[window_index]);
|
|
||||||
return js_global.getInstanceTemplate();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create(allocator: Allocator) !Snapshot {
|
|
||||||
var external_references = collectExternalReferences();
|
var external_references = collectExternalReferences();
|
||||||
|
|
||||||
var params = v8.initCreateParams();
|
var params: v8.CreateParams = undefined;
|
||||||
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
|
v8.v8__Isolate__CreateParams__CONSTRUCT(¶ms);
|
||||||
defer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
|
params.array_buffer_allocator = v8.v8__ArrayBuffer__Allocator__NewDefaultAllocator();
|
||||||
|
defer v8.v8__ArrayBuffer__Allocator__DELETE(params.array_buffer_allocator.?);
|
||||||
params.external_references = @ptrCast(&external_references);
|
params.external_references = @ptrCast(&external_references);
|
||||||
|
|
||||||
var snapshot_creator: v8.SnapshotCreator = undefined;
|
const snapshot_creator = v8.v8__SnapshotCreator__CREATE(¶ms);
|
||||||
v8.SnapshotCreator.init(&snapshot_creator, ¶ms);
|
defer v8.v8__SnapshotCreator__DESTRUCT(snapshot_creator);
|
||||||
defer snapshot_creator.deinit();
|
|
||||||
|
|
||||||
var data_start: usize = 0;
|
var data_start: usize = 0;
|
||||||
const isolate = snapshot_creator.getIsolate();
|
const isolate = v8.v8__SnapshotCreator__getIsolate(snapshot_creator).?;
|
||||||
|
|
||||||
{
|
{
|
||||||
// CreateBlob, which we'll call once everything is setup, MUST NOT
|
// CreateBlob, which we'll call once everything is setup, MUST NOT
|
||||||
// be called from an active HandleScope. Hence we have this scope to
|
// be called from an active HandleScope. Hence we have this scope to
|
||||||
// clean it up before we call CreateBlob
|
// clean it up before we call CreateBlob
|
||||||
var handle_scope: v8.HandleScope = undefined;
|
var handle_scope: v8.HandleScope = undefined;
|
||||||
v8.HandleScope.init(&handle_scope, isolate);
|
v8.v8__HandleScope__CONSTRUCT(&handle_scope, isolate);
|
||||||
defer handle_scope.deinit();
|
defer v8.v8__HandleScope__DESTRUCT(&handle_scope);
|
||||||
|
|
||||||
// Create templates (constructors only) FIRST
|
// Create templates (constructors only) FIRST
|
||||||
var templates: [JsApis.len]v8.FunctionTemplate = undefined;
|
var templates: [JsApis.len]*v8.FunctionTemplate = undefined;
|
||||||
inline for (JsApis, 0..) |JsApi, i| {
|
inline for (JsApis, 0..) |JsApi, i| {
|
||||||
@setEvalBranchQuota(10_000);
|
@setEvalBranchQuota(10_000);
|
||||||
templates[i] = generateConstructor(JsApi, isolate);
|
templates[i] = generateConstructor(JsApi, isolate);
|
||||||
@@ -159,23 +148,21 @@ pub fn create(allocator: Allocator) !Snapshot {
|
|||||||
// This must come before attachClass so inheritance is set up first
|
// This must come before attachClass so inheritance is set up first
|
||||||
inline for (JsApis, 0..) |JsApi, i| {
|
inline for (JsApis, 0..) |JsApi, i| {
|
||||||
if (comptime protoIndexLookup(JsApi)) |proto_index| {
|
if (comptime protoIndexLookup(JsApi)) |proto_index| {
|
||||||
templates[i].inherit(templates[proto_index]);
|
v8.v8__FunctionTemplate__Inherit(templates[i], templates[proto_index]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the global template to inherit from Window's template
|
// Set up the global template to inherit from Window's template
|
||||||
// This way the global object gets all Window properties through inheritance
|
// This way the global object gets all Window properties through inheritance
|
||||||
const global_template = createGlobalTemplate(isolate, templates[0..]);
|
const context = v8.v8__Context__New(isolate, null, null);
|
||||||
|
v8.v8__Context__Enter(context);
|
||||||
const context = v8.Context.init(isolate, global_template, null);
|
defer v8.v8__Context__Exit(context);
|
||||||
context.enter();
|
|
||||||
defer context.exit();
|
|
||||||
|
|
||||||
// Add templates to context snapshot
|
// Add templates to context snapshot
|
||||||
var last_data_index: usize = 0;
|
var last_data_index: usize = 0;
|
||||||
inline for (JsApis, 0..) |_, i| {
|
inline for (JsApis, 0..) |_, i| {
|
||||||
@setEvalBranchQuota(10_000);
|
@setEvalBranchQuota(10_000);
|
||||||
const data_index = snapshot_creator.addDataWithContext(context, @ptrCast(templates[i].handle));
|
const data_index = v8.v8__SnapshotCreator__AddData(snapshot_creator, @ptrCast(templates[i]));
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
data_start = data_index;
|
data_start = data_index;
|
||||||
last_data_index = data_index;
|
last_data_index = data_index;
|
||||||
@@ -193,16 +180,18 @@ pub fn create(allocator: Allocator) !Snapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Realize all templates by getting their functions and attaching to global
|
// Realize all templates by getting their functions and attaching to global
|
||||||
const global_obj = context.getGlobal();
|
const global_obj = v8.v8__Context__Global(context);
|
||||||
|
|
||||||
inline for (JsApis, 0..) |JsApi, i| {
|
inline for (JsApis, 0..) |JsApi, i| {
|
||||||
const func = templates[i].getFunction(context);
|
const func = v8.v8__FunctionTemplate__GetFunction(templates[i], context);
|
||||||
|
|
||||||
// Attach to global if it has a name
|
// Attach to global if it has a name
|
||||||
if (@hasDecl(JsApi.Meta, "name")) {
|
if (@hasDecl(JsApi.Meta, "name")) {
|
||||||
if (@hasDecl(JsApi.Meta, "constructor_alias")) {
|
if (@hasDecl(JsApi.Meta, "constructor_alias")) {
|
||||||
const v8_class_name = v8.String.initUtf8(isolate, JsApi.Meta.constructor_alias);
|
const alias = JsApi.Meta.constructor_alias;
|
||||||
_ = global_obj.setValue(context, v8_class_name, func);
|
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, alias.ptr, v8.kNormal, @intCast(alias.len));
|
||||||
|
var maybe_result: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result);
|
||||||
|
|
||||||
// @TODO: This is wrong. This name should be registered with the
|
// @TODO: This is wrong. This name should be registered with the
|
||||||
// illegalConstructorCallback. I.e. new Image() is OK, but
|
// illegalConstructorCallback. I.e. new Image() is OK, but
|
||||||
@@ -210,11 +199,19 @@ pub fn create(allocator: Allocator) !Snapshot {
|
|||||||
// But we _have_ to register the name, i.e. HTMLImageElement
|
// But we _have_ to register the name, i.e. HTMLImageElement
|
||||||
// has to be registered so, for now, instead of creating another
|
// has to be registered so, for now, instead of creating another
|
||||||
// template, we just hook it into the constructor.
|
// template, we just hook it into the constructor.
|
||||||
const illegal_class_name = v8.String.initUtf8(isolate, JsApi.Meta.name);
|
const name = JsApi.Meta.name;
|
||||||
_ = global_obj.setValue(context, illegal_class_name, func);
|
const illegal_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
|
var maybe_result2: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Object__DefineOwnProperty(global_obj, context, illegal_class_name, func, 0, &maybe_result2);
|
||||||
} else {
|
} else {
|
||||||
const v8_class_name = v8.String.initUtf8(isolate, JsApi.Meta.name);
|
const name = JsApi.Meta.name;
|
||||||
_ = global_obj.setValue(context, v8_class_name, func);
|
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
|
var maybe_result: v8.MaybeBool = undefined;
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,8 +219,10 @@ pub fn create(allocator: Allocator) !Snapshot {
|
|||||||
{
|
{
|
||||||
// If we want to overwrite the built-in console, we have to
|
// If we want to overwrite the built-in console, we have to
|
||||||
// delete the built-in one.
|
// delete the built-in one.
|
||||||
const console_key = v8.String.initUtf8(isolate, "console");
|
const console_key = v8.v8__String__NewFromUtf8(isolate, "console", v8.kNormal, 7);
|
||||||
if (global_obj.deleteValue(context, console_key) == false) {
|
var maybe_deleted: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Object__Delete(global_obj, context, console_key, &maybe_deleted);
|
||||||
|
if (maybe_deleted.value == false) {
|
||||||
return error.ConsoleDeleteError;
|
return error.ConsoleDeleteError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,39 +232,63 @@ pub fn create(allocator: Allocator) !Snapshot {
|
|||||||
// TODO: see if newer V8 engines have a way around this.
|
// TODO: see if newer V8 engines have a way around this.
|
||||||
inline for (JsApis, 0..) |JsApi, i| {
|
inline for (JsApis, 0..) |JsApi, i| {
|
||||||
if (comptime protoIndexLookup(JsApi)) |proto_index| {
|
if (comptime protoIndexLookup(JsApi)) |proto_index| {
|
||||||
const proto_obj = templates[proto_index].getFunction(context).toObject();
|
const proto_func = v8.v8__FunctionTemplate__GetFunction(templates[proto_index], context);
|
||||||
const self_obj = templates[i].getFunction(context).toObject();
|
const proto_obj: *const v8.Object = @ptrCast(proto_func);
|
||||||
_ = self_obj.setPrototype(context, proto_obj);
|
|
||||||
|
const self_func = v8.v8__FunctionTemplate__GetFunction(templates[i], context);
|
||||||
|
const self_obj: *const v8.Object = @ptrCast(self_func);
|
||||||
|
|
||||||
|
var maybe_result: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Object__SetPrototype(self_obj, context, proto_obj, &maybe_result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Custom exception
|
// Custom exception
|
||||||
// TODO: this is an horrible hack, I can't figure out how to do this cleanly.
|
// TODO: this is an horrible hack, I can't figure out how to do this cleanly.
|
||||||
const code = v8.String.initUtf8(isolate, "DOMException.prototype.__proto__ = Error.prototype");
|
const code_str = "DOMException.prototype.__proto__ = Error.prototype";
|
||||||
_ = try (try v8.Script.compile(context, code, null)).run(context);
|
const code = v8.v8__String__NewFromUtf8(isolate, code_str.ptr, v8.kNormal, @intCast(code_str.len));
|
||||||
|
const script = v8.v8__Script__Compile(context, code, null) orelse return error.ScriptCompileFailed;
|
||||||
|
_ = v8.v8__Script__Run(script, context) orelse return error.ScriptRunFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshot_creator.setDefaultContext(context);
|
v8.v8__SnapshotCreator__setDefaultContext(snapshot_creator, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = snapshot_creator.createBlob(v8.FunctionCodeHandling.kKeep);
|
const blob = v8.v8__SnapshotCreator__createBlob(snapshot_creator, v8.kKeep);
|
||||||
const owned = try allocator.dupe(u8, blob.data[0..@intCast(blob.raw_size)]);
|
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.owns_data = true,
|
.owns_data = true,
|
||||||
.data_start = data_start,
|
.data_start = data_start,
|
||||||
.external_references = external_references,
|
.external_references = external_references,
|
||||||
.startup_data = .{ .data = owned.ptr, .raw_size = @intCast(owned.len) },
|
.startup_data = blob,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to check if a JsApi has a NamedIndexed handler
|
||||||
|
fn hasNamedIndexedGetter(comptime JsApi: type) bool {
|
||||||
|
const declarations = @typeInfo(JsApi).@"struct".decls;
|
||||||
|
inline for (declarations) |d| {
|
||||||
|
const value = @field(JsApi, d.name);
|
||||||
|
const T = @TypeOf(value);
|
||||||
|
if (T == bridge.NamedIndexed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Count total callbacks needed for external_references array
|
// Count total callbacks needed for external_references array
|
||||||
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;
|
|
||||||
|
// +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)
|
||||||
@@ -285,13 +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.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;
|
||||||
@@ -300,6 +328,15 @@ fn countExternalReferences() comptime_int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In debug mode, add unknown property callbacks for types without NamedIndexed
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
inline for (JsApis) |JsApi| {
|
||||||
|
if (!hasNamedIndexedGetter(JsApi)) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return count + 1; // +1 for null terminator
|
return count + 1; // +1 for null terminator
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +347,9 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
references[idx] = @bitCast(@intFromPtr(&illegalConstructorCallback));
|
references[idx] = @bitCast(@intFromPtr(&illegalConstructorCallback));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
|
|
||||||
|
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")) {
|
||||||
references[idx] = @bitCast(@intFromPtr(JsApi.constructor.func));
|
references[idx] = @bitCast(@intFromPtr(JsApi.constructor.func));
|
||||||
@@ -341,6 +381,10 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
} 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;
|
||||||
@@ -356,6 +400,16 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In debug mode, collect unknown property callbacks for types without NamedIndexed
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
inline for (JsApis) |JsApi| {
|
||||||
|
if (!hasNamedIndexedGetter(JsApi)) {
|
||||||
|
references[idx] = @bitCast(@intFromPtr(bridge.unknownObjectPropertyCallback(JsApi)));
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return references;
|
return references;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +419,7 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
// via `new ClassName()` - but they could, for example, be created in
|
// via `new ClassName()` - but they could, for example, be created in
|
||||||
// Zig and returned from a function call, which is why we need the
|
// Zig and returned from a function call, which is why we need the
|
||||||
// FunctionTemplate.
|
// FunctionTemplate.
|
||||||
fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTemplate {
|
fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionTemplate {
|
||||||
const callback = blk: {
|
const callback = blk: {
|
||||||
if (@hasDecl(JsApi, "constructor")) {
|
if (@hasDecl(JsApi, "constructor")) {
|
||||||
break :blk JsApi.constructor.func;
|
break :blk JsApi.constructor.func;
|
||||||
@@ -375,19 +429,66 @@ fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTem
|
|||||||
break :blk illegalConstructorCallback;
|
break :blk illegalConstructorCallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
const template = v8.FunctionTemplate.initCallback(isolate, callback);
|
const template = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?);
|
||||||
if (!@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
|
{
|
||||||
template.getInstanceTemplate().setInternalFieldCount(1);
|
const internal_field_count = comptime countInternalFields(JsApi);
|
||||||
|
if (internal_field_count > 0) {
|
||||||
|
const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
||||||
|
v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, internal_field_count);
|
||||||
}
|
}
|
||||||
const class_name = v8.String.initUtf8(isolate, if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi));
|
}
|
||||||
template.setClassName(class_name);
|
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));
|
||||||
|
v8.v8__FunctionTemplate__SetClassName(template, class_name);
|
||||||
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 = template.getPrototypeTemplate();
|
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;
|
||||||
|
|
||||||
inline for (declarations) |d| {
|
inline for (declarations) |d| {
|
||||||
const name: [:0]const u8 = d.name;
|
const name: [:0]const u8 = d.name;
|
||||||
const value = @field(JsApi, name);
|
const value = @field(JsApi, name);
|
||||||
@@ -395,60 +496,84 @@ fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionT
|
|||||||
|
|
||||||
switch (definition) {
|
switch (definition) {
|
||||||
bridge.Accessor => {
|
bridge.Accessor => {
|
||||||
const js_name = v8.String.initUtf8(isolate, name).toName();
|
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
const getter_callback = v8.FunctionTemplate.initCallback(isolate, value.getter);
|
const getter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.getter }).?);
|
||||||
if (value.setter == null) {
|
if (value.setter == null) {
|
||||||
if (value.static) {
|
if (value.static) {
|
||||||
template.setAccessorGetter(js_name, getter_callback);
|
v8.v8__Template__SetAccessorProperty__DEFAULT(@ptrCast(template), js_name, getter_callback);
|
||||||
} else {
|
} else {
|
||||||
target.setAccessorGetter(js_name, getter_callback);
|
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(prototype, js_name, getter_callback);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
std.debug.assert(value.static == false);
|
std.debug.assert(value.static == false);
|
||||||
const setter_callback = v8.FunctionTemplate.initCallback(isolate, value.setter);
|
}
|
||||||
target.setAccessorGetterAndSetter(js_name, getter_callback, setter_callback);
|
const setter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.setter.? }).?);
|
||||||
|
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT2(prototype, js_name, getter_callback, setter_callback);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bridge.Function => {
|
bridge.Function => {
|
||||||
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
|
const function_template = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func, .length = value.arity }).?);
|
||||||
const js_name = v8.String.initUtf8(isolate, name).toName();
|
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
if (value.static) {
|
if (value.static) {
|
||||||
template.set(js_name, function_template, v8.PropertyAttribute.None);
|
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
|
||||||
} else {
|
} else {
|
||||||
target.set(js_name, function_template, v8.PropertyAttribute.None);
|
v8.v8__Template__Set(@ptrCast(prototype), js_name, @ptrCast(function_template), v8.None);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bridge.Indexed => {
|
bridge.Indexed => {
|
||||||
const configuration = v8.IndexedPropertyHandlerConfiguration{
|
var configuration: v8.IndexedPropertyHandlerConfiguration = .{
|
||||||
.getter = value.getter,
|
.getter = value.getter,
|
||||||
|
.enumerator = value.enumerator,
|
||||||
|
.setter = null,
|
||||||
|
.query = null,
|
||||||
|
.deleter = null,
|
||||||
|
.definer = null,
|
||||||
|
.descriptor = null,
|
||||||
|
.data = null,
|
||||||
|
.flags = 0,
|
||||||
};
|
};
|
||||||
target.setIndexedProperty(configuration, null);
|
v8.v8__ObjectTemplate__SetIndexedHandler(instance, &configuration);
|
||||||
},
|
},
|
||||||
bridge.NamedIndexed => template.getInstanceTemplate().setNamedProperty(.{
|
bridge.NamedIndexed => {
|
||||||
|
var configuration: v8.NamedPropertyHandlerConfiguration = .{
|
||||||
.getter = value.getter,
|
.getter = value.getter,
|
||||||
.setter = value.setter,
|
.setter = value.setter,
|
||||||
|
.query = null,
|
||||||
.deleter = value.deleter,
|
.deleter = value.deleter,
|
||||||
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
|
.enumerator = null,
|
||||||
}, null),
|
.definer = null,
|
||||||
|
.descriptor = null,
|
||||||
|
.data = null,
|
||||||
|
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
||||||
|
};
|
||||||
|
v8.v8__ObjectTemplate__SetNamedHandler(instance, &configuration);
|
||||||
|
has_named_index_getter = true;
|
||||||
|
},
|
||||||
bridge.Iterator => {
|
bridge.Iterator => {
|
||||||
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
|
const function_template = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func }).?);
|
||||||
const js_name = if (value.async)
|
const js_name = if (value.async)
|
||||||
v8.Symbol.getAsyncIterator(isolate).toName()
|
v8.v8__Symbol__GetAsyncIterator(isolate)
|
||||||
else
|
else
|
||||||
v8.Symbol.getIterator(isolate).toName();
|
v8.v8__Symbol__GetIterator(isolate);
|
||||||
target.set(js_name, function_template, v8.PropertyAttribute.None);
|
v8.v8__Template__Set(@ptrCast(prototype), js_name, @ptrCast(function_template), v8.None);
|
||||||
},
|
},
|
||||||
bridge.Property => {
|
bridge.Property => {
|
||||||
const js_value = switch (value) {
|
const js_value = switch (value.value) {
|
||||||
.int => |v| js.simpleZigValueToJs(isolate, v, true, false),
|
.null => js.simpleZigValueToJs(.{ .handle = isolate }, null, true, false),
|
||||||
|
inline .bool, .int, .float, .string => |v| js.simpleZigValueToJs(.{ .handle = isolate }, v, true, false),
|
||||||
};
|
};
|
||||||
|
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
|
|
||||||
const js_name = v8.String.initUtf8(isolate, name).toName();
|
{
|
||||||
// apply it both to the type itself
|
const flags = if (value.readonly) v8.ReadOnly + v8.DontDelete else 0;
|
||||||
template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
|
v8.v8__Template__Set(@ptrCast(prototype), js_name, js_value, flags);
|
||||||
|
}
|
||||||
|
|
||||||
// and to instances of the type
|
if (value.template) {
|
||||||
target.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
|
// apply it both to the type itself (e.g. Node.Elem)
|
||||||
|
v8.v8__Template__Set(@ptrCast(template), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
bridge.Constructor => {}, // already handled in generateConstructor
|
bridge.Constructor => {}, // already handled in generateConstructor
|
||||||
else => {},
|
else => {},
|
||||||
@@ -456,15 +581,31 @@ fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionT
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (@hasDecl(JsApi.Meta, "htmldda")) {
|
if (@hasDecl(JsApi.Meta, "htmldda")) {
|
||||||
const instance_template = template.getInstanceTemplate();
|
v8.v8__ObjectTemplate__MarkAsUndetectable(instance);
|
||||||
instance_template.markAsUndetectable();
|
v8.v8__ObjectTemplate__SetCallAsFunctionHandler(instance, JsApi.Meta.callable.func);
|
||||||
instance_template.setCallAsFunctionHandler(JsApi.Meta.callable.func);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (@hasDecl(JsApi.Meta, "name")) {
|
if (@hasDecl(JsApi.Meta, "name")) {
|
||||||
const js_name = v8.Symbol.getToStringTag(isolate).toName();
|
const js_name = v8.v8__Symbol__GetToStringTag(isolate);
|
||||||
const instance_template = template.getInstanceTemplate();
|
const js_value = v8.v8__String__NewFromUtf8(isolate, JsApi.Meta.name.ptr, v8.kNormal, @intCast(JsApi.Meta.name.len));
|
||||||
instance_template.set(js_name, v8.String.initUtf8(isolate, JsApi.Meta.name), v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
|
v8.v8__Template__Set(@ptrCast(instance), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
if (!has_named_index_getter) {
|
||||||
|
var configuration: v8.NamedPropertyHandlerConfiguration = .{
|
||||||
|
.getter = bridge.unknownObjectPropertyCallback(JsApi),
|
||||||
|
.setter = null,
|
||||||
|
.query = null,
|
||||||
|
.deleter = null,
|
||||||
|
.enumerator = null,
|
||||||
|
.definer = null,
|
||||||
|
.descriptor = null,
|
||||||
|
.data = null,
|
||||||
|
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
||||||
|
};
|
||||||
|
v8.v8__ObjectTemplate__SetNamedHandler(instance, &configuration);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,10 +623,15 @@ fn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Shared illegal constructor callback for types without explicit constructors
|
// Shared illegal constructor callback for types without explicit constructors
|
||||||
fn illegalConstructorCallback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
fn illegalConstructorCallback(raw_info: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(raw_info);
|
||||||
const iso = info.getIsolate();
|
|
||||||
log.warn(.js, "Illegal constructor call", .{});
|
log.warn(.js, "Illegal constructor call", .{});
|
||||||
const js_exception = iso.throwException(js._createException(iso, "Illegal Constructor"));
|
|
||||||
info.getReturnValue().set(js_exception);
|
const message = v8.v8__String__NewFromUtf8(isolate, "Illegal Constructor", v8.kNormal, 19);
|
||||||
|
const js_exception = v8.v8__Exception__TypeError(message);
|
||||||
|
|
||||||
|
_ = v8.v8__Isolate__ThrowException(isolate, js_exception);
|
||||||
|
var return_value: v8.ReturnValue = undefined;
|
||||||
|
v8.v8__FunctionCallbackInfo__GetReturnValue(raw_info, &return_value);
|
||||||
|
v8.v8__ReturnValue__Set(return_value, js_exception);
|
||||||
}
|
}
|
||||||
|
|||||||
111
src/browser/js/String.zig
Normal file
111
src/browser/js/String.zig
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const SSO = @import("../../string.zig").String;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const String = @This();
|
||||||
|
|
||||||
|
local: *const js.Local,
|
||||||
|
handle: *const v8.String,
|
||||||
|
|
||||||
|
pub fn toSlice(self: String) ![]u8 {
|
||||||
|
return self._toSlice(false, self.local.call_arena);
|
||||||
|
}
|
||||||
|
pub fn toSliceZ(self: String) ![:0]u8 {
|
||||||
|
return self._toSlice(true, self.local.call_arena);
|
||||||
|
}
|
||||||
|
pub fn toSliceWithAlloc(self: String, allocator: Allocator) ![]u8 {
|
||||||
|
return self._toSlice(false, allocator);
|
||||||
|
}
|
||||||
|
fn _toSlice(self: String, comptime null_terminate: bool, allocator: Allocator) !(if (null_terminate) [:0]u8 else []u8) {
|
||||||
|
const local = self.local;
|
||||||
|
const handle = self.handle;
|
||||||
|
const isolate = local.isolate.handle;
|
||||||
|
|
||||||
|
const len = v8.v8__String__Utf8Length(handle, isolate);
|
||||||
|
const buf = try (if (comptime null_terminate) allocator.allocSentinel(u8, @intCast(len), 0) else allocator.alloc(u8, @intCast(len)));
|
||||||
|
const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(n == len);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toSSO(self: String, comptime global: bool) !(if (global) SSO.Global else SSO) {
|
||||||
|
if (comptime global) {
|
||||||
|
return .{ .str = try self.toSSOWithAlloc(self.local.ctx.origin.arena) };
|
||||||
|
}
|
||||||
|
return self.toSSOWithAlloc(self.local.call_arena);
|
||||||
|
}
|
||||||
|
pub fn toSSOWithAlloc(self: String, allocator: Allocator) !SSO {
|
||||||
|
const handle = self.handle;
|
||||||
|
const isolate = self.local.isolate.handle;
|
||||||
|
|
||||||
|
const len: usize = @intCast(v8.v8__String__Utf8Length(handle, isolate));
|
||||||
|
|
||||||
|
if (len <= 12) {
|
||||||
|
var content: [12]u8 = undefined;
|
||||||
|
const n = v8.v8__String__WriteUtf8(handle, isolate, &content[0], content.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(n == len);
|
||||||
|
}
|
||||||
|
// Weird that we do this _after_, but we have to..I've seen weird issues
|
||||||
|
// in ReleaseMode where v8 won't write to content if it starts off zero
|
||||||
|
// initiated
|
||||||
|
@memset(content[len..], 0);
|
||||||
|
return .{ .len = @intCast(len), .payload = .{ .content = content } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const buf = try allocator.alloc(u8, len);
|
||||||
|
const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(n == len);
|
||||||
|
}
|
||||||
|
|
||||||
|
var prefix: [4]u8 = @splat(0);
|
||||||
|
@memcpy(&prefix, buf[0..4]);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.len = @intCast(len),
|
||||||
|
.payload = .{ .heap = .{
|
||||||
|
.prefix = prefix,
|
||||||
|
.ptr = buf.ptr,
|
||||||
|
} },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(self: String, writer: *std.Io.Writer) !void {
|
||||||
|
const local = self.local;
|
||||||
|
const handle = self.handle;
|
||||||
|
const isolate = local.isolate.handle;
|
||||||
|
|
||||||
|
var small: [1024]u8 = undefined;
|
||||||
|
const len = v8.v8__String__Utf8Length(handle, isolate);
|
||||||
|
var buf = if (len < 1024) &small else local.call_arena.alloc(u8, @intCast(len)) catch return error.WriteFailed;
|
||||||
|
|
||||||
|
const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||||
|
return writer.writeAll(buf[0..n]);
|
||||||
|
}
|
||||||
129
src/browser/js/TaggedOpaque.zig
Normal file
129
src/browser/js/TaggedOpaque.zig
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
// 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 bridge = js.bridge;
|
||||||
|
|
||||||
|
// When we return a Zig object to V8, we put it on the heap and pass it into
|
||||||
|
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
|
||||||
|
// function parameter, we know what type it _should_ be.
|
||||||
|
//
|
||||||
|
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
|
||||||
|
// to the parameter type:
|
||||||
|
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
|
||||||
|
//
|
||||||
|
// But there are 2 reasons we can't do that.
|
||||||
|
//
|
||||||
|
// == Reason 1 ==
|
||||||
|
// The JS code might pass the wrong type:
|
||||||
|
//
|
||||||
|
// var cat = new Cat();
|
||||||
|
// cat.setOwner(new Cat());
|
||||||
|
//
|
||||||
|
// The zig_setOwner method expects the 2nd parameter to be an *Owner, but
|
||||||
|
// the JS code passed a *Cat.
|
||||||
|
//
|
||||||
|
// To solve this issue, we tag every returned value so that we can check what
|
||||||
|
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
|
||||||
|
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
|
||||||
|
//
|
||||||
|
// == Reason 2 ==
|
||||||
|
// Because of prototype inheritance, even "correct" code can be a challenge. For
|
||||||
|
// example, say the above JavaScript is fixed:
|
||||||
|
//
|
||||||
|
// var cat = new Cat();
|
||||||
|
// cat.setOwner(new Owner("Leto"));
|
||||||
|
//
|
||||||
|
// The issue is that setOwner might not expect an *Owner, but rather a
|
||||||
|
// *Person, which is the prototype for Owner. Now our Zig code is expecting
|
||||||
|
// a *Person, but it was (correctly) given an *Owner.
|
||||||
|
// For this reason, we also store the prototype chain.
|
||||||
|
const TaggedOpaque = @This();
|
||||||
|
|
||||||
|
prototype_len: u16,
|
||||||
|
prototype_chain: [*]const PrototypeChainEntry,
|
||||||
|
|
||||||
|
// Ptr to the Zig instance. Between the context where it's called (i.e.
|
||||||
|
// we have the comptime parameter info for all functions), and the index field
|
||||||
|
// we can figure out what type this is.
|
||||||
|
value: *anyopaque,
|
||||||
|
|
||||||
|
// When we're asked to describe an object via the Inspector, we _must_ include
|
||||||
|
// the proper subtype (and description) fields in the returned JSON.
|
||||||
|
// V8 will give us a Value and ask us for the subtype. From the js.Value we
|
||||||
|
// can get a js.Object, and from the js.Object, we can get out TaggedOpaque
|
||||||
|
// which is where we store the subtype.
|
||||||
|
subtype: ?bridge.SubType,
|
||||||
|
|
||||||
|
pub const PrototypeChainEntry = struct {
|
||||||
|
index: bridge.JsApiLookup.BackingInt,
|
||||||
|
offset: u16, // offset to the _proto field
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reverses the mapZigInstanceToJs, making sure that our TaggedOpaque
|
||||||
|
// contains a ptr to the correct type.
|
||||||
|
pub fn fromJS(comptime R: type, js_obj_handle: *const v8.Object) !R {
|
||||||
|
const ti = @typeInfo(R);
|
||||||
|
if (ti != .pointer) {
|
||||||
|
@compileError("non-pointer Zig parameter type: " ++ @typeName(R));
|
||||||
|
}
|
||||||
|
|
||||||
|
const T = ti.pointer.child;
|
||||||
|
const JsApi = bridge.Struct(T).JsApi;
|
||||||
|
|
||||||
|
if (@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
|
||||||
|
// Empty structs aren't stored as TOAs and there's no data
|
||||||
|
// stored in the JSObject's IntenrnalField. Why bother when
|
||||||
|
// we can just return an empty struct here?
|
||||||
|
return @constCast(@as(*const T, &.{}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const internal_field_count = v8.v8__Object__InternalFieldCount(js_obj_handle);
|
||||||
|
// if it isn't an empty struct, then the v8.Object should have an
|
||||||
|
// InternalFieldCount > 0, since our toa pointer should be embedded
|
||||||
|
// at index 0 of the internal field count.
|
||||||
|
if (internal_field_count == 0) {
|
||||||
|
return error.InvalidArgument;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bridge.JsApiLookup.has(JsApi)) {
|
||||||
|
@compileError("unknown Zig type: " ++ @typeName(R));
|
||||||
|
}
|
||||||
|
|
||||||
|
const tao_ptr = v8.v8__Object__GetAlignedPointerFromInternalField(js_obj_handle, 0).?;
|
||||||
|
const tao: *TaggedOpaque = @ptrCast(@alignCast(tao_ptr));
|
||||||
|
const expected_type_index = bridge.JsApiLookup.getId(JsApi);
|
||||||
|
|
||||||
|
const prototype_chain = tao.prototype_chain[0..tao.prototype_len];
|
||||||
|
if (prototype_chain[0].index == expected_type_index) {
|
||||||
|
return @ptrCast(@alignCast(tao.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ok, let's walk up the chain
|
||||||
|
var ptr = @intFromPtr(tao.value);
|
||||||
|
for (prototype_chain[1..]) |proto| {
|
||||||
|
ptr += proto.offset; // the offset to the _proto field
|
||||||
|
const proto_ptr: **anyopaque = @ptrFromInt(ptr);
|
||||||
|
if (proto.index == expected_type_index) {
|
||||||
|
return @ptrCast(@alignCast(proto_ptr.*));
|
||||||
|
}
|
||||||
|
ptr = @intFromPtr(proto_ptr.*);
|
||||||
|
}
|
||||||
|
return error.InvalidArgument;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -20,63 +20,131 @@ const std = @import("std");
|
|||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const TryCatch = @This();
|
const TryCatch = @This();
|
||||||
|
|
||||||
inner: v8.TryCatch,
|
handle: v8.TryCatch,
|
||||||
context: *const js.Context,
|
local: *const js.Local,
|
||||||
|
|
||||||
pub fn init(self: *TryCatch, context: *const js.Context) void {
|
pub fn init(self: *TryCatch, l: *const js.Local) void {
|
||||||
self.context = context;
|
self.local = l;
|
||||||
self.inner.init(context.isolate);
|
v8.v8__TryCatch__CONSTRUCT(&self.handle, l.isolate.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hasCaught(self: TryCatch) bool {
|
pub fn hasCaught(self: TryCatch) bool {
|
||||||
return self.inner.hasCaught();
|
return v8.v8__TryCatch__HasCaught(&self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// the caller needs to deinit the string returned
|
pub fn rethrow(self: *TryCatch) void {
|
||||||
pub fn exception(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
if (comptime IS_DEBUG) {
|
||||||
const msg = self.inner.getException() orelse return null;
|
std.debug.assert(self.hasCaught());
|
||||||
return try self.context.valueToString(msg, .{ .allocator = allocator });
|
}
|
||||||
|
_ = v8.v8__TryCatch__ReThrow(&self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// the caller needs to deinit the string returned
|
pub fn caught(self: TryCatch, allocator: Allocator) ?Caught {
|
||||||
pub fn stack(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
if (self.hasCaught() == false) {
|
||||||
const context = self.context;
|
return null;
|
||||||
const s = self.inner.getStackTrace(context.v8_context) orelse return null;
|
|
||||||
return try context.valueToString(s, .{ .allocator = allocator });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// the caller needs to deinit the string returned
|
const l = self.local;
|
||||||
pub fn sourceLine(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
const line: ?u32 = blk: {
|
||||||
const context = self.context;
|
const handle = v8.v8__TryCatch__Message(&self.handle) orelse return null;
|
||||||
const msg = self.inner.getMessage() orelse return null;
|
const line = v8.v8__Message__GetLineNumber(handle, l.handle);
|
||||||
const sl = msg.getSourceLine(context.v8_context) orelse return null;
|
break :blk if (line < 0) null else @intCast(line);
|
||||||
return try context.jsStringToZig(sl, .{ .allocator = allocator });
|
};
|
||||||
|
|
||||||
|
const exception: ?[]const u8 = blk: {
|
||||||
|
const handle = v8.v8__TryCatch__Exception(&self.handle) orelse break :blk null;
|
||||||
|
var js_val = js.Value{ .local = l, .handle = handle };
|
||||||
|
|
||||||
|
// If it's an Error object, try to get the message property
|
||||||
|
if (js_val.isObject()) {
|
||||||
|
const js_obj = js_val.toObject();
|
||||||
|
if (js_obj.has("message")) {
|
||||||
|
js_val = js_obj.get("message") catch break :blk null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sourceLineNumber(self: TryCatch) ?u32 {
|
if (js_val.isString()) |js_str| {
|
||||||
const context = self.context;
|
break :blk js_str.toSliceWithAlloc(allocator) catch |err| @errorName(err);
|
||||||
const msg = self.inner.getMessage() orelse return null;
|
}
|
||||||
return msg.getLineNumber(context.v8_context);
|
break :blk null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stack: ?[]const u8 = blk: {
|
||||||
|
const handle = v8.v8__TryCatch__StackTrace(&self.handle, l.handle) orelse break :blk null;
|
||||||
|
var js_val = js.Value{ .local = l, .handle = handle };
|
||||||
|
|
||||||
|
// If it's an Error object, try to get the stack property
|
||||||
|
if (js_val.isObject()) {
|
||||||
|
const js_obj = js_val.toObject();
|
||||||
|
if (js_obj.has("stack")) {
|
||||||
|
js_val = js_obj.get("stack") catch break :blk null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// a shorthand method to return either the entire stack message
|
if (js_val.isString()) |js_str| {
|
||||||
// or just the exception message
|
break :blk js_str.toSliceWithAlloc(allocator) catch |err| @errorName(err);
|
||||||
// - in Debug mode return the stack if available
|
|
||||||
// - otherwise return the exception if available
|
|
||||||
// the caller needs to deinit the string returned
|
|
||||||
pub fn err(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
|
||||||
if (comptime @import("builtin").mode == .Debug) {
|
|
||||||
if (try self.stack(allocator)) |msg| {
|
|
||||||
return msg;
|
|
||||||
}
|
}
|
||||||
|
break :blk null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.line = line,
|
||||||
|
.stack = stack,
|
||||||
|
.caught = true,
|
||||||
|
.exception = exception,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return try self.exception(allocator);
|
|
||||||
|
pub fn caughtOrError(self: TryCatch, allocator: Allocator, err: anyerror) Caught {
|
||||||
|
return self.caught(allocator) orelse .{
|
||||||
|
.caught = false,
|
||||||
|
.line = null,
|
||||||
|
.stack = null,
|
||||||
|
.exception = @errorName(err),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *TryCatch) void {
|
pub fn deinit(self: *TryCatch) void {
|
||||||
self.inner.deinit();
|
v8.v8__TryCatch__DESTRUCT(&self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const Caught = struct {
|
||||||
|
line: ?u32 = null,
|
||||||
|
caught: bool = false,
|
||||||
|
stack: ?[]const u8 = null,
|
||||||
|
exception: ?[]const u8 = null,
|
||||||
|
|
||||||
|
pub fn format(self: Caught, writer: *std.Io.Writer) !void {
|
||||||
|
const separator = @import("../../log.zig").separator();
|
||||||
|
try writer.print("{s}exception: {?s}", .{ separator, self.exception });
|
||||||
|
try writer.print("{s}stack: {?s}", .{ separator, self.stack });
|
||||||
|
try writer.print("{s}line: {?d}", .{ separator, self.line });
|
||||||
|
try writer.print("{s}caught: {any}", .{ separator, self.caught });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logFmt(self: Caught, comptime prefix: []const u8, writer: anytype) !void {
|
||||||
|
try writer.write(prefix ++ ".exception", self.exception orelse "???");
|
||||||
|
try writer.write(prefix ++ ".stack", self.stack orelse "na");
|
||||||
|
try writer.write(prefix ++ ".line", self.line);
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -18,87 +18,370 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
|
const SSO = @import("../../string.zig").String;
|
||||||
|
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const PersistentValue = v8.Persistent(v8.Value);
|
|
||||||
|
|
||||||
const Value = @This();
|
const Value = @This();
|
||||||
js_val: v8.Value,
|
|
||||||
context: *js.Context,
|
local: *const js.Local,
|
||||||
|
handle: *const v8.Value,
|
||||||
|
|
||||||
pub fn isObject(self: Value) bool {
|
pub fn isObject(self: Value) bool {
|
||||||
return self.js_val.isObject();
|
return v8.v8__Value__IsObject(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isString(self: Value) bool {
|
pub fn isString(self: Value) ?js.String {
|
||||||
return self.js_val.isString();
|
const handle = self.handle;
|
||||||
|
if (!v8.v8__Value__IsString(handle)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return .{ .local = self.local, .handle = @ptrCast(handle) };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isArray(self: Value) bool {
|
pub fn isArray(self: Value) bool {
|
||||||
return self.js_val.isArray();
|
return v8.v8__Value__IsArray(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isSymbol(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsSymbol(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isFunction(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsFunction(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isNull(self: Value) bool {
|
pub fn isNull(self: Value) bool {
|
||||||
return self.js_val.isNull();
|
return v8.v8__Value__IsNull(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isUndefined(self: Value) bool {
|
pub fn isUndefined(self: Value) bool {
|
||||||
return self.js_val.isUndefined();
|
return v8.v8__Value__IsUndefined(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
|
pub fn isNullOrUndefined(self: Value) bool {
|
||||||
return self.context.valueToString(self.js_val, .{ .allocator = allocator });
|
return v8.v8__Value__IsNullOrUndefined(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fromJson(ctx: *js.Context, json: []const u8) !Value {
|
pub fn isNumber(self: Value) bool {
|
||||||
const json_string = v8.String.initUtf8(ctx.isolate, json);
|
return v8.v8__Value__IsNumber(self.handle);
|
||||||
const value = try v8.Json.parse(ctx.v8_context, json_string);
|
|
||||||
return Value{ .context = ctx, .js_val = value };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn persist(self: Value) !Value {
|
pub fn isNumberObject(self: Value) bool {
|
||||||
const js_val = self.js_val;
|
return v8.v8__Value__IsNumberObject(self.handle);
|
||||||
var context = self.context;
|
}
|
||||||
|
|
||||||
const persisted = PersistentValue.init(context.isolate, js_val);
|
pub fn isInt32(self: Value) bool {
|
||||||
try context.js_value_list.append(context.arena, persisted);
|
return v8.v8__Value__IsInt32(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
return Value{ .context = context, .js_val = persisted.toValue() };
|
pub fn isUint32(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsUint32(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isBigInt(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsBigInt(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isBigIntObject(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsBigIntObject(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isBoolean(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsBoolean(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isBooleanObject(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsBooleanObject(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isTrue(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsTrue(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isFalse(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsFalse(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isTypedArray(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsTypedArray(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isArrayBufferView(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsArrayBufferView(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isArrayBuffer(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsArrayBuffer(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isUint8Array(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsUint8Array(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isUint8ClampedArray(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsUint8ClampedArray(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isInt8Array(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsInt8Array(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isUint16Array(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsUint16Array(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isInt16Array(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsInt16Array(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isUint32Array(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsUint32Array(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isInt32Array(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsInt32Array(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isBigUint64Array(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsBigUint64Array(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isBigInt64Array(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsBigInt64Array(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isPromise(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsPromise(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toBool(self: Value) bool {
|
||||||
|
return v8.v8__Value__BooleanValue(self.handle, self.local.isolate.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn typeOf(self: Value) js.String {
|
||||||
|
const str_handle = v8.v8__Value__TypeOf(self.handle, self.local.isolate.handle).?;
|
||||||
|
return js.String{ .local = self.local, .handle = str_handle };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toF32(self: Value) !f32 {
|
||||||
|
return @floatCast(try self.toF64());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toF64(self: Value) !f64 {
|
||||||
|
var maybe: v8.MaybeF64 = undefined;
|
||||||
|
v8.v8__Value__NumberValue(self.handle, self.local.handle, &maybe);
|
||||||
|
if (!maybe.has_value) {
|
||||||
|
return error.JsException;
|
||||||
|
}
|
||||||
|
return maybe.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toI32(self: Value) !i32 {
|
||||||
|
var maybe: v8.MaybeI32 = undefined;
|
||||||
|
v8.v8__Value__Int32Value(self.handle, self.local.handle, &maybe);
|
||||||
|
if (!maybe.has_value) {
|
||||||
|
return error.JsException;
|
||||||
|
}
|
||||||
|
return maybe.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toU32(self: Value) !u32 {
|
||||||
|
var maybe: v8.MaybeU32 = undefined;
|
||||||
|
v8.v8__Value__Uint32Value(self.handle, self.local.handle, &maybe);
|
||||||
|
if (!maybe.has_value) {
|
||||||
|
return error.JsException;
|
||||||
|
}
|
||||||
|
return maybe.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toPromise(self: Value) js.Promise {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(self.isPromise());
|
||||||
|
}
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = @ptrCast(self.handle),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toString(self: Value) !js.String {
|
||||||
|
const l = self.local;
|
||||||
|
const value_handle: *const v8.Value = blk: {
|
||||||
|
if (self.isSymbol()) {
|
||||||
|
break :blk @ptrCast(v8.v8__Symbol__Description(@ptrCast(self.handle), l.isolate.handle).?);
|
||||||
|
}
|
||||||
|
break :blk self.handle;
|
||||||
|
};
|
||||||
|
|
||||||
|
const str_handle = v8.v8__Value__ToString(value_handle, l.handle) orelse return error.JsException;
|
||||||
|
return .{ .local = self.local, .handle = str_handle };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toSSO(self: Value, comptime global: bool) !(if (global) SSO.Global else SSO) {
|
||||||
|
return (try self.toString()).toSSO(global);
|
||||||
|
}
|
||||||
|
pub fn toSSOWithAlloc(self: Value, allocator: Allocator) !SSO {
|
||||||
|
return (try self.toString()).toSSOWithAlloc(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toStringSlice(self: Value) ![]u8 {
|
||||||
|
return (try self.toString()).toSlice();
|
||||||
|
}
|
||||||
|
pub fn toStringSliceZ(self: Value) ![:0]u8 {
|
||||||
|
return (try self.toString()).toSliceZ();
|
||||||
|
}
|
||||||
|
pub fn toStringSliceWithAlloc(self: Value, allocator: Allocator) ![]u8 {
|
||||||
|
return (try self.toString()).toSliceWithAlloc(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toJson(self: Value, allocator: Allocator) ![]u8 {
|
||||||
|
const local = self.local;
|
||||||
|
const str_handle = v8.v8__JSON__Stringify(local.handle, self.handle, null) orelse return error.JsException;
|
||||||
|
return js.String.toSliceWithAlloc(.{ .local = local, .handle = str_handle }, allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently does not support host objects (Blob, File, etc.) or transferables
|
||||||
|
// which require delegate callbacks to be implemented.
|
||||||
|
pub fn structuredClone(self: Value) !Value {
|
||||||
|
const local = self.local;
|
||||||
|
const v8_context = local.handle;
|
||||||
|
const v8_isolate = local.isolate.handle;
|
||||||
|
|
||||||
|
const size, const data = blk: {
|
||||||
|
const serializer = v8.v8__ValueSerializer__New(v8_isolate, null) orelse return error.JsException;
|
||||||
|
defer v8.v8__ValueSerializer__DELETE(serializer);
|
||||||
|
|
||||||
|
var write_result: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__ValueSerializer__WriteHeader(serializer);
|
||||||
|
v8.v8__ValueSerializer__WriteValue(serializer, v8_context, self.handle, &write_result);
|
||||||
|
if (!write_result.has_value or !write_result.value) {
|
||||||
|
return error.JsException;
|
||||||
|
}
|
||||||
|
|
||||||
|
var size: usize = undefined;
|
||||||
|
const data = v8.v8__ValueSerializer__Release(serializer, &size) orelse return error.JsException;
|
||||||
|
break :blk .{ size, data };
|
||||||
|
};
|
||||||
|
|
||||||
|
defer v8.v8__ValueSerializer__FreeBuffer(data);
|
||||||
|
|
||||||
|
const cloned_handle = blk: {
|
||||||
|
const deserializer = v8.v8__ValueDeserializer__New(v8_isolate, data, size, null) orelse return error.JsException;
|
||||||
|
defer v8.v8__ValueDeserializer__DELETE(deserializer);
|
||||||
|
|
||||||
|
var read_header_result: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__ValueDeserializer__ReadHeader(deserializer, v8_context, &read_header_result);
|
||||||
|
if (!read_header_result.has_value or !read_header_result.value) {
|
||||||
|
return error.JsException;
|
||||||
|
}
|
||||||
|
break :blk v8.v8__ValueDeserializer__ReadValue(deserializer, v8_context) orelse return error.JsException;
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{ .local = local, .handle = cloned_handle };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn persist(self: Value) !Global {
|
||||||
|
return self._persist(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn temp(self: Value) !Temp {
|
||||||
|
return self._persist(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Global else Temp) {
|
||||||
|
var ctx = self.local.ctx;
|
||||||
|
|
||||||
|
var global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
|
if (comptime is_global) {
|
||||||
|
try ctx.trackGlobal(global);
|
||||||
|
return .{ .handle = global, .origin = {} };
|
||||||
|
}
|
||||||
|
try ctx.trackTemp(global);
|
||||||
|
return .{ .handle = global, .origin = ctx.origin };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toZig(self: Value, comptime T: type) !T {
|
pub fn toZig(self: Value, comptime T: type) !T {
|
||||||
return self.context.jsValueToZig(T, self.js_val);
|
return self.local.jsValueToZig(T, self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toObject(self: Value) js.Object {
|
pub fn toObject(self: Value) js.Object {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(self.isObject());
|
||||||
|
}
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.context = self.context,
|
.local = self.local,
|
||||||
.js_obj = self.js_val.castTo(v8.Object),
|
.handle = @ptrCast(self.handle),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toArray(self: Value) js.Array {
|
pub fn toArray(self: Value) js.Array {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(self.isArray());
|
||||||
|
}
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.context = self.context,
|
.local = self.local,
|
||||||
.js_arr = self.js_val.castTo(v8.Array),
|
.handle = @ptrCast(self.handle),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub const Value = struct {
|
pub fn toBigInt(self: Value) js.BigInt {
|
||||||
// value: v8.Value,
|
if (comptime IS_DEBUG) {
|
||||||
// context: *const Context,
|
std.debug.assert(self.isBigInt());
|
||||||
|
}
|
||||||
|
|
||||||
// // the caller needs to deinit the string returned
|
return .{
|
||||||
// pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
|
.handle = @ptrCast(self.handle),
|
||||||
// return self.context.valueToString(self.value, .{ .allocator = allocator });
|
};
|
||||||
// }
|
}
|
||||||
|
|
||||||
// pub fn fromJson(ctx: *Context, json: []const u8) !Value {
|
pub fn format(self: Value, writer: *std.Io.Writer) !void {
|
||||||
// const json_string = v8.String.initUtf8(ctx.isolate, json);
|
if (comptime IS_DEBUG) {
|
||||||
// const value = try v8.Json.parse(ctx.v8_context, json_string);
|
return self.local.debugValue(self, writer);
|
||||||
// return Value{ .context = ctx, .value = value };
|
}
|
||||||
// }
|
const js_str = self.toString() catch return error.WriteFailed;
|
||||||
// };
|
return js_str.format(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Temp = G(.temp);
|
||||||
|
pub const Global = G(.global);
|
||||||
|
|
||||||
|
const GlobalType = enum(u8) {
|
||||||
|
temp,
|
||||||
|
global,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn G(comptime global_type: GlobalType) type {
|
||||||
|
return struct {
|
||||||
|
handle: v8.Global,
|
||||||
|
origin: if (global_type == .temp) *js.Origin else void,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
v8.v8__Global__Reset(&self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local(self: *const Self, l: *const js.Local) Value {
|
||||||
|
return .{
|
||||||
|
.local = l,
|
||||||
|
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isEqual(self: *const Self, other: Value) bool {
|
||||||
|
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn release(self: *const Self) void {
|
||||||
|
self.origin.releaseTemp(self.handle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -18,11 +18,18 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
|
const lp = @import("lightpanda");
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
const Page = @import("../Page.zig");
|
||||||
|
const Session = @import("../Session.zig");
|
||||||
|
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const Caller = @import("Caller.zig");
|
const Caller = @import("Caller.zig");
|
||||||
|
const Context = @import("Context.zig");
|
||||||
|
const Origin = @import("Origin.zig");
|
||||||
|
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
pub fn Builder(comptime T: type) type {
|
pub fn Builder(comptime T: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
@@ -33,16 +40,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 {
|
||||||
@@ -57,16 +64,29 @@ pub fn Builder(comptime T: type) type {
|
|||||||
return Callable.init(T, func, opts);
|
return Callable.init(T, func, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn property(value: anytype) Property {
|
pub fn property(value: anytype, opts: Property.Opts) Property {
|
||||||
switch (@typeInfo(@TypeOf(value))) {
|
switch (@typeInfo(@TypeOf(value))) {
|
||||||
.comptime_int, .int => return .{ .int = value },
|
.bool => return Property.init(.{ .bool = value }, opts),
|
||||||
|
.null => return Property.init(.null, opts),
|
||||||
|
.comptime_int, .int => return Property.init(.{ .int = value }, opts),
|
||||||
|
.comptime_float, .float => return Property.init(.{ .float = value }, opts),
|
||||||
|
.pointer => |ptr| switch (ptr.size) {
|
||||||
|
.one => {
|
||||||
|
const one_info = @typeInfo(ptr.child);
|
||||||
|
if (one_info == .array and one_info.array.child == u8) {
|
||||||
|
return Property.init(.{ .string = value }, opts);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
@compileError("Property for " ++ @typeName(@TypeOf(value)) ++ " hasn't been defined yet");
|
@compileError("Property for " ++ @typeName(@TypeOf(value)) ++ " hasn't been defined yet");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prototypeChain() [prototypeChainLength(T)]js.PrototypeChainEntry {
|
const PrototypeChainEntry = @import("TaggedOpaque.zig").PrototypeChainEntry;
|
||||||
var entries: [prototypeChainLength(T)]js.PrototypeChainEntry = undefined;
|
pub fn prototypeChain() [prototypeChainLength(T)]PrototypeChainEntry {
|
||||||
|
var entries: [prototypeChainLength(T)]PrototypeChainEntry = undefined;
|
||||||
|
|
||||||
entries[0] = .{ .offset = 0, .index = JsApiLookup.getId(T.JsApi) };
|
entries[0] = .{ .offset = 0, .index = JsApiLookup.getId(T.JsApi) };
|
||||||
|
|
||||||
@@ -85,11 +105,39 @@ pub fn Builder(comptime T: type) type {
|
|||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn finalizer(comptime func: *const fn (self: *T, shutdown: bool, session: *Session) void) Finalizer {
|
||||||
|
return .{
|
||||||
|
.from_zig = struct {
|
||||||
|
fn wrap(ptr: *anyopaque, session: *Session) void {
|
||||||
|
func(@ptrCast(@alignCast(ptr)), true, session);
|
||||||
|
}
|
||||||
|
}.wrap,
|
||||||
|
|
||||||
|
.from_v8 = struct {
|
||||||
|
fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
|
||||||
|
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
|
||||||
|
const fc: *Origin.FinalizerCallback = @ptrCast(@alignCast(ptr));
|
||||||
|
|
||||||
|
const origin = fc.origin;
|
||||||
|
const value_ptr = fc.ptr;
|
||||||
|
if (origin.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
|
||||||
|
func(@ptrCast(@alignCast(value_ptr)), false, fc.session);
|
||||||
|
origin.release(value_ptr);
|
||||||
|
} else {
|
||||||
|
// A bit weird, but v8 _requires_ that we release it
|
||||||
|
// If we don't. We'll 100% crash.
|
||||||
|
v8.v8__Global__Reset(&fc.global);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.wrap,
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Constructor = struct {
|
pub const Constructor = struct {
|
||||||
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
|
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
||||||
|
|
||||||
const Opts = struct {
|
const Opts = struct {
|
||||||
dom_exception: bool = false,
|
dom_exception: bool = false,
|
||||||
@@ -97,12 +145,13 @@ pub const Constructor = struct {
|
|||||||
|
|
||||||
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Constructor {
|
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Constructor {
|
||||||
return .{ .func = struct {
|
return .{ .func = struct {
|
||||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
||||||
var caller = Caller.init(info);
|
var caller: Caller = undefined;
|
||||||
|
caller.init(v8_isolate);
|
||||||
defer caller.deinit();
|
defer caller.deinit();
|
||||||
|
|
||||||
caller.constructor(T, func, info, .{
|
caller.constructor(T, func, handle.?, .{
|
||||||
.dom_exception = opts.dom_exception,
|
.dom_exception = opts.dom_exception,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -112,88 +161,67 @@ pub const Constructor = struct {
|
|||||||
|
|
||||||
pub const Function = struct {
|
pub const Function = struct {
|
||||||
static: bool,
|
static: bool,
|
||||||
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
|
arity: usize,
|
||||||
|
noop: bool = false,
|
||||||
|
cache: ?Caller.Function.Opts.Caching = null,
|
||||||
|
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,
|
||||||
.func = struct {
|
.arity = getArity(@TypeOf(func)),
|
||||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
.func = if (opts.noop) noopFunction else struct {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
var caller = Caller.init(info);
|
Caller.Function.call(T, handle.?, func, opts);
|
||||||
defer caller.deinit();
|
|
||||||
|
|
||||||
if (comptime opts.static) {
|
|
||||||
caller.function(T, func, info, .{
|
|
||||||
.dom_exception = opts.dom_exception,
|
|
||||||
.as_typed_array = opts.as_typed_array,
|
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
caller.method(T, func, info, .{
|
|
||||||
.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 {
|
||||||
|
var count: usize = 0;
|
||||||
|
var params = @typeInfo(T).@"fn".params;
|
||||||
|
for (params[1..]) |p| { // start at 1, skip self
|
||||||
|
const PT = p.type.?;
|
||||||
|
if (PT == *Page or PT == *const Page) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (@typeInfo(PT) == .optional) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Accessor = struct {
|
pub const Accessor = struct {
|
||||||
static: bool = false,
|
static: bool = false,
|
||||||
getter: ?*const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void = null,
|
cache: ?Caller.Function.Opts.Caching = null,
|
||||||
setter: ?*const fn (?*const v8.C_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,
|
||||||
|
|
||||||
const Opts = struct {
|
fn init(comptime T: type, comptime getter: anytype, comptime setter: anytype, comptime opts: Caller.Function.Opts) Accessor {
|
||||||
static: bool = false,
|
|
||||||
cache: ?[]const u8 = null, // @ZIGDOM
|
|
||||||
as_typed_array: bool = false,
|
|
||||||
null_as_undefined: 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(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
Caller.Function.call(T, handle.?, getter, opts);
|
||||||
var caller = Caller.init(info);
|
|
||||||
defer caller.deinit();
|
|
||||||
|
|
||||||
caller.method(T, getter, info, .{
|
|
||||||
.as_typed_array = opts.as_typed_array,
|
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}.wrap;
|
}.wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (@typeInfo(@TypeOf(setter)) != .null) {
|
if (@typeInfo(@TypeOf(setter)) != .null) {
|
||||||
accessor.setter = struct {
|
accessor.setter = struct {
|
||||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
Caller.Function.call(T, handle.?, setter, opts);
|
||||||
std.debug.assert(info.length() == 1);
|
|
||||||
|
|
||||||
var caller = Caller.init(info);
|
|
||||||
defer caller.deinit();
|
|
||||||
|
|
||||||
caller.method(T, setter, info, .{
|
|
||||||
.as_typed_array = opts.as_typed_array,
|
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}.wrap;
|
}.wrap;
|
||||||
}
|
}
|
||||||
@@ -203,32 +231,52 @@ pub const Accessor = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const Indexed = struct {
|
pub const Indexed = struct {
|
||||||
getter: *const fn (idx: u32, raw_info: ?*const v8.C_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{
|
||||||
fn wrap(idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
.enumerator = null,
|
||||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
.getter = struct {
|
||||||
var caller = Caller.init(info);
|
fn wrap(idx: u32, 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();
|
defer caller.deinit();
|
||||||
return caller.getIndex(T, getter, idx, info, .{
|
|
||||||
|
return caller.getIndex(T, getter, idx, handle.?, .{
|
||||||
.as_typed_array = opts.as_typed_array,
|
.as_typed_array = opts.as_typed_array,
|
||||||
.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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const NamedIndexed = struct {
|
pub const NamedIndexed = struct {
|
||||||
getter: *const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8,
|
getter: *const fn (c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
|
||||||
setter: ?*const fn (c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 = null,
|
setter: ?*const fn (c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 = null,
|
||||||
deleter: ?*const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 = null,
|
deleter: ?*const fn (c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 = null,
|
||||||
|
|
||||||
const Opts = struct {
|
const Opts = struct {
|
||||||
as_typed_array: bool = false,
|
as_typed_array: bool = false,
|
||||||
@@ -237,11 +285,13 @@ pub const NamedIndexed = struct {
|
|||||||
|
|
||||||
fn init(comptime T: type, comptime getter: anytype, setter: anytype, deleter: anytype, comptime opts: Opts) NamedIndexed {
|
fn init(comptime T: type, comptime getter: anytype, setter: anytype, deleter: anytype, comptime opts: Opts) NamedIndexed {
|
||||||
const getter_fn = struct {
|
const getter_fn = struct {
|
||||||
fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||||
var caller = Caller.init(info);
|
var caller: Caller = undefined;
|
||||||
|
caller.init(v8_isolate);
|
||||||
defer caller.deinit();
|
defer caller.deinit();
|
||||||
return caller.getNamedIndex(T, getter, .{ .handle = c_name.? }, info, .{
|
|
||||||
|
return caller.getNamedIndex(T, getter, c_name.?, handle.?, .{
|
||||||
.as_typed_array = opts.as_typed_array,
|
.as_typed_array = opts.as_typed_array,
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
.null_as_undefined = opts.null_as_undefined,
|
||||||
});
|
});
|
||||||
@@ -249,12 +299,13 @@ pub const NamedIndexed = struct {
|
|||||||
}.wrap;
|
}.wrap;
|
||||||
|
|
||||||
const setter_fn = if (@typeInfo(@TypeOf(setter)) == .null) null else struct {
|
const setter_fn = if (@typeInfo(@TypeOf(setter)) == .null) null else struct {
|
||||||
fn wrap(c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
fn wrap(c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||||
var caller = Caller.init(info);
|
var caller: Caller = undefined;
|
||||||
|
caller.init(v8_isolate);
|
||||||
defer caller.deinit();
|
defer caller.deinit();
|
||||||
|
|
||||||
return caller.setNamedIndex(T, setter, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info, .{
|
return caller.setNamedIndex(T, setter, c_name.?, c_value.?, handle.?, .{
|
||||||
.as_typed_array = opts.as_typed_array,
|
.as_typed_array = opts.as_typed_array,
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
.null_as_undefined = opts.null_as_undefined,
|
||||||
});
|
});
|
||||||
@@ -262,12 +313,13 @@ pub const NamedIndexed = struct {
|
|||||||
}.wrap;
|
}.wrap;
|
||||||
|
|
||||||
const deleter_fn = if (@typeInfo(@TypeOf(deleter)) == .null) null else struct {
|
const deleter_fn = if (@typeInfo(@TypeOf(deleter)) == .null) null else struct {
|
||||||
fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||||
var caller = Caller.init(info);
|
var caller: Caller = undefined;
|
||||||
|
caller.init(v8_isolate);
|
||||||
defer caller.deinit();
|
defer caller.deinit();
|
||||||
|
|
||||||
return caller.deleteNamedIndex(T, deleter, .{ .handle = c_name.? }, info, .{
|
return caller.deleteNamedIndex(T, deleter, c_name.?, handle.?, .{
|
||||||
.as_typed_array = opts.as_typed_array,
|
.as_typed_array = opts.as_typed_array,
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
.null_as_undefined = opts.null_as_undefined,
|
||||||
});
|
});
|
||||||
@@ -283,7 +335,7 @@ pub const NamedIndexed = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const Iterator = struct {
|
pub const Iterator = struct {
|
||||||
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
|
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
||||||
async: bool,
|
async: bool,
|
||||||
|
|
||||||
const Opts = struct {
|
const Opts = struct {
|
||||||
@@ -296,8 +348,8 @@ pub const Iterator = struct {
|
|||||||
return .{
|
return .{
|
||||||
.async = opts.async,
|
.async = opts.async,
|
||||||
.func = struct {
|
.func = struct {
|
||||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
const info = Caller.FunctionCallbackInfo{ .handle = handle.? };
|
||||||
info.getReturnValue().set(info.getThis());
|
info.getReturnValue().set(info.getThis());
|
||||||
}
|
}
|
||||||
}.wrap,
|
}.wrap,
|
||||||
@@ -307,11 +359,10 @@ pub const Iterator = struct {
|
|||||||
return .{
|
return .{
|
||||||
.async = opts.async,
|
.async = opts.async,
|
||||||
.func = struct {
|
.func = struct {
|
||||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
return Caller.Function.call(T, handle.?, struct_or_func, .{
|
||||||
var caller = Caller.init(info);
|
.null_as_undefined = opts.null_as_undefined,
|
||||||
defer caller.deinit();
|
});
|
||||||
caller.method(T, struct_or_func, info, .{});
|
|
||||||
}
|
}
|
||||||
}.wrap,
|
}.wrap,
|
||||||
};
|
};
|
||||||
@@ -319,7 +370,7 @@ pub const Iterator = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const Callable = struct {
|
pub const Callable = struct {
|
||||||
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
|
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
||||||
|
|
||||||
const Opts = struct {
|
const Opts = struct {
|
||||||
null_as_undefined: bool = false,
|
null_as_undefined: bool = false,
|
||||||
@@ -327,11 +378,8 @@ 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(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
Caller.Function.call(T, handle.?, func, .{
|
||||||
var caller = Caller.init(info);
|
|
||||||
defer caller.deinit();
|
|
||||||
caller.method(T, func, info, .{
|
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
.null_as_undefined = opts.null_as_undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -339,10 +387,191 @@ pub const Callable = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Property = union(enum) {
|
pub const Property = struct {
|
||||||
|
value: Value,
|
||||||
|
template: bool,
|
||||||
|
readonly: bool,
|
||||||
|
|
||||||
|
const Value = union(enum) {
|
||||||
|
null,
|
||||||
int: i64,
|
int: i64,
|
||||||
|
float: f64,
|
||||||
|
bool: bool,
|
||||||
|
string: []const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Opts = struct {
|
||||||
|
template: bool,
|
||||||
|
readonly: bool = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn init(value: Value, opts: Opts) Property {
|
||||||
|
return .{
|
||||||
|
.value = value,
|
||||||
|
.template = opts.template,
|
||||||
|
.readonly = opts.readonly,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Finalizer = struct {
|
||||||
|
// The finalizer wrapper when called from Zig. This is only called on
|
||||||
|
// Origin.deinit
|
||||||
|
from_zig: *const fn (ctx: *anyopaque, session: *Session) void,
|
||||||
|
|
||||||
|
// The finalizer wrapper when called from V8. This may never be called
|
||||||
|
// (hence why we fallback to calling in Origin.deinit). If it is called,
|
||||||
|
// it is only ever called after we SetWeak on the Global.
|
||||||
|
from_v8: *const fn (?*const v8.WeakCallbackInfo) callconv(.c) void,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
|
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||||
|
var caller: Caller = undefined;
|
||||||
|
caller.init(v8_isolate);
|
||||||
|
defer caller.deinit();
|
||||||
|
|
||||||
|
const local = &caller.local;
|
||||||
|
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(local.isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
|
||||||
|
const property: []const u8 = js.String.toSlice(.{ .local = local, .handle = @ptrCast(c_name.?) }) catch {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const page = local.ctx.page;
|
||||||
|
const document = page.document;
|
||||||
|
|
||||||
|
if (document.getElementById(property, page)) |el| {
|
||||||
|
const js_val = local.zigValueToJs(el, .{}) catch return 0;
|
||||||
|
var pc = Caller.PropertyCallbackInfo{ .handle = handle.? };
|
||||||
|
pc.getReturnValue().set(js_val);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
if (std.mem.startsWith(u8, property, "__")) {
|
||||||
|
// some frameworks will extend built-in types using a __ prefix
|
||||||
|
// these should always be safe to ignore.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ignored = std.StaticStringMap(void).initComptime(.{
|
||||||
|
.{ "Deno", {} },
|
||||||
|
.{ "process", {} },
|
||||||
|
.{ "ShadyDOM", {} },
|
||||||
|
.{ "ShadyCSS", {} },
|
||||||
|
|
||||||
|
// a lot of sites seem to like having their own window.config.
|
||||||
|
.{ "config", {} },
|
||||||
|
|
||||||
|
.{ "litNonce", {} },
|
||||||
|
.{ "litHtmlVersions", {} },
|
||||||
|
.{ "litElementVersions", {} },
|
||||||
|
.{ "litHtmlPolyfillSupport", {} },
|
||||||
|
.{ "litElementHydrateSupport", {} },
|
||||||
|
.{ "litElementPolyfillSupport", {} },
|
||||||
|
.{ "reactiveElementVersions", {} },
|
||||||
|
|
||||||
|
.{ "recaptcha", {} },
|
||||||
|
.{ "grecaptcha", {} },
|
||||||
|
.{ "___grecaptcha_cfg", {} },
|
||||||
|
.{ "__recaptcha_api", {} },
|
||||||
|
.{ "__google_recaptcha_client", {} },
|
||||||
|
|
||||||
|
.{ "CLOSURE_FLAGS", {} },
|
||||||
|
.{ "__REACT_DEVTOOLS_GLOBAL_HOOK__", {} },
|
||||||
|
.{ "ApplePaySession", {} },
|
||||||
|
});
|
||||||
|
if (!ignored.has(property)) {
|
||||||
|
const key = std.fmt.bufPrint(&local.ctx.page.buf, "Window:{s}", .{property}) catch return 0;
|
||||||
|
logUnknownProperty(local, key) catch return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// not intercepted
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only used for debugging
|
||||||
|
pub fn unknownObjectPropertyCallback(comptime JsApi: type) *const fn (?*const v8.Name, ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
|
if (comptime !IS_DEBUG) {
|
||||||
|
@compileError("unknownObjectPropertyCallback should only be used in debug builds");
|
||||||
|
}
|
||||||
|
|
||||||
|
return struct {
|
||||||
|
fn wrap(c_name: ?*const v8.Name, 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();
|
||||||
|
|
||||||
|
const local = &caller.local;
|
||||||
|
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(local.isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
|
||||||
|
const property: []const u8 = js.String.toSlice(.{ .local = local, .handle = @ptrCast(c_name.?) }) catch {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, property, "__")) {
|
||||||
|
// some frameworks will extend built-in types using a __ prefix
|
||||||
|
// these should always be safe to ignore.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, property, "jQuery")) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JsApi == @import("../webapi/cdata/Text.zig").JsApi or JsApi == @import("../webapi/cdata/Comment.zig").JsApi) {
|
||||||
|
if (std.mem.eql(u8, property, "tagName")) {
|
||||||
|
// knockout does this, a lot.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JsApi == @import("../webapi/element/Html.zig").JsApi or JsApi == @import("../webapi/Element.zig").JsApi or JsApi == @import("../webapi/element/html/Custom.zig").JsApi) {
|
||||||
|
// react ?
|
||||||
|
if (std.mem.eql(u8, property, "props")) return 0;
|
||||||
|
if (std.mem.eql(u8, property, "hydrated")) return 0;
|
||||||
|
if (std.mem.eql(u8, property, "isHydrated")) return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JsApi == @import("../webapi/Console.zig").JsApi) {
|
||||||
|
if (std.mem.eql(u8, property, "firebug")) return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ignored = std.StaticStringMap(void).initComptime(.{});
|
||||||
|
if (!ignored.has(property)) {
|
||||||
|
const key = std.fmt.bufPrint(&local.ctx.page.buf, "{s}:{s}", .{ if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi), property }) catch return 0;
|
||||||
|
logUnknownProperty(local, key) catch return 0;
|
||||||
|
}
|
||||||
|
// not intercepted
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}.wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logUnknownProperty(local: *const js.Local, key: []const u8) !void {
|
||||||
|
const ctx = local.ctx;
|
||||||
|
const gop = try ctx.unknown_properties.getOrPut(ctx.arena, key);
|
||||||
|
if (gop.found_existing) {
|
||||||
|
gop.value_ptr.count += 1;
|
||||||
|
} else {
|
||||||
|
gop.key_ptr.* = try ctx.arena.dupe(u8, key);
|
||||||
|
gop.value_ptr.* = .{
|
||||||
|
.count = 1,
|
||||||
|
.first_stack = try ctx.arena.dupe(u8, (try local.stackTrace()) orelse "???"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Given a Type, returns the length of the prototype chain, including self
|
// Given a Type, returns the length of the prototype chain, including self
|
||||||
fn prototypeChainLength(comptime T: type) usize {
|
fn prototypeChainLength(comptime T: type) usize {
|
||||||
var l: usize = 1;
|
var l: usize = 1;
|
||||||
@@ -503,6 +732,8 @@ 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/FontFace.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"),
|
||||||
@@ -529,16 +760,24 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/element/Html.zig"),
|
@import("../webapi/element/Html.zig"),
|
||||||
@import("../webapi/element/html/IFrame.zig"),
|
@import("../webapi/element/html/IFrame.zig"),
|
||||||
@import("../webapi/element/html/Anchor.zig"),
|
@import("../webapi/element/html/Anchor.zig"),
|
||||||
|
@import("../webapi/element/html/Area.zig"),
|
||||||
@import("../webapi/element/html/Audio.zig"),
|
@import("../webapi/element/html/Audio.zig"),
|
||||||
|
@import("../webapi/element/html/Base.zig"),
|
||||||
@import("../webapi/element/html/Body.zig"),
|
@import("../webapi/element/html/Body.zig"),
|
||||||
@import("../webapi/element/html/BR.zig"),
|
@import("../webapi/element/html/BR.zig"),
|
||||||
@import("../webapi/element/html/Button.zig"),
|
@import("../webapi/element/html/Button.zig"),
|
||||||
@import("../webapi/element/html/Canvas.zig"),
|
@import("../webapi/element/html/Canvas.zig"),
|
||||||
@import("../webapi/element/html/Custom.zig"),
|
@import("../webapi/element/html/Custom.zig"),
|
||||||
@import("../webapi/element/html/Data.zig"),
|
@import("../webapi/element/html/Data.zig"),
|
||||||
|
@import("../webapi/element/html/DataList.zig"),
|
||||||
|
@import("../webapi/element/html/Details.zig"),
|
||||||
@import("../webapi/element/html/Dialog.zig"),
|
@import("../webapi/element/html/Dialog.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/Font.zig"),
|
||||||
@import("../webapi/element/html/Form.zig"),
|
@import("../webapi/element/html/Form.zig"),
|
||||||
@import("../webapi/element/html/Generic.zig"),
|
@import("../webapi/element/html/Generic.zig"),
|
||||||
@import("../webapi/element/html/Head.zig"),
|
@import("../webapi/element/html/Head.zig"),
|
||||||
@@ -547,20 +786,43 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/element/html/Html.zig"),
|
@import("../webapi/element/html/Html.zig"),
|
||||||
@import("../webapi/element/html/Image.zig"),
|
@import("../webapi/element/html/Image.zig"),
|
||||||
@import("../webapi/element/html/Input.zig"),
|
@import("../webapi/element/html/Input.zig"),
|
||||||
|
@import("../webapi/element/html/Label.zig"),
|
||||||
|
@import("../webapi/element/html/Legend.zig"),
|
||||||
@import("../webapi/element/html/LI.zig"),
|
@import("../webapi/element/html/LI.zig"),
|
||||||
@import("../webapi/element/html/Link.zig"),
|
@import("../webapi/element/html/Link.zig"),
|
||||||
|
@import("../webapi/element/html/Map.zig"),
|
||||||
@import("../webapi/element/html/Media.zig"),
|
@import("../webapi/element/html/Media.zig"),
|
||||||
@import("../webapi/element/html/Meta.zig"),
|
@import("../webapi/element/html/Meta.zig"),
|
||||||
|
@import("../webapi/element/html/Meter.zig"),
|
||||||
|
@import("../webapi/element/html/Mod.zig"),
|
||||||
|
@import("../webapi/element/html/Object.zig"),
|
||||||
@import("../webapi/element/html/OL.zig"),
|
@import("../webapi/element/html/OL.zig"),
|
||||||
|
@import("../webapi/element/html/OptGroup.zig"),
|
||||||
@import("../webapi/element/html/Option.zig"),
|
@import("../webapi/element/html/Option.zig"),
|
||||||
|
@import("../webapi/element/html/Output.zig"),
|
||||||
@import("../webapi/element/html/Paragraph.zig"),
|
@import("../webapi/element/html/Paragraph.zig"),
|
||||||
|
@import("../webapi/element/html/Picture.zig"),
|
||||||
|
@import("../webapi/element/html/Param.zig"),
|
||||||
|
@import("../webapi/element/html/Pre.zig"),
|
||||||
|
@import("../webapi/element/html/Progress.zig"),
|
||||||
|
@import("../webapi/element/html/Quote.zig"),
|
||||||
@import("../webapi/element/html/Script.zig"),
|
@import("../webapi/element/html/Script.zig"),
|
||||||
@import("../webapi/element/html/Select.zig"),
|
@import("../webapi/element/html/Select.zig"),
|
||||||
@import("../webapi/element/html/Slot.zig"),
|
@import("../webapi/element/html/Slot.zig"),
|
||||||
|
@import("../webapi/element/html/Source.zig"),
|
||||||
|
@import("../webapi/element/html/Span.zig"),
|
||||||
@import("../webapi/element/html/Style.zig"),
|
@import("../webapi/element/html/Style.zig"),
|
||||||
|
@import("../webapi/element/html/Table.zig"),
|
||||||
|
@import("../webapi/element/html/TableCaption.zig"),
|
||||||
|
@import("../webapi/element/html/TableCell.zig"),
|
||||||
|
@import("../webapi/element/html/TableCol.zig"),
|
||||||
|
@import("../webapi/element/html/TableRow.zig"),
|
||||||
|
@import("../webapi/element/html/TableSection.zig"),
|
||||||
@import("../webapi/element/html/Template.zig"),
|
@import("../webapi/element/html/Template.zig"),
|
||||||
@import("../webapi/element/html/TextArea.zig"),
|
@import("../webapi/element/html/TextArea.zig"),
|
||||||
|
@import("../webapi/element/html/Time.zig"),
|
||||||
@import("../webapi/element/html/Title.zig"),
|
@import("../webapi/element/html/Title.zig"),
|
||||||
|
@import("../webapi/element/html/Track.zig"),
|
||||||
@import("../webapi/element/html/Video.zig"),
|
@import("../webapi/element/html/Video.zig"),
|
||||||
@import("../webapi/element/html/UL.zig"),
|
@import("../webapi/element/html/UL.zig"),
|
||||||
@import("../webapi/element/html/Unknown.zig"),
|
@import("../webapi/element/html/Unknown.zig"),
|
||||||
@@ -568,6 +830,8 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/element/svg/Generic.zig"),
|
@import("../webapi/element/svg/Generic.zig"),
|
||||||
@import("../webapi/encoding/TextDecoder.zig"),
|
@import("../webapi/encoding/TextDecoder.zig"),
|
||||||
@import("../webapi/encoding/TextEncoder.zig"),
|
@import("../webapi/encoding/TextEncoder.zig"),
|
||||||
|
@import("../webapi/encoding/TextEncoderStream.zig"),
|
||||||
|
@import("../webapi/encoding/TextDecoderStream.zig"),
|
||||||
@import("../webapi/Event.zig"),
|
@import("../webapi/Event.zig"),
|
||||||
@import("../webapi/event/CompositionEvent.zig"),
|
@import("../webapi/event/CompositionEvent.zig"),
|
||||||
@import("../webapi/event/CustomEvent.zig"),
|
@import("../webapi/event/CustomEvent.zig"),
|
||||||
@@ -579,7 +843,12 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/event/PopStateEvent.zig"),
|
@import("../webapi/event/PopStateEvent.zig"),
|
||||||
@import("../webapi/event/UIEvent.zig"),
|
@import("../webapi/event/UIEvent.zig"),
|
||||||
@import("../webapi/event/MouseEvent.zig"),
|
@import("../webapi/event/MouseEvent.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/MessageChannel.zig"),
|
@import("../webapi/MessageChannel.zig"),
|
||||||
@import("../webapi/MessagePort.zig"),
|
@import("../webapi/MessagePort.zig"),
|
||||||
@import("../webapi/media/MediaError.zig"),
|
@import("../webapi/media/MediaError.zig"),
|
||||||
@@ -599,11 +868,16 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/streams/ReadableStream.zig"),
|
@import("../webapi/streams/ReadableStream.zig"),
|
||||||
@import("../webapi/streams/ReadableStreamDefaultReader.zig"),
|
@import("../webapi/streams/ReadableStreamDefaultReader.zig"),
|
||||||
@import("../webapi/streams/ReadableStreamDefaultController.zig"),
|
@import("../webapi/streams/ReadableStreamDefaultController.zig"),
|
||||||
|
@import("../webapi/streams/WritableStream.zig"),
|
||||||
|
@import("../webapi/streams/WritableStreamDefaultWriter.zig"),
|
||||||
|
@import("../webapi/streams/WritableStreamDefaultController.zig"),
|
||||||
|
@import("../webapi/streams/TransformStream.zig"),
|
||||||
@import("../webapi/Node.zig"),
|
@import("../webapi/Node.zig"),
|
||||||
@import("../webapi/storage/storage.zig"),
|
@import("../webapi/storage/storage.zig"),
|
||||||
@import("../webapi/URL.zig"),
|
@import("../webapi/URL.zig"),
|
||||||
@import("../webapi/Window.zig"),
|
@import("../webapi/Window.zig"),
|
||||||
@import("../webapi/Performance.zig"),
|
@import("../webapi/Performance.zig"),
|
||||||
|
@import("../webapi/PluginArray.zig"),
|
||||||
@import("../webapi/MutationObserver.zig"),
|
@import("../webapi/MutationObserver.zig"),
|
||||||
@import("../webapi/IntersectionObserver.zig"),
|
@import("../webapi/IntersectionObserver.zig"),
|
||||||
@import("../webapi/CustomElementRegistry.zig"),
|
@import("../webapi/CustomElementRegistry.zig"),
|
||||||
@@ -611,10 +885,19 @@ 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/FileList.zig"),
|
||||||
|
@import("../webapi/FileReader.zig"),
|
||||||
@import("../webapi/Screen.zig"),
|
@import("../webapi/Screen.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/WebGLRenderingContext.zig"),
|
||||||
|
@import("../webapi/canvas/OffscreenCanvas.zig"),
|
||||||
|
@import("../webapi/canvas/OffscreenCanvasRenderingContext2D.zig"),
|
||||||
|
@import("../webapi/SubtleCrypto.zig"),
|
||||||
|
@import("../webapi/Selection.zig"),
|
||||||
|
@import("../webapi/ImageData.zig"),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -17,25 +17,35 @@
|
|||||||
// 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");
|
||||||
pub const v8 = @import("v8");
|
pub const v8 = @import("v8").c;
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
const string = @import("../../string.zig");
|
||||||
|
|
||||||
pub const Env = @import("Env.zig");
|
pub const Env = @import("Env.zig");
|
||||||
pub const bridge = @import("bridge.zig");
|
pub const bridge = @import("bridge.zig");
|
||||||
pub const ExecutionWorld = @import("ExecutionWorld.zig");
|
pub const Caller = @import("Caller.zig");
|
||||||
|
pub const Origin = @import("Origin.zig");
|
||||||
pub const Context = @import("Context.zig");
|
pub const Context = @import("Context.zig");
|
||||||
|
pub const Local = @import("Local.zig");
|
||||||
pub const Inspector = @import("Inspector.zig");
|
pub const Inspector = @import("Inspector.zig");
|
||||||
pub const Snapshot = @import("Snapshot.zig");
|
pub const Snapshot = @import("Snapshot.zig");
|
||||||
pub const Platform = @import("Platform.zig");
|
pub const Platform = @import("Platform.zig");
|
||||||
|
pub const Isolate = @import("Isolate.zig");
|
||||||
|
pub const HandleScope = @import("HandleScope.zig");
|
||||||
|
|
||||||
// TODO: Is "This" really necessary?
|
|
||||||
pub const This = @import("This.zig");
|
|
||||||
pub const Value = @import("Value.zig");
|
pub const Value = @import("Value.zig");
|
||||||
pub const Array = @import("Array.zig");
|
pub const Array = @import("Array.zig");
|
||||||
|
pub const String = @import("String.zig");
|
||||||
pub const Object = @import("Object.zig");
|
pub const Object = @import("Object.zig");
|
||||||
pub const TryCatch = @import("TryCatch.zig");
|
pub const TryCatch = @import("TryCatch.zig");
|
||||||
pub const Function = @import("Function.zig");
|
pub const Function = @import("Function.zig");
|
||||||
|
pub const Promise = @import("Promise.zig");
|
||||||
|
pub const Module = @import("Module.zig");
|
||||||
|
pub const BigInt = @import("BigInt.zig");
|
||||||
|
pub const Number = @import("Number.zig");
|
||||||
|
pub const Integer = @import("Integer.zig");
|
||||||
|
pub const PromiseResolver = @import("PromiseResolver.zig");
|
||||||
|
pub const PromiseRejection = @import("PromiseRejection.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
@@ -68,246 +78,144 @@ pub const ArrayBuffer = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const PromiseResolver = struct {
|
pub const ArrayType = enum(u8) {
|
||||||
context: *Context,
|
int8,
|
||||||
resolver: v8.PromiseResolver,
|
uint8,
|
||||||
|
uint8_clamped,
|
||||||
pub fn promise(self: PromiseResolver) Promise {
|
int16,
|
||||||
return self.resolver.getPromise();
|
uint16,
|
||||||
}
|
int32,
|
||||||
|
uint32,
|
||||||
pub fn resolve(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
|
float16,
|
||||||
self._resolve(value) catch |err| {
|
float32,
|
||||||
log.err(.bug, "resolve", .{ .source = source, .err = err, .persistent = false });
|
float64,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
fn _resolve(self: PromiseResolver, value: anytype) !void {
|
|
||||||
const context = self.context;
|
|
||||||
const js_value = try context.zigValueToJs(value, .{});
|
|
||||||
|
|
||||||
if (self.resolver.resolve(context.v8_context, js_value) == null) {
|
pub fn ArrayBufferRef(comptime kind: ArrayType) type {
|
||||||
return error.FailedToResolvePromise;
|
return struct {
|
||||||
}
|
const Self = @This();
|
||||||
self.context.runMicrotasks();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
|
const BackingInt = switch (kind) {
|
||||||
self._reject(value) catch |err| {
|
.int8 => i8,
|
||||||
log.err(.bug, "reject", .{ .source = source, .err = err, .persistent = false });
|
.uint8, .uint8_clamped => u8,
|
||||||
|
.int16 => i16,
|
||||||
|
.uint16 => u16,
|
||||||
|
.int32 => i32,
|
||||||
|
.uint32 => u32,
|
||||||
|
.float16 => f16,
|
||||||
|
.float32 => f32,
|
||||||
|
.float64 => f64,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
fn _reject(self: PromiseResolver, value: anytype) !void {
|
|
||||||
const context = self.context;
|
|
||||||
const js_value = try context.zigValueToJs(value);
|
|
||||||
|
|
||||||
if (self.resolver.reject(context.v8_context, js_value) == null) {
|
local: *const Local,
|
||||||
return error.FailedToRejectPromise;
|
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);
|
||||||
}
|
}
|
||||||
self.context.runMicrotasks();
|
|
||||||
|
pub fn local(self: *const Global, l: *const Local) Self {
|
||||||
|
return .{ .local = l, .handle = v8.v8__Global__Get(&self.handle, l.isolate.handle).? };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const PersistentPromiseResolver = struct {
|
pub fn init(local: *const Local, size: usize) Self {
|
||||||
context: *Context,
|
const ctx = local.ctx;
|
||||||
resolver: v8.Persistent(v8.PromiseResolver),
|
const isolate = ctx.isolate;
|
||||||
|
const bits = switch (@typeInfo(BackingInt)) {
|
||||||
pub fn deinit(self: *PersistentPromiseResolver) void {
|
.int => |n| n.bits,
|
||||||
self.resolver.deinit();
|
.float => |f| f.bits,
|
||||||
}
|
else => unreachable,
|
||||||
|
|
||||||
pub fn promise(self: PersistentPromiseResolver) Promise {
|
|
||||||
return self.resolver.castToPromiseResolver().getPromise();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resolve(self: PersistentPromiseResolver, comptime source: []const u8, value: anytype) void {
|
|
||||||
self._resolve(value) catch |err| {
|
|
||||||
log.err(.bug, "resolve", .{ .source = source, .err = err, .persistent = true });
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
fn _resolve(self: PersistentPromiseResolver, value: anytype) !void {
|
|
||||||
const context = self.context;
|
|
||||||
const js_value = try context.zigValueToJs(value, .{});
|
|
||||||
defer context.runMicrotasks();
|
|
||||||
|
|
||||||
if (self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) == null) {
|
var array_buffer: *const v8.ArrayBuffer = undefined;
|
||||||
return error.FailedToResolvePromise;
|
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).?;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reject(self: PersistentPromiseResolver, comptime source: []const u8, value: anytype) void {
|
const handle: *const v8.Value = switch (comptime kind) {
|
||||||
self._reject(value) catch |err| {
|
.int8 => @ptrCast(v8.v8__Int8Array__New(array_buffer, 0, size).?),
|
||||||
log.err(.bug, "reject", .{ .source = source, .err = err, .persistent = true });
|
.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.trackGlobal(global);
|
||||||
|
|
||||||
|
return .{ .handle = global };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _reject(self: PersistentPromiseResolver, value: anytype) !void {
|
// If a WebAPI takes a []const u8, then we'll coerce any JS value to that string
|
||||||
const context = self.context;
|
// so null -> "null". But if a WebAPI takes an optional string, ?[]const u8,
|
||||||
const js_value = try context.zigValueToJs(value, .{});
|
// how should we handle null? If the parameter _isn't_ passed, then it's obvious
|
||||||
defer context.runMicrotasks();
|
// 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
|
||||||
// resolver.reject will return null if the promise isn't pending
|
// ?[]const u8 will be `null`. If you want it to be "null", use a `.js.NullableString`.
|
||||||
if (self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) == null) {
|
pub const NullableString = struct {
|
||||||
return error.FailedToRejectPromise;
|
value: []const u8,
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Promise = v8.Promise;
|
|
||||||
|
|
||||||
// When doing jsValueToZig, string ([]const u8) are managed by the
|
|
||||||
// call_arena. That means that if the API wants to persist the string
|
|
||||||
// (which is relatively common), it needs to dupe it again.
|
|
||||||
// If the parameter is an Env.String rather than a []const u8, then
|
|
||||||
// the page's arena will be used (rather than the call arena).
|
|
||||||
pub const String = struct {
|
|
||||||
string: []const u8,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Exception = struct {
|
pub const Exception = struct {
|
||||||
inner: v8.Value,
|
local: *const Local,
|
||||||
context: *const Context,
|
handle: *const v8.Value,
|
||||||
|
|
||||||
// the caller needs to deinit the string returned
|
|
||||||
pub fn exception(self: Exception, allocator: Allocator) ![]const u8 {
|
|
||||||
return self.context.valueToString(self.inner, .{ .allocator = allocator });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn UndefinedOr(comptime T: type) type {
|
|
||||||
return union(enum) {
|
|
||||||
undefined: void,
|
|
||||||
value: T,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// An interface for types that want to have their jsScopeEnd function be
|
|
||||||
// called when the call context ends
|
|
||||||
const CallScopeEndCallback = struct {
|
|
||||||
ptr: *anyopaque,
|
|
||||||
callScopeEndFn: *const fn (ptr: *anyopaque) void,
|
|
||||||
|
|
||||||
fn init(ptr: anytype) CallScopeEndCallback {
|
|
||||||
const T = @TypeOf(ptr);
|
|
||||||
const ptr_info = @typeInfo(T);
|
|
||||||
|
|
||||||
const gen = struct {
|
|
||||||
pub fn callScopeEnd(pointer: *anyopaque) void {
|
|
||||||
const self: T = @ptrCast(@alignCast(pointer));
|
|
||||||
return ptr_info.pointer.child.jsCallScopeEnd(self);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.ptr = ptr,
|
|
||||||
.callScopeEndFn = gen.callScopeEnd,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn callScopeEnd(self: CallScopeEndCallback) void {
|
|
||||||
self.callScopeEndFn(self.ptr);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Callback called on global's property missing.
|
|
||||||
// Return true to intercept the execution or false to let the call
|
|
||||||
// continue the chain.
|
|
||||||
pub const GlobalMissingCallback = struct {
|
|
||||||
ptr: *anyopaque,
|
|
||||||
missingFn: *const fn (ptr: *anyopaque, name: []const u8, ctx: *Context) bool,
|
|
||||||
|
|
||||||
pub fn init(ptr: anytype) GlobalMissingCallback {
|
|
||||||
const T = @TypeOf(ptr);
|
|
||||||
const ptr_info = @typeInfo(T);
|
|
||||||
|
|
||||||
const gen = struct {
|
|
||||||
pub fn missing(pointer: *anyopaque, name: []const u8, ctx: *Context) bool {
|
|
||||||
const self: T = @ptrCast(@alignCast(pointer));
|
|
||||||
return ptr_info.pointer.child.missing(self, name, ctx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.ptr = ptr,
|
|
||||||
.missingFn = gen.missing,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn missing(self: GlobalMissingCallback, name: []const u8, ctx: *Context) bool {
|
|
||||||
return self.missingFn(self.ptr, name, ctx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Attributes that return a primitive type are setup directly on the
|
|
||||||
// FunctionTemplate when the Env is setup. More complex types need a v8.Context
|
|
||||||
// and cannot be set directly on the FunctionTemplate.
|
|
||||||
// We default to saying types are primitives because that's mostly what
|
|
||||||
// we have. If we add a new complex type that isn't explictly handled here,
|
|
||||||
// we'll get a compiler error in simpleZigValueToJs, and can then explicitly
|
|
||||||
// add the type here.
|
|
||||||
pub fn isComplexAttributeType(ti: std.builtin.Type) bool {
|
|
||||||
return switch (ti) {
|
|
||||||
.array => true,
|
|
||||||
else => false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// These are simple types that we can convert to JS with only an isolate. This
|
// These are simple types that we can convert to JS with only an isolate. This
|
||||||
// is separated from the Caller's zigValueToJs to make it available when we
|
// is separated from the Caller's zigValueToJs to make it available when we
|
||||||
// don't have a caller (i.e., when setting static attributes on types)
|
// don't have a caller (i.e., when setting static attributes on types)
|
||||||
pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bool, comptime null_as_undefined: bool) if (fail) v8.Value else ?v8.Value {
|
pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool, comptime null_as_undefined: bool) if (fail) *const v8.Value else ?*const v8.Value {
|
||||||
switch (@typeInfo(@TypeOf(value))) {
|
switch (@typeInfo(@TypeOf(value))) {
|
||||||
.void => return v8.initUndefined(isolate).toValue(),
|
.void => return isolate.initUndefined(),
|
||||||
.null => if (comptime null_as_undefined) return v8.initUndefined(isolate).toValue() else return v8.initNull(isolate).toValue(),
|
.null => if (comptime null_as_undefined) return isolate.initUndefined() else return isolate.initNull(),
|
||||||
.bool => return v8.getValue(if (value) v8.initTrue(isolate) else v8.initFalse(isolate)),
|
.bool => return if (value) isolate.initTrue() else isolate.initFalse(),
|
||||||
.int => |n| switch (n.signedness) {
|
.int => |n| {
|
||||||
.signed => {
|
if (comptime n.bits <= 32) {
|
||||||
if (value > 0 and value <= 4_294_967_295) {
|
return @ptrCast(isolate.initInteger(value).handle);
|
||||||
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
|
|
||||||
}
|
}
|
||||||
if (value >= -2_147_483_648 and value <= 2_147_483_647) {
|
if (value >= 0 and value <= 4_294_967_295) {
|
||||||
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
|
return @ptrCast(isolate.initInteger(@as(u32, @intCast(value))).handle);
|
||||||
}
|
}
|
||||||
if (comptime n.bits <= 64) {
|
return @ptrCast(isolate.initBigInt(value).handle);
|
||||||
return v8.getValue(v8.BigInt.initI64(isolate, @intCast(value)));
|
|
||||||
}
|
|
||||||
@compileError(@typeName(value) ++ " is not supported");
|
|
||||||
},
|
|
||||||
.unsigned => {
|
|
||||||
if (value <= 4_294_967_295) {
|
|
||||||
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
|
|
||||||
}
|
|
||||||
if (comptime n.bits <= 64) {
|
|
||||||
return v8.getValue(v8.BigInt.initU64(isolate, @intCast(value)));
|
|
||||||
}
|
|
||||||
@compileError(@typeName(value) ++ " is not supported");
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
.comptime_int => {
|
.comptime_int => {
|
||||||
if (value >= 0) {
|
if (value > -2_147_483_648 and value <= 4_294_967_295) {
|
||||||
if (value <= 4_294_967_295) {
|
return @ptrCast(isolate.initInteger(value).handle);
|
||||||
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
|
|
||||||
}
|
}
|
||||||
return v8.BigInt.initU64(isolate, @intCast(value)).toValue();
|
return @ptrCast(isolate.initBigInt(value).handle);
|
||||||
}
|
|
||||||
if (value >= -2_147_483_648) {
|
|
||||||
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
|
|
||||||
}
|
|
||||||
return v8.BigInt.initI64(isolate, @intCast(value)).toValue();
|
|
||||||
},
|
|
||||||
.comptime_float => return v8.Number.init(isolate, value).toValue(),
|
|
||||||
.float => |f| switch (f.bits) {
|
|
||||||
64 => return v8.Number.init(isolate, value).toValue(),
|
|
||||||
32 => return v8.Number.init(isolate, @floatCast(value)).toValue(),
|
|
||||||
else => @compileError(@typeName(value) ++ " is not supported"),
|
|
||||||
},
|
},
|
||||||
|
.float, .comptime_float => return @ptrCast(isolate.initNumber(value).handle),
|
||||||
.pointer => |ptr| {
|
.pointer => |ptr| {
|
||||||
if (ptr.size == .slice and ptr.child == u8) {
|
if (ptr.size == .slice and ptr.child == u8) {
|
||||||
return v8.String.initUtf8(isolate, value).toValue();
|
return @ptrCast(isolate.initStringHandle(value));
|
||||||
}
|
}
|
||||||
if (ptr.size == .one) {
|
if (ptr.size == .one) {
|
||||||
const one_info = @typeInfo(ptr.child);
|
const one_info = @typeInfo(ptr.child);
|
||||||
if (one_info == .array and one_info.array.child == u8) {
|
if (one_info == .array and one_info.array.child == u8) {
|
||||||
return v8.String.initUtf8(isolate, value).toValue();
|
return @ptrCast(isolate.initStringHandle(value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -317,22 +225,23 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
|
|||||||
return simpleZigValueToJs(isolate, v, fail, null_as_undefined);
|
return simpleZigValueToJs(isolate, v, fail, null_as_undefined);
|
||||||
}
|
}
|
||||||
if (comptime null_as_undefined) {
|
if (comptime null_as_undefined) {
|
||||||
return v8.initUndefined(isolate).toValue();
|
return isolate.initUndefined();
|
||||||
}
|
}
|
||||||
return v8.initNull(isolate).toValue();
|
return isolate.initNull();
|
||||||
},
|
},
|
||||||
.@"struct" => {
|
.@"struct" => {
|
||||||
switch (@TypeOf(value)) {
|
switch (@TypeOf(value)) {
|
||||||
|
string.String => return isolate.initStringHandle(value.str()),
|
||||||
ArrayBuffer => {
|
ArrayBuffer => {
|
||||||
const values = value.values;
|
const values = value.values;
|
||||||
const len = values.len;
|
const len = values.len;
|
||||||
var array_buffer: v8.ArrayBuffer = undefined;
|
const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, len);
|
||||||
const backing_store = v8.BackingStore.init(isolate, len);
|
if (len > 0) {
|
||||||
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
|
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]);
|
||||||
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
|
}
|
||||||
|
const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
|
||||||
return .{ .handle = array_buffer.handle };
|
return @ptrCast(v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?);
|
||||||
},
|
},
|
||||||
// zig fmt: off
|
// zig fmt: off
|
||||||
TypedArray(u8), TypedArray(u16), TypedArray(u32), TypedArray(u64),
|
TypedArray(u8), TypedArray(u16), TypedArray(u32), TypedArray(u64),
|
||||||
@@ -349,37 +258,38 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
|
|||||||
else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)),
|
else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)),
|
||||||
};
|
};
|
||||||
|
|
||||||
var array_buffer: v8.ArrayBuffer = undefined;
|
var array_buffer: *const v8.ArrayBuffer = undefined;
|
||||||
if (len == 0) {
|
if (len == 0) {
|
||||||
array_buffer = v8.ArrayBuffer.init(isolate, 0);
|
array_buffer = v8.v8__ArrayBuffer__New(isolate.handle, 0).?;
|
||||||
} else {
|
} else {
|
||||||
const buffer_len = len * bits / 8;
|
const buffer_len = len * bits / 8;
|
||||||
const backing_store = v8.BackingStore.init(isolate, buffer_len);
|
const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, buffer_len).?;
|
||||||
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
|
const data: [*]u8 = @ptrCast(@alignCast(v8.v8__BackingStore__Data(backing_store)));
|
||||||
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);
|
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);
|
||||||
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
|
const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
|
||||||
|
array_buffer = v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (@typeInfo(value_type)) {
|
switch (@typeInfo(value_type)) {
|
||||||
.int => |n| switch (n.signedness) {
|
.int => |n| switch (n.signedness) {
|
||||||
.unsigned => switch (n.bits) {
|
.unsigned => switch (n.bits) {
|
||||||
8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(),
|
8 => return @ptrCast(v8.v8__Uint8Array__New(array_buffer, 0, len).?),
|
||||||
16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(),
|
16 => return @ptrCast(v8.v8__Uint16Array__New(array_buffer, 0, len).?),
|
||||||
32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(),
|
32 => return @ptrCast(v8.v8__Uint32Array__New(array_buffer, 0, len).?),
|
||||||
64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(),
|
64 => return @ptrCast(v8.v8__BigUint64Array__New(array_buffer, 0, len).?),
|
||||||
else => {},
|
else => {},
|
||||||
},
|
},
|
||||||
.signed => switch (n.bits) {
|
.signed => switch (n.bits) {
|
||||||
8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(),
|
8 => return @ptrCast(v8.v8__Int8Array__New(array_buffer, 0, len).?),
|
||||||
16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(),
|
16 => return @ptrCast(v8.v8__Int16Array__New(array_buffer, 0, len).?),
|
||||||
32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(),
|
32 => return @ptrCast(v8.v8__Int32Array__New(array_buffer, 0, len).?),
|
||||||
64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(),
|
64 => return @ptrCast(v8.v8__BigInt64Array__New(array_buffer, 0, len).?),
|
||||||
else => {},
|
else => {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
.float => |f| switch (f.bits) {
|
.float => |f| switch (f.bits) {
|
||||||
32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(),
|
32 => return @ptrCast(v8.v8__Float32Array__New(array_buffer, 0, len).?),
|
||||||
64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(),
|
64 => return @ptrCast(v8.v8__Float64Array__New(array_buffer, 0, len).?),
|
||||||
else => {},
|
else => {},
|
||||||
},
|
},
|
||||||
else => {},
|
else => {},
|
||||||
@@ -388,6 +298,7 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
|
|||||||
// but this can never be valid.
|
// but this can never be valid.
|
||||||
@compileError("Invalid TypeArray type: " ++ @typeName(value_type));
|
@compileError("Invalid TypeArray type: " ++ @typeName(value_type));
|
||||||
},
|
},
|
||||||
|
inline String, BigInt, Integer, Number, Value, Object => return value.handle,
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -406,76 +317,6 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _createException(isolate: v8.Isolate, msg: []const u8) v8.Value {
|
|
||||||
return v8.Exception.initError(v8.String.initUtf8(isolate, msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn classNameForStruct(comptime Struct: type) []const u8 {
|
|
||||||
if (@hasDecl(Struct, "js_name")) {
|
|
||||||
return Struct.js_name;
|
|
||||||
}
|
|
||||||
@setEvalBranchQuota(10_000);
|
|
||||||
const full_name = @typeName(Struct);
|
|
||||||
const last = std.mem.lastIndexOfScalar(u8, full_name, '.') orelse return full_name;
|
|
||||||
return full_name[last + 1 ..];
|
|
||||||
}
|
|
||||||
|
|
||||||
// When we return a Zig object to V8, we put it on the heap and pass it into
|
|
||||||
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
|
|
||||||
// function parameter, we know what type it _should_ be.
|
|
||||||
//
|
|
||||||
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
|
|
||||||
// to the parameter type:
|
|
||||||
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
|
|
||||||
//
|
|
||||||
// But there are 2 reasons we can't do that.
|
|
||||||
//
|
|
||||||
// == Reason 1 ==
|
|
||||||
// The JS code might pass the wrong type:
|
|
||||||
//
|
|
||||||
// var cat = new Cat();
|
|
||||||
// cat.setOwner(new Cat());
|
|
||||||
//
|
|
||||||
// The zig_setOwner method expects the 2nd parameter to be an *Owner, but
|
|
||||||
// the JS code passed a *Cat.
|
|
||||||
//
|
|
||||||
// To solve this issue, we tag every returned value so that we can check what
|
|
||||||
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
|
|
||||||
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
|
|
||||||
//
|
|
||||||
// == Reason 2 ==
|
|
||||||
// Because of prototype inheritance, even "correct" code can be a challenge. For
|
|
||||||
// example, say the above JavaScript is fixed:
|
|
||||||
//
|
|
||||||
// var cat = new Cat();
|
|
||||||
// cat.setOwner(new Owner("Leto"));
|
|
||||||
//
|
|
||||||
// The issue is that setOwner might not expect an *Owner, but rather a
|
|
||||||
// *Person, which is the prototype for Owner. Now our Zig code is expecting
|
|
||||||
// a *Person, but it was (correctly) given an *Owner.
|
|
||||||
// For this reason, we also store the prototype chain.
|
|
||||||
pub const TaggedAnyOpaque = struct {
|
|
||||||
prototype_len: u16,
|
|
||||||
prototype_chain: [*]const PrototypeChainEntry,
|
|
||||||
|
|
||||||
// Ptr to the Zig instance. Between the context where it's called (i.e.
|
|
||||||
// we have the comptime parameter info for all functions), and the index field
|
|
||||||
// we can figure out what type this is.
|
|
||||||
value: *anyopaque,
|
|
||||||
|
|
||||||
// When we're asked to describe an object via the Inspector, we _must_ include
|
|
||||||
// the proper subtype (and description) fields in the returned JSON.
|
|
||||||
// V8 will give us a Value and ask us for the subtype. From the v8.Value we
|
|
||||||
// can get a v8.Object, and from the v8.Object, we can get out TaggedAnyOpaque
|
|
||||||
// which is where we store the subtype.
|
|
||||||
subtype: ?bridge.SubType,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const PrototypeChainEntry = struct {
|
|
||||||
index: bridge.JsApiLookup.BackingInt,
|
|
||||||
offset: u16, // offset to the _proto field
|
|
||||||
};
|
|
||||||
|
|
||||||
// These are here, and not in Inspector.zig, because Inspector.zig isn't always
|
// These are here, and not in Inspector.zig, because Inspector.zig isn't always
|
||||||
// included (e.g. in the wpt build).
|
// included (e.g. in the wpt build).
|
||||||
|
|
||||||
@@ -483,10 +324,10 @@ pub const PrototypeChainEntry = struct {
|
|||||||
// it'll call this function to gets its [optional] subtype - which, from V8's
|
// it'll call this function to gets its [optional] subtype - which, from V8's
|
||||||
// point of view, is an arbitrary string.
|
// point of view, is an arbitrary string.
|
||||||
pub export fn v8_inspector__Client__IMPL__valueSubtype(
|
pub export fn v8_inspector__Client__IMPL__valueSubtype(
|
||||||
_: *v8.c.InspectorClientImpl,
|
_: *v8.InspectorClientImpl,
|
||||||
c_value: *const v8.C_Value,
|
c_value: *const v8.Value,
|
||||||
) callconv(.c) [*c]const u8 {
|
) callconv(.c) [*c]const u8 {
|
||||||
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
|
const external_entry = Inspector.getTaggedOpaque(c_value) orelse return null;
|
||||||
return if (external_entry.subtype) |st| @tagName(st) else null;
|
return if (external_entry.subtype) |st| @tagName(st) else null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,19 +336,19 @@ pub export fn v8_inspector__Client__IMPL__valueSubtype(
|
|||||||
// present, even if it's empty. So if we have a subType for the value, we'll
|
// present, even if it's empty. So if we have a subType for the value, we'll
|
||||||
// put an empty description.
|
// put an empty description.
|
||||||
pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
|
pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
|
||||||
_: *v8.c.InspectorClientImpl,
|
_: *v8.InspectorClientImpl,
|
||||||
v8_context: *const v8.C_Context,
|
v8_context: *const v8.Context,
|
||||||
c_value: *const v8.C_Value,
|
c_value: *const v8.Value,
|
||||||
) callconv(.c) [*c]const u8 {
|
) callconv(.c) [*c]const u8 {
|
||||||
_ = v8_context;
|
_ = v8_context;
|
||||||
|
|
||||||
// We _must_ include a non-null description in order for the subtype value
|
// We _must_ include a non-null description in order for the subtype value
|
||||||
// to be included. Besides that, I don't know if the value has any meaning
|
// to be included. Besides that, I don't know if the value has any meaning
|
||||||
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
|
const external_entry = Inspector.getTaggedOpaque(c_value) orelse return null;
|
||||||
return if (external_entry.subtype == null) null else "";
|
return if (external_entry.subtype == null) null else "";
|
||||||
}
|
}
|
||||||
|
|
||||||
test "TaggedAnyOpaque" {
|
test "TaggedAnyOpaque" {
|
||||||
// If we grow this, fine, but it should be a conscious decision
|
// If we grow this, fine, but it should be a conscious decision
|
||||||
try std.testing.expectEqual(24, @sizeOf(TaggedAnyOpaque));
|
try std.testing.expectEqual(24, @sizeOf(@import("TaggedOpaque.zig")));
|
||||||
}
|
}
|
||||||
|
|||||||
682
src/browser/markdown.zig
Normal file
682
src/browser/markdown.zig
Normal file
@@ -0,0 +1,682 @@
|
|||||||
|
// 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 URL = @import("URL.zig");
|
||||||
|
const TreeWalker = @import("webapi/TreeWalker.zig");
|
||||||
|
const CData = @import("webapi/CData.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
const isAllWhitespace = @import("../string.zig").isAllWhitespace;
|
||||||
|
|
||||||
|
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 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 {
|
||||||
|
const tag = el.getTag();
|
||||||
|
return !tag.isMetadata() and tag != .svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getAnchorLabel(el: *Element) ?[]const u8 {
|
||||||
|
return el.getAttributeSafe(comptime .wrap("aria-label")) orelse el.getAttributeSafe(comptime .wrap("title"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hasBlockDescendant(root: *Node) bool {
|
||||||
|
var tw = TreeWalker.FullExcludeSelf.Elements.init(root, .{});
|
||||||
|
while (tw.next()) |el| {
|
||||||
|
if (el.getTag().isBlock()) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hasVisibleContent(root: *Node) bool {
|
||||||
|
var tw = TreeWalker.FullExcludeSelf.init(root, .{});
|
||||||
|
while (tw.next()) |node| {
|
||||||
|
if (isSignificantText(node)) return true;
|
||||||
|
if (node.is(Element)) |el| {
|
||||||
|
if (!isVisibleElement(el)) {
|
||||||
|
tw.skipChildren();
|
||||||
|
} else if (el.getTag() == .img) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 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 (tag.isBlock() 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| {
|
||||||
|
const absolute_src = URL.resolve(page.call_arena, page.base(), src, .{ .encode = true }) catch src;
|
||||||
|
try writer.writeAll(absolute_src);
|
||||||
|
}
|
||||||
|
try writer.writeAll(")");
|
||||||
|
state.last_char_was_newline = false;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.anchor => {
|
||||||
|
const has_content = hasVisibleContent(el.asNode());
|
||||||
|
const label = getAnchorLabel(el);
|
||||||
|
const href_raw = el.getAttributeSafe(comptime .wrap("href"));
|
||||||
|
|
||||||
|
if (!has_content and label == null and href_raw == null) return;
|
||||||
|
|
||||||
|
const has_block = hasBlockDescendant(el.asNode());
|
||||||
|
const href = if (href_raw) |h| URL.resolve(page.call_arena, page.base(), h, .{ .encode = true }) catch h else null;
|
||||||
|
|
||||||
|
if (has_block) {
|
||||||
|
try renderChildren(el.asNode(), state, writer, page);
|
||||||
|
if (href) |h| {
|
||||||
|
if (!state.last_char_was_newline) try writer.writeByte('\n');
|
||||||
|
try writer.writeAll("([](");
|
||||||
|
try writer.writeAll(h);
|
||||||
|
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('[');
|
||||||
|
if (has_content) {
|
||||||
|
try renderChildren(el.asNode(), state, writer, page);
|
||||||
|
} else {
|
||||||
|
try writer.writeAll(label orelse "");
|
||||||
|
}
|
||||||
|
try writer.writeAll("](");
|
||||||
|
if (href) |h| {
|
||||||
|
try writer.writeAll(h);
|
||||||
|
}
|
||||||
|
try writer.writeAll(")\n");
|
||||||
|
state.last_char_was_newline = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try writer.writeByte('[');
|
||||||
|
if (has_content) {
|
||||||
|
try renderChildren(el.asNode(), state, writer, page);
|
||||||
|
} else {
|
||||||
|
try writer.writeAll(label orelse "");
|
||||||
|
}
|
||||||
|
try writer.writeAll("](");
|
||||||
|
if (href) |h| {
|
||||||
|
try writer.writeAll(h);
|
||||||
|
}
|
||||||
|
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 (tag.isBlock() 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();
|
||||||
|
page.url = "http://localhost/";
|
||||||
|
|
||||||
|
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=\"/relative\">Link</a>", "[Link](http://localhost/relative)\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
|
||||||
|
\\([](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](http://localhost/1)
|
||||||
|
\\[Link 2](http://localhost/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](http://localhost/1).
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: skip empty links" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<a href="/"></a>
|
||||||
|
\\<a href="/"><svg></svg></a>
|
||||||
|
,
|
||||||
|
\\[](http://localhost/)
|
||||||
|
\\[](http://localhost/)
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: resolve links" {
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
const page = try testing.test_session.createPage();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
page.url = "https://example.com/a/index.html";
|
||||||
|
|
||||||
|
const doc = page.window._document;
|
||||||
|
const div = try doc.createElement("div", null, page);
|
||||||
|
try page.parseHtmlAsChildren(div.asNode(),
|
||||||
|
\\<a href="b">Link</a>
|
||||||
|
\\<img src="../c.png" alt="Img">
|
||||||
|
\\<a href="/my page">Space</a>
|
||||||
|
);
|
||||||
|
|
||||||
|
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||||
|
defer aw.deinit();
|
||||||
|
try dump(div.asNode(), .{}, &aw.writer, page);
|
||||||
|
|
||||||
|
try testing.expectString(
|
||||||
|
\\[Link](https://example.com/a/b)
|
||||||
|
\\
|
||||||
|
\\[Space](https://example.com/my%20page)
|
||||||
|
\\
|
||||||
|
, aw.written());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: anchor fallback label" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<a href="/discord" aria-label="Discord Server"><svg></svg></a>
|
||||||
|
, "[Discord Server](http://localhost/discord)\n");
|
||||||
|
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<a href="/search" title="Search Site"><svg></svg></a>
|
||||||
|
, "[Search Site](http://localhost/search)\n");
|
||||||
|
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<a href="/no-label"><svg></svg></a>
|
||||||
|
, "[](http://localhost/no-label)\n");
|
||||||
|
}
|
||||||
@@ -17,12 +17,17 @@
|
|||||||
// 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 lp = @import("lightpanda");
|
||||||
const h5e = @import("html5ever.zig");
|
const h5e = @import("html5ever.zig");
|
||||||
|
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
const Node = @import("../webapi/Node.zig");
|
const Node = @import("../webapi/Node.zig");
|
||||||
const Element = @import("../webapi/Element.zig");
|
const Element = @import("../webapi/Element.zig");
|
||||||
|
|
||||||
|
pub const AttributeIterator = h5e.AttributeIterator;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
pub const ParsedNode = struct {
|
pub const ParsedNode = struct {
|
||||||
node: *Node,
|
node: *Node,
|
||||||
@@ -104,7 +109,7 @@ pub fn parseXML(self: *Parser, xml: []const u8) void {
|
|||||||
xml.len,
|
xml.len,
|
||||||
&self.container,
|
&self.container,
|
||||||
self,
|
self,
|
||||||
createElementCallback,
|
createXMLElementCallback,
|
||||||
getDataCallback,
|
getDataCallback,
|
||||||
appendCallback,
|
appendCallback,
|
||||||
parseErrorCallback,
|
parseErrorCallback,
|
||||||
@@ -162,7 +167,7 @@ pub const Streaming = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(self: *Streaming) !void {
|
pub fn start(self: *Streaming) !void {
|
||||||
std.debug.assert(self.handle == null);
|
lp.assert(self.handle == null, "Parser.start non-null handle", .{});
|
||||||
|
|
||||||
self.handle = h5e.html5ever_streaming_parser_create(
|
self.handle = h5e.html5ever_streaming_parser_create(
|
||||||
&self.parser.container,
|
&self.parser.container,
|
||||||
@@ -225,17 +230,26 @@ fn _popCallback(self: *Parser, node: *Node) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn createElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {
|
fn createElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {
|
||||||
|
return _createElementCallbackWithDefaultnamespace(ctx, data, qname, attributes, .unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn createXMLElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {
|
||||||
|
return _createElementCallbackWithDefaultnamespace(ctx, data, qname, attributes, .xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _createElementCallbackWithDefaultnamespace(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator, default_namespace: Element.Namespace) ?*anyopaque {
|
||||||
const self: *Parser = @ptrCast(@alignCast(ctx));
|
const self: *Parser = @ptrCast(@alignCast(ctx));
|
||||||
return self._createElementCallback(data, qname, attributes) catch |err| {
|
return self._createElementCallback(data, qname, attributes, default_namespace) catch |err| {
|
||||||
self.err = .{ .err = err, .source = .create_element };
|
self.err = .{ .err = err, .source = .create_element };
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) !*anyopaque {
|
fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator, default_namespace: Element.Namespace) !*anyopaque {
|
||||||
const page = self.page;
|
const page = self.page;
|
||||||
const name = qname.local.slice();
|
const name = qname.local.slice();
|
||||||
const namespace = qname.ns.slice();
|
const namespace_string = qname.ns.slice();
|
||||||
const node = try page.createElement(namespace, name, attributes);
|
const namespace = if (namespace_string.len == 0) default_namespace else Element.Namespace.parse(namespace_string);
|
||||||
|
const node = try page.createElementNS(namespace, name, attributes);
|
||||||
|
|
||||||
const pn = try self.arena.create(ParsedNode);
|
const pn = try self.arena.create(ParsedNode);
|
||||||
pn.* = .{
|
pn.* = .{
|
||||||
@@ -348,7 +362,7 @@ fn getDataCallback(ctx: *anyopaque) callconv(.c) *anyopaque {
|
|||||||
const pn: *ParsedNode = @ptrCast(@alignCast(ctx));
|
const pn: *ParsedNode = @ptrCast(@alignCast(ctx));
|
||||||
// For non-elements, data is null. But, we expect this to only ever
|
// For non-elements, data is null. But, we expect this to only ever
|
||||||
// be called for elements.
|
// be called for elements.
|
||||||
std.debug.assert(pn.data != null);
|
lp.assert(pn.data != null, "Parser.getDataCallback null data", .{});
|
||||||
return pn.data.?;
|
return pn.data.?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,6 +377,17 @@ fn _appendCallback(self: *Parser, parent: *Node, node_or_text: h5e.NodeOrText) !
|
|||||||
switch (node_or_text.toUnion()) {
|
switch (node_or_text.toUnion()) {
|
||||||
.node => |cpn| {
|
.node => |cpn| {
|
||||||
const child = getNode(cpn);
|
const child = getNode(cpn);
|
||||||
|
if (child._parent) |previous_parent| {
|
||||||
|
// html5ever says this can't happen, but we might be screwing up
|
||||||
|
// the node on our side. We shouldn't be, but we're seeing this
|
||||||
|
// in the wild, and I'm not sure why. In debug, let's crash so
|
||||||
|
// we can try to figure it out. In release, let's disconnect
|
||||||
|
// the child first.
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
unreachable;
|
||||||
|
}
|
||||||
|
self.page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() });
|
||||||
|
}
|
||||||
try self.page.appendNew(parent, .{ .node = child });
|
try self.page.appendNew(parent, .{ .node = child });
|
||||||
},
|
},
|
||||||
.text => |txt| try self.page.appendNew(parent, .{ .text = txt }),
|
.text => |txt| try self.page.appendNew(parent, .{ .text = txt }),
|
||||||
@@ -399,7 +424,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 }, .{});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -16,8 +16,6 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
// Gets the Parent of child.
|
// Gets the Parent of child.
|
||||||
// HtmlElement.of(script) -> *HTMLElement
|
// HtmlElement.of(script) -> *HTMLElement
|
||||||
pub fn Struct(comptime T: type) type {
|
pub fn Struct(comptime T: type) type {
|
||||||
@@ -28,37 +26,3 @@ pub fn Struct(comptime T: type) type {
|
|||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates an enum of N enums. Doesn't perserve their underlying integer
|
|
||||||
pub fn mergeEnums(comptime enums: []const type) type {
|
|
||||||
const field_count = blk: {
|
|
||||||
var count: usize = 0;
|
|
||||||
inline for (enums) |e| {
|
|
||||||
count += @typeInfo(e).@"enum".fields.len;
|
|
||||||
}
|
|
||||||
break :blk count;
|
|
||||||
};
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
var fields: [field_count]std.builtin.Type.EnumField = undefined;
|
|
||||||
for (enums) |e| {
|
|
||||||
for (@typeInfo(e).@"enum".fields) |f| {
|
|
||||||
fields[i] = .{
|
|
||||||
.name = f.name,
|
|
||||||
.value = i,
|
|
||||||
};
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return @Type(.{ .@"enum" = .{
|
|
||||||
.decls = &.{},
|
|
||||||
.tag_type = blk: {
|
|
||||||
if (field_count <= std.math.maxInt(u8)) break :blk u8;
|
|
||||||
if (field_count <= std.math.maxInt(u16)) break :blk u16;
|
|
||||||
unreachable;
|
|
||||||
},
|
|
||||||
.fields = &fields,
|
|
||||||
.is_exhaustive = true,
|
|
||||||
} });
|
|
||||||
}
|
|
||||||
|
|||||||
489
src/browser/structured_data.zig
Normal file
489
src/browser/structured_data.zig
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
// 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 URL = @import("URL.zig");
|
||||||
|
const TreeWalker = @import("webapi/TreeWalker.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
/// Key-value pair for structured data properties.
|
||||||
|
pub const Property = struct {
|
||||||
|
key: []const u8,
|
||||||
|
value: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const AlternateLink = struct {
|
||||||
|
href: []const u8,
|
||||||
|
hreflang: ?[]const u8,
|
||||||
|
type: ?[]const u8,
|
||||||
|
title: ?[]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const StructuredData = struct {
|
||||||
|
json_ld: []const []const u8,
|
||||||
|
open_graph: []const Property,
|
||||||
|
twitter_card: []const Property,
|
||||||
|
meta: []const Property,
|
||||||
|
links: []const Property,
|
||||||
|
alternate: []const AlternateLink,
|
||||||
|
|
||||||
|
pub fn jsonStringify(self: *const StructuredData, jw: anytype) !void {
|
||||||
|
try jw.beginObject();
|
||||||
|
|
||||||
|
try jw.objectField("jsonLd");
|
||||||
|
try jw.write(self.json_ld);
|
||||||
|
|
||||||
|
try jw.objectField("openGraph");
|
||||||
|
try writeProperties(jw, self.open_graph);
|
||||||
|
|
||||||
|
try jw.objectField("twitterCard");
|
||||||
|
try writeProperties(jw, self.twitter_card);
|
||||||
|
|
||||||
|
try jw.objectField("meta");
|
||||||
|
try writeProperties(jw, self.meta);
|
||||||
|
|
||||||
|
try jw.objectField("links");
|
||||||
|
try writeProperties(jw, self.links);
|
||||||
|
|
||||||
|
if (self.alternate.len > 0) {
|
||||||
|
try jw.objectField("alternate");
|
||||||
|
try jw.beginArray();
|
||||||
|
for (self.alternate) |alt| {
|
||||||
|
try jw.beginObject();
|
||||||
|
try jw.objectField("href");
|
||||||
|
try jw.write(alt.href);
|
||||||
|
if (alt.hreflang) |v| {
|
||||||
|
try jw.objectField("hreflang");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
if (alt.type) |v| {
|
||||||
|
try jw.objectField("type");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
if (alt.title) |v| {
|
||||||
|
try jw.objectField("title");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
try jw.endArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Serializes properties as a JSON object. When a key appears multiple times
|
||||||
|
/// (e.g. multiple og:image tags), values are grouped into an array.
|
||||||
|
/// Alternatives considered: always-array values (verbose), or an array of
|
||||||
|
/// {key, value} pairs (preserves order but less ergonomic for consumers).
|
||||||
|
fn writeProperties(jw: anytype, properties: []const Property) !void {
|
||||||
|
try jw.beginObject();
|
||||||
|
for (properties, 0..) |prop, i| {
|
||||||
|
// Skip keys already written by an earlier occurrence.
|
||||||
|
var already_written = false;
|
||||||
|
for (properties[0..i]) |prev| {
|
||||||
|
if (std.mem.eql(u8, prev.key, prop.key)) {
|
||||||
|
already_written = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (already_written) continue;
|
||||||
|
|
||||||
|
// Count total occurrences to decide string vs array.
|
||||||
|
var count: usize = 0;
|
||||||
|
for (properties) |p| {
|
||||||
|
if (std.mem.eql(u8, p.key, prop.key)) count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.objectField(prop.key);
|
||||||
|
if (count == 1) {
|
||||||
|
try jw.write(prop.value);
|
||||||
|
} else {
|
||||||
|
try jw.beginArray();
|
||||||
|
for (properties) |p| {
|
||||||
|
if (std.mem.eql(u8, p.key, prop.key)) {
|
||||||
|
try jw.write(p.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try jw.endArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract all structured data from the page.
|
||||||
|
pub fn collectStructuredData(
|
||||||
|
root: *Node,
|
||||||
|
arena: Allocator,
|
||||||
|
page: *Page,
|
||||||
|
) !StructuredData {
|
||||||
|
var json_ld: std.ArrayList([]const u8) = .empty;
|
||||||
|
var open_graph: std.ArrayList(Property) = .empty;
|
||||||
|
var twitter_card: std.ArrayList(Property) = .empty;
|
||||||
|
var meta: std.ArrayList(Property) = .empty;
|
||||||
|
var links: std.ArrayList(Property) = .empty;
|
||||||
|
var alternate: std.ArrayList(AlternateLink) = .empty;
|
||||||
|
|
||||||
|
// Extract language from the root <html> element.
|
||||||
|
if (root.is(Element)) |root_el| {
|
||||||
|
if (root_el.getAttributeSafe(comptime .wrap("lang"))) |lang| {
|
||||||
|
try meta.append(arena, .{ .key = "language", .value = lang });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Root is document — check documentElement.
|
||||||
|
var children = root.childrenIterator();
|
||||||
|
while (children.next()) |child| {
|
||||||
|
const el = child.is(Element) orelse continue;
|
||||||
|
if (el.getTag() == .html) {
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("lang"))) |lang| {
|
||||||
|
try meta.append(arena, .{ .key = "language", .value = lang });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tw = TreeWalker.Full.init(root, .{});
|
||||||
|
while (tw.next()) |node| {
|
||||||
|
const el = node.is(Element) orelse continue;
|
||||||
|
|
||||||
|
switch (el.getTag()) {
|
||||||
|
.script => {
|
||||||
|
try collectJsonLd(el, arena, &json_ld);
|
||||||
|
tw.skipChildren();
|
||||||
|
},
|
||||||
|
.meta => collectMeta(el, &open_graph, &twitter_card, &meta, arena) catch {},
|
||||||
|
.title => try collectTitle(node, arena, &meta),
|
||||||
|
.link => try collectLink(el, arena, page, &links, &alternate),
|
||||||
|
// Skip body subtree for non-JSON-LD — all other metadata is in <head>.
|
||||||
|
// JSON-LD can appear in <body> so we don't skip the whole body.
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.json_ld = json_ld.items,
|
||||||
|
.open_graph = open_graph.items,
|
||||||
|
.twitter_card = twitter_card.items,
|
||||||
|
.meta = meta.items,
|
||||||
|
.links = links.items,
|
||||||
|
.alternate = alternate.items,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectJsonLd(
|
||||||
|
el: *Element,
|
||||||
|
arena: Allocator,
|
||||||
|
json_ld: *std.ArrayList([]const u8),
|
||||||
|
) !void {
|
||||||
|
const type_attr = el.getAttributeSafe(comptime .wrap("type")) orelse return;
|
||||||
|
if (!std.ascii.eqlIgnoreCase(type_attr, "application/ld+json")) return;
|
||||||
|
|
||||||
|
var buf: std.Io.Writer.Allocating = .init(arena);
|
||||||
|
try el.asNode().getTextContent(&buf.writer);
|
||||||
|
const text = buf.written();
|
||||||
|
if (text.len > 0) {
|
||||||
|
try json_ld.append(arena, std.mem.trim(u8, text, &std.ascii.whitespace));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectMeta(
|
||||||
|
el: *Element,
|
||||||
|
open_graph: *std.ArrayList(Property),
|
||||||
|
twitter_card: *std.ArrayList(Property),
|
||||||
|
meta: *std.ArrayList(Property),
|
||||||
|
arena: Allocator,
|
||||||
|
) !void {
|
||||||
|
// charset: <meta charset="..."> (no content attribute needed).
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("charset"))) |charset| {
|
||||||
|
try meta.append(arena, .{ .key = "charset", .value = charset });
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = el.getAttributeSafe(comptime .wrap("content")) orelse return;
|
||||||
|
|
||||||
|
// Open Graph: <meta property="og:...">
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("property"))) |property| {
|
||||||
|
if (std.mem.startsWith(u8, property, "og:")) {
|
||||||
|
try open_graph.append(arena, .{ .key = property[3..], .value = content });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Article, profile, etc. are OG sub-namespaces.
|
||||||
|
if (std.mem.startsWith(u8, property, "article:") or
|
||||||
|
std.mem.startsWith(u8, property, "profile:") or
|
||||||
|
std.mem.startsWith(u8, property, "book:") or
|
||||||
|
std.mem.startsWith(u8, property, "music:") or
|
||||||
|
std.mem.startsWith(u8, property, "video:"))
|
||||||
|
{
|
||||||
|
try open_graph.append(arena, .{ .key = property, .value = content });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Twitter Cards: <meta name="twitter:...">
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("name"))) |name| {
|
||||||
|
if (std.mem.startsWith(u8, name, "twitter:")) {
|
||||||
|
try twitter_card.append(arena, .{ .key = name[8..], .value = content });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard meta tags by name.
|
||||||
|
const known_names = [_][]const u8{
|
||||||
|
"description", "author", "keywords", "robots",
|
||||||
|
"viewport", "generator", "theme-color",
|
||||||
|
};
|
||||||
|
for (known_names) |known| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(name, known)) {
|
||||||
|
try meta.append(arena, .{ .key = known, .value = content });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// http-equiv (e.g. Content-Type, refresh)
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("http-equiv"))) |http_equiv| {
|
||||||
|
try meta.append(arena, .{ .key = http_equiv, .value = content });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectTitle(
|
||||||
|
node: *Node,
|
||||||
|
arena: Allocator,
|
||||||
|
meta: *std.ArrayList(Property),
|
||||||
|
) !void {
|
||||||
|
var buf: std.Io.Writer.Allocating = .init(arena);
|
||||||
|
try node.getTextContent(&buf.writer);
|
||||||
|
const text = std.mem.trim(u8, buf.written(), &std.ascii.whitespace);
|
||||||
|
if (text.len > 0) {
|
||||||
|
try meta.append(arena, .{ .key = "title", .value = text });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectLink(
|
||||||
|
el: *Element,
|
||||||
|
arena: Allocator,
|
||||||
|
page: *Page,
|
||||||
|
links: *std.ArrayList(Property),
|
||||||
|
alternate: *std.ArrayList(AlternateLink),
|
||||||
|
) !void {
|
||||||
|
const rel = el.getAttributeSafe(comptime .wrap("rel")) orelse return;
|
||||||
|
const raw_href = el.getAttributeSafe(comptime .wrap("href")) orelse return;
|
||||||
|
const href = URL.resolve(arena, page.base(), raw_href, .{ .encode = true }) catch raw_href;
|
||||||
|
|
||||||
|
if (std.ascii.eqlIgnoreCase(rel, "alternate")) {
|
||||||
|
try alternate.append(arena, .{
|
||||||
|
.href = href,
|
||||||
|
.hreflang = el.getAttributeSafe(comptime .wrap("hreflang")),
|
||||||
|
.type = el.getAttributeSafe(comptime .wrap("type")),
|
||||||
|
.title = el.getAttributeSafe(comptime .wrap("title")),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relevant_rels = [_][]const u8{
|
||||||
|
"canonical", "icon", "manifest", "shortcut icon",
|
||||||
|
"apple-touch-icon", "search", "author", "license",
|
||||||
|
"dns-prefetch", "preconnect",
|
||||||
|
};
|
||||||
|
for (relevant_rels) |known| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(rel, known)) {
|
||||||
|
try links.append(arena, .{ .key = known, .value = href });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tests ---
|
||||||
|
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
|
||||||
|
fn testStructuredData(html: []const u8) !StructuredData {
|
||||||
|
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);
|
||||||
|
|
||||||
|
return collectStructuredData(div.asNode(), page.call_arena, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findProperty(props: []const Property, key: []const u8) ?[]const u8 {
|
||||||
|
for (props) |p| {
|
||||||
|
if (std.mem.eql(u8, p.key, key)) return p.value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: json-ld" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<script type="application/ld+json">
|
||||||
|
\\{"@context":"https://schema.org","@type":"Article","headline":"Test"}
|
||||||
|
\\</script>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, data.json_ld.len);
|
||||||
|
try testing.expect(std.mem.indexOf(u8, data.json_ld[0], "Article") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: multiple json-ld" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<script type="application/ld+json">{"@type":"Organization"}</script>
|
||||||
|
\\<script type="application/ld+json">{"@type":"BreadcrumbList"}</script>
|
||||||
|
\\<script type="text/javascript">var x = 1;</script>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(2, data.json_ld.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: open graph" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<meta property="og:title" content="My Page">
|
||||||
|
\\<meta property="og:description" content="A description">
|
||||||
|
\\<meta property="og:image" content="https://example.com/img.jpg">
|
||||||
|
\\<meta property="og:url" content="https://example.com">
|
||||||
|
\\<meta property="og:type" content="article">
|
||||||
|
\\<meta property="article:published_time" content="2026-03-10">
|
||||||
|
);
|
||||||
|
try testing.expectEqual(6, data.open_graph.len);
|
||||||
|
try testing.expectEqual("My Page", findProperty(data.open_graph, "title").?);
|
||||||
|
try testing.expectEqual("article", findProperty(data.open_graph, "type").?);
|
||||||
|
try testing.expectEqual("2026-03-10", findProperty(data.open_graph, "article:published_time").?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: open graph duplicate keys" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<meta property="og:title" content="My Page">
|
||||||
|
\\<meta property="og:image" content="https://example.com/img1.jpg">
|
||||||
|
\\<meta property="og:image" content="https://example.com/img2.jpg">
|
||||||
|
\\<meta property="og:image" content="https://example.com/img3.jpg">
|
||||||
|
);
|
||||||
|
// Duplicate keys are preserved as separate Property entries.
|
||||||
|
try testing.expectEqual(4, data.open_graph.len);
|
||||||
|
|
||||||
|
// Verify serialization groups duplicates into arrays.
|
||||||
|
const json = try std.json.Stringify.valueAlloc(testing.allocator, data, .{});
|
||||||
|
defer testing.allocator.free(json);
|
||||||
|
|
||||||
|
const parsed = try std.json.parseFromSlice(std.json.Value, testing.allocator, json, .{});
|
||||||
|
defer parsed.deinit();
|
||||||
|
const og = parsed.value.object.get("openGraph").?.object;
|
||||||
|
// "title" appears once → string.
|
||||||
|
switch (og.get("title").?) {
|
||||||
|
.string => {},
|
||||||
|
else => return error.TestUnexpectedResult,
|
||||||
|
}
|
||||||
|
// "image" appears 3 times → array.
|
||||||
|
switch (og.get("image").?) {
|
||||||
|
.array => |arr| try testing.expectEqual(3, arr.items.len),
|
||||||
|
else => return error.TestUnexpectedResult,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: twitter card" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<meta name="twitter:card" content="summary_large_image">
|
||||||
|
\\<meta name="twitter:site" content="@example">
|
||||||
|
\\<meta name="twitter:title" content="My Page">
|
||||||
|
);
|
||||||
|
try testing.expectEqual(3, data.twitter_card.len);
|
||||||
|
try testing.expectEqual("summary_large_image", findProperty(data.twitter_card, "card").?);
|
||||||
|
try testing.expectEqual("@example", findProperty(data.twitter_card, "site").?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: meta tags" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<title>Page Title</title>
|
||||||
|
\\<meta name="description" content="A test page">
|
||||||
|
\\<meta name="author" content="Test Author">
|
||||||
|
\\<meta name="keywords" content="test, example">
|
||||||
|
\\<meta name="robots" content="index, follow">
|
||||||
|
);
|
||||||
|
try testing.expectEqual("Page Title", findProperty(data.meta, "title").?);
|
||||||
|
try testing.expectEqual("A test page", findProperty(data.meta, "description").?);
|
||||||
|
try testing.expectEqual("Test Author", findProperty(data.meta, "author").?);
|
||||||
|
try testing.expectEqual("test, example", findProperty(data.meta, "keywords").?);
|
||||||
|
try testing.expectEqual("index, follow", findProperty(data.meta, "robots").?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: link elements" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<link rel="canonical" href="https://example.com/page">
|
||||||
|
\\<link rel="icon" href="/favicon.ico">
|
||||||
|
\\<link rel="manifest" href="/manifest.json">
|
||||||
|
\\<link rel="stylesheet" href="/style.css">
|
||||||
|
);
|
||||||
|
try testing.expectEqual(3, data.links.len);
|
||||||
|
try testing.expectEqual("https://example.com/page", findProperty(data.links, "canonical").?);
|
||||||
|
// stylesheet should be filtered out
|
||||||
|
try testing.expectEqual(null, findProperty(data.links, "stylesheet"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: alternate links" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<link rel="alternate" href="https://example.com/fr" hreflang="fr" title="French">
|
||||||
|
\\<link rel="alternate" href="https://example.com/de" hreflang="de">
|
||||||
|
);
|
||||||
|
try testing.expectEqual(2, data.alternate.len);
|
||||||
|
try testing.expectEqual("fr", data.alternate[0].hreflang.?);
|
||||||
|
try testing.expectEqual("French", data.alternate[0].title.?);
|
||||||
|
try testing.expectEqual("de", data.alternate[1].hreflang.?);
|
||||||
|
try testing.expectEqual(null, data.alternate[1].title);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: non-metadata elements ignored" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<div>Just text</div>
|
||||||
|
\\<p>More text</p>
|
||||||
|
\\<a href="/link">Link</a>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(0, data.json_ld.len);
|
||||||
|
try testing.expectEqual(0, data.open_graph.len);
|
||||||
|
try testing.expectEqual(0, data.twitter_card.len);
|
||||||
|
try testing.expectEqual(0, data.meta.len);
|
||||||
|
try testing.expectEqual(0, data.links.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: charset and http-equiv" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<meta charset="utf-8">
|
||||||
|
\\<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
);
|
||||||
|
try testing.expectEqual("utf-8", findProperty(data.meta, "charset").?);
|
||||||
|
try testing.expectEqual("text/html; charset=utf-8", findProperty(data.meta, "Content-Type").?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: mixed content" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<title>My Site</title>
|
||||||
|
\\<meta property="og:title" content="OG Title">
|
||||||
|
\\<meta name="twitter:card" content="summary">
|
||||||
|
\\<meta name="description" content="A page">
|
||||||
|
\\<link rel="canonical" href="https://example.com">
|
||||||
|
\\<script type="application/ld+json">{"@type":"WebSite"}</script>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, data.json_ld.len);
|
||||||
|
try testing.expectEqual(1, data.open_graph.len);
|
||||||
|
try testing.expectEqual(1, data.twitter_card.len);
|
||||||
|
try testing.expectEqual("My Site", findProperty(data.meta, "title").?);
|
||||||
|
try testing.expectEqual("A page", findProperty(data.meta, "description").?);
|
||||||
|
try testing.expectEqual(1, data.links.len);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -98,6 +98,64 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=mime_parsing>
|
||||||
|
// MIME types are lowercased
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "TEXT/HTML" });
|
||||||
|
testing.expectEqual("text/html", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "Application/JSON" });
|
||||||
|
testing.expectEqual("application/json", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MIME with parameters - lowercased
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "text/html; charset=UTF-8" });
|
||||||
|
testing.expectEqual("text/html; charset=utf-8", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any ASCII string is accepted and lowercased (no MIME structure validation)
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "invalid" });
|
||||||
|
testing.expectEqual("invalid", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "/" });
|
||||||
|
testing.expectEqual("/", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-ASCII characters cause empty string (chars outside U+0020-U+007E)
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "ý/x" });
|
||||||
|
testing.expectEqual("", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "text/plàin" });
|
||||||
|
testing.expectEqual("", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control characters cause empty string
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "text/html\x00" });
|
||||||
|
testing.expectEqual("", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty type stays empty
|
||||||
|
{
|
||||||
|
const blob = new Blob([]);
|
||||||
|
testing.expectEqual("", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "" });
|
||||||
|
testing.expectEqual("", blob.type);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id=slice>
|
<script id=slice>
|
||||||
{
|
{
|
||||||
const parts = ["la", "symphonie", "des", "éclairs"];
|
const parts = ["la", "symphonie", "des", "éclairs"];
|
||||||
|
|||||||
137
src/browser/tests/canvas/canvas_rendering_context_2d.html
Normal file
137
src/browser/tests/canvas/canvas_rendering_context_2d.html
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=CanvasRenderingContext2D>
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
testing.expectEqual(true, ctx instanceof CanvasRenderingContext2D);
|
||||||
|
// We can't really test this but let's try to call it at least.
|
||||||
|
ctx.fillRect(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=CanvasRenderingContext2D#fillStyle>
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
|
||||||
|
// Black by default.
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#000000");
|
||||||
|
ctx.fillStyle = "red";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#ff0000");
|
||||||
|
ctx.fillStyle = "rebeccapurple";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#663399");
|
||||||
|
// No changes made if color is invalid.
|
||||||
|
ctx.fillStyle = "invalid-color";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#663399");
|
||||||
|
ctx.fillStyle = "#fc0";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#ffcc00");
|
||||||
|
ctx.fillStyle = "#ff0000";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#ff0000");
|
||||||
|
ctx.fillStyle = "#fF00000F";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "rgba(255, 0, 0, 0.06)");
|
||||||
|
}
|
||||||
|
</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="CanvasRenderingContext2D#getImageData">
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
element.width = 100;
|
||||||
|
element.height = 50;
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, 10, 20);
|
||||||
|
testing.expectEqual(true, imageData instanceof ImageData);
|
||||||
|
testing.expectEqual(imageData.width, 10);
|
||||||
|
testing.expectEqual(imageData.height, 20);
|
||||||
|
testing.expectEqual(imageData.data.length, 10 * 20 * 4);
|
||||||
|
testing.expectEqual(true, imageData.data instanceof Uint8ClampedArray);
|
||||||
|
|
||||||
|
// Undrawn canvas should return transparent black pixels.
|
||||||
|
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#getImageData invalid">
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
|
||||||
|
// Zero or negative width/height should throw IndexSizeError.
|
||||||
|
testing.expectError('Index or size', () => ctx.getImageData(0, 0, 0, 10));
|
||||||
|
testing.expectError('Index or size', () => ctx.getImageData(0, 0, 10, 0));
|
||||||
|
testing.expectError('Index or size', () => ctx.getImageData(0, 0, -5, 10));
|
||||||
|
testing.expectError('Index or size', () => ctx.getImageData(0, 0, 10, -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>
|
||||||
87
src/browser/tests/canvas/offscreen_canvas.html
Normal file
87
src/browser/tests/canvas/offscreen_canvas.html
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<!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>
|
||||||
|
|
||||||
|
<script id=OffscreenCanvasRenderingContext2D#getImageData>
|
||||||
|
{
|
||||||
|
const canvas = new OffscreenCanvas(100, 50);
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, 10, 20);
|
||||||
|
testing.expectEqual(true, imageData instanceof ImageData);
|
||||||
|
testing.expectEqual(imageData.width, 10);
|
||||||
|
testing.expectEqual(imageData.height, 20);
|
||||||
|
testing.expectEqual(imageData.data.length, 10 * 20 * 4);
|
||||||
|
|
||||||
|
// Undrawn canvas should return transparent black pixels.
|
||||||
|
testing.expectEqual(imageData.data[0], 0);
|
||||||
|
testing.expectEqual(imageData.data[1], 0);
|
||||||
|
testing.expectEqual(imageData.data[2], 0);
|
||||||
|
testing.expectEqual(imageData.data[3], 0);
|
||||||
|
|
||||||
|
// Zero or negative dimensions should throw.
|
||||||
|
testing.expectError('Index or size', () => ctx.getImageData(0, 0, 0, 10));
|
||||||
|
testing.expectError('Index or size', () => ctx.getImageData(0, 0, 10, -5));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
87
src/browser/tests/canvas/webgl_rendering_context.html
Normal file
87
src/browser/tests/canvas/webgl_rendering_context.html
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=WebGLRenderingContext#getSupportedExtensions>
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("webgl");
|
||||||
|
testing.expectEqual(true, ctx instanceof WebGLRenderingContext);
|
||||||
|
|
||||||
|
const supportedExtensions = ctx.getSupportedExtensions();
|
||||||
|
// The order Chrome prefer.
|
||||||
|
const expectedExtensions = [
|
||||||
|
"ANGLE_instanced_arrays",
|
||||||
|
"EXT_blend_minmax",
|
||||||
|
"EXT_clip_control",
|
||||||
|
"EXT_color_buffer_half_float",
|
||||||
|
"EXT_depth_clamp",
|
||||||
|
"EXT_disjoint_timer_query",
|
||||||
|
"EXT_float_blend",
|
||||||
|
"EXT_frag_depth",
|
||||||
|
"EXT_polygon_offset_clamp",
|
||||||
|
"EXT_shader_texture_lod",
|
||||||
|
"EXT_texture_compression_bptc",
|
||||||
|
"EXT_texture_compression_rgtc",
|
||||||
|
"EXT_texture_filter_anisotropic",
|
||||||
|
"EXT_texture_mirror_clamp_to_edge",
|
||||||
|
"EXT_sRGB",
|
||||||
|
"KHR_parallel_shader_compile",
|
||||||
|
"OES_element_index_uint",
|
||||||
|
"OES_fbo_render_mipmap",
|
||||||
|
"OES_standard_derivatives",
|
||||||
|
"OES_texture_float",
|
||||||
|
"OES_texture_float_linear",
|
||||||
|
"OES_texture_half_float",
|
||||||
|
"OES_texture_half_float_linear",
|
||||||
|
"OES_vertex_array_object",
|
||||||
|
"WEBGL_blend_func_extended",
|
||||||
|
"WEBGL_color_buffer_float",
|
||||||
|
"WEBGL_compressed_texture_astc",
|
||||||
|
"WEBGL_compressed_texture_etc",
|
||||||
|
"WEBGL_compressed_texture_etc1",
|
||||||
|
"WEBGL_compressed_texture_pvrtc",
|
||||||
|
"WEBGL_compressed_texture_s3tc",
|
||||||
|
"WEBGL_compressed_texture_s3tc_srgb",
|
||||||
|
"WEBGL_debug_renderer_info",
|
||||||
|
"WEBGL_debug_shaders",
|
||||||
|
"WEBGL_depth_texture",
|
||||||
|
"WEBGL_draw_buffers",
|
||||||
|
"WEBGL_lose_context",
|
||||||
|
"WEBGL_multi_draw",
|
||||||
|
"WEBGL_polygon_mode"
|
||||||
|
];
|
||||||
|
|
||||||
|
testing.expectEqual(expectedExtensions.length, supportedExtensions.length);
|
||||||
|
for (let i = 0; i < expectedExtensions.length; i++) {
|
||||||
|
testing.expectEqual(expectedExtensions[i], supportedExtensions[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=WebGLRenderingCanvas#getExtension>
|
||||||
|
// WEBGL_debug_renderer_info
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("webgl");
|
||||||
|
const rendererInfo = ctx.getExtension("WEBGL_debug_renderer_info");
|
||||||
|
testing.expectEqual(true, rendererInfo instanceof WEBGL_debug_renderer_info);
|
||||||
|
|
||||||
|
const { UNMASKED_VENDOR_WEBGL, UNMASKED_RENDERER_WEBGL } = rendererInfo;
|
||||||
|
testing.expectEqual(UNMASKED_VENDOR_WEBGL, 0x9245);
|
||||||
|
testing.expectEqual(UNMASKED_RENDERER_WEBGL, 0x9246);
|
||||||
|
|
||||||
|
testing.expectEqual("", ctx.getParameter(UNMASKED_VENDOR_WEBGL));
|
||||||
|
testing.expectEqual("", ctx.getParameter(UNMASKED_RENDERER_WEBGL));
|
||||||
|
}
|
||||||
|
|
||||||
|
// WEBGL_lose_context
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("webgl");
|
||||||
|
const loseContext = ctx.getExtension("WEBGL_lose_context");
|
||||||
|
testing.expectEqual(true, loseContext instanceof WEBGL_lose_context);
|
||||||
|
|
||||||
|
loseContext.loseContext();
|
||||||
|
loseContext.restoreContext();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -201,8 +201,8 @@ cdataClassName<!DOCTYPE html>
|
|||||||
root.appendChild(cdata);
|
root.appendChild(cdata);
|
||||||
root.appendChild(elem2);
|
root.appendChild(elem2);
|
||||||
|
|
||||||
testing.expectEqual('LAST', cdata.nextElementSibling.tagName);
|
testing.expectEqual('last', cdata.nextElementSibling.tagName);
|
||||||
testing.expectEqual('FIRST', cdata.previousElementSibling.tagName);
|
testing.expectEqual('first', cdata.previousElementSibling.tagName);
|
||||||
}
|
}
|
||||||
</script>
|
</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>
|
||||||
25
src/browser/tests/cdp/dom3.html
Normal file
25
src/browser/tests/cdp/dom3.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Test Page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Test Page</h1>
|
||||||
|
<nav>
|
||||||
|
<a href="/page1" id="link1">First Link</a>
|
||||||
|
<a href="/page2" id="link2">Second Link</a>
|
||||||
|
</nav>
|
||||||
|
<form id="testForm" action="/submit" method="post">
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input type="text" id="username" name="username" placeholder="Enter username">
|
||||||
|
|
||||||
|
<label for="email">Email:</label>
|
||||||
|
<input type="email" id="email" name="email" placeholder="Enter email">
|
||||||
|
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" name="password">
|
||||||
|
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
28
src/browser/tests/console/console.html
Normal file
28
src/browser/tests/console/console.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id="time">
|
||||||
|
// should not crash
|
||||||
|
console.time();
|
||||||
|
console.timeLog();
|
||||||
|
console.timeEnd();
|
||||||
|
|
||||||
|
console.time("test");
|
||||||
|
console.timeLog("test");
|
||||||
|
console.timeEnd("test");
|
||||||
|
|
||||||
|
testing.expectEqual(true, true);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="count">
|
||||||
|
// should not crash
|
||||||
|
console.count();
|
||||||
|
console.count();
|
||||||
|
console.countReset();
|
||||||
|
|
||||||
|
console.count("test");
|
||||||
|
console.count("test");
|
||||||
|
console.countReset("test");
|
||||||
|
|
||||||
|
testing.expectEqual(true, true);
|
||||||
|
</script>
|
||||||
@@ -16,41 +16,119 @@
|
|||||||
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>
|
||||||
|
testing.expectEqual(true, crypto.subtle instanceof SubtleCrypto);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=sign-and-verify-hmac>
|
||||||
|
testing.async(async () => {
|
||||||
|
let key = await crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: "HMAC",
|
||||||
|
hash: { name: "SHA-512" },
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
["sign", "verify"],
|
||||||
|
);
|
||||||
|
|
||||||
|
testing.expectEqual(true, key instanceof CryptoKey);
|
||||||
|
|
||||||
|
const raw = await crypto.subtle.exportKey("raw", key);
|
||||||
|
testing.expectEqual(128, raw.byteLength);
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
const signature = await crypto.subtle.sign(
|
||||||
|
"HMAC",
|
||||||
|
key,
|
||||||
|
encoder.encode("Hello, world!")
|
||||||
|
);
|
||||||
|
|
||||||
|
testing.expectEqual(true, signature instanceof ArrayBuffer);
|
||||||
|
|
||||||
|
const result = await window.crypto.subtle.verify(
|
||||||
|
{ name: "HMAC" },
|
||||||
|
key,
|
||||||
|
signature,
|
||||||
|
encoder.encode("Hello, world!")
|
||||||
|
);
|
||||||
|
|
||||||
|
testing.expectEqual(true, result);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=derive-shared-key-x25519>
|
||||||
|
testing.async(async () => {
|
||||||
|
const { privateKey, publicKey } = await crypto.subtle.generateKey(
|
||||||
|
{ name: "X25519" },
|
||||||
|
true,
|
||||||
|
["deriveBits"],
|
||||||
|
);
|
||||||
|
|
||||||
|
testing.expectEqual(true, privateKey instanceof CryptoKey);
|
||||||
|
testing.expectEqual(true, publicKey instanceof CryptoKey);
|
||||||
|
|
||||||
|
const sharedKey = await crypto.subtle.deriveBits(
|
||||||
|
{
|
||||||
|
name: "X25519",
|
||||||
|
public: publicKey,
|
||||||
|
},
|
||||||
|
privateKey,
|
||||||
|
128,
|
||||||
|
);
|
||||||
|
|
||||||
|
testing.expectEqual(16, sharedKey.byteLength);
|
||||||
|
});
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -20,8 +20,10 @@
|
|||||||
{
|
{
|
||||||
testing.expectEqual('\\30 abc', CSS.escape('0abc'));
|
testing.expectEqual('\\30 abc', CSS.escape('0abc'));
|
||||||
testing.expectEqual('\\31 23', CSS.escape('123'));
|
testing.expectEqual('\\31 23', CSS.escape('123'));
|
||||||
testing.expectEqual('\\-test', CSS.escape('-test'));
|
testing.expectEqual('\\-', CSS.escape('-'));
|
||||||
testing.expectEqual('\\--test', CSS.escape('--test'));
|
testing.expectEqual('-test', CSS.escape('-test'));
|
||||||
|
testing.expectEqual('--test', CSS.escape('--test'));
|
||||||
|
testing.expectEqual('-\\33 ', CSS.escape('-3'));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -67,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>
|
||||||
|
|||||||
63
src/browser/tests/css/font_face.html
Normal file
63
src/browser/tests/css/font_face.html
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id="constructor_basic">
|
||||||
|
{
|
||||||
|
const face = new FontFace("TestFont", "url(test.woff)");
|
||||||
|
testing.expectTrue(face instanceof FontFace);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="constructor_name">
|
||||||
|
{
|
||||||
|
testing.expectEqual('FontFace', FontFace.name);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="family_property">
|
||||||
|
{
|
||||||
|
const face = new FontFace("MyFont", "url(font.woff2)");
|
||||||
|
testing.expectEqual("MyFont", face.family);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="status_is_loaded">
|
||||||
|
{
|
||||||
|
const face = new FontFace("F", "url(f.woff)");
|
||||||
|
testing.expectEqual("loaded", face.status);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="loaded_is_promise">
|
||||||
|
{
|
||||||
|
const face = new FontFace("F", "url(f.woff)");
|
||||||
|
testing.expectTrue(face.loaded instanceof Promise);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="load_returns_promise">
|
||||||
|
{
|
||||||
|
const face = new FontFace("F", "url(f.woff)");
|
||||||
|
testing.expectTrue(face.load() instanceof Promise);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="default_descriptors">
|
||||||
|
{
|
||||||
|
const face = new FontFace("F", "url(f.woff)");
|
||||||
|
testing.expectEqual("normal", face.style);
|
||||||
|
testing.expectEqual("normal", face.weight);
|
||||||
|
testing.expectEqual("normal", face.stretch);
|
||||||
|
testing.expectEqual("normal", face.variant);
|
||||||
|
testing.expectEqual("normal", face.featureSettings);
|
||||||
|
testing.expectEqual("auto", face.display);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_add">
|
||||||
|
{
|
||||||
|
const face = new FontFace("AddedFont", "url(added.woff)");
|
||||||
|
const result = document.fonts.add(face);
|
||||||
|
testing.expectTrue(result === document.fonts);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
80
src/browser/tests/css/font_face_set.html
Normal file
80
src/browser/tests/css/font_face_set.html
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<!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>
|
||||||
|
|
||||||
|
<script id="document_fonts_addEventListener">
|
||||||
|
{
|
||||||
|
let loading = false;
|
||||||
|
document.fonts.addEventListener('loading', function() {
|
||||||
|
loading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
let loadingdone = false;
|
||||||
|
document.fonts.addEventListener('loadingdone', function() {
|
||||||
|
loadingdone = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.fonts.load("italic bold 16px Roboto");
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual(true, loading);
|
||||||
|
testing.expectEqual(true, loadingdone);
|
||||||
|
});
|
||||||
|
testing.expectEqual(true, true);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -205,3 +205,217 @@
|
|||||||
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>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_non_ascii_custom_property">
|
||||||
|
{
|
||||||
|
// Regression test: accessing element.style must not crash when the inline
|
||||||
|
// style attribute contains CSS custom properties with non-ASCII (UTF-8
|
||||||
|
// multibyte) names, such as French accented characters.
|
||||||
|
// The CSS Tokenizer's consumeName() must advance over whole UTF-8 sequences
|
||||||
|
// rather than byte-by-byte to avoid landing on a continuation byte.
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.setAttribute('style',
|
||||||
|
'--color-store-bulles-\u00e9t\u00e9-fg: #6a818f;' +
|
||||||
|
'--color-store-soir\u00e9es-odl-fg: #56b3b3;' +
|
||||||
|
'color: red;'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Must not crash, and ASCII properties that follow non-ASCII ones must be readable.
|
||||||
|
testing.expectEqual('red', div.style.getPropertyValue('color'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_normalize_zero_to_0px">
|
||||||
|
{
|
||||||
|
// Per CSSOM spec, unitless zero in length properties should serialize as "0px"
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
div.style.width = '0';
|
||||||
|
testing.expectEqual('0px', div.style.width);
|
||||||
|
|
||||||
|
div.style.margin = '0';
|
||||||
|
testing.expectEqual('0px', div.style.margin);
|
||||||
|
|
||||||
|
div.style.padding = '0';
|
||||||
|
testing.expectEqual('0px', div.style.padding);
|
||||||
|
|
||||||
|
div.style.top = '0';
|
||||||
|
testing.expectEqual('0px', div.style.top);
|
||||||
|
|
||||||
|
// Scroll properties
|
||||||
|
div.style.scrollMarginTop = '0';
|
||||||
|
testing.expectEqual('0px', div.style.scrollMarginTop);
|
||||||
|
|
||||||
|
div.style.scrollPaddingBottom = '0';
|
||||||
|
testing.expectEqual('0px', div.style.scrollPaddingBottom);
|
||||||
|
|
||||||
|
// Multi-column
|
||||||
|
div.style.columnWidth = '0';
|
||||||
|
testing.expectEqual('0px', div.style.columnWidth);
|
||||||
|
|
||||||
|
div.style.columnRuleWidth = '0';
|
||||||
|
testing.expectEqual('0px', div.style.columnRuleWidth);
|
||||||
|
|
||||||
|
// Outline shorthand
|
||||||
|
div.style.outline = '0';
|
||||||
|
testing.expectEqual('0px', div.style.outline);
|
||||||
|
|
||||||
|
// Shapes
|
||||||
|
div.style.shapeMargin = '0';
|
||||||
|
testing.expectEqual('0px', div.style.shapeMargin);
|
||||||
|
|
||||||
|
// Non-length properties should not be affected
|
||||||
|
div.style.opacity = '0';
|
||||||
|
testing.expectEqual('0', div.style.opacity);
|
||||||
|
|
||||||
|
div.style.zIndex = '0';
|
||||||
|
testing.expectEqual('0', div.style.zIndex);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_normalize_first_baseline">
|
||||||
|
{
|
||||||
|
// "first baseline" should serialize canonically as "baseline"
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
div.style.alignItems = 'first baseline';
|
||||||
|
testing.expectEqual('baseline', div.style.alignItems);
|
||||||
|
|
||||||
|
div.style.alignContent = 'first baseline';
|
||||||
|
testing.expectEqual('baseline', div.style.alignContent);
|
||||||
|
|
||||||
|
div.style.alignSelf = 'first baseline';
|
||||||
|
testing.expectEqual('baseline', div.style.alignSelf);
|
||||||
|
|
||||||
|
div.style.justifySelf = 'first baseline';
|
||||||
|
testing.expectEqual('baseline', div.style.justifySelf);
|
||||||
|
|
||||||
|
// "last baseline" should remain unchanged
|
||||||
|
div.style.alignItems = 'last baseline';
|
||||||
|
testing.expectEqual('last baseline', div.style.alignItems);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_normalize_duplicate_values">
|
||||||
|
{
|
||||||
|
// For 2-value shorthand properties, "X X" should collapse to "X"
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
div.style.placeContent = 'center center';
|
||||||
|
testing.expectEqual('center', div.style.placeContent);
|
||||||
|
|
||||||
|
div.style.placeContent = 'start start';
|
||||||
|
testing.expectEqual('start', div.style.placeContent);
|
||||||
|
|
||||||
|
div.style.gap = '10px 10px';
|
||||||
|
testing.expectEqual('10px', div.style.gap);
|
||||||
|
|
||||||
|
// Different values should not collapse
|
||||||
|
div.style.placeContent = 'center start';
|
||||||
|
testing.expectEqual('center start', div.style.placeContent);
|
||||||
|
|
||||||
|
div.style.gap = '10px 20px';
|
||||||
|
testing.expectEqual('10px 20px', div.style.gap);
|
||||||
|
|
||||||
|
// New shorthands
|
||||||
|
div.style.overflow = 'hidden hidden';
|
||||||
|
testing.expectEqual('hidden', div.style.overflow);
|
||||||
|
|
||||||
|
div.style.scrollSnapAlign = 'start start';
|
||||||
|
testing.expectEqual('start', div.style.scrollSnapAlign);
|
||||||
|
|
||||||
|
div.style.overscrollBehavior = 'auto auto';
|
||||||
|
testing.expectEqual('auto', div.style.overscrollBehavior);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_normalize_anchor_size">
|
||||||
|
{
|
||||||
|
// anchor-size() should serialize with dashed ident (anchor name) before size keyword
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
// Already canonical order - should stay the same
|
||||||
|
div.style.width = 'anchor-size(--foo width)';
|
||||||
|
testing.expectEqual('anchor-size(--foo width)', div.style.width);
|
||||||
|
|
||||||
|
// Non-canonical order - should be reordered
|
||||||
|
div.style.width = 'anchor-size(width --foo)';
|
||||||
|
testing.expectEqual('anchor-size(--foo width)', div.style.width);
|
||||||
|
|
||||||
|
// With fallback value
|
||||||
|
div.style.width = 'anchor-size(height --bar, 100px)';
|
||||||
|
testing.expectEqual('anchor-size(--bar height, 100px)', div.style.width);
|
||||||
|
|
||||||
|
// Different size keywords
|
||||||
|
div.style.width = 'anchor-size(block --baz)';
|
||||||
|
testing.expectEqual('anchor-size(--baz block)', div.style.width);
|
||||||
|
|
||||||
|
div.style.width = 'anchor-size(inline --qux)';
|
||||||
|
testing.expectEqual('anchor-size(--qux inline)', div.style.width);
|
||||||
|
|
||||||
|
div.style.width = 'anchor-size(self-block --test)';
|
||||||
|
testing.expectEqual('anchor-size(--test self-block)', div.style.width);
|
||||||
|
|
||||||
|
div.style.width = 'anchor-size(self-inline --test)';
|
||||||
|
testing.expectEqual('anchor-size(--test self-inline)', div.style.width);
|
||||||
|
|
||||||
|
// Without anchor name (implicit default anchor)
|
||||||
|
div.style.width = 'anchor-size(width)';
|
||||||
|
testing.expectEqual('anchor-size(width)', div.style.width);
|
||||||
|
|
||||||
|
// Nested anchor-size in fallback
|
||||||
|
div.style.width = 'anchor-size(width --foo, anchor-size(height --bar))';
|
||||||
|
testing.expectEqual('anchor-size(--foo width, anchor-size(--bar height))', div.style.width);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -53,3 +53,78 @@
|
|||||||
testing.expectEqual('NO-CONSTRUCTOR-ELEMENT', el.tagName);
|
testing.expectEqual('NO-CONSTRUCTOR-ELEMENT', el.tagName);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div id=clone_container></div>
|
||||||
|
|
||||||
|
<script id=clone>
|
||||||
|
{
|
||||||
|
let calls = 0;
|
||||||
|
class MyCloneElementA extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
calls += 1;
|
||||||
|
$('#clone_container').appendChild(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('my-clone_element_a', MyCloneElementA);
|
||||||
|
const original = document.createElement('my-clone_element_a');
|
||||||
|
$('#clone_container').cloneNode(true);
|
||||||
|
testing.expectEqual(2, calls);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id=fragment_clone_container></div>
|
||||||
|
|
||||||
|
<script id=clone_fragment>
|
||||||
|
{
|
||||||
|
let calls = 0;
|
||||||
|
class MyFragmentCloneElement extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
calls += 1;
|
||||||
|
$('#fragment_clone_container').appendChild(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('my-fragment-clone-element', MyFragmentCloneElement);
|
||||||
|
|
||||||
|
// Create a DocumentFragment with a custom element
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
const customEl = document.createElement('my-fragment-clone-element');
|
||||||
|
fragment.appendChild(customEl);
|
||||||
|
|
||||||
|
// Clone the fragment - this should trigger the crash
|
||||||
|
// because the constructor will attach the element during cloning
|
||||||
|
const clonedFragment = fragment.cloneNode(true);
|
||||||
|
testing.expectEqual(2, calls);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id=range_clone_container></div>
|
||||||
|
|
||||||
|
<script id=clone_range>
|
||||||
|
{
|
||||||
|
let calls = 0;
|
||||||
|
class MyRangeCloneElement extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
calls += 1;
|
||||||
|
$('#range_clone_container').appendChild(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('my-range-clone-element', MyRangeCloneElement);
|
||||||
|
|
||||||
|
// Create a container with a custom element
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const customEl = document.createElement('my-range-clone-element');
|
||||||
|
container.appendChild(customEl);
|
||||||
|
|
||||||
|
// Create a range that includes the custom element
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(container);
|
||||||
|
|
||||||
|
// Clone the range contents - this should trigger the crash
|
||||||
|
// because the constructor will attach the element during cloning
|
||||||
|
const clonedContents = range.cloneContents();
|
||||||
|
testing.expectEqual(2, calls);
|
||||||
|
}
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script>
|
||||||
|
// Test that document.open/write/close throw InvalidStateError during custom element
|
||||||
|
// reactions when the element is parsed from HTML
|
||||||
|
|
||||||
|
window.constructorOpenException = null;
|
||||||
|
window.constructorWriteException = null;
|
||||||
|
window.constructorCloseException = null;
|
||||||
|
window.constructorCalled = false;
|
||||||
|
|
||||||
|
class ThrowTestElement extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
window.constructorCalled = true;
|
||||||
|
|
||||||
|
// Try document.open on the same document during constructor - should throw
|
||||||
|
try {
|
||||||
|
document.open();
|
||||||
|
} catch (e) {
|
||||||
|
window.constructorOpenException = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try document.write on the same document during constructor - should throw
|
||||||
|
try {
|
||||||
|
document.write('<b>test</b>');
|
||||||
|
} catch (e) {
|
||||||
|
window.constructorWriteException = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try document.close on the same document during constructor - should throw
|
||||||
|
try {
|
||||||
|
document.close();
|
||||||
|
} catch (e) {
|
||||||
|
window.constructorCloseException = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('throw-test-element', ThrowTestElement);
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- This element will be parsed from HTML, triggering the constructor -->
|
||||||
|
<throw-test-element id="test-element"></throw-test-element>
|
||||||
|
|
||||||
|
<script id="verify_throws">
|
||||||
|
{
|
||||||
|
// Verify the constructor was called
|
||||||
|
testing.expectEqual(true, window.constructorCalled);
|
||||||
|
|
||||||
|
// Verify document.open threw InvalidStateError
|
||||||
|
testing.expectEqual(true, window.constructorOpenException !== null);
|
||||||
|
testing.expectEqual('InvalidStateError', window.constructorOpenException.name);
|
||||||
|
|
||||||
|
// Verify document.write threw InvalidStateError
|
||||||
|
testing.expectEqual(true, window.constructorWriteException !== null);
|
||||||
|
testing.expectEqual('InvalidStateError', window.constructorWriteException.name);
|
||||||
|
|
||||||
|
// Verify document.close threw InvalidStateError
|
||||||
|
testing.expectEqual(true, window.constructorCloseException !== null);
|
||||||
|
testing.expectEqual('InvalidStateError', window.constructorCloseException.name);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
@@ -27,329 +27,329 @@
|
|||||||
customElements.define('my-early', MyEarly);
|
customElements.define('my-early', MyEarly);
|
||||||
testing.expectEqual(true, early.upgraded);
|
testing.expectEqual(true, early.upgraded);
|
||||||
testing.expectEqual(1, constructorCalled);
|
testing.expectEqual(1, constructorCalled);
|
||||||
testing.expectEqual(1, connectedCalled);
|
// testing.expectEqual(1, connectedCalled);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
// {
|
||||||
let order = [];
|
// let order = [];
|
||||||
|
|
||||||
class UpgradeParent extends HTMLElement {
|
// class UpgradeParent extends HTMLElement {
|
||||||
constructor() {
|
// constructor() {
|
||||||
super();
|
// super();
|
||||||
order.push('parent-constructor');
|
// order.push('parent-constructor');
|
||||||
}
|
// }
|
||||||
|
|
||||||
connectedCallback() {
|
// connectedCallback() {
|
||||||
order.push('parent-connected');
|
// order.push('parent-connected');
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
class UpgradeChild extends HTMLElement {
|
// class UpgradeChild extends HTMLElement {
|
||||||
constructor() {
|
// constructor() {
|
||||||
super();
|
// super();
|
||||||
order.push('child-constructor');
|
// order.push('child-constructor');
|
||||||
}
|
// }
|
||||||
|
|
||||||
connectedCallback() {
|
// connectedCallback() {
|
||||||
order.push('child-connected');
|
// order.push('child-connected');
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<upgrade-parent><upgrade-child></upgrade-child></upgrade-parent>';
|
// container.innerHTML = '<upgrade-parent><upgrade-child></upgrade-child></upgrade-parent>';
|
||||||
document.body.appendChild(container);
|
// document.body.appendChild(container);
|
||||||
|
|
||||||
testing.expectEqual(0, order.length);
|
// testing.expectEqual(0, order.length);
|
||||||
|
|
||||||
customElements.define('upgrade-parent', UpgradeParent);
|
// customElements.define('upgrade-parent', UpgradeParent);
|
||||||
testing.expectEqual(2, order.length);
|
// testing.expectEqual(2, order.length);
|
||||||
testing.expectEqual('parent-constructor', order[0]);
|
// testing.expectEqual('parent-constructor', order[0]);
|
||||||
testing.expectEqual('parent-connected', order[1]);
|
// testing.expectEqual('parent-connected', order[1]);
|
||||||
|
|
||||||
customElements.define('upgrade-child', UpgradeChild);
|
// customElements.define('upgrade-child', UpgradeChild);
|
||||||
testing.expectEqual(4, order.length);
|
// testing.expectEqual(4, order.length);
|
||||||
testing.expectEqual('child-constructor', order[2]);
|
// testing.expectEqual('child-constructor', order[2]);
|
||||||
testing.expectEqual('child-connected', order[3]);
|
// testing.expectEqual('child-connected', order[3]);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let connectedCalled = 0;
|
// let connectedCalled = 0;
|
||||||
|
|
||||||
class DetachedUpgrade extends HTMLElement {
|
// class DetachedUpgrade extends HTMLElement {
|
||||||
connectedCallback() {
|
// connectedCallback() {
|
||||||
connectedCalled++;
|
// connectedCalled++;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<detached-upgrade></detached-upgrade>';
|
// container.innerHTML = '<detached-upgrade></detached-upgrade>';
|
||||||
|
|
||||||
testing.expectEqual(0, connectedCalled);
|
// testing.expectEqual(0, connectedCalled);
|
||||||
|
|
||||||
customElements.define('detached-upgrade', DetachedUpgrade);
|
// customElements.define('detached-upgrade', DetachedUpgrade);
|
||||||
testing.expectEqual(0, connectedCalled);
|
// testing.expectEqual(0, connectedCalled);
|
||||||
|
|
||||||
document.body.appendChild(container);
|
// document.body.appendChild(container);
|
||||||
testing.expectEqual(1, connectedCalled);
|
// testing.expectEqual(1, connectedCalled);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let constructorCalled = 0;
|
// let constructorCalled = 0;
|
||||||
let connectedCalled = 0;
|
// let connectedCalled = 0;
|
||||||
|
|
||||||
class ManualUpgrade extends HTMLElement {
|
// class ManualUpgrade extends HTMLElement {
|
||||||
constructor() {
|
// constructor() {
|
||||||
super();
|
// super();
|
||||||
constructorCalled++;
|
// constructorCalled++;
|
||||||
this.manuallyUpgraded = true;
|
// this.manuallyUpgraded = true;
|
||||||
}
|
// }
|
||||||
|
|
||||||
connectedCallback() {
|
// connectedCallback() {
|
||||||
connectedCalled++;
|
// connectedCalled++;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
customElements.define('manual-upgrade', ManualUpgrade);
|
// customElements.define('manual-upgrade', ManualUpgrade);
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<manual-upgrade id="m1"><manual-upgrade id="m2"></manual-upgrade></manual-upgrade>';
|
// container.innerHTML = '<manual-upgrade id="m1"><manual-upgrade id="m2"></manual-upgrade></manual-upgrade>';
|
||||||
|
|
||||||
testing.expectEqual(2, constructorCalled);
|
// testing.expectEqual(2, constructorCalled);
|
||||||
testing.expectEqual(0, connectedCalled);
|
// testing.expectEqual(0, connectedCalled);
|
||||||
|
|
||||||
customElements.upgrade(container);
|
// customElements.upgrade(container);
|
||||||
|
|
||||||
testing.expectEqual(2, constructorCalled);
|
// testing.expectEqual(2, constructorCalled);
|
||||||
testing.expectEqual(0, connectedCalled);
|
// testing.expectEqual(0, connectedCalled);
|
||||||
|
|
||||||
const m1 = container.querySelector('#m1');
|
// const m1 = container.querySelector('#m1');
|
||||||
const m2 = container.querySelector('#m2');
|
// const m2 = container.querySelector('#m2');
|
||||||
testing.expectEqual(true, m1.manuallyUpgraded);
|
// testing.expectEqual(true, m1.manuallyUpgraded);
|
||||||
testing.expectEqual(true, m2.manuallyUpgraded);
|
// testing.expectEqual(true, m2.manuallyUpgraded);
|
||||||
|
|
||||||
document.body.appendChild(container);
|
// document.body.appendChild(container);
|
||||||
testing.expectEqual(2, connectedCalled);
|
// testing.expectEqual(2, connectedCalled);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let alreadyUpgradedCalled = 0;
|
// let alreadyUpgradedCalled = 0;
|
||||||
|
|
||||||
class AlreadyUpgraded extends HTMLElement {
|
// class AlreadyUpgraded extends HTMLElement {
|
||||||
constructor() {
|
// constructor() {
|
||||||
super();
|
// super();
|
||||||
alreadyUpgradedCalled++;
|
// alreadyUpgradedCalled++;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const elem = document.createElement('div');
|
// const elem = document.createElement('div');
|
||||||
elem.innerHTML = '<already-upgraded></already-upgraded>';
|
// elem.innerHTML = '<already-upgraded></already-upgraded>';
|
||||||
document.body.appendChild(elem);
|
// document.body.appendChild(elem);
|
||||||
|
|
||||||
customElements.define('already-upgraded', AlreadyUpgraded);
|
// customElements.define('already-upgraded', AlreadyUpgraded);
|
||||||
testing.expectEqual(1, alreadyUpgradedCalled);
|
// testing.expectEqual(1, alreadyUpgradedCalled);
|
||||||
|
|
||||||
customElements.upgrade(elem);
|
// customElements.upgrade(elem);
|
||||||
testing.expectEqual(1, alreadyUpgradedCalled);
|
// testing.expectEqual(1, alreadyUpgradedCalled);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let attributeChangedCalls = [];
|
// let attributeChangedCalls = [];
|
||||||
|
|
||||||
class UpgradeWithAttrs extends HTMLElement {
|
// class UpgradeWithAttrs extends HTMLElement {
|
||||||
static get observedAttributes() {
|
// static get observedAttributes() {
|
||||||
return ['data-foo', 'data-bar'];
|
// return ['data-foo', 'data-bar'];
|
||||||
}
|
// }
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
// attributeChangedCallback(name, oldValue, newValue) {
|
||||||
attributeChangedCalls.push({ name, oldValue, newValue });
|
// attributeChangedCalls.push({ name, oldValue, newValue });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<upgrade-with-attrs data-foo="hello" data-bar="world"></upgrade-with-attrs>';
|
// container.innerHTML = '<upgrade-with-attrs data-foo="hello" data-bar="world"></upgrade-with-attrs>';
|
||||||
document.body.appendChild(container);
|
// document.body.appendChild(container);
|
||||||
|
|
||||||
testing.expectEqual(0, attributeChangedCalls.length);
|
// testing.expectEqual(0, attributeChangedCalls.length);
|
||||||
|
|
||||||
customElements.define('upgrade-with-attrs', UpgradeWithAttrs);
|
// customElements.define('upgrade-with-attrs', UpgradeWithAttrs);
|
||||||
|
|
||||||
testing.expectEqual(2, attributeChangedCalls.length);
|
// testing.expectEqual(2, attributeChangedCalls.length);
|
||||||
testing.expectEqual('data-foo', attributeChangedCalls[0].name);
|
// testing.expectEqual('data-foo', attributeChangedCalls[0].name);
|
||||||
testing.expectEqual(null, attributeChangedCalls[0].oldValue);
|
// testing.expectEqual(null, attributeChangedCalls[0].oldValue);
|
||||||
testing.expectEqual('hello', attributeChangedCalls[0].newValue);
|
// testing.expectEqual('hello', attributeChangedCalls[0].newValue);
|
||||||
testing.expectEqual('data-bar', attributeChangedCalls[1].name);
|
// testing.expectEqual('data-bar', attributeChangedCalls[1].name);
|
||||||
testing.expectEqual(null, attributeChangedCalls[1].oldValue);
|
// testing.expectEqual(null, attributeChangedCalls[1].oldValue);
|
||||||
testing.expectEqual('world', attributeChangedCalls[1].newValue);
|
// testing.expectEqual('world', attributeChangedCalls[1].newValue);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let attributeChangedCalls = [];
|
// let attributeChangedCalls = [];
|
||||||
let connectedCalls = 0;
|
// let connectedCalls = 0;
|
||||||
|
|
||||||
class DetachedWithAttrs extends HTMLElement {
|
// class DetachedWithAttrs extends HTMLElement {
|
||||||
static get observedAttributes() {
|
// static get observedAttributes() {
|
||||||
return ['foo'];
|
// return ['foo'];
|
||||||
}
|
// }
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
// attributeChangedCallback(name, oldValue, newValue) {
|
||||||
attributeChangedCalls.push({ name, oldValue, newValue });
|
// attributeChangedCalls.push({ name, oldValue, newValue });
|
||||||
}
|
// }
|
||||||
|
|
||||||
connectedCallback() {
|
// connectedCallback() {
|
||||||
connectedCalls++;
|
// connectedCalls++;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<detached-with-attrs foo="bar"></detached-with-attrs>';
|
// container.innerHTML = '<detached-with-attrs foo="bar"></detached-with-attrs>';
|
||||||
|
|
||||||
testing.expectEqual(0, attributeChangedCalls.length);
|
// testing.expectEqual(0, attributeChangedCalls.length);
|
||||||
|
|
||||||
customElements.define('detached-with-attrs', DetachedWithAttrs);
|
// customElements.define('detached-with-attrs', DetachedWithAttrs);
|
||||||
|
|
||||||
testing.expectEqual(0, attributeChangedCalls.length);
|
// testing.expectEqual(0, attributeChangedCalls.length);
|
||||||
testing.expectEqual(0, connectedCalls);
|
// testing.expectEqual(0, connectedCalls);
|
||||||
|
|
||||||
document.body.appendChild(container);
|
// document.body.appendChild(container);
|
||||||
|
|
||||||
testing.expectEqual(1, attributeChangedCalls.length);
|
// testing.expectEqual(1, attributeChangedCalls.length);
|
||||||
testing.expectEqual('foo', attributeChangedCalls[0].name);
|
// testing.expectEqual('foo', attributeChangedCalls[0].name);
|
||||||
testing.expectEqual(null, attributeChangedCalls[0].oldValue);
|
// testing.expectEqual(null, attributeChangedCalls[0].oldValue);
|
||||||
testing.expectEqual('bar', attributeChangedCalls[0].newValue);
|
// testing.expectEqual('bar', attributeChangedCalls[0].newValue);
|
||||||
testing.expectEqual(1, connectedCalls);
|
// testing.expectEqual(1, connectedCalls);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let attributeChangedCalls = [];
|
// let attributeChangedCalls = [];
|
||||||
let constructorCalled = 0;
|
// let constructorCalled = 0;
|
||||||
|
|
||||||
class ManualUpgradeWithAttrs extends HTMLElement {
|
// class ManualUpgradeWithAttrs extends HTMLElement {
|
||||||
static get observedAttributes() {
|
// static get observedAttributes() {
|
||||||
return ['x', 'y'];
|
// return ['x', 'y'];
|
||||||
}
|
// }
|
||||||
|
|
||||||
constructor() {
|
// constructor() {
|
||||||
super();
|
// super();
|
||||||
constructorCalled++;
|
// constructorCalled++;
|
||||||
}
|
// }
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
// attributeChangedCallback(name, oldValue, newValue) {
|
||||||
attributeChangedCalls.push({ name, oldValue, newValue });
|
// attributeChangedCalls.push({ name, oldValue, newValue });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
customElements.define('manual-upgrade-with-attrs', ManualUpgradeWithAttrs);
|
// customElements.define('manual-upgrade-with-attrs', ManualUpgradeWithAttrs);
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<manual-upgrade-with-attrs x="1" y="2"></manual-upgrade-with-attrs>';
|
// container.innerHTML = '<manual-upgrade-with-attrs x="1" y="2"></manual-upgrade-with-attrs>';
|
||||||
|
|
||||||
testing.expectEqual(1, constructorCalled);
|
// testing.expectEqual(1, constructorCalled);
|
||||||
testing.expectEqual(2, attributeChangedCalls.length);
|
// testing.expectEqual(2, attributeChangedCalls.length);
|
||||||
|
|
||||||
const elem = container.querySelector('manual-upgrade-with-attrs');
|
// const elem = container.querySelector('manual-upgrade-with-attrs');
|
||||||
elem.setAttribute('z', '3');
|
// elem.setAttribute('z', '3');
|
||||||
|
|
||||||
customElements.upgrade(container);
|
// customElements.upgrade(container);
|
||||||
|
|
||||||
testing.expectEqual(1, constructorCalled);
|
// testing.expectEqual(1, constructorCalled);
|
||||||
testing.expectEqual(2, attributeChangedCalls.length);
|
// testing.expectEqual(2, attributeChangedCalls.length);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let attributeChangedCalls = [];
|
// let attributeChangedCalls = [];
|
||||||
|
|
||||||
class MixedAttrs extends HTMLElement {
|
// class MixedAttrs extends HTMLElement {
|
||||||
static get observedAttributes() {
|
// static get observedAttributes() {
|
||||||
return ['watched'];
|
// return ['watched'];
|
||||||
}
|
// }
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
// attributeChangedCallback(name, oldValue, newValue) {
|
||||||
attributeChangedCalls.push({ name, oldValue, newValue });
|
// attributeChangedCalls.push({ name, oldValue, newValue });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<mixed-attrs watched="yes" ignored="no" also-ignored="maybe"></mixed-attrs>';
|
// container.innerHTML = '<mixed-attrs watched="yes" ignored="no" also-ignored="maybe"></mixed-attrs>';
|
||||||
document.body.appendChild(container);
|
// document.body.appendChild(container);
|
||||||
|
|
||||||
testing.expectEqual(0, attributeChangedCalls.length);
|
// testing.expectEqual(0, attributeChangedCalls.length);
|
||||||
|
|
||||||
customElements.define('mixed-attrs', MixedAttrs);
|
// customElements.define('mixed-attrs', MixedAttrs);
|
||||||
|
|
||||||
testing.expectEqual(1, attributeChangedCalls.length);
|
// testing.expectEqual(1, attributeChangedCalls.length);
|
||||||
testing.expectEqual('watched', attributeChangedCalls[0].name);
|
// testing.expectEqual('watched', attributeChangedCalls[0].name);
|
||||||
testing.expectEqual('yes', attributeChangedCalls[0].newValue);
|
// testing.expectEqual('yes', attributeChangedCalls[0].newValue);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let attributeChangedCalls = [];
|
// let attributeChangedCalls = [];
|
||||||
|
|
||||||
class EmptyAttr extends HTMLElement {
|
// class EmptyAttr extends HTMLElement {
|
||||||
static get observedAttributes() {
|
// static get observedAttributes() {
|
||||||
return ['empty', 'non-empty'];
|
// return ['empty', 'non-empty'];
|
||||||
}
|
// }
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
// attributeChangedCallback(name, oldValue, newValue) {
|
||||||
attributeChangedCalls.push({ name, oldValue, newValue });
|
// attributeChangedCalls.push({ name, oldValue, newValue });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<empty-attr empty="" non-empty="value"></empty-attr>';
|
// container.innerHTML = '<empty-attr empty="" non-empty="value"></empty-attr>';
|
||||||
document.body.appendChild(container);
|
// document.body.appendChild(container);
|
||||||
|
|
||||||
customElements.define('empty-attr', EmptyAttr);
|
// customElements.define('empty-attr', EmptyAttr);
|
||||||
|
|
||||||
testing.expectEqual(2, attributeChangedCalls.length);
|
// testing.expectEqual(2, attributeChangedCalls.length);
|
||||||
testing.expectEqual('empty', attributeChangedCalls[0].name);
|
// testing.expectEqual('empty', attributeChangedCalls[0].name);
|
||||||
testing.expectEqual('', attributeChangedCalls[0].newValue);
|
// testing.expectEqual('', attributeChangedCalls[0].newValue);
|
||||||
testing.expectEqual('non-empty', attributeChangedCalls[1].name);
|
// testing.expectEqual('non-empty', attributeChangedCalls[1].name);
|
||||||
testing.expectEqual('value', attributeChangedCalls[1].newValue);
|
// testing.expectEqual('value', attributeChangedCalls[1].newValue);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let parentCalls = [];
|
// let parentCalls = [];
|
||||||
let childCalls = [];
|
// let childCalls = [];
|
||||||
|
|
||||||
class NestedParent extends HTMLElement {
|
// class NestedParent extends HTMLElement {
|
||||||
static get observedAttributes() {
|
// static get observedAttributes() {
|
||||||
return ['parent-attr'];
|
// return ['parent-attr'];
|
||||||
}
|
// }
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
// attributeChangedCallback(name, oldValue, newValue) {
|
||||||
parentCalls.push({ name, oldValue, newValue });
|
// parentCalls.push({ name, oldValue, newValue });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
class NestedChild extends HTMLElement {
|
// class NestedChild extends HTMLElement {
|
||||||
static get observedAttributes() {
|
// static get observedAttributes() {
|
||||||
return ['child-attr'];
|
// return ['child-attr'];
|
||||||
}
|
// }
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
// attributeChangedCallback(name, oldValue, newValue) {
|
||||||
childCalls.push({ name, oldValue, newValue });
|
// childCalls.push({ name, oldValue, newValue });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<nested-parent parent-attr="p"><nested-child child-attr="c"></nested-child></nested-parent>';
|
// container.innerHTML = '<nested-parent parent-attr="p"><nested-child child-attr="c"></nested-child></nested-parent>';
|
||||||
document.body.appendChild(container);
|
// document.body.appendChild(container);
|
||||||
|
|
||||||
testing.expectEqual(0, parentCalls.length);
|
// testing.expectEqual(0, parentCalls.length);
|
||||||
testing.expectEqual(0, childCalls.length);
|
// testing.expectEqual(0, childCalls.length);
|
||||||
|
|
||||||
customElements.define('nested-parent', NestedParent);
|
// customElements.define('nested-parent', NestedParent);
|
||||||
|
|
||||||
testing.expectEqual(1, parentCalls.length);
|
// testing.expectEqual(1, parentCalls.length);
|
||||||
testing.expectEqual('parent-attr', parentCalls[0].name);
|
// testing.expectEqual('parent-attr', parentCalls[0].name);
|
||||||
testing.expectEqual('p', parentCalls[0].newValue);
|
// testing.expectEqual('p', parentCalls[0].newValue);
|
||||||
testing.expectEqual(0, childCalls.length);
|
// testing.expectEqual(0, childCalls.length);
|
||||||
|
|
||||||
customElements.define('nested-child', NestedChild);
|
// customElements.define('nested-child', NestedChild);
|
||||||
|
|
||||||
testing.expectEqual(1, parentCalls.length);
|
// testing.expectEqual(1, parentCalls.length);
|
||||||
testing.expectEqual(1, childCalls.length);
|
// testing.expectEqual(1, childCalls.length);
|
||||||
testing.expectEqual('child-attr', childCalls[0].name);
|
// testing.expectEqual('child-attr', childCalls[0].name);
|
||||||
testing.expectEqual('c', childCalls[0].newValue);
|
// testing.expectEqual('c', childCalls[0].newValue);
|
||||||
}
|
// }
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
<body></body>
|
<body></body>
|
||||||
<script src="../testing.js"></script>
|
<script src="../testing.js"></script>
|
||||||
<script id=createElement>
|
<script id=createElement>
|
||||||
const div = document.createElement('div');
|
testing.expectEqual(1, document.createElement.length);
|
||||||
testing.expectEqual("DIV", div.tagName);
|
|
||||||
div.id = "hello";
|
const div1 = document.createElement('div');
|
||||||
|
testing.expectEqual(true, div1 instanceof HTMLDivElement);
|
||||||
|
testing.expectEqual("DIV", div1.tagName);
|
||||||
|
div1.id = "hello";
|
||||||
testing.expectEqual(null, $('#hello'));
|
testing.expectEqual(null, $('#hello'));
|
||||||
|
|
||||||
document.getElementsByTagName('body')[0].appendChild(div);
|
const div2 = document.createElement('DIV');
|
||||||
testing.expectEqual(div, $('#hello'));
|
testing.expectEqual(true, div2 instanceof HTMLDivElement);
|
||||||
|
|
||||||
|
document.getElementsByTagName('body')[0].appendChild(div1);
|
||||||
|
testing.expectEqual(div1, $('#hello'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,17 @@
|
|||||||
<body></body>
|
<body></body>
|
||||||
<script src="../testing.js"></script>
|
<script src="../testing.js"></script>
|
||||||
<script id=createElementNS>
|
<script id=createElementNS>
|
||||||
const htmlDiv = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
|
const htmlDiv1 = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
|
||||||
testing.expectEqual('DIV', htmlDiv.tagName);
|
testing.expectEqual('DIV', htmlDiv1.tagName);
|
||||||
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv.namespaceURI);
|
testing.expectEqual(true, htmlDiv1 instanceof HTMLDivElement);
|
||||||
|
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');
|
||||||
|
testing.expectEqual('DIV', htmlDiv2.tagName);
|
||||||
|
testing.expectEqual(false, htmlDiv2 instanceof HTMLDivElement);
|
||||||
|
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');
|
||||||
testing.expectEqual('RecT', svgRect.tagName);
|
testing.expectEqual('RecT', svgRect.tagName);
|
||||||
@@ -19,12 +27,13 @@
|
|||||||
testing.expectEqual('http://www.w3.org/XML/1998/namespace', xmlElement.namespaceURI);
|
testing.expectEqual('http://www.w3.org/XML/1998/namespace', xmlElement.namespaceURI);
|
||||||
|
|
||||||
const nullNsElement = document.createElementNS(null, 'span');
|
const nullNsElement = document.createElementNS(null, 'span');
|
||||||
testing.expectEqual('SPAN', nullNsElement.tagName);
|
testing.expectEqual('span', nullNsElement.tagName);
|
||||||
testing.expectEqual('http://www.w3.org/1999/xhtml', nullNsElement.namespaceURI);
|
testing.expectEqual(null, nullNsElement.namespaceURI);
|
||||||
|
|
||||||
const unknownNsElement = document.createElementNS('http://example.com/unknown', 'custom');
|
const unknownNsElement = document.createElementNS('http://example.com/unknown', 'custom');
|
||||||
testing.expectEqual('CUSTOM', unknownNsElement.tagName);
|
testing.expectEqual('custom', unknownNsElement.tagName);
|
||||||
testing.expectEqual('http://www.w3.org/1999/xhtml', unknownNsElement.namespaceURI);
|
// Should be http://example.com/unknown
|
||||||
|
testing.expectEqual('http://lightpanda.io/unsupported/namespace', unknownNsElement.namespaceURI);
|
||||||
|
|
||||||
const regularDiv = document.createElement('div');
|
const regularDiv = document.createElement('div');
|
||||||
testing.expectEqual('DIV', regularDiv.tagName);
|
testing.expectEqual('DIV', regularDiv.tagName);
|
||||||
@@ -36,5 +45,5 @@
|
|||||||
testing.expectEqual('te:ST', custom.tagName);
|
testing.expectEqual('te:ST', custom.tagName);
|
||||||
testing.expectEqual('te', custom.prefix);
|
testing.expectEqual('te', custom.prefix);
|
||||||
testing.expectEqual('ST', custom.localName);
|
testing.expectEqual('ST', custom.localName);
|
||||||
testing.expectEqual('http://www.w3.org/1999/xhtml', custom.namespaceURI); // Should be test
|
testing.expectEqual('http://lightpanda.io/unsupported/namespace', custom.namespaceURI); // Should be test
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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,8 +12,12 @@
|
|||||||
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("visible", document.visibilityState);
|
||||||
|
testing.expectEqual(false, document.prerendering);
|
||||||
|
testing.expectEqual(undefined, Document.prerendering);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=headAndbody>
|
<script id=headAndbody>
|
||||||
@@ -22,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>
|
||||||
@@ -51,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>
|
||||||
@@ -64,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 -->
|
||||||
@@ -171,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>
|
||||||
|
|||||||
@@ -41,4 +41,53 @@
|
|||||||
testing.expectEqual("DIV", newElement.tagName);
|
testing.expectEqual("DIV", newElement.tagName);
|
||||||
testing.expectEqual("after begin", newElement.innerText);
|
testing.expectEqual("after begin", newElement.innerText);
|
||||||
testing.expectEqual("afterbegin", newElement.className);
|
testing.expectEqual("afterbegin", newElement.className);
|
||||||
|
|
||||||
|
const fuzzWrapper = document.createElement("div");
|
||||||
|
fuzzWrapper.id = "fuzz-wrapper";
|
||||||
|
document.body.appendChild(fuzzWrapper);
|
||||||
|
|
||||||
|
const fuzzCases = [
|
||||||
|
// These cases have no <body> element (or empty body), so nothing is inserted
|
||||||
|
{ name: "empty string", html: "", expectElements: 0 },
|
||||||
|
{ name: "comment only", html: "<!-- comment -->", expectElements: 0 },
|
||||||
|
{ name: "doctype only", html: "<!DOCTYPE html>", expectElements: 0 },
|
||||||
|
{ name: "full empty doc", html: "<!DOCTYPE html><html><head></head><body></body></html>", expectElements: 0 },
|
||||||
|
|
||||||
|
{ name: "whitespace only", html: " ", expectElements: 0 },
|
||||||
|
{ name: "newlines only", html: "\n\n\n", expectElements: 0 },
|
||||||
|
{ name: "just text", html: "plain text", expectElements: 0 },
|
||||||
|
// Head-only elements: Extracted from <head> container
|
||||||
|
{ name: "empty meta", html: "<meta>", expectElements: 1 },
|
||||||
|
{ name: "empty title", html: "<title></title>", expectElements: 1 },
|
||||||
|
{ name: "empty head", html: "<head></head>", expectElements: 0 }, // Container with no children
|
||||||
|
{ name: "empty body", html: "<body></body>", expectElements: 0 }, // Container with no children
|
||||||
|
{ name: "empty html", html: "<html></html>", expectElements: 0 }, // Container with no children
|
||||||
|
{ name: "meta only", html: "<meta charset='utf-8'>", expectElements: 1 },
|
||||||
|
{ name: "title only", html: "<title>Test</title>", expectElements: 1 },
|
||||||
|
{ name: "link only", html: "<link rel='stylesheet' href='test.css'>", expectElements: 1 },
|
||||||
|
{ name: "meta and title", html: "<meta charset='utf-8'><title>Test</title>", expectElements: 2 },
|
||||||
|
{ name: "script only", html: "<script>console.log('hi')<\/script>", expectElements: 1 },
|
||||||
|
{ name: "style only", html: "<style>body { color: red; }<\/style>", expectElements: 1 },
|
||||||
|
{ name: "unclosed div", html: "<div>content", expectElements: 1 },
|
||||||
|
{ name: "unclosed span", html: "<span>text", expectElements: 1 },
|
||||||
|
{ name: "invalid tag", html: "<notarealtag>content</notarealtag>", expectElements: 1 },
|
||||||
|
{ name: "malformed", html: "<<div>>test<</div>>", expectElements: 1 }, // Parser handles it
|
||||||
|
{ name: "just closing tag", html: "</div>", expectElements: 0 },
|
||||||
|
{ name: "nested empty", html: "<div><div></div></div>", expectElements: 1 },
|
||||||
|
{ name: "multiple top-level", html: "<span>1</span><span>2</span><span>3</span>", expectElements: 3 },
|
||||||
|
{ name: "mixed text and elements", html: "Text before<b>bold</b>Text after", expectElements: 1 },
|
||||||
|
{ name: "deeply nested", html: "<div><div><div><span>deep</span></div></div></div>", expectElements: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
fuzzCases.forEach((tc, idx) => {
|
||||||
|
fuzzWrapper.innerHTML = "";
|
||||||
|
fuzzWrapper.insertAdjacentHTML("beforeend", tc.html);
|
||||||
|
if (tc.expectElements !== fuzzWrapper.childElementCount) {
|
||||||
|
console.warn(`Fuzz idx: ${idx}`);
|
||||||
|
testing.expectEqual(tc.expectElements, fuzzWrapper.childElementCount);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
document.body.removeChild(fuzzWrapper);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
<main>Main content</main>
|
<main>Main content</main>
|
||||||
|
|
||||||
<script id=byId name="test1">
|
<script id=byId name="test1">
|
||||||
|
testing.expectEqual(1, document.querySelector.length);
|
||||||
testing.expectError("SyntaxError: Syntax Error", () => document.querySelector(''));
|
testing.expectError("SyntaxError: Syntax Error", () => document.querySelector(''));
|
||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(12, err.code);
|
testing.expectEqual(12, err.code);
|
||||||
@@ -269,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>
|
||||||
|
|||||||
@@ -111,3 +111,15 @@
|
|||||||
const containerDataTest = document.querySelector('#container [data-test]');
|
const containerDataTest = document.querySelector('#container [data-test]');
|
||||||
testing.expectEqual('First', containerDataTest.innerText);
|
testing.expectEqual('First', containerDataTest.innerText);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<link rel="preload" as="image" imagesrcset="url1.png 1x, url2.png 2x" id="preload-link">
|
||||||
|
|
||||||
|
<script id="commaInAttrValue">
|
||||||
|
// Commas inside quoted attribute values must not be treated as selector separators
|
||||||
|
const el = document.querySelector('link[rel="preload"][as="image"][imagesrcset="url1.png 1x, url2.png 2x"]');
|
||||||
|
testing.expectEqual('preload-link', el.id);
|
||||||
|
|
||||||
|
// Also test with single quotes inside selector
|
||||||
|
const el2 = document.querySelector("link[imagesrcset='url1.png 1x, url2.png 2x']");
|
||||||
|
testing.expectEqual('preload-link', el2.id);
|
||||||
|
</script>
|
||||||
|
|||||||
344
src/browser/tests/document/replace_children.html
Normal file
344
src/browser/tests/document/replace_children.html
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>document.replaceChildren Tests</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="test">Original content</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script id=error_multiple_elements>
|
||||||
|
{
|
||||||
|
// Test that we cannot have more than one Element child
|
||||||
|
const doc = new Document();
|
||||||
|
const div1 = doc.createElement('div');
|
||||||
|
const div2 = doc.createElement('div');
|
||||||
|
|
||||||
|
testing.expectError('HierarchyRequest', () => {
|
||||||
|
doc.replaceChildren(div1, div2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=error_multiple_elements_via_fragment>
|
||||||
|
{
|
||||||
|
// Test that we cannot have more than one Element child via DocumentFragment
|
||||||
|
const doc = new Document();
|
||||||
|
const fragment = doc.createDocumentFragment();
|
||||||
|
fragment.appendChild(doc.createElement('div'));
|
||||||
|
fragment.appendChild(doc.createElement('span'));
|
||||||
|
|
||||||
|
testing.expectError('HierarchyRequest', () => {
|
||||||
|
doc.replaceChildren(fragment);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=error_multiple_doctypes>
|
||||||
|
{
|
||||||
|
// Test that we cannot have more than one DocumentType child
|
||||||
|
const doc = new Document();
|
||||||
|
const doctype1 = doc.implementation.createDocumentType('html', '', '');
|
||||||
|
const doctype2 = doc.implementation.createDocumentType('html', '', '');
|
||||||
|
|
||||||
|
testing.expectError('HierarchyRequest', () => {
|
||||||
|
doc.replaceChildren(doctype1, doctype2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=error_text_node>
|
||||||
|
{
|
||||||
|
// Test that we cannot insert Text nodes directly into Document
|
||||||
|
const doc = new Document();
|
||||||
|
|
||||||
|
testing.expectError('HierarchyRequest', () => {
|
||||||
|
doc.replaceChildren('Just text');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=error_text_with_element>
|
||||||
|
{
|
||||||
|
// Test that we cannot insert Text nodes even with valid Element
|
||||||
|
const doc = new Document();
|
||||||
|
const html = doc.createElement('html');
|
||||||
|
|
||||||
|
testing.expectError('HierarchyRequest', () => {
|
||||||
|
doc.replaceChildren('Text 1', html, 'Text 2');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=error_append_multiple_elements>
|
||||||
|
{
|
||||||
|
// Test that append also validates
|
||||||
|
const doc = new Document();
|
||||||
|
doc.append(doc.createElement('html'));
|
||||||
|
|
||||||
|
const div = doc.createElement('div');
|
||||||
|
testing.expectError('HierarchyRequest', () => {
|
||||||
|
doc.append(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=error_prepend_multiple_elements>
|
||||||
|
{
|
||||||
|
// Test that prepend also validates
|
||||||
|
const doc = new Document();
|
||||||
|
doc.prepend(doc.createElement('html'));
|
||||||
|
|
||||||
|
const div = doc.createElement('div');
|
||||||
|
testing.expectError('HierarchyRequest', () => {
|
||||||
|
doc.prepend(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=error_append_text>
|
||||||
|
{
|
||||||
|
// Test that append rejects text nodes
|
||||||
|
const doc = new Document();
|
||||||
|
|
||||||
|
testing.expectError('HierarchyRequest', () => {
|
||||||
|
doc.append('text');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=error_prepend_text>
|
||||||
|
{
|
||||||
|
// Test that prepend rejects text nodes
|
||||||
|
const doc = new Document();
|
||||||
|
|
||||||
|
testing.expectError('HierarchyRequest', () => {
|
||||||
|
doc.prepend('text');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_with_single_element>
|
||||||
|
{
|
||||||
|
const doc = new Document();
|
||||||
|
const html = doc.createElement('html');
|
||||||
|
html.id = 'replaced';
|
||||||
|
html.textContent = 'New content';
|
||||||
|
|
||||||
|
doc.replaceChildren(html);
|
||||||
|
|
||||||
|
testing.expectEqual(1, doc.childNodes.length);
|
||||||
|
testing.expectEqual(html, doc.firstChild);
|
||||||
|
testing.expectEqual('replaced', doc.firstChild.id);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_with_comments>
|
||||||
|
{
|
||||||
|
const doc = new Document();
|
||||||
|
const comment1 = doc.createComment('Comment 1');
|
||||||
|
const html = doc.createElement('html');
|
||||||
|
const comment2 = doc.createComment('Comment 2');
|
||||||
|
|
||||||
|
doc.replaceChildren(comment1, html, comment2);
|
||||||
|
|
||||||
|
testing.expectEqual(3, doc.childNodes.length);
|
||||||
|
testing.expectEqual('#comment', doc.firstChild.nodeName);
|
||||||
|
testing.expectEqual('Comment 1', doc.firstChild.textContent);
|
||||||
|
testing.expectEqual('html', doc.childNodes[1].nodeName);
|
||||||
|
testing.expectEqual('#comment', doc.lastChild.nodeName);
|
||||||
|
testing.expectEqual('Comment 2', doc.lastChild.textContent);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_with_empty>
|
||||||
|
{
|
||||||
|
const doc = new Document();
|
||||||
|
// First add some content
|
||||||
|
const div = doc.createElement('div');
|
||||||
|
doc.replaceChildren(div);
|
||||||
|
testing.expectEqual(1, doc.childNodes.length);
|
||||||
|
|
||||||
|
// Now replace with nothing
|
||||||
|
doc.replaceChildren();
|
||||||
|
|
||||||
|
testing.expectEqual(0, doc.childNodes.length);
|
||||||
|
testing.expectEqual(null, doc.firstChild);
|
||||||
|
testing.expectEqual(null, doc.lastChild);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_removes_old_children>
|
||||||
|
{
|
||||||
|
const doc = new Document();
|
||||||
|
const comment1 = doc.createComment('old');
|
||||||
|
|
||||||
|
doc.replaceChildren(comment1);
|
||||||
|
testing.expectEqual(1, doc.childNodes.length);
|
||||||
|
testing.expectEqual(doc, comment1.parentNode);
|
||||||
|
|
||||||
|
const html = doc.createElement('html');
|
||||||
|
html.id = 'new';
|
||||||
|
|
||||||
|
doc.replaceChildren(html);
|
||||||
|
|
||||||
|
// Old child should be removed
|
||||||
|
testing.expectEqual(null, comment1.parentNode);
|
||||||
|
testing.expectEqual(1, doc.childNodes.length);
|
||||||
|
testing.expectEqual('new', doc.firstChild.id);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_with_document_fragment_valid>
|
||||||
|
{
|
||||||
|
const doc = new Document();
|
||||||
|
const fragment = doc.createDocumentFragment();
|
||||||
|
const html = doc.createElement('html');
|
||||||
|
const comment = doc.createComment('comment');
|
||||||
|
|
||||||
|
fragment.appendChild(comment);
|
||||||
|
fragment.appendChild(html);
|
||||||
|
|
||||||
|
doc.replaceChildren(fragment);
|
||||||
|
|
||||||
|
// Fragment contents should be moved
|
||||||
|
testing.expectEqual(2, doc.childNodes.length);
|
||||||
|
testing.expectEqual('#comment', doc.firstChild.nodeName);
|
||||||
|
testing.expectEqual('html', doc.lastChild.nodeName);
|
||||||
|
|
||||||
|
// Fragment should be empty now
|
||||||
|
testing.expectEqual(0, fragment.childNodes.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_maintains_child_order>
|
||||||
|
{
|
||||||
|
const doc = new Document();
|
||||||
|
const nodes = [];
|
||||||
|
|
||||||
|
// Document can have: comment, processing instruction, doctype, element
|
||||||
|
nodes.push(doc.createComment('comment'));
|
||||||
|
nodes.push(doc.createElement('html'));
|
||||||
|
|
||||||
|
doc.replaceChildren(...nodes);
|
||||||
|
|
||||||
|
testing.expectEqual(2, doc.childNodes.length);
|
||||||
|
testing.expectEqual('#comment', doc.childNodes[0].nodeName);
|
||||||
|
testing.expectEqual('html', doc.childNodes[1].nodeName);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_with_nested_structure>
|
||||||
|
{
|
||||||
|
const doc = new Document();
|
||||||
|
const outer = doc.createElement('html');
|
||||||
|
outer.id = 'outer';
|
||||||
|
const middle = doc.createElement('body');
|
||||||
|
middle.id = 'middle';
|
||||||
|
const inner = doc.createElement('span');
|
||||||
|
inner.id = 'inner';
|
||||||
|
inner.textContent = 'Nested';
|
||||||
|
|
||||||
|
middle.appendChild(inner);
|
||||||
|
outer.appendChild(middle);
|
||||||
|
|
||||||
|
doc.replaceChildren(outer);
|
||||||
|
|
||||||
|
testing.expectEqual(1, doc.childNodes.length);
|
||||||
|
testing.expectEqual('outer', doc.firstChild.id);
|
||||||
|
|
||||||
|
const foundInner = doc.getElementById('inner');
|
||||||
|
testing.expectEqual(inner, foundInner);
|
||||||
|
testing.expectEqual('Nested', foundInner.textContent);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=consecutive_replaces>
|
||||||
|
{
|
||||||
|
const doc = new Document();
|
||||||
|
const html1 = doc.createElement('html');
|
||||||
|
html1.id = 'first-replace';
|
||||||
|
doc.replaceChildren(html1);
|
||||||
|
testing.expectEqual('first-replace', doc.firstChild.id);
|
||||||
|
|
||||||
|
// Replace element with comments
|
||||||
|
const comment = doc.createComment('in between');
|
||||||
|
doc.replaceChildren(comment);
|
||||||
|
testing.expectEqual(1, doc.childNodes.length);
|
||||||
|
testing.expectEqual('#comment', doc.firstChild.nodeName);
|
||||||
|
|
||||||
|
// Replace comments with new element
|
||||||
|
const html2 = doc.createElement('html');
|
||||||
|
html2.id = 'second-replace';
|
||||||
|
doc.replaceChildren(html2);
|
||||||
|
testing.expectEqual('second-replace', doc.firstChild.id);
|
||||||
|
testing.expectEqual(1, doc.childNodes.length);
|
||||||
|
|
||||||
|
// First element should no longer be in document
|
||||||
|
testing.expectEqual(null, html1.parentNode);
|
||||||
|
testing.expectEqual(null, comment.parentNode);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_with_comments_only>
|
||||||
|
{
|
||||||
|
const doc = new Document();
|
||||||
|
const comment1 = doc.createComment('First');
|
||||||
|
const comment2 = doc.createComment('Second');
|
||||||
|
|
||||||
|
doc.replaceChildren(comment1, comment2);
|
||||||
|
|
||||||
|
testing.expectEqual(2, doc.childNodes.length);
|
||||||
|
testing.expectEqual('#comment', doc.firstChild.nodeName);
|
||||||
|
testing.expectEqual('First', doc.firstChild.textContent);
|
||||||
|
testing.expectEqual('#comment', doc.lastChild.nodeName);
|
||||||
|
testing.expectEqual('Second', doc.lastChild.textContent);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=error_fragment_with_text>
|
||||||
|
{
|
||||||
|
// DocumentFragment with text should fail when inserted into Document
|
||||||
|
const doc = new Document();
|
||||||
|
const fragment = doc.createDocumentFragment();
|
||||||
|
fragment.appendChild(doc.createTextNode('text'));
|
||||||
|
fragment.appendChild(doc.createElement('html'));
|
||||||
|
|
||||||
|
testing.expectError('HierarchyRequest', () => {
|
||||||
|
doc.replaceChildren(fragment);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=append_valid_nodes>
|
||||||
|
{
|
||||||
|
const doc = new Document();
|
||||||
|
const comment = doc.createComment('test');
|
||||||
|
const html = doc.createElement('html');
|
||||||
|
|
||||||
|
doc.append(comment);
|
||||||
|
testing.expectEqual(1, doc.childNodes.length);
|
||||||
|
|
||||||
|
doc.append(html);
|
||||||
|
testing.expectEqual(2, doc.childNodes.length);
|
||||||
|
testing.expectEqual('#comment', doc.firstChild.nodeName);
|
||||||
|
testing.expectEqual('html', doc.lastChild.nodeName);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=prepend_valid_nodes>
|
||||||
|
{
|
||||||
|
const doc = new Document();
|
||||||
|
const html = doc.createElement('html');
|
||||||
|
const comment = doc.createComment('test');
|
||||||
|
|
||||||
|
doc.prepend(html);
|
||||||
|
testing.expectEqual(1, doc.childNodes.length);
|
||||||
|
|
||||||
|
doc.prepend(comment);
|
||||||
|
testing.expectEqual(2, doc.childNodes.length);
|
||||||
|
testing.expectEqual('#comment', doc.firstChild.nodeName);
|
||||||
|
testing.expectEqual('html', doc.lastChild.nodeName);
|
||||||
|
}
|
||||||
|
</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;
|
||||||
@@ -168,7 +182,7 @@
|
|||||||
const root = doc.documentElement;
|
const root = doc.documentElement;
|
||||||
testing.expectEqual(true, root !== null);
|
testing.expectEqual(true, root !== null);
|
||||||
// TODO: XML documents should preserve case, but we currently uppercase
|
// TODO: XML documents should preserve case, but we currently uppercase
|
||||||
testing.expectEqual('ROOT', root.tagName);
|
testing.expectEqual('root', root.tagName);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -206,10 +220,9 @@
|
|||||||
const doc = impl.createDocument('http://example.com', 'prefix:localName', null);
|
const doc = impl.createDocument('http://example.com', 'prefix:localName', null);
|
||||||
|
|
||||||
const root = doc.documentElement;
|
const root = doc.documentElement;
|
||||||
// TODO: XML documents should preserve case, but we currently uppercase
|
testing.expectEqual('prefix:localName', root.tagName);
|
||||||
testing.expectEqual('prefix:LOCALNAME', root.tagName);
|
// TODO: Custom namespaces are being replaced with an empty value
|
||||||
// TODO: Custom namespaces are being overridden to XHTML namespace
|
testing.expectEqual('http://lightpanda.io/unsupported/namespace', root.namespaceURI);
|
||||||
testing.expectEqual('http://www.w3.org/1999/xhtml', root.namespaceURI);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -224,8 +237,7 @@
|
|||||||
doc.documentElement.appendChild(child);
|
doc.documentElement.appendChild(child);
|
||||||
|
|
||||||
testing.expectEqual(1, doc.documentElement.childNodes.length);
|
testing.expectEqual(1, doc.documentElement.childNodes.length);
|
||||||
// TODO: XML documents should preserve case, but we currently uppercase
|
testing.expectEqual('child', doc.documentElement.firstChild.tagName);
|
||||||
testing.expectEqual('CHILD', doc.documentElement.firstChild.tagName);
|
|
||||||
testing.expectEqual('Test', doc.documentElement.firstChild.textContent);
|
testing.expectEqual('Test', doc.documentElement.firstChild.textContent);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,11 +3,19 @@
|
|||||||
<body></body>
|
<body></body>
|
||||||
|
|
||||||
<script id=basic>
|
<script id=basic>
|
||||||
|
{
|
||||||
{
|
{
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
testing.expectEqual('object', typeof parser);
|
testing.expectEqual('object', typeof parser);
|
||||||
testing.expectEqual('function', typeof parser.parseFromString);
|
testing.expectEqual('function', typeof parser.parseFromString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Empty XML is a parse error (no root element)
|
||||||
|
const parser = new DOMParser();
|
||||||
|
testing.expectError('Error', () => parser.parseFromString('', 'text/xml'));
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=parseSimpleHTML>
|
<script id=parseSimpleHTML>
|
||||||
@@ -364,14 +372,14 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (const mime of mimes) {
|
for (const mime of mimes) {
|
||||||
const doc = parser.parseFromString(sampleXML, "text/xml");
|
const doc = parser.parseFromString(sampleXML, mime);
|
||||||
const { firstChild: { childNodes, children: collection, tagName }, children } = doc;
|
const { firstChild: { childNodes, children: collection, tagName }, children } = doc;
|
||||||
// doc.
|
// doc.
|
||||||
testing.expectEqual(true, doc instanceof XMLDocument);
|
testing.expectEqual(true, doc instanceof XMLDocument);
|
||||||
testing.expectEqual(1, children.length);
|
testing.expectEqual(1, children.length);
|
||||||
// firstChild.
|
// firstChild.
|
||||||
// TODO: Modern browsers expect this in lowercase.
|
// TODO: Modern browsers expect this in lowercase.
|
||||||
testing.expectEqual("CATALOG", tagName);
|
testing.expectEqual("catalog", tagName);
|
||||||
testing.expectEqual(25, childNodes.length);
|
testing.expectEqual(25, childNodes.length);
|
||||||
testing.expectEqual(12, collection.length);
|
testing.expectEqual(12, collection.length);
|
||||||
// Check children of first child.
|
// Check children of first child.
|
||||||
@@ -379,13 +387,35 @@
|
|||||||
const {children: elements, id} = collection.item(i);
|
const {children: elements, id} = collection.item(i);
|
||||||
testing.expectEqual("bk" + (100 + i + 1), id);
|
testing.expectEqual("bk" + (100 + i + 1), id);
|
||||||
// TODO: Modern browsers expect these in lowercase.
|
// TODO: Modern browsers expect these in lowercase.
|
||||||
testing.expectEqual("AUTHOR", elements.item(0).tagName);
|
testing.expectEqual("author", elements.item(0).tagName);
|
||||||
testing.expectEqual("TITLE", elements.item(1).tagName);
|
testing.expectEqual("title", elements.item(1).tagName);
|
||||||
testing.expectEqual("GENRE", elements.item(2).tagName);
|
testing.expectEqual("genre", elements.item(2).tagName);
|
||||||
testing.expectEqual("PRICE", elements.item(3).tagName);
|
testing.expectEqual("price", elements.item(3).tagName);
|
||||||
testing.expectEqual("PUBLISH_DATE", elements.item(4).tagName);
|
testing.expectEqual("publish_date", elements.item(4).tagName);
|
||||||
testing.expectEqual("DESCRIPTION", elements.item(5).tagName);
|
testing.expectEqual("description", elements.item(5).tagName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=getElementsByTagName-xml>
|
||||||
|
{
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString('<layout><row><col>A</col><col>B</col></row></layout>', 'text/xml');
|
||||||
|
|
||||||
|
// Test getElementsByTagName on document
|
||||||
|
const rows = doc.getElementsByTagName('row');
|
||||||
|
testing.expectEqual(1, rows.length);
|
||||||
|
|
||||||
|
// Test getElementsByTagName on element
|
||||||
|
const row = rows[0];
|
||||||
|
const cols = row.getElementsByTagName('col');
|
||||||
|
testing.expectEqual(2, cols.length);
|
||||||
|
testing.expectEqual('A', cols[0].textContent);
|
||||||
|
testing.expectEqual('B', cols[1].textContent);
|
||||||
|
|
||||||
|
// Test getElementsByTagName('*') on element
|
||||||
|
const allElements = row.getElementsByTagName('*');
|
||||||
|
testing.expectEqual(2, allElements.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -248,7 +248,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id=legacy></a>
|
<a id=legacy></a>
|
||||||
<script id=legacy>
|
<script id=legacy>
|
||||||
{
|
{
|
||||||
let a = document.getElementById('legacy').attributes;
|
let a = document.getElementById('legacy').attributes;
|
||||||
@@ -266,3 +266,19 @@
|
|||||||
testing.expectEqual('abc123', a[0].value);
|
testing.expectEqual('abc123', a[0].value);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div id="nsa"></div>
|
||||||
|
<script id=non-string-attr>
|
||||||
|
{
|
||||||
|
let nsa = document.getElementById('nsa');
|
||||||
|
|
||||||
|
nsa.setAttribute('int', 1);
|
||||||
|
testing.expectEqual('1', nsa.getAttribute('int'));
|
||||||
|
|
||||||
|
nsa.setAttribute('obj', {});
|
||||||
|
testing.expectEqual('[object Object]', nsa.getAttribute('obj'));
|
||||||
|
|
||||||
|
nsa.setAttribute('arr', []);
|
||||||
|
testing.expectEqual('', nsa.getAttribute('arr'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user