mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-29 16:10:04 +00:00
Compare commits
1512 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e25c33eaa6 | ||
|
|
7bddc0a89c | ||
|
|
403f42bf38 | ||
|
|
b2e301418f | ||
|
|
334a2e44a1 | ||
|
|
c9121a03d2 | ||
|
|
cc93180d57 | ||
|
|
4062a425cb | ||
|
|
cce533ebb6 | ||
|
|
48df38cbfe | ||
|
|
f982f073df | ||
|
|
34999f12ca | ||
|
|
c8d5665653 | ||
|
|
ddebaf87d0 | ||
|
|
6b80cd6109 | ||
|
|
7635d8d2a5 | ||
|
|
634e3e35a0 | ||
|
|
da3dc58199 | ||
|
|
4f99df694b | ||
|
|
982b8e2d72 | ||
|
|
6e7c8d7ae2 | ||
|
|
3c858f522b | ||
|
|
f2a30f8cdd | ||
|
|
43785bfab4 | ||
|
|
78edf6d324 | ||
|
|
73565c4493 | ||
|
|
8c37cac957 | ||
|
|
eceab76b6f | ||
|
|
1f81b6ddc4 | ||
|
|
52c3aadd24 | ||
|
|
ad87573d09 | ||
|
|
20fbfc8544 | ||
|
|
7695c8403f | ||
|
|
421983d06e | ||
|
|
328c681a8f | ||
|
|
48d94d0f68 | ||
|
|
10ad5d763e | ||
|
|
2a78c946e4 | ||
|
|
a7872aa054 | ||
|
|
5c228ae0a1 | ||
|
|
ce73f7ac5a | ||
|
|
64107f5957 | ||
|
|
8a1795d56f | ||
|
|
b104c3bfe8 | ||
|
|
82e3f126ff | ||
|
|
175488563e | ||
|
|
da51cdd11d | ||
|
|
a8a47b138f | ||
|
|
b63d4cf675 | ||
|
|
03b999c592 | ||
|
|
a91afab038 | ||
|
|
d4747b5386 | ||
|
|
41b81c8b05 | ||
|
|
552831364d | ||
|
|
42b5e32473 | ||
|
|
e9c36fd6f8 | ||
|
|
952dfbef36 | ||
|
|
254984b600 | ||
|
|
8cbc58d257 | ||
|
|
e6cc3e8c34 | ||
|
|
516335e0ed | ||
|
|
01798ed7f8 | ||
|
|
fcad67a854 | ||
|
|
e359ffead0 | ||
|
|
eb09041859 | ||
|
|
b3d52c966d | ||
|
|
3fb8a14348 | ||
|
|
84a949e7c7 | ||
|
|
eaf1cb26b2 | ||
|
|
f37962d3de | ||
|
|
511e957d4b | ||
|
|
71df03b729 | ||
|
|
839052f4b8 | ||
|
|
7c18d857f0 | ||
|
|
947e672d18 | ||
|
|
96942960a9 | ||
|
|
8b0118e2c8 | ||
|
|
5f9a7a5381 | ||
|
|
6897d72c3e | ||
|
|
aae9a505e0 | ||
|
|
45196e022b | ||
|
|
b9e4c44d63 | ||
|
|
0a9e5b66ee | ||
|
|
8b99e82743 | ||
|
|
059fb85e22 | ||
|
|
8997df861a | ||
|
|
e65667963f | ||
|
|
3d51667fc8 | ||
|
|
7fc6e97cd8 | ||
|
|
1473e58a41 | ||
|
|
2394b2f44f | ||
|
|
516bd98198 | ||
|
|
7d8688a130 | ||
|
|
631ec70058 | ||
|
|
6fd51cfdc0 | ||
|
|
6857b74623 | ||
|
|
5ec4305a9f | ||
|
|
88baff96d0 | ||
|
|
e871f0002b | ||
|
|
7358d48e35 | ||
|
|
178fbf0fca | ||
|
|
a50597ff27 | ||
|
|
e4cb78abee | ||
|
|
732884a3b2 | ||
|
|
80f2c42c69 | ||
|
|
49a5a39659 | ||
|
|
a4a7040b98 | ||
|
|
de5a7d5b99 | ||
|
|
3f92e388be | ||
|
|
25c941b847 | ||
|
|
24b6934d3b | ||
|
|
d286ab406c | ||
|
|
ef6a7a6904 | ||
|
|
c61eda0d24 | ||
|
|
ad226b6fb1 | ||
|
|
24491f0dfe | ||
|
|
870fd1654d | ||
|
|
38bc912e4e | ||
|
|
315c9a2d92 | ||
|
|
a14ad6f700 | ||
|
|
76dcdfb98c | ||
|
|
99c09ba8a1 | ||
|
|
0f18b76813 | ||
|
|
8504e4cd22 | ||
|
|
ebe793e0e7 | ||
|
|
965c6cf4d9 | ||
|
|
2b1ab3184e | ||
|
|
e7d21c2dbe | ||
|
|
11906d9d71 | ||
|
|
ac5a64d77a | ||
|
|
c86c851c60 | ||
|
|
721cf98486 | ||
|
|
84bbb6efd4 | ||
|
|
f897cda6cd | ||
|
|
2da8b25b09 | ||
|
|
3f94fd90dd | ||
|
|
bc6be22cb4 | ||
|
|
e23604e08d | ||
|
|
be858ac9ce | ||
|
|
137ab4a557 | ||
|
|
bad0fc386d | ||
|
|
641c7b2c89 | ||
|
|
21be3db51f | ||
|
|
e978857820 | ||
|
|
3bf596c54c | ||
|
|
aedb823b4d | ||
|
|
7a417435cc | ||
|
|
497d6e80f7 | ||
|
|
ae6ab34e72 | ||
|
|
4c26161728 | ||
|
|
1731dca5dd | ||
|
|
ee2caff46e | ||
|
|
db8fb8b05d | ||
|
|
bec7e141dc | ||
|
|
ab85b4b129 | ||
|
|
b030049b40 | ||
|
|
1338a3d89d | ||
|
|
181178296f | ||
|
|
df7888d6fb | ||
|
|
dd15f5e052 | ||
|
|
f348d85b11 | ||
|
|
8c8a05b8c1 | ||
|
|
34d2fc1503 | ||
|
|
9b3fa809bf | ||
|
|
59535c112e | ||
|
|
04e5a6425a | ||
|
|
424dddf67b | ||
|
|
f0d6ae2a00 | ||
|
|
25298a32fa | ||
|
|
ba28bf01b7 | ||
|
|
d15c29b1a3 | ||
|
|
b083910a51 | ||
|
|
235aad32a6 | ||
|
|
a818560344 | ||
|
|
8f179becf7 | ||
|
|
e1695a0874 | ||
|
|
af7498d283 | ||
|
|
3e2a4d8053 | ||
|
|
29982e2caf | ||
|
|
5fea1df42b | ||
|
|
a041162b32 | ||
|
|
32cd3981d8 | ||
|
|
ca5af87196 | ||
|
|
a8164f612f | ||
|
|
d3bb0b6ff0 | ||
|
|
0ef10c1e13 | ||
|
|
4017911373 | ||
|
|
048034d4b1 | ||
|
|
fcb3f08bcb | ||
|
|
d2a05bb622 | ||
|
|
f7254ee169 | ||
|
|
a0e5c9d570 | ||
|
|
8291e4ba73 | ||
|
|
b324be3b0b | ||
|
|
6ba0ba7126 | ||
|
|
1d8e0629af | ||
|
|
42df54869f | ||
|
|
7b758b85ec | ||
|
|
82987ec401 | ||
|
|
71707b5aa7 | ||
|
|
ca2df83928 | ||
|
|
085771c2f0 | ||
|
|
607a638858 | ||
|
|
5f6d06d05d | ||
|
|
19ecb87b07 | ||
|
|
2a332c0883 | ||
|
|
bb773c6c13 | ||
|
|
238de489c1 | ||
|
|
6b4db330d8 | ||
|
|
ea5d7c0dee | ||
|
|
0f189f1af3 | ||
|
|
0f1b8dd51a | ||
|
|
d7e6946a78 | ||
|
|
255b7b1a54 | ||
|
|
79e1c751a1 | ||
|
|
fc745b9614 | ||
|
|
95b1baebd2 | ||
|
|
56fe1ceb97 | ||
|
|
863a51e556 | ||
|
|
69b3064b45 | ||
|
|
fb3eab1aa8 | ||
|
|
32c7399f26 | ||
|
|
955351b5bd | ||
|
|
75f6c67b6e | ||
|
|
700a3e6ed9 | ||
|
|
00702448c7 | ||
|
|
5074827d51 | ||
|
|
ceb0711e42 | ||
|
|
ddb5824b58 | ||
|
|
39f9209374 | ||
|
|
5fea4cf760 | ||
|
|
0e5ec86ca9 | ||
|
|
8b95211055 | ||
|
|
a27339b954 | ||
|
|
028b728760 | ||
|
|
18e63df01e | ||
|
|
5f459c0901 | ||
|
|
a90bcde38c | ||
|
|
603e7d922e | ||
|
|
861126f810 | ||
|
|
eb9b706ebc | ||
|
|
de9cbae0b2 | ||
|
|
25e890986f | ||
|
|
f66627dd04 | ||
|
|
924eb33b3f | ||
|
|
1b288c541a | ||
|
|
2612b8c86f | ||
|
|
3e2796d456 | ||
|
|
7092913863 | ||
|
|
67625fc347 | ||
|
|
eb55030b06 | ||
|
|
6e1b2d50f2 | ||
|
|
c6f72c44b8 | ||
|
|
d38ded0f26 | ||
|
|
ec20b7bd3a | ||
|
|
0766cf464a | ||
|
|
867f00e091 | ||
|
|
c823b8d7ae | ||
|
|
393d4d336c | ||
|
|
2cb3f2d03d | ||
|
|
279f2dd633 | ||
|
|
dec051a6e0 | ||
|
|
790fdd320c | ||
|
|
feb4a364a7 | ||
|
|
1140149e1e | ||
|
|
2ee9599b6e | ||
|
|
188d45e002 | ||
|
|
7c4c2f7860 | ||
|
|
90b7f2ff3b | ||
|
|
d3f0041e93 | ||
|
|
9d60142828 | ||
|
|
68d5edca60 | ||
|
|
1b369489df | ||
|
|
600ddfbf2d | ||
|
|
415d4dde2a | ||
|
|
1867245ed3 | ||
|
|
71d34592d9 | ||
|
|
db2927eea7 | ||
|
|
bb01a5cb31 | ||
|
|
815319140f | ||
|
|
6e6082119f | ||
|
|
da48ffe05c | ||
|
|
081979be3b | ||
|
|
3673956c1c | ||
|
|
bdd3c274ed | ||
|
|
423034d5c4 | ||
|
|
19fd2b12c0 | ||
|
|
21cd17873f | ||
|
|
9870fa9e34 | ||
|
|
938cd5e136 | ||
|
|
e8025ad4b3 | ||
|
|
07fa141aaa | ||
|
|
18bdf1e8b3 | ||
|
|
5be977005e | ||
|
|
282b64278e | ||
|
|
7263d484de | ||
|
|
bdb059b6c9 | ||
|
|
de3f5011bc | ||
|
|
de9faffa33 | ||
|
|
f67ca69e05 | ||
|
|
dd19e880c5 | ||
|
|
b5e8fa007c | ||
|
|
c3555bfcab | ||
|
|
0383db8788 | ||
|
|
d7af122c18 | ||
|
|
e15b8145b1 | ||
|
|
d75f5f9231 | ||
|
|
9939797792 | ||
|
|
5248b9fc6f | ||
|
|
e15295bdac | ||
|
|
4e1f96e09c | ||
|
|
96cfdebced | ||
|
|
944f34b833 | ||
|
|
1023b2ca9c | ||
|
|
16318bb9f6 | ||
|
|
350586335d | ||
|
|
9d809499a5 | ||
|
|
fdd52c17d7 | ||
|
|
1461d029db | ||
|
|
07cefd71df | ||
|
|
abab10b2cc | ||
|
|
e37d4a6756 | ||
|
|
e2a1ce623c | ||
|
|
0ff243266c | ||
|
|
645da2e307 | ||
|
|
5fd95788f9 | ||
|
|
bd29f168e0 | ||
|
|
dc97e33cd6 | ||
|
|
caf7cb07cd | ||
|
|
ad5df53ee7 | ||
|
|
95920bf207 | ||
|
|
6700166841 | ||
|
|
b8196cd06e | ||
|
|
c28afbf193 | ||
|
|
84ffffb3f3 | ||
|
|
b2c030140c | ||
|
|
90138ed574 | ||
|
|
92f131bbe4 | ||
|
|
338580087e | ||
|
|
deda53a842 | ||
|
|
5391854c82 | ||
|
|
e288bfbec4 | ||
|
|
377fe5bc40 | ||
|
|
d264ff2801 | ||
|
|
a21bb6b02d | ||
|
|
37ecd5cdef | ||
|
|
07a87dfba7 | ||
|
|
9e4db89521 | ||
|
|
536d394e41 | ||
|
|
c0580c7ad0 | ||
|
|
488e72ef4e | ||
|
|
01c224d301 | ||
|
|
eaf95a85a8 | ||
|
|
ba1d084660 | ||
|
|
2e64c461c3 | ||
|
|
ce5dad722f | ||
|
|
7675feca91 | ||
|
|
c66d74e135 | ||
|
|
54d6eed740 | ||
|
|
dc4b75070d | ||
|
|
830eb74725 | ||
|
|
4f21d8d7a8 | ||
|
|
424deb8faf | ||
|
|
b4a40f1257 | ||
|
|
9296c10ca4 | ||
|
|
fbe65cd542 | ||
|
|
ccbb6e4789 | ||
|
|
d70f436304 | ||
|
|
16aaa8201c | ||
|
|
acc1f2f3d7 | ||
|
|
433d254c70 | ||
|
|
ea4eebd2d6 | ||
|
|
3c00a527dd | ||
|
|
f72a354066 | ||
|
|
7c92e0e9ce | ||
|
|
4f6868728d | ||
|
|
0ec4522f9e | ||
|
|
c6e0c6d096 | ||
|
|
dc0fb9ed8a | ||
|
|
66d9eaee78 | ||
|
|
3797272faf | ||
|
|
682b302d04 | ||
|
|
1de10f9b05 | ||
|
|
c4391ff058 | ||
|
|
3822e3f8d9 | ||
|
|
f8f99f3878 | ||
|
|
e77e4acea9 | ||
|
|
c6de444d0b | ||
|
|
89e38c34b8 | ||
|
|
246d17972c | ||
|
|
55a8b37ef8 | ||
|
|
445183001b | ||
|
|
ca9e2200da | ||
|
|
eba3f84c04 | ||
|
|
867e6a8f4b | ||
|
|
df9779ec59 | ||
|
|
1b71d1e46d | ||
|
|
0a58918f47 | ||
|
|
afbd927fc0 | ||
|
|
2aa09ae18d | ||
|
|
09789b0b72 | ||
|
|
2426abd17a | ||
|
|
db4a97743f | ||
|
|
7ca98ed344 | ||
|
|
c9d3d17999 | ||
|
|
628049cfd7 | ||
|
|
ae9a11da53 | ||
|
|
7e097482bc | ||
|
|
df1b151587 | ||
|
|
45eb59a5aa | ||
|
|
687c17bbe2 | ||
|
|
7505aec706 | ||
|
|
c7b414492d | ||
|
|
14b0095822 | ||
|
|
a1256b46c8 | ||
|
|
094270dff7 | ||
|
|
d4e24dabc2 | ||
|
|
842df0d112 | ||
|
|
cfa9427d7c | ||
|
|
3c01e24f02 | ||
|
|
22dbf63ff9 | ||
|
|
814f7394a0 | ||
|
|
9a4cebaa1b | ||
|
|
c30207ac63 | ||
|
|
77afbddb91 | ||
|
|
18feeabe15 | ||
|
|
c3811d3a14 | ||
|
|
f20d6b551d | ||
|
|
311bcadacb | ||
|
|
2189c8cd82 | ||
|
|
6553bb8147 | ||
|
|
dea492fd64 | ||
|
|
00ab7f04fa | ||
|
|
d3ba714aba | ||
|
|
748b37f1d6 | ||
|
|
b83b188aff | ||
|
|
cfefa32603 | ||
|
|
85d8db3ef9 | ||
|
|
3c14dbe382 | ||
|
|
b49b2af11f | ||
|
|
425a36aa51 | ||
|
|
ec0b9de713 | ||
|
|
9f13b14f6d | ||
|
|
01e83b45b5 | ||
|
|
f80566e0cb | ||
|
|
42afacf0af | ||
|
|
2e61e7e682 | ||
|
|
3de9267ea7 | ||
|
|
8c99d4fcd2 | ||
|
|
be4e6e5ba5 | ||
|
|
1b5efea6eb | ||
|
|
6554f80fad | ||
|
|
2e8a9f809e | ||
|
|
dc66032720 | ||
|
|
c9433782d8 | ||
|
|
fef5586ff5 | ||
|
|
1f4a2fd654 | ||
|
|
8243385af6 | ||
|
|
26ce9b2d4a | ||
|
|
119f3169e2 | ||
|
|
16bd22ee01 | ||
|
|
f4a5f73ab2 | ||
|
|
e61a4564ea | ||
|
|
e72edee1f2 | ||
|
|
e8c150fcac | ||
|
|
52418932b1 | ||
|
|
4f81cb9333 | ||
|
|
db46f47b96 | ||
|
|
edfe5594ba | ||
|
|
f25e972594 | ||
|
|
d5488bdd42 | ||
|
|
bbff64bc96 | ||
|
|
635afefdeb | ||
|
|
fd3e67a0b4 | ||
|
|
729a6021ee | ||
|
|
309f254c2c | ||
|
|
5c37f04d64 | ||
|
|
7c3dd8e852 | ||
|
|
66ddedbaf3 | ||
|
|
7981b17897 | ||
|
|
62137d47c8 | ||
|
|
e3b5437f61 | ||
|
|
934693924e | ||
|
|
308fd92a46 | ||
|
|
da1eb71ad0 | ||
|
|
576dbb7ce6 | ||
|
|
d0c381b3df | ||
|
|
55178a81c6 | ||
|
|
249308380b | ||
|
|
d91bec08c3 | ||
|
|
e23ef4b0be | ||
|
|
6037521c49 | ||
|
|
a27fac3677 | ||
|
|
21f2eb664e | ||
|
|
81546ef4b0 | ||
|
|
4b90c8fd45 | ||
|
|
c643fb8aac | ||
|
|
0cae6ceca3 | ||
|
|
5cde59b53c | ||
|
|
7df67630af | ||
|
|
0c89dca261 | ||
|
|
6b953b8793 | ||
|
|
0d1defcf27 | ||
|
|
c1db9c19b3 | ||
|
|
95487755ed | ||
|
|
4813469659 | ||
|
|
4dfd357c0b | ||
|
|
4ca0486518 | ||
|
|
b139c05960 | ||
|
|
3d32759030 | ||
|
|
badfe39a3d | ||
|
|
060e2db351 | ||
|
|
ed802c0404 | ||
|
|
5d8739bfb2 | ||
|
|
086faf44fc | ||
|
|
e5eaa90c61 | ||
|
|
b24807ea29 | ||
|
|
d68bae9bc2 | ||
|
|
b891fb4502 | ||
|
|
ea69b3b4e3 | ||
|
|
23c8616ba5 | ||
|
|
b25c91affd | ||
|
|
151cefe0ec | ||
|
|
3412ff94bc | ||
|
|
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 | ||
|
|
060afcd459 | ||
|
|
5d1522a61f | ||
|
|
b1b54afc56 | ||
|
|
2abc490732 | ||
|
|
d4807df2e9 | ||
|
|
d5f4ca15cc | ||
|
|
e642c85ebd | ||
|
|
3930524bbf | ||
|
|
7ea0cdba36 | ||
|
|
612b3a26b7 | ||
|
|
56d89895a8 | ||
|
|
21d502b81f | ||
|
|
dd3de6efea | ||
|
|
d934fe6d4e | ||
|
|
dab6345885 | ||
|
|
39874137d6 | ||
|
|
89f215c3ee | ||
|
|
408d3f0a53 | ||
|
|
a010684ce9 | ||
|
|
a4a98da4a4 | ||
|
|
6f30d459d5 | ||
|
|
622ca3121f | ||
|
|
71f27a55e1 | ||
|
|
c92903aae5 | ||
|
|
518e0aa07a | ||
|
|
b908b0bf8a | ||
|
|
f9fa5be324 | ||
|
|
8ec6bb1577 | ||
|
|
70f8c53703 | ||
|
|
6d5a984413 | ||
|
|
5fa8fbc6f8 | ||
|
|
7050d5fc68 | ||
|
|
6af9d12f71 | ||
|
|
a54e1db784 | ||
|
|
2319b0fda5 | ||
|
|
6864a22721 | ||
|
|
c9d0e2097d | ||
|
|
d8f7eb3f24 | ||
|
|
90ee919f45 | ||
|
|
ddc6431720 | ||
|
|
2ea6557fb7 | ||
|
|
15358c1928 | ||
|
|
d65025b3cb | ||
|
|
54fa3bc054 | ||
|
|
68f5fa738c | ||
|
|
2ea57ba979 | ||
|
|
1acc0b0dc8 | ||
|
|
645ec79fce | ||
|
|
97e897e80e | ||
|
|
6f72eeae65 | ||
|
|
a845b2e35e | ||
|
|
b164ffeb95 | ||
|
|
7ba34af884 | ||
|
|
7f543ac7c8 | ||
|
|
a1bf92c07f | ||
|
|
0b221615b7 | ||
|
|
f81a9b54a7 | ||
|
|
05da040ce1 | ||
|
|
b911051842 | ||
|
|
a67f46b550 | ||
|
|
dcde19de3c | ||
|
|
a8b4e8c1bc | ||
|
|
7b0e256408 | ||
|
|
5a974f0d77 | ||
|
|
f7fe8d00fb | ||
|
|
946b6d8226 | ||
|
|
25366f0e47 | ||
|
|
562e8e8d87 | ||
|
|
11ff9ed366 | ||
|
|
9a9f2ab94b | ||
|
|
27048fb06d | ||
|
|
e103ee1ffa | ||
|
|
acebbb9041 | ||
|
|
0264c94426 | ||
|
|
88de72a9ea | ||
|
|
9306adc786 | ||
|
|
43c30f8a34 | ||
|
|
7c7240d5ab | ||
|
|
169582c992 | ||
|
|
7b74161e9c | ||
|
|
633e98c8f4 | ||
|
|
5743c4fc93 | ||
|
|
9984b3445f | ||
|
|
90a7e96181 | ||
|
|
00d4ac6137 | ||
|
|
ee432c54b8 | ||
|
|
76ec3eb738 | ||
|
|
37832c63a4 | ||
|
|
d1c33f0872 | ||
|
|
4684b8611d | ||
|
|
f4961ee8b2 | ||
|
|
27f6f4243f | ||
|
|
dcf1d34889 | ||
|
|
76f30dc985 | ||
|
|
2d6c37fa6f | ||
|
|
3e52abf471 | ||
|
|
d697944b5a | ||
|
|
cf14b9e762 | ||
|
|
121cf40062 | ||
|
|
abc89b7eae | ||
|
|
dc33c4d5fd | ||
|
|
087086c308 | ||
|
|
05cb5221d4 | ||
|
|
0fff379ee0 | ||
|
|
0c23818470 | ||
|
|
25dbac9945 | ||
|
|
b379b775f9 | ||
|
|
7cc2c2344e | ||
|
|
d50f6b830a | ||
|
|
8f2921f61f | ||
|
|
e9ec089f76 | ||
|
|
dca99c338e | ||
|
|
cc3a498294 | ||
|
|
c88cb35b84 | ||
|
|
8be7a9f2bc | ||
|
|
899567328e | ||
|
|
9f3cb4349d | ||
|
|
b2b890b8b1 | ||
|
|
f266dbc171 | ||
|
|
b28ac8ca19 | ||
|
|
248ce4f1a8 | ||
|
|
872ec33662 | ||
|
|
b3e6186c78 | ||
|
|
a31497937b | ||
|
|
90088c5d7c | ||
|
|
4c8abd4680 | ||
|
|
a25fb4a8e4 | ||
|
|
29efb467f0 | ||
|
|
ffe2bc9a02 | ||
|
|
8105dff167 | ||
|
|
8d992d74c0 | ||
|
|
296fa2a2f4 | ||
|
|
a9e6051867 | ||
|
|
0fcb316837 | ||
|
|
c0704f822b | ||
|
|
ba974f695d | ||
|
|
3ca82b9ab5 | ||
|
|
df4e5d859f | ||
|
|
67875036c5 | ||
|
|
83f008de1f | ||
|
|
7183b0339b | ||
|
|
9969ff7165 | ||
|
|
0ca97d01ac | ||
|
|
fc4dbb6184 | ||
|
|
9b16212d4b | ||
|
|
4d67cfa340 | ||
|
|
2bd38608e9 | ||
|
|
6ce117e5fa | ||
|
|
2b10b1c17a | ||
|
|
bbf58a2807 | ||
|
|
44ffcaeed8 | ||
|
|
a597d31505 | ||
|
|
6dbd008724 | ||
|
|
7d47f8623a | ||
|
|
7c755483b1 | ||
|
|
e387e005d8 | ||
|
|
c9f6cb7520 | ||
|
|
596ee82a52 | ||
|
|
79b62e0dfc | ||
|
|
e67cf21917 | ||
|
|
8fb1c3971c | ||
|
|
437df18a07 | ||
|
|
8215f2fd8f | ||
|
|
af7f51a647 | ||
|
|
3ab09d87f2 | ||
|
|
4c1d82162f | ||
|
|
3830e2610b | ||
|
|
e3265d400e | ||
|
|
d9c53a3def | ||
|
|
da32440a14 | ||
|
|
25ad3559f7 | ||
|
|
8fbd64955f | ||
|
|
32c83d166d | ||
|
|
d95b19d31b | ||
|
|
9e62e72d1f | ||
|
|
29259c23d7 | ||
|
|
3d6af216dc | ||
|
|
f475aa09e8 | ||
|
|
1278dc28cd | ||
|
|
33ee2fb1a0 | ||
|
|
2ac90262b7 | ||
|
|
bb1ea39c54 | ||
|
|
a087386af3 | ||
|
|
fe96bc7895 | ||
|
|
7a69e3fc9b | ||
|
|
566fa72bcd | ||
|
|
520e197e0e | ||
|
|
c15ef590c2 | ||
|
|
098eeea8f7 | ||
|
|
c3f8f9de54 | ||
|
|
ba4900b61f | ||
|
|
3e03f7559f | ||
|
|
46f8a11339 | ||
|
|
b3a0aaaeea | ||
|
|
aa5e71112e | ||
|
|
22303d2ae8 | ||
|
|
9dbfac02b2 | ||
|
|
6f43d9979d | ||
|
|
d63a045534 | ||
|
|
fe2d309d33 | ||
|
|
94ca2c41e4 | ||
|
|
8873e613d2 | ||
|
|
761b35b199 | ||
|
|
8a2641d213 | ||
|
|
e47091f9a1 | ||
|
|
ea399390ef | ||
|
|
d26869278f | ||
|
|
1639ff1b98 | ||
|
|
9b3107d4fe | ||
|
|
4bebc4c142 | ||
|
|
ac0601b141 | ||
|
|
6040cd3338 | ||
|
|
f93403d3dc | ||
|
|
82cd5d4bab | ||
|
|
0d3055716e | ||
|
|
c9b4067686 | ||
|
|
52dcc6765a | ||
|
|
eab328e2b5 | ||
|
|
23146f64ab | ||
|
|
a6d3a3d0ab | ||
|
|
5eb54bbc95 | ||
|
|
a4fa40743a | ||
|
|
6d8c6a947e | ||
|
|
13cf0096ad | ||
|
|
bd0f1d2884 | ||
|
|
5671580c2d | ||
|
|
669c934ae0 | ||
|
|
b568eb4e1e | ||
|
|
4d8d6c10c6 | ||
|
|
3667fbc49e | ||
|
|
269c880ee0 | ||
|
|
fe89aad621 | ||
|
|
38fb5b101e | ||
|
|
3d8b1abda4 | ||
|
|
0b141e44ae | ||
|
|
695ed817e4 | ||
|
|
f0d9d53588 | ||
|
|
471e94d58e | ||
|
|
7b6776345a | ||
|
|
68763d9a30 | ||
|
|
bead805680 | ||
|
|
34f0857b4f | ||
|
|
b25e46de2e | ||
|
|
86ae004825 | ||
|
|
a355d9e517 | ||
|
|
61aca85632 | ||
|
|
159165490d | ||
|
|
9c8299f13f | ||
|
|
27e58181fb | ||
|
|
02a0727870 | ||
|
|
7c9d7259e6 | ||
|
|
ddb83cf9c5 | ||
|
|
3662d1681e | ||
|
|
6534dc4c4f | ||
|
|
395f93240d | ||
|
|
ac85341cab | ||
|
|
01d71323fc | ||
|
|
ee7852665e | ||
|
|
9d7b80c1ac | ||
|
|
907298c6b1 | ||
|
|
cc53fec08d | ||
|
|
ab165d3f1f | ||
|
|
7c34cb5852 | ||
|
|
71d57c1e27 | ||
|
|
6a5e088c52 | ||
|
|
8ec9f634b4 | ||
|
|
0e4cfbfe6b | ||
|
|
370c3a49a7 | ||
|
|
a7e0110acb | ||
|
|
3769715582 | ||
|
|
0d8dd84df5 | ||
|
|
e98bb16255 | ||
|
|
6a098665fa | ||
|
|
47b4b68e60 | ||
|
|
53ccefc15c | ||
|
|
0e1b966dce | ||
|
|
9132bc2375 | ||
|
|
49c0e95664 | ||
|
|
97e920b68f | ||
|
|
3538c77b78 | ||
|
|
4bc4b2aeac | ||
|
|
38030c7d21 | ||
|
|
e74d45d6c2 | ||
|
|
0479813494 | ||
|
|
ef3ba13979 | ||
|
|
f82cfca2ee | ||
|
|
4b204265c9 | ||
|
|
fbb37633f0 | ||
|
|
09328aeb5a | ||
|
|
5284d75cb7 | ||
|
|
aac35ae868 | ||
|
|
65751a69ae | ||
|
|
121c49e9c3 | ||
|
|
0beae3b1a6 | ||
|
|
57ce4e16a9 | ||
|
|
9370e298d2 | ||
|
|
240e8b3502 | ||
|
|
eecadb3962 | ||
|
|
08d7f544dd | ||
|
|
a673eb89b6 | ||
|
|
f5d3dede6b | ||
|
|
e41d53019f | ||
|
|
637a105e5d | ||
|
|
8e16c587c8 | ||
|
|
1cde0bb8b8 | ||
|
|
61a1a2564e | ||
|
|
dd3781a1ea | ||
|
|
ff9f9bae1d | ||
|
|
aa3a402f70 | ||
|
|
c9882e10a4 | ||
|
|
7cb06f3e58 | ||
|
|
60c1f19581 | ||
|
|
b6420f75e2 | ||
|
|
45e74d3336 | ||
|
|
dc040dfc37 | ||
|
|
9071d98cbe | ||
|
|
74ffc273ef | ||
|
|
2a4cbbe569 | ||
|
|
63eeadad1d | ||
|
|
2de0d4bc48 | ||
|
|
c0da6994da | ||
|
|
568a4428ba | ||
|
|
4823e0b188 | ||
|
|
0690dd9550 | ||
|
|
b5eceb52fb | ||
|
|
c90e9c165b | ||
|
|
a61e87c5dd | ||
|
|
abd3ee9c5d | ||
|
|
3dd61aeb71 | ||
|
|
6a46a9ba47 | ||
|
|
fd39168106 | ||
|
|
6a48f6df25 | ||
|
|
e807c9b6be | ||
|
|
af8970bbb9 | ||
|
|
07931dd75f | ||
|
|
bfa2e6b4dd | ||
|
|
129b59a43f | ||
|
|
4b60f56e5f | ||
|
|
ee7c38045f | ||
|
|
d18253d50b | ||
|
|
c9b9ef9934 | ||
|
|
f968db63e9 | ||
|
|
92572c977b | ||
|
|
493c5b41f8 | ||
|
|
92ae2c46b6 | ||
|
|
613428c54c | ||
|
|
bde8b64ba3 | ||
|
|
e74a286d70 | ||
|
|
1e090f9d30 | ||
|
|
a1064a54cc | ||
|
|
dbd500cab9 | ||
|
|
0bc0a38704 | ||
|
|
9f587ab24b | ||
|
|
8858f889b4 | ||
|
|
833a33678c | ||
|
|
34c10e1e48 | ||
|
|
8ce8c7a0f3 | ||
|
|
94bcb30f11 | ||
|
|
819424fd3b | ||
|
|
f25b8fc7b0 | ||
|
|
0d57356c11 | ||
|
|
8775564e04 | ||
|
|
15dff342a6 | ||
|
|
45c7184fde | ||
|
|
2ddaa351ab | ||
|
|
afe9ee5367 | ||
|
|
8348f2dcc8 | ||
|
|
63f489d39f | ||
|
|
8bbf57c199 | ||
|
|
67f63a6bb3 | ||
|
|
18b51de696 | ||
|
|
d23eacbd37 | ||
|
|
444ae00129 | ||
|
|
6280232e91 | ||
|
|
23e3a1d012 | ||
|
|
71af78caea | ||
|
|
e1d9732a60 | ||
|
|
058f86ec5f | ||
|
|
0da87e1d5e | ||
|
|
be0a808f01 | ||
|
|
a0fa232a3a | ||
|
|
4a4602137b | ||
|
|
6d6f1340af | ||
|
|
35a728e69f | ||
|
|
218d08b1f6 | ||
|
|
219245be95 | ||
|
|
aa1742db63 | ||
|
|
e336c67857 | ||
|
|
871fd46c89 | ||
|
|
f536f16926 | ||
|
|
d3c00cdd52 | ||
|
|
6b990f8f12 | ||
|
|
3c010f0e73 | ||
|
|
357df22fab | ||
|
|
470f5b5029 | ||
|
|
216b1664bd | ||
|
|
cbe2124387 | ||
|
|
11934233a0 | ||
|
|
de9a0c0166 | ||
|
|
5c9ff9d1a2 | ||
|
|
0142520bb8 | ||
|
|
b4f9f968f6 | ||
|
|
9a7bafb02c | ||
|
|
3e44d5bfdf | ||
|
|
f4d58c8823 | ||
|
|
4d192f5930 | ||
|
|
20cbf99cdf | ||
|
|
6784388a42 | ||
|
|
b504a79bf7 | ||
|
|
1b9b49f045 | ||
|
|
095413c6c5 | ||
|
|
b9486e8935 | ||
|
|
302b9f9dd7 | ||
|
|
57aa267f24 | ||
|
|
ce351afb9a | ||
|
|
7b513bd29d | ||
|
|
0e65bfc78b | ||
|
|
afaf105cb0 | ||
|
|
629297e0c2 | ||
|
|
1aca22f219 | ||
|
|
bd3da38fc8 | ||
|
|
991c2c18de | ||
|
|
54a2e7650a | ||
|
|
5819cfb438 | ||
|
|
38ca58d71e | ||
|
|
c1c0edab9f | ||
|
|
8670938397 | ||
|
|
83b552780e | ||
|
|
b8cc74f377 | ||
|
|
c3ba39c80f | ||
|
|
ff3a9c51f3 | ||
|
|
19dfea7762 | ||
|
|
c311828217 | ||
|
|
5ae74d6924 | ||
|
|
04f719c33c | ||
|
|
7ab88e9a71 | ||
|
|
1164da5e7a | ||
|
|
6742646e89 | ||
|
|
6cf01631ad | ||
|
|
7a5cade510 | ||
|
|
c5a1d8a8bd | ||
|
|
32bad5f8bb | ||
|
|
5ec5647395 | ||
|
|
4e9f7c729d | ||
|
|
4c0437b3fb | ||
|
|
de71b97b1f | ||
|
|
21d008c6c2 | ||
|
|
9138a3c881 | ||
|
|
8b3f36c1f8 | ||
|
|
d397d75aca | ||
|
|
618b28a292 | ||
|
|
c966211481 | ||
|
|
5ae1190ddd | ||
|
|
fb9cce747d | ||
|
|
1a04ebce35 | ||
|
|
59bbfc4e06 | ||
|
|
d3973172e8 | ||
|
|
cdd31353c5 | ||
|
|
b047cb6dc1 |
55
.github/actions/install/action.yml
vendored
55
.github/actions/install/action.yml
vendored
@@ -2,10 +2,6 @@ name: "Browsercore install"
|
|||||||
description: "Install deps for the project browsercore"
|
description: "Install deps for the project browsercore"
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
zig:
|
|
||||||
description: 'Zig version to install'
|
|
||||||
required: false
|
|
||||||
default: '0.15.1'
|
|
||||||
arch:
|
arch:
|
||||||
description: 'CPU arch used to select the v8 lib'
|
description: 'CPU arch used to select the v8 lib'
|
||||||
required: false
|
required: false
|
||||||
@@ -17,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.1.33'
|
default: 'v0.3.1'
|
||||||
v8:
|
v8:
|
||||||
description: 'v8 version to install'
|
description: 'v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
@@ -26,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"
|
||||||
@@ -36,11 +36,13 @@ 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
|
||||||
- uses: mlugg/setup-zig@v2
|
- uses: mlugg/setup-zig@v2
|
||||||
with:
|
|
||||||
version: ${{ inputs.zig }}
|
# Rust Toolchain for html5ever
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
- name: Cache v8
|
- name: Cache v8
|
||||||
id: cache-v8
|
id: cache-v8
|
||||||
@@ -49,46 +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/out/${{ inputs.os }}/debug/obj/zig/
|
mkdir -p v8
|
||||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/debug/obj/zig/libc_v8.a
|
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/libc_v8${{inputs.debug == 'true' && '_debug' || '' }}.a
|
||||||
|
|
||||||
mkdir -p v8/out/${{ inputs.os }}/release/obj/zig/
|
|
||||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/release/obj/zig/libc_v8.a
|
|
||||||
|
|
||||||
- name: Cache libiconv
|
|
||||||
id: cache-libiconv
|
|
||||||
uses: actions/cache@v4
|
|
||||||
env:
|
|
||||||
cache-name: cache-libiconv
|
|
||||||
with:
|
|
||||||
path: ${{ inputs.cache-dir }}/libiconv
|
|
||||||
key: vendor/libiconv/libiconv-1.17
|
|
||||||
|
|
||||||
- name: download libiconv
|
|
||||||
if: ${{ steps.cache-libiconv.outputs.cache-hit != 'true' }}
|
|
||||||
shell: bash
|
|
||||||
run: make download-libiconv
|
|
||||||
|
|
||||||
- name: build libiconv
|
|
||||||
shell: bash
|
|
||||||
run: make build-libiconv
|
|
||||||
|
|
||||||
- name: build mimalloc
|
|
||||||
shell: bash
|
|
||||||
run: make install-mimalloc
|
|
||||||
|
|
||||||
- name: build netsurf
|
|
||||||
shell: bash
|
|
||||||
run: make install-netsurf
|
|
||||||
|
|||||||
53
.github/workflows/build.yml
vendored
53
.github/workflows/build.yml
vendored
@@ -5,8 +5,12 @@ env:
|
|||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
|
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
|
||||||
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
|
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
|
||||||
|
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "2 2 * * *"
|
- cron: "2 2 * * *"
|
||||||
|
|
||||||
@@ -23,10 +27,10 @@ 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@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||||
@@ -37,8 +41,11 @@ jobs:
|
|||||||
os: ${{env.OS}}
|
os: ${{env.OS}}
|
||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
|
|
||||||
|
- name: v8 snapshot
|
||||||
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
@@ -53,7 +60,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
tag: nightly
|
tag: ${{ env.RELEASE }}
|
||||||
|
makeLatest: true
|
||||||
|
|
||||||
build-linux-aarch64:
|
build-linux-aarch64:
|
||||||
env:
|
env:
|
||||||
@@ -61,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
|
||||||
@@ -75,8 +83,11 @@ jobs:
|
|||||||
os: ${{env.OS}}
|
os: ${{env.OS}}
|
||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
|
|
||||||
|
- name: v8 snapshot
|
||||||
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
@@ -91,7 +102,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
tag: nightly
|
tag: ${{ env.RELEASE }}
|
||||||
|
makeLatest: true
|
||||||
|
|
||||||
build-macos-aarch64:
|
build-macos-aarch64:
|
||||||
env:
|
env:
|
||||||
@@ -101,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
|
||||||
@@ -115,8 +127,11 @@ jobs:
|
|||||||
os: ${{env.OS}}
|
os: ${{env.OS}}
|
||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
|
|
||||||
|
- name: v8 snapshot
|
||||||
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
@@ -131,20 +146,16 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
tag: nightly
|
tag: ${{ env.RELEASE }}
|
||||||
|
makeLatest: true
|
||||||
|
|
||||||
build-macos-x86_64:
|
build-macos-x86_64:
|
||||||
env:
|
env:
|
||||||
ARCH: x86_64
|
ARCH: x86_64
|
||||||
OS: macos
|
OS: macos
|
||||||
|
|
||||||
# macos-13 runs on x86 CPU. see
|
runs-on: macos-14-large
|
||||||
# https://github.com/actions/runner-images?tab=readme-ov-file
|
timeout-minutes: 20
|
||||||
# If we want to build for macos-14 or superior, we need to switch to
|
|
||||||
# macos-14-large.
|
|
||||||
# No need for now, but maybe we will need it in the short term.
|
|
||||||
runs-on: macos-13
|
|
||||||
timeout-minutes: 15
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -158,8 +169,11 @@ jobs:
|
|||||||
os: ${{env.OS}}
|
os: ${{env.OS}}
|
||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
|
|
||||||
|
- name: v8 snapshot
|
||||||
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
@@ -174,4 +188,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
tag: nightly
|
tag: ${{ env.RELEASE }}
|
||||||
|
makeLatest: true
|
||||||
|
|||||||
68
.github/workflows/e2e-integration-test.yml
vendored
Normal file
68
.github/workflows/e2e-integration-test.yml
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
name: e2e-integration-test
|
||||||
|
|
||||||
|
env:
|
||||||
|
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "4 4 * * *"
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
zig-build-release:
|
||||||
|
name: zig build release
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
# Don't run the CI with draft PR.
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
|
- 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 }})
|
||||||
|
|
||||||
|
- name: upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: lightpanda-build-release
|
||||||
|
path: |
|
||||||
|
zig-out/bin/lightpanda
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
demo-scripts:
|
||||||
|
name: demo-integration-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
|
||||||
|
|
||||||
|
- name: run end to end integration tests
|
||||||
|
run: |
|
||||||
|
./lightpanda serve --log_level error & echo $! > LPD.pid
|
||||||
|
go run integration/main.go
|
||||||
|
kill `cat LPD.pid`
|
||||||
75
.github/workflows/e2e-test.yml
vendored
75
.github/workflows/e2e-test.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
|||||||
if: github.event.pull_request.draft == false
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
- name: zig build release
|
- name: zig build release
|
||||||
run: zig build -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 }})
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@@ -122,10 +122,19 @@ jobs:
|
|||||||
needs: zig-build-release
|
needs: zig-build-release
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MAX_MEMORY: 27000
|
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
|
||||||
@@ -150,22 +159,53 @@ jobs:
|
|||||||
go run ws/main.go & echo $! > WS.pid
|
go run ws/main.go & echo $! > WS.pid
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
|
- name: run lightpanda in cgroup
|
||||||
|
run: |
|
||||||
|
if [ ! -f /sys/fs/cgroup/cgroup.controllers ]; then
|
||||||
|
echo "cgroup v2 not available: /sys/fs/cgroup/cgroup.controllers missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p $CG_ROOT/$CG
|
||||||
|
cgexec -g memory:$CG ./lightpanda serve & echo $! > LPD.pid
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
- name: run puppeteer
|
- name: run puppeteer
|
||||||
run: |
|
run: |
|
||||||
./lightpanda serve & echo $! > LPD.pid
|
|
||||||
sleep 2
|
|
||||||
RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1
|
RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1
|
||||||
cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM
|
cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM
|
||||||
kill `cat LPD.pid`
|
kill `cat LPD.pid`
|
||||||
|
|
||||||
|
PID=$(cat LPD.pid)
|
||||||
|
while kill -0 $PID 2>/dev/null; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ ! -f $CG_ROOT/$CG/memory.peak ]; then
|
||||||
|
echo "memory.peak not available in $CG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cat $CG_ROOT/$CG/memory.peak > LPD.cg_mem_peak
|
||||||
|
|
||||||
- name: puppeteer result
|
- name: puppeteer result
|
||||||
run: cat puppeteer.out
|
run: cat puppeteer.out
|
||||||
|
|
||||||
- name: memory regression
|
- name: cgroup memory regression
|
||||||
|
run: |
|
||||||
|
PEAK_BYTES=$(cat LPD.cg_mem_peak)
|
||||||
|
PEAK_KB=$((PEAK_BYTES / 1024))
|
||||||
|
echo "memory.peak_bytes=$PEAK_BYTES"
|
||||||
|
echo "memory.peak_kb=$PEAK_KB"
|
||||||
|
test "$PEAK_KB" -le "$MAX_CG_PEAK"
|
||||||
|
|
||||||
|
- name: virtual memory regression
|
||||||
run: |
|
run: |
|
||||||
export LPD_VmHWM=`cat LPD.VmHWM`
|
export LPD_VmHWM=`cat LPD.VmHWM`
|
||||||
echo "Peak resident set size: $LPD_VmHWM"
|
echo "Peak resident set size: $LPD_VmHWM"
|
||||||
test "$LPD_VmHWM" -le "$MAX_MEMORY"
|
test "$LPD_VmHWM" -le "$MAX_VmHWM"
|
||||||
|
|
||||||
|
- name: cleanup cgroup
|
||||||
|
run: rmdir $CG_ROOT/$CG
|
||||||
|
|
||||||
- name: duration regression
|
- name: duration regression
|
||||||
run: |
|
run: |
|
||||||
@@ -178,7 +218,8 @@ jobs:
|
|||||||
export AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
|
export AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
|
||||||
export TOTAL_DURATION=`cat puppeteer.out|grep 'total duration'|sed 's/total duration (ms) //'`
|
export TOTAL_DURATION=`cat puppeteer.out|grep 'total duration'|sed 's/total duration (ms) //'`
|
||||||
export LPD_VmHWM=`cat LPD.VmHWM`
|
export LPD_VmHWM=`cat LPD.VmHWM`
|
||||||
echo "{\"duration_total\":${TOTAL_DURATION},\"duration_avg\":${AVG_DURATION},\"mem_peak\":${LPD_VmHWM}}" > bench.json
|
export LPD_CG_PEAK_KB=$(( $(cat LPD.cg_mem_peak) / 1024 ))
|
||||||
|
echo "{\"duration_total\":${TOTAL_DURATION},\"duration_avg\":${AVG_DURATION},\"mem_peak\":${LPD_VmHWM},\"cg_mem_peak\":${LPD_CG_PEAK_KB}}" > bench.json
|
||||||
cat bench.json
|
cat bench.json
|
||||||
|
|
||||||
- name: run hyperfine
|
- name: run hyperfine
|
||||||
@@ -230,3 +271,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/
|
||||||
|
|||||||
91
.github/workflows/wpt.yml
vendored
91
.github/workflows/wpt.yml
vendored
@@ -15,14 +15,14 @@ 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@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||||
@@ -30,8 +30,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: 120
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: fork
|
||||||
|
repository: 'lightpanda-io/wpt'
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# The hosts are configured manually on the self host runner.
|
||||||
|
# - name: create custom hosts
|
||||||
|
# run: ./wpt make-hosts-file | sudo tee -a /etc/hosts
|
||||||
|
|
||||||
|
- name: generate manifest
|
||||||
|
run: ./wpt manifest
|
||||||
|
|
||||||
|
- name: download lightpanda release
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: lightpanda-build-release
|
||||||
|
|
||||||
|
- run: chmod a+x ./lightpanda
|
||||||
|
|
||||||
|
- name: download wptrunner
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: wptrunner
|
||||||
|
|
||||||
|
- run: chmod a+x ./wptrunner
|
||||||
|
|
||||||
|
- name: run test with json output
|
||||||
|
run: |
|
||||||
|
./wpt serve 2> /dev/null & echo $! > WPT.pid
|
||||||
|
sleep 10s
|
||||||
|
./wptrunner -lpd-path ./lightpanda -json -concurrency 1 > wpt.json
|
||||||
|
kill `cat WPT.pid`
|
||||||
|
|
||||||
- name: write commit
|
- name: write commit
|
||||||
run: |
|
run: |
|
||||||
@@ -48,7 +125,7 @@ jobs:
|
|||||||
|
|
||||||
perf-fmt:
|
perf-fmt:
|
||||||
name: perf-fmt
|
name: perf-fmt
|
||||||
needs: wpt
|
needs: run-wpt
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|||||||
13
.github/workflows/zig-fmt.yml
vendored
13
.github/workflows/zig-fmt.yml
vendored
@@ -1,8 +1,5 @@
|
|||||||
name: zig-fmt
|
name: zig-fmt
|
||||||
|
|
||||||
env:
|
|
||||||
ZIG_VERSION: 0.15.1
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
@@ -32,14 +29,13 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: mlugg/setup-zig@v2
|
- uses: actions/checkout@v6
|
||||||
with:
|
|
||||||
version: ${{ env.ZIG_VERSION }}
|
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# Zig version used from the `minimum_zig_version` field in build.zig.zon
|
||||||
|
- uses: mlugg/setup-zig@v2
|
||||||
|
|
||||||
- name: Run zig fmt
|
- name: Run zig fmt
|
||||||
id: fmt
|
id: fmt
|
||||||
run: |
|
run: |
|
||||||
@@ -58,6 +54,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "${delimiter}" >> "${GITHUB_OUTPUT}"
|
echo "${delimiter}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- name: Fail the job
|
- name: Fail the job
|
||||||
if: steps.fmt.outputs.zig_fmt_errs != ''
|
if: steps.fmt.outputs.zig_fmt_errs != ''
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
|||||||
44
.github/workflows/zig-test.yml
vendored
44
.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,11 +37,9 @@ 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
|
||||||
|
|
||||||
@@ -54,36 +51,11 @@ jobs:
|
|||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
- name: zig build debug
|
|
||||||
run: zig build
|
|
||||||
|
|
||||||
- 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
|
||||||
@@ -104,7 +76,7 @@ jobs:
|
|||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
- name: zig build test
|
- name: zig build test
|
||||||
run: zig build 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: |
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,7 +1,6 @@
|
|||||||
zig-cache
|
|
||||||
/.zig-cache/
|
/.zig-cache/
|
||||||
|
/.lp-cache/
|
||||||
zig-out
|
zig-out
|
||||||
/vendor/netsurf/out
|
|
||||||
/vendor/libiconv/
|
|
||||||
lightpanda.id
|
lightpanda.id
|
||||||
/v8/
|
/src/html5ever/target/
|
||||||
|
src/snapshot.bin
|
||||||
|
|||||||
36
.gitmodules
vendored
36
.gitmodules
vendored
@@ -1,36 +0,0 @@
|
|||||||
[submodule "vendor/netsurf/libwapcaplet"]
|
|
||||||
path = vendor/netsurf/libwapcaplet
|
|
||||||
url = https://github.com/lightpanda-io/libwapcaplet.git/
|
|
||||||
[submodule "vendor/netsurf/libparserutils"]
|
|
||||||
path = vendor/netsurf/libparserutils
|
|
||||||
url = https://github.com/lightpanda-io/libparserutils.git/
|
|
||||||
[submodule "vendor/netsurf/libdom"]
|
|
||||||
path = vendor/netsurf/libdom
|
|
||||||
url = https://github.com/lightpanda-io/libdom.git/
|
|
||||||
[submodule "vendor/netsurf/share/netsurf-buildsystem"]
|
|
||||||
path = vendor/netsurf/share/netsurf-buildsystem
|
|
||||||
url = https://github.com/lightpanda-io/netsurf-buildsystem.git
|
|
||||||
[submodule "vendor/netsurf/libhubbub"]
|
|
||||||
path = vendor/netsurf/libhubbub
|
|
||||||
url = https://github.com/lightpanda-io/libhubbub.git/
|
|
||||||
[submodule "tests/wpt"]
|
|
||||||
path = tests/wpt
|
|
||||||
url = https://github.com/lightpanda-io/wpt
|
|
||||||
[submodule "vendor/mimalloc"]
|
|
||||||
path = vendor/mimalloc
|
|
||||||
url = https://github.com/microsoft/mimalloc.git/
|
|
||||||
[submodule "vendor/nghttp2"]
|
|
||||||
path = vendor/nghttp2
|
|
||||||
url = https://github.com/nghttp2/nghttp2.git
|
|
||||||
[submodule "vendor/mbedtls"]
|
|
||||||
path = vendor/mbedtls
|
|
||||||
url = https://github.com/Mbed-TLS/mbedtls.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
|
|
||||||
62
Dockerfile
62
Dockerfile
@@ -1,60 +1,69 @@
|
|||||||
FROM debian:stable
|
FROM debian:stable-slim
|
||||||
|
|
||||||
ARG MINISIG=0.12
|
ARG MINISIG=0.12
|
||||||
ARG ZIG=0.15.1
|
|
||||||
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.1.33
|
ARG ZIG_V8=v0.3.1
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
RUN apt-get update -yq && \
|
RUN apt-get update -yq && \
|
||||||
apt-get install -yq xz-utils \
|
apt-get install -yq xz-utils ca-certificates \
|
||||||
python3 ca-certificates git \
|
|
||||||
pkg-config libglib2.0-dev \
|
pkg-config libglib2.0-dev \
|
||||||
gperf libexpat1-dev \
|
clang make curl git
|
||||||
cmake clang \
|
|
||||||
curl git
|
# Get Rust
|
||||||
|
RUN curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal -y
|
||||||
|
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||||
|
|
||||||
# install minisig
|
# install minisig
|
||||||
RUN curl --fail -L -O https://github.com/jedisct1/minisign/releases/download/${MINISIG}/minisign-${MINISIG}-linux.tar.gz && \
|
RUN curl --fail -L -O https://github.com/jedisct1/minisign/releases/download/${MINISIG}/minisign-${MINISIG}-linux.tar.gz && \
|
||||||
tar xvzf minisign-${MINISIG}-linux.tar.gz
|
tar xvzf minisign-${MINISIG}-linux.tar.gz -C /
|
||||||
|
|
||||||
|
# clone lightpanda
|
||||||
|
RUN git clone https://github.com/lightpanda-io/browser.git
|
||||||
|
WORKDIR /browser
|
||||||
|
|
||||||
# install zig
|
# install zig
|
||||||
RUN case $TARGETPLATFORM in \
|
RUN ZIG=$(grep '\.minimum_zig_version = "' "build.zig.zon" | cut -d'"' -f2) && \
|
||||||
|
case $TARGETPLATFORM in \
|
||||||
"linux/arm64") ARCH="aarch64" ;; \
|
"linux/arm64") ARCH="aarch64" ;; \
|
||||||
*) ARCH="x86_64" ;; \
|
*) ARCH="x86_64" ;; \
|
||||||
esac && \
|
esac && \
|
||||||
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz && \
|
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz && \
|
||||||
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz.minisig && \
|
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz.minisig && \
|
||||||
minisign-linux/${ARCH}/minisign -Vm zig-${ARCH}-linux-${ZIG}.tar.xz -P ${ZIG_MINISIG} && \
|
/minisign-linux/${ARCH}/minisign -Vm zig-${ARCH}-linux-${ZIG}.tar.xz -P ${ZIG_MINISIG} && \
|
||||||
tar xvf zig-${ARCH}-linux-${ZIG}.tar.xz && \
|
tar xvf zig-${ARCH}-linux-${ZIG}.tar.xz && \
|
||||||
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
|
||||||
|
|
||||||
# clone lightpanda
|
|
||||||
RUN git clone https://github.com/lightpanda-io/browser.git
|
|
||||||
|
|
||||||
WORKDIR /browser
|
|
||||||
|
|
||||||
# install deps
|
# install deps
|
||||||
RUN git submodule init && \
|
RUN git submodule init && \
|
||||||
git submodule update --recursive
|
git submodule update --recursive
|
||||||
|
|
||||||
RUN make install-libiconv && \
|
|
||||||
make install-netsurf && \
|
|
||||||
make install-mimalloc
|
|
||||||
|
|
||||||
# download and install v8
|
# download and install v8
|
||||||
RUN case $TARGETPLATFORM in \
|
RUN case $TARGETPLATFORM in \
|
||||||
"linux/arm64") ARCH="aarch64" ;; \
|
"linux/arm64") ARCH="aarch64" ;; \
|
||||||
*) ARCH="x86_64" ;; \
|
*) ARCH="x86_64" ;; \
|
||||||
esac && \
|
esac && \
|
||||||
curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \
|
curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \
|
||||||
mkdir -p v8/out/linux/release/obj/zig/ && \
|
mkdir -p v8/ && \
|
||||||
mv libc_v8.a v8/out/linux/release/obj/zig/libc_v8.a
|
mv libc_v8.a v8/libc_v8.a
|
||||||
|
|
||||||
|
# build v8 snapshot
|
||||||
|
RUN zig build -Doptimize=ReleaseFast \
|
||||||
|
-Dprebuilt_v8_path=v8/libc_v8.a \
|
||||||
|
snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
# build release
|
# build release
|
||||||
RUN make build
|
RUN zig build -Doptimize=ReleaseFast \
|
||||||
|
-Dsnapshot_path=../../snapshot.bin \
|
||||||
|
-Dprebuilt_v8_path=v8/libc_v8.a \
|
||||||
|
-Dgit_commit=$(git rev-parse --short HEAD)
|
||||||
|
|
||||||
|
FROM debian:stable-slim
|
||||||
|
|
||||||
|
RUN apt-get update -yq && \
|
||||||
|
apt-get install -yq tini
|
||||||
|
|
||||||
FROM debian:stable-slim
|
FROM debian:stable-slim
|
||||||
|
|
||||||
@@ -62,7 +71,12 @@ FROM debian:stable-slim
|
|||||||
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||||
|
|
||||||
COPY --from=0 /browser/zig-out/bin/lightpanda /bin/lightpanda
|
COPY --from=0 /browser/zig-out/bin/lightpanda /bin/lightpanda
|
||||||
|
COPY --from=1 /usr/bin/tini /usr/bin/tini
|
||||||
|
|
||||||
EXPOSE 9222/tcp
|
EXPOSE 9222/tcp
|
||||||
|
|
||||||
CMD ["/bin/lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222"]
|
# Lightpanda install only some signal handlers, and PID 1 doesn't have a default SIGTERM signal handler.
|
||||||
|
# Using "tini" as PID1 ensures that signals work as expected, so e.g. "docker stop" will not hang.
|
||||||
|
# (See https://github.com/krallin/tini#why-tini).
|
||||||
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
|
CMD ["/bin/lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222", "--log_level", "info"]
|
||||||
|
|||||||
@@ -5,14 +5,6 @@ List](https://spdx.org/licenses/).
|
|||||||
|
|
||||||
The default license for this project is [AGPL-3.0-only](LICENSE).
|
The default license for this project is [AGPL-3.0-only](LICENSE).
|
||||||
|
|
||||||
## MIT
|
|
||||||
|
|
||||||
The following files are licensed under MIT:
|
|
||||||
|
|
||||||
```
|
|
||||||
src/polyfill/fetch.js
|
|
||||||
```
|
|
||||||
|
|
||||||
The following directories and their subdirectories are licensed under their
|
The following directories and their subdirectories are licensed under their
|
||||||
original upstream licenses:
|
original upstream licenses:
|
||||||
|
|
||||||
|
|||||||
203
Makefile
203
Makefile
@@ -34,7 +34,7 @@ endif
|
|||||||
|
|
||||||
## Display this help screen
|
## Display this help screen
|
||||||
help:
|
help:
|
||||||
@printf "\e[36m%-35s %s\e[0m\n" "Command" "Usage"
|
@printf "\033[36m%-35s %s\033[0m\n" "Command" "Usage"
|
||||||
@sed -n -e '/^## /{'\
|
@sed -n -e '/^## /{'\
|
||||||
-e 's/## //g;'\
|
-e 's/## //g;'\
|
||||||
-e 'h;'\
|
-e 'h;'\
|
||||||
@@ -47,202 +47,63 @@ help:
|
|||||||
|
|
||||||
# $(ZIG) commands
|
# $(ZIG) commands
|
||||||
# ------------
|
# ------------
|
||||||
.PHONY: build build-dev run run-release shell test bench download-zig wpt data get-v8 build-v8 build-v8-dev
|
.PHONY: build build-v8-snapshot build-dev run run-release shell test bench data end2end
|
||||||
.PHONY: end2end
|
|
||||||
|
|
||||||
zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2)
|
## Build v8 snapshot
|
||||||
|
build-v8-snapshot:
|
||||||
|
@printf "\033[36mBuilding v8 snapshot (release safe)...\033[0m\n"
|
||||||
|
@$(ZIG) build -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||||
|
@printf "\033[33mBuild OK\033[0m\n"
|
||||||
|
|
||||||
## Download the zig recommended version
|
## Build in release-fast mode
|
||||||
download-zig:
|
build: build-v8-snapshot
|
||||||
$(eval url = "https://ziglang.org/download/$(zig_version)/zig-$(OS)-$(ARCH)-$(zig_version).tar.xz")
|
@printf "\033[36mBuilding (release fast)...\033[0m\n"
|
||||||
$(eval dest = "/tmp/zig-$(OS)-$(ARCH)-$(zig_version).tar.xz")
|
@$(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 "\e[36mDownload zig version $(zig_version)...\e[0m\n"
|
@printf "\033[33mBuild OK\033[0m\n"
|
||||||
@curl -o "$(dest)" -L "$(url)" || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
|
||||||
@printf "\e[33mDownloaded $(dest)\e[0m\n"
|
|
||||||
|
|
||||||
## Build in release-safe mode
|
|
||||||
build:
|
|
||||||
@printf "\e[36mBuilding (release safe)...\e[0m\n"
|
|
||||||
$(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
|
||||||
@printf "\e[33mBuild OK\e[0m\n"
|
|
||||||
|
|
||||||
## Build in debug mode
|
## Build in debug mode
|
||||||
build-dev:
|
build-dev:
|
||||||
@printf "\e[36mBuilding (debug)...\e[0m\n"
|
@printf "\033[36mBuilding (debug)...\033[0m\n"
|
||||||
@$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
@$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||||
@printf "\e[33mBuild OK\e[0m\n"
|
@printf "\033[33mBuild OK\033[0m\n"
|
||||||
|
|
||||||
## Run the server in release mode
|
## Run the server in release mode
|
||||||
run: build
|
run: build
|
||||||
@printf "\e[36mRunning...\e[0m\n"
|
@printf "\033[36mRunning...\033[0m\n"
|
||||||
@./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
|
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;)
|
||||||
|
|
||||||
## Run the server in debug mode
|
## Run the server in debug mode
|
||||||
run-debug: build-dev
|
run-debug: build-dev
|
||||||
@printf "\e[36mRunning...\e[0m\n"
|
@printf "\033[36mRunning...\033[0m\n"
|
||||||
@./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
|
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;)
|
||||||
|
|
||||||
## Run a JS shell in debug mode
|
## Run a JS shell in debug mode
|
||||||
shell:
|
shell:
|
||||||
@printf "\e[36mBuilding shell...\e[0m\n"
|
@printf "\033[36mBuilding shell...\033[0m\n"
|
||||||
@$(ZIG) build shell || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
@$(ZIG) build shell || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||||
|
|
||||||
## Run WPT tests
|
## Test - `grep` is used to filter out the huge compile command on build
|
||||||
wpt:
|
ifeq ($(OS), macos)
|
||||||
@printf "\e[36mBuilding wpt...\e[0m\n"
|
|
||||||
@$(ZIG) build wpt -- $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
|
||||||
|
|
||||||
wpt-summary:
|
|
||||||
@printf "\e[36mBuilding wpt...\e[0m\n"
|
|
||||||
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
|
||||||
|
|
||||||
## Test
|
|
||||||
test:
|
test:
|
||||||
@TEST_FILTER='${F}' $(ZIG) build test -freference-trace --summary all
|
@script -q /dev/null sh -c 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace' 2>&1 \
|
||||||
|
| grep --line-buffered -v "^/.*zig test -freference-trace"
|
||||||
|
else
|
||||||
|
test:
|
||||||
|
@script -qec 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace' /dev/null 2>&1 \
|
||||||
|
| grep --line-buffered -v "^/.*zig test -freference-trace"
|
||||||
|
endif
|
||||||
|
|
||||||
## Run demo/runner end to end tests
|
## Run demo/runner end to end tests
|
||||||
end2end:
|
end2end:
|
||||||
@test -d ../demo
|
@test -d ../demo
|
||||||
cd ../demo && go run runner/main.go
|
cd ../demo && go run runner/main.go
|
||||||
|
|
||||||
## v8
|
|
||||||
get-v8:
|
|
||||||
@printf "\e[36mGetting v8 source...\e[0m\n"
|
|
||||||
@$(ZIG) build get-v8
|
|
||||||
|
|
||||||
build-v8-dev:
|
|
||||||
@printf "\e[36mBuilding v8 (dev)...\e[0m\n"
|
|
||||||
@$(ZIG) build build-v8
|
|
||||||
|
|
||||||
build-v8:
|
|
||||||
@printf "\e[36mBuilding v8...\e[0m\n"
|
|
||||||
@$(ZIG) build -Doptimize=ReleaseSafe build-v8
|
|
||||||
|
|
||||||
# Install and build required dependencies commands
|
# Install and build required dependencies commands
|
||||||
# ------------
|
# ------------
|
||||||
.PHONY: install-submodule
|
.PHONY: install
|
||||||
.PHONY: install-libiconv
|
|
||||||
.PHONY: _install-netsurf install-netsurf clean-netsurf test-netsurf install-netsurf-dev
|
|
||||||
.PHONY: install-mimalloc install-mimalloc-dev clean-mimalloc
|
|
||||||
.PHONY: install-dev install
|
|
||||||
|
|
||||||
## Install and build dependencies for release
|
install: build
|
||||||
install: install-submodule install-libiconv install-netsurf install-mimalloc
|
|
||||||
|
|
||||||
## Install and build dependencies for dev
|
|
||||||
install-dev: install-submodule install-libiconv install-netsurf-dev install-mimalloc-dev
|
|
||||||
|
|
||||||
install-netsurf-dev: _install-netsurf
|
|
||||||
install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG
|
|
||||||
|
|
||||||
install-netsurf: _install-netsurf
|
|
||||||
install-netsurf: OPTCFLAGS := -DNDEBUG
|
|
||||||
|
|
||||||
BC_NS := $(BC)vendor/netsurf/out/$(OS)-$(ARCH)
|
|
||||||
ICONV := $(BC)vendor/libiconv/out/$(OS)-$(ARCH)
|
|
||||||
# TODO: add Linux iconv path (I guess it depends on the distro)
|
|
||||||
# TODO: this way of linking libiconv is not ideal. We should have a more generic way
|
|
||||||
# and stick to a specif version. Maybe build from source. Anyway not now.
|
|
||||||
_install-netsurf: clean-netsurf
|
|
||||||
@printf "\e[36mInstalling NetSurf...\e[0m\n" && \
|
|
||||||
ls $(ICONV)/lib/libiconv.a 1> /dev/null || (printf "\e[33mERROR: you need to execute 'make install-libiconv'\e[0m\n"; exit 1;) && \
|
|
||||||
mkdir -p $(BC_NS) && \
|
|
||||||
cp -R vendor/netsurf/share $(BC_NS) && \
|
|
||||||
export PREFIX=$(BC_NS) && \
|
|
||||||
export OPTLDFLAGS="-L$(ICONV)/lib" && \
|
|
||||||
export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \
|
|
||||||
printf "\e[33mInstalling libwapcaplet...\e[0m\n" && \
|
|
||||||
cd vendor/netsurf/libwapcaplet && \
|
|
||||||
BUILDDIR=$(BC_NS)/build/libwapcaplet make install && \
|
|
||||||
cd ../libparserutils && \
|
|
||||||
printf "\e[33mInstalling libparserutils...\e[0m\n" && \
|
|
||||||
BUILDDIR=$(BC_NS)/build/libparserutils make install && \
|
|
||||||
cd ../libhubbub && \
|
|
||||||
printf "\e[33mInstalling libhubbub...\e[0m\n" && \
|
|
||||||
BUILDDIR=$(BC_NS)/build/libhubbub make install && \
|
|
||||||
rm src/treebuilder/autogenerated-element-type.c && \
|
|
||||||
cd ../libdom && \
|
|
||||||
printf "\e[33mInstalling libdom...\e[0m\n" && \
|
|
||||||
BUILDDIR=$(BC_NS)/build/libdom make install && \
|
|
||||||
printf "\e[33mRunning libdom example...\e[0m\n" && \
|
|
||||||
cd examples && \
|
|
||||||
$(ZIG) cc \
|
|
||||||
-I$(ICONV)/include \
|
|
||||||
-I$(BC_NS)/include \
|
|
||||||
-L$(ICONV)/lib \
|
|
||||||
-L$(BC_NS)/lib \
|
|
||||||
-liconv \
|
|
||||||
-ldom \
|
|
||||||
-lhubbub \
|
|
||||||
-lparserutils \
|
|
||||||
-lwapcaplet \
|
|
||||||
-o a.out \
|
|
||||||
dom-structure-dump.c \
|
|
||||||
$(ICONV)/lib/libiconv.a && \
|
|
||||||
./a.out > /dev/null && \
|
|
||||||
rm a.out && \
|
|
||||||
printf "\e[36mDone NetSurf $(OS)\e[0m\n"
|
|
||||||
|
|
||||||
clean-netsurf:
|
|
||||||
@printf "\e[36mCleaning NetSurf build...\e[0m\n" && \
|
|
||||||
rm -Rf $(BC_NS)
|
|
||||||
|
|
||||||
test-netsurf:
|
|
||||||
@printf "\e[36mTesting NetSurf...\e[0m\n" && \
|
|
||||||
export PREFIX=$(BC_NS) && \
|
|
||||||
export LDFLAGS="-L$(ICONV)/lib -L$(BC_NS)/lib" && \
|
|
||||||
export CFLAGS="-I$(ICONV)/include -I$(BC_NS)/include" && \
|
|
||||||
cd vendor/netsurf/libdom && \
|
|
||||||
BUILDDIR=$(BC_NS)/build/libdom make test
|
|
||||||
|
|
||||||
download-libiconv:
|
|
||||||
ifeq ("$(wildcard vendor/libiconv/libiconv-1.17)","")
|
|
||||||
@mkdir -p vendor/libiconv
|
|
||||||
@cd vendor/libiconv && \
|
|
||||||
curl -L https://github.com/lightpanda-io/libiconv/releases/download/1.17/libiconv-1.17.tar.gz | tar -xvzf -
|
|
||||||
endif
|
|
||||||
|
|
||||||
build-libiconv: clean-libiconv
|
|
||||||
@cd vendor/libiconv/libiconv-1.17 && \
|
|
||||||
./configure --prefix=$(ICONV) --enable-static && \
|
|
||||||
make && make install
|
|
||||||
|
|
||||||
install-libiconv: download-libiconv build-libiconv
|
|
||||||
|
|
||||||
clean-libiconv:
|
|
||||||
ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","")
|
|
||||||
@cd vendor/libiconv/libiconv-1.17 && \
|
|
||||||
make clean
|
|
||||||
endif
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
.PHONY: _build_mimalloc
|
|
||||||
|
|
||||||
MIMALLOC := $(BC)vendor/mimalloc/out/$(OS)-$(ARCH)
|
|
||||||
_build_mimalloc: clean-mimalloc
|
|
||||||
@mkdir -p $(MIMALLOC)/build && \
|
|
||||||
cd $(MIMALLOC)/build && \
|
|
||||||
cmake -DMI_BUILD_SHARED=OFF -DMI_BUILD_OBJECT=OFF -DMI_BUILD_TESTS=OFF -DMI_OVERRIDE=OFF $(OPTS) ../../.. && \
|
|
||||||
make && \
|
|
||||||
mkdir -p $(MIMALLOC)/lib
|
|
||||||
|
|
||||||
install-mimalloc-dev: _build_mimalloc
|
|
||||||
install-mimalloc-dev: OPTS=-DCMAKE_BUILD_TYPE=Debug
|
|
||||||
install-mimalloc-dev:
|
|
||||||
@cd $(MIMALLOC) && \
|
|
||||||
mv build/libmimalloc-debug.a lib/libmimalloc.a
|
|
||||||
|
|
||||||
install-mimalloc: _build_mimalloc
|
|
||||||
install-mimalloc:
|
|
||||||
@cd $(MIMALLOC) && \
|
|
||||||
mv build/libmimalloc.a lib/libmimalloc.a
|
|
||||||
|
|
||||||
clean-mimalloc:
|
|
||||||
@rm -Rf $(MIMALLOC)/build
|
|
||||||
|
|
||||||
## Init and update git submodule
|
|
||||||
install-submodule:
|
|
||||||
@git submodule init && \
|
|
||||||
git submodule update
|
|
||||||
|
|||||||
204
README.md
204
README.md
@@ -18,7 +18,7 @@ Lightpanda is the open-source browser made for headless usage:
|
|||||||
|
|
||||||
- Javascript execution
|
- Javascript execution
|
||||||
- Support of Web APIs (partial, WIP)
|
- Support of Web APIs (partial, WIP)
|
||||||
- Compatible with Playwright[^1], Puppeteer, chromedp through CDP
|
- Compatible with Playwright[^1], Puppeteer, chromedp through [CDP](https://chromedevtools.github.io/devtools-protocol/)
|
||||||
|
|
||||||
Fast web automation for AI agents, LLM training, scraping and testing:
|
Fast web automation for AI agents, LLM training, scraping and testing:
|
||||||
|
|
||||||
@@ -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 => {
|
||||||
@@ -140,13 +166,14 @@ You may still encounter errors or crashes. Please open an issue with specifics i
|
|||||||
|
|
||||||
Here are the key features we have implemented:
|
Here are the key features we have implemented:
|
||||||
|
|
||||||
- [x] HTTP loader (based on Libcurl)
|
- [x] HTTP loader ([Libcurl](https://curl.se/libcurl/))
|
||||||
- [x] HTML parser and DOM tree (based on Netsurf libs)
|
- [x] HTML parser ([html5ever](https://github.com/servo/html5ever))
|
||||||
- [x] Javascript support (v8)
|
- [x] DOM tree
|
||||||
|
- [x] Javascript support ([v8](https://v8.dev/))
|
||||||
- [x] DOM APIs
|
- [x] DOM APIs
|
||||||
- [x] Ajax
|
- [x] Ajax
|
||||||
- [x] XHR API
|
- [x] XHR API
|
||||||
- [x] Fetch API (polyfill)
|
- [x] Fetch API
|
||||||
- [x] DOM dump
|
- [x] DOM dump
|
||||||
- [x] CDP/websockets server
|
- [x] CDP/websockets server
|
||||||
- [x] Click
|
- [x] Click
|
||||||
@@ -155,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.
|
||||||
|
|
||||||
@@ -164,103 +192,57 @@ You can also follow the progress of our Javascript support in our dedicated [zig
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.1`. You have to
|
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.2`. You have to
|
||||||
install it with the right version in order to build the project.
|
install it with the right version in order to build the project.
|
||||||
|
|
||||||
Lightpanda also depends on
|
Lightpanda also depends on
|
||||||
[zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime/) (with v8),
|
[zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime/) (with v8),
|
||||||
[Libcurl](https://curl.se/libcurl/),
|
[Libcurl](https://curl.se/libcurl/) and [html5ever](https://github.com/servo/html5ever).
|
||||||
[Netsurf libs](https://www.netsurf-browser.org/) and
|
|
||||||
[Mimalloc](https://microsoft.github.io/mimalloc).
|
|
||||||
|
|
||||||
To be able to build the v8 engine for zig-js-runtime, you have to install some libs:
|
To be able to build the v8 engine for zig-js-runtime, you have to install some libs:
|
||||||
|
|
||||||
For Debian/Ubuntu based Linux:
|
For **Debian/Ubuntu based Linux**:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo apt install xz-utils \
|
sudo apt install xz-utils ca-certificates \
|
||||||
python3 ca-certificates git \
|
|
||||||
pkg-config libglib2.0-dev \
|
pkg-config libglib2.0-dev \
|
||||||
gperf libexpat1-dev unzip rsync \
|
clang make curl git
|
||||||
cmake clang
|
|
||||||
```
|
```
|
||||||
|
You also need to [install Rust](https://rust-lang.org/tools/install/).
|
||||||
|
|
||||||
For systems with [Nix](https://nixos.org/download/), you can use the devShell:
|
For systems with [**Nix**](https://nixos.org/download/), you can use the devShell:
|
||||||
```
|
```
|
||||||
nix develop
|
nix develop
|
||||||
```
|
```
|
||||||
|
|
||||||
For MacOS, you only need cmake:
|
For **MacOS**, you need cmake and [Rust](https://rust-lang.org/tools/install/).
|
||||||
|
|
||||||
```
|
```
|
||||||
brew install cmake
|
brew install cmake
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install and build dependencies
|
### Build and run
|
||||||
|
|
||||||
#### All in one build
|
You an build the entire browser with `make build` or `make build-dev` for debug
|
||||||
|
env.
|
||||||
|
|
||||||
You can run `make install` to install deps all in one (or `make install-dev` if you need the development versions).
|
But you can directly use the zig command: `zig build run`.
|
||||||
|
|
||||||
Be aware that the build task is very long and cpu consuming, as you will build from sources all dependencies, including the v8 Javascript engine.
|
#### Embed v8 snapshot
|
||||||
|
|
||||||
#### Step by step build dependency
|
Lighpanda uses v8 snapshot. By default, it is created on startup but you can
|
||||||
|
embed it by using the following commands:
|
||||||
The project uses git submodules for dependencies.
|
|
||||||
|
|
||||||
To init or update the submodules in the `vendor/` directory:
|
|
||||||
|
|
||||||
|
Generate the snapshot.
|
||||||
```
|
```
|
||||||
make install-submodule
|
zig build snapshot_creator -- src/snapshot.bin
|
||||||
```
|
```
|
||||||
|
|
||||||
**iconv**
|
Build using the snapshot binary.
|
||||||
|
|
||||||
libiconv is an internationalization library used by Netsurf.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
make install-libiconv
|
zig build -Dsnapshot_path=../../snapshot.bin
|
||||||
```
|
```
|
||||||
|
|
||||||
**Netsurf libs**
|
See [#1279](https://github.com/lightpanda-io/browser/pull/1279) for more details.
|
||||||
|
|
||||||
Netsurf libs are used for HTML parsing and DOM tree generation.
|
|
||||||
|
|
||||||
```
|
|
||||||
make install-netsurf
|
|
||||||
```
|
|
||||||
|
|
||||||
For dev env, use `make install-netsurf-dev`.
|
|
||||||
|
|
||||||
**Mimalloc**
|
|
||||||
|
|
||||||
Mimalloc is used as a C memory allocator.
|
|
||||||
|
|
||||||
```
|
|
||||||
make install-mimalloc
|
|
||||||
```
|
|
||||||
|
|
||||||
For dev env, use `make install-mimalloc-dev`.
|
|
||||||
|
|
||||||
Note: when Mimalloc is built in dev mode, you can dump memory stats with the
|
|
||||||
env var `MIMALLOC_SHOW_STATS=1`. See
|
|
||||||
[https://microsoft.github.io/mimalloc/environment.html](https://microsoft.github.io/mimalloc/environment.html).
|
|
||||||
|
|
||||||
**v8**
|
|
||||||
|
|
||||||
First, get the tools necessary for building V8, as well as the V8 source code:
|
|
||||||
|
|
||||||
```
|
|
||||||
make get-v8
|
|
||||||
```
|
|
||||||
|
|
||||||
Next, build v8. This build task is very long and cpu consuming, as you will build v8 from sources.
|
|
||||||
|
|
||||||
```
|
|
||||||
make build-v8
|
|
||||||
```
|
|
||||||
|
|
||||||
For dev env, use `make build-v8-dev`.
|
|
||||||
|
|
||||||
## Test
|
## Test
|
||||||
|
|
||||||
@@ -287,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,13 +1,36 @@
|
|||||||
.{
|
.{
|
||||||
.name = .browser,
|
.name = .browser,
|
||||||
.paths = .{""},
|
|
||||||
.version = "0.0.0",
|
.version = "0.0.0",
|
||||||
.fingerprint = 0xda130f3af836cea0,
|
.fingerprint = 0xda130f3af836cea0, // Changing this has security and trust implications.
|
||||||
|
.minimum_zig_version = "0.15.2",
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
.v8 = .{
|
.v8 = .{
|
||||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/305bb3706716d32d59b2ffa674731556caa1002b.tar.gz",
|
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.1.tar.gz",
|
||||||
.hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5",
|
.hash = "v8-0.0.0-xddH64J7BAC81mkf6G9RbEJxS-W3TIRl5iFnShwbqCqy",
|
||||||
|
|
||||||
},
|
},
|
||||||
//.v8 = .{ .path = "../zig-v8-fork" }
|
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||||
|
.brotli = .{
|
||||||
|
// v1.2.0
|
||||||
|
.url = "https://github.com/google/brotli/archive/028fb5a23661f123017c060daa546b55cf4bde29.tar.gz",
|
||||||
|
.hash = "N-V-__8AAJudKgCQCuIiH6MJjAiIJHfg_tT_Ew-0vZwVkCo_",
|
||||||
},
|
},
|
||||||
|
.zlib = .{
|
||||||
|
.url = "https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz",
|
||||||
|
.hash = "N-V-__8AAJ2cNgAgfBtAw33Bxfu1IWISDeKKSr3DAqoAysIJ",
|
||||||
|
},
|
||||||
|
.nghttp2 = .{
|
||||||
|
.url = "https://github.com/nghttp2/nghttp2/releases/download/v1.68.0/nghttp2-1.68.0.tar.gz",
|
||||||
|
.hash = "N-V-__8AAL15vQCI63ZL6Zaz5hJg6JTEgYXGbLnMFSnf7FT3",
|
||||||
|
},
|
||||||
|
.@"boringssl-zig" = .{
|
||||||
|
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
|
||||||
|
.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 = .{""},
|
||||||
}
|
}
|
||||||
|
|||||||
51
flake.lock
generated
51
flake.lock
generated
@@ -1,5 +1,26 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"fenix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1770708269,
|
||||||
|
"narHash": "sha256-OnZW86app7hHJJoB5lC9GNXY5QBBIESJB+sIdwEyld0=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"rev": "6b5325a017a9a9fe7e6252ccac3680cc7181cd63",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"flake-compat": {
|
"flake-compat": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
@@ -75,11 +96,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1756822655,
|
"lastModified": 1768649915,
|
||||||
"narHash": "sha256-xQAk8xLy7srAkR5NMZFsQFioL02iTHuuEIs3ohGpgdk=",
|
"narHash": "sha256-jc21hKogFnxU7KXSVTRmxC7u5D4RHwm9BAvDf5/Z1Uo=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "4bdac60bfe32c41103ae500ddf894c258291dd61",
|
"rev": "3e3f3c7f9977dc123c23ee21e8085ed63daf8c37",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -91,12 +112,30 @@
|
|||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"fenix": "fenix",
|
||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"zigPkgs": "zigPkgs",
|
"zigPkgs": "zigPkgs",
|
||||||
"zlsPkg": "zlsPkg"
|
"zlsPkg": "zlsPkg"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"rust-analyzer-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1770668050,
|
||||||
|
"narHash": "sha256-Q05yaIZtQrBKHpyWaPmyJmDRj0lojnVf8nUFE0vydcY=",
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"rev": "9efc1f709f3c8134c3acac5d3592a8e4c184a0c6",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"ref": "nightly",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"systems": {
|
"systems": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1681028828,
|
"lastModified": 1681028828,
|
||||||
@@ -136,11 +175,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1756555914,
|
"lastModified": 1770598090,
|
||||||
"narHash": "sha256-7yoSPIVEuL+3Wzf6e7NHuW3zmruHizRrYhGerjRHTLI=",
|
"narHash": "sha256-k+82IDgTd9o5sxHIqGlvfwseKln3Ejx1edGtDltuPXo=",
|
||||||
"owner": "mitchellh",
|
"owner": "mitchellh",
|
||||||
"repo": "zig-overlay",
|
"repo": "zig-overlay",
|
||||||
"rev": "d0df3a2fd0f11134409d6d5ea0e510e5e477f7d6",
|
"rev": "142495696982c88edddc8e17e4da90d8164acadf",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
12
flake.nix
12
flake.nix
@@ -11,6 +11,11 @@
|
|||||||
zlsPkg.inputs.zig-overlay.follows = "zigPkgs";
|
zlsPkg.inputs.zig-overlay.follows = "zigPkgs";
|
||||||
zlsPkg.inputs.nixpkgs.follows = "nixpkgs";
|
zlsPkg.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
|
||||||
|
fenix = {
|
||||||
|
url = "github:nix-community/fenix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -19,6 +24,7 @@
|
|||||||
nixpkgs,
|
nixpkgs,
|
||||||
zigPkgs,
|
zigPkgs,
|
||||||
zlsPkg,
|
zlsPkg,
|
||||||
|
fenix,
|
||||||
flake-utils,
|
flake-utils,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
@@ -36,6 +42,8 @@
|
|||||||
inherit system overlays;
|
inherit system overlays;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
rustToolchain = fenix.packages.${system}.stable.toolchain;
|
||||||
|
|
||||||
# We need crtbeginS.o for building.
|
# We need crtbeginS.o for building.
|
||||||
crtFiles = pkgs.runCommand "crt-files" { } ''
|
crtFiles = pkgs.runCommand "crt-files" { } ''
|
||||||
mkdir -p $out/lib
|
mkdir -p $out/lib
|
||||||
@@ -49,8 +57,9 @@
|
|||||||
targetPkgs =
|
targetPkgs =
|
||||||
pkgs: with pkgs; [
|
pkgs: with pkgs; [
|
||||||
# Build Tools
|
# Build Tools
|
||||||
zigpkgs."0.15.1"
|
zigpkgs."0.15.2"
|
||||||
zls
|
zls
|
||||||
|
rustToolchain
|
||||||
python3
|
python3
|
||||||
pkg-config
|
pkg-config
|
||||||
cmake
|
cmake
|
||||||
@@ -66,7 +75,6 @@
|
|||||||
glib.dev
|
glib.dev
|
||||||
glibc.dev
|
glibc.dev
|
||||||
zlib
|
zlib
|
||||||
zlib.dev
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
|||||||
113
src/App.zig
Normal file
113
src/App.zig
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
// 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 log = @import("log.zig");
|
||||||
|
const Config = @import("Config.zig");
|
||||||
|
const Snapshot = @import("browser/js/Snapshot.zig");
|
||||||
|
const Platform = @import("browser/js/Platform.zig");
|
||||||
|
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
||||||
|
const RobotStore = @import("browser/Robots.zig").RobotStore;
|
||||||
|
|
||||||
|
pub const Http = @import("http/Http.zig");
|
||||||
|
pub const ArenaPool = @import("ArenaPool.zig");
|
||||||
|
|
||||||
|
const App = @This();
|
||||||
|
|
||||||
|
http: Http,
|
||||||
|
config: *const Config,
|
||||||
|
platform: Platform,
|
||||||
|
snapshot: Snapshot,
|
||||||
|
telemetry: Telemetry,
|
||||||
|
allocator: Allocator,
|
||||||
|
arena_pool: ArenaPool,
|
||||||
|
robots: RobotStore,
|
||||||
|
app_dir_path: ?[]const u8,
|
||||||
|
shutdown: bool = false,
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, config: *const Config) !*App {
|
||||||
|
const app = try allocator.create(App);
|
||||||
|
errdefer allocator.destroy(app);
|
||||||
|
|
||||||
|
app.config = config;
|
||||||
|
app.allocator = allocator;
|
||||||
|
|
||||||
|
app.robots = RobotStore.init(allocator);
|
||||||
|
|
||||||
|
app.http = try Http.init(allocator, &app.robots, config);
|
||||||
|
errdefer app.http.deinit();
|
||||||
|
|
||||||
|
app.platform = try Platform.init();
|
||||||
|
errdefer app.platform.deinit();
|
||||||
|
|
||||||
|
app.snapshot = try Snapshot.load();
|
||||||
|
errdefer app.snapshot.deinit();
|
||||||
|
|
||||||
|
app.app_dir_path = getAndMakeAppDir(allocator);
|
||||||
|
|
||||||
|
app.telemetry = try Telemetry.init(app, config.mode);
|
||||||
|
errdefer app.telemetry.deinit();
|
||||||
|
|
||||||
|
app.arena_pool = ArenaPool.init(allocator, 512, 1024 * 16);
|
||||||
|
errdefer app.arena_pool.deinit();
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *App) void {
|
||||||
|
if (@atomicRmw(bool, &self.shutdown, .Xchg, true, .monotonic)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allocator = self.allocator;
|
||||||
|
if (self.app_dir_path) |app_dir_path| {
|
||||||
|
allocator.free(app_dir_path);
|
||||||
|
self.app_dir_path = null;
|
||||||
|
}
|
||||||
|
self.telemetry.deinit();
|
||||||
|
self.robots.deinit();
|
||||||
|
self.http.deinit();
|
||||||
|
self.snapshot.deinit();
|
||||||
|
self.platform.deinit();
|
||||||
|
self.arena_pool.deinit();
|
||||||
|
|
||||||
|
allocator.destroy(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getAndMakeAppDir(allocator: Allocator) ?[]const u8 {
|
||||||
|
if (@import("builtin").is_test) {
|
||||||
|
return allocator.dupe(u8, "/tmp") catch unreachable;
|
||||||
|
}
|
||||||
|
const app_dir_path = std.fs.getAppDataDir(allocator, "lightpanda") catch |err| {
|
||||||
|
log.warn(.app, "get data dir", .{ .err = err });
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
std.fs.cwd().makePath(app_dir_path) catch |err| switch (err) {
|
||||||
|
error.PathAlreadyExists => return app_dir_path,
|
||||||
|
else => {
|
||||||
|
allocator.free(app_dir_path);
|
||||||
|
log.warn(.app, "create data dir", .{ .err = err, .path = app_dir_path });
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return app_dir_path;
|
||||||
|
}
|
||||||
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();
|
||||||
|
}
|
||||||
849
src/Config.zig
Normal file
849
src/Config.zig
Normal file
@@ -0,0 +1,849 @@
|
|||||||
|
// 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");
|
||||||
|
|
||||||
|
pub const RunMode = enum {
|
||||||
|
help,
|
||||||
|
fetch,
|
||||||
|
serve,
|
||||||
|
version,
|
||||||
|
mcp,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const CDP_MAX_HTTP_REQUEST_SIZE = 4096;
|
||||||
|
|
||||||
|
// max message size
|
||||||
|
// +14 for max websocket payload overhead
|
||||||
|
// +140 for the max control packet that might be interleaved in a message
|
||||||
|
pub const CDP_MAX_MESSAGE_SIZE = 512 * 1024 + 14 + 140;
|
||||||
|
|
||||||
|
mode: Mode,
|
||||||
|
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 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,
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
\\
|
||||||
|
;
|
||||||
|
|
||||||
|
// 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' or 'markdown'.
|
||||||
|
\\ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
419
src/Notification.zig
Normal file
419
src/Notification.zig
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const lp = @import("lightpanda");
|
||||||
|
|
||||||
|
const log = @import("log.zig");
|
||||||
|
const Page = @import("browser/Page.zig");
|
||||||
|
const Transfer = @import("http/Client.zig").Transfer;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const List = std.DoublyLinkedList;
|
||||||
|
|
||||||
|
// Allows code to register for and emit events.
|
||||||
|
// Keeps two lists
|
||||||
|
// 1 - for a given event type, a linked list of all the listeners
|
||||||
|
// 2 - for a given listener, a list of all it's registration
|
||||||
|
// The 2nd one is so that a listener can unregister all of it's listeners
|
||||||
|
// (there's currently no need for a listener to unregister only 1 or more
|
||||||
|
// specific listener).
|
||||||
|
//
|
||||||
|
// Scoping is important. Imagine we created a global singleton registry, and our
|
||||||
|
// 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
|
||||||
|
// emit a "network_bytes_sent" message. It would be easy, and it would work.
|
||||||
|
// That is, it would work until multiple CDP clients connect, and because
|
||||||
|
// everything's just one big global, events from one CDP session would be sent
|
||||||
|
// to all CDP clients.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// "scope". This would have a run-time cost and still require some coordination
|
||||||
|
// between components to share a common scope.
|
||||||
|
//
|
||||||
|
// Instead, the approach that we take is to have a notification instance per
|
||||||
|
// CDP connection (BrowserContext). Each CDP connection has its own notification
|
||||||
|
// that is shared across all Sessions (tabs) within that connection. This ensures
|
||||||
|
// proper isolation between different CDP clients while allowing a single client
|
||||||
|
// to receive events from all its tabs.
|
||||||
|
const Notification = @This();
|
||||||
|
// Every event type (which are hard-coded), has a list of Listeners.
|
||||||
|
// When the event happens, we dispatch to those listener.
|
||||||
|
event_listeners: EventListeners,
|
||||||
|
|
||||||
|
// list of listeners for a specified receiver
|
||||||
|
// @intFromPtr(receiver) -> [listener1, listener2, ...]
|
||||||
|
// Used when `unregisterAll` is called.
|
||||||
|
listeners: std.AutoHashMapUnmanaged(usize, std.ArrayList(*Listener)),
|
||||||
|
|
||||||
|
allocator: Allocator,
|
||||||
|
mem_pool: std.heap.MemoryPool(Listener),
|
||||||
|
|
||||||
|
const EventListeners = struct {
|
||||||
|
page_remove: List = .{},
|
||||||
|
page_created: List = .{},
|
||||||
|
page_navigate: List = .{},
|
||||||
|
page_navigated: List = .{},
|
||||||
|
page_network_idle: List = .{},
|
||||||
|
page_network_almost_idle: List = .{},
|
||||||
|
page_frame_created: List = .{},
|
||||||
|
http_request_fail: List = .{},
|
||||||
|
http_request_start: List = .{},
|
||||||
|
http_request_intercept: List = .{},
|
||||||
|
http_request_done: List = .{},
|
||||||
|
http_request_auth_required: List = .{},
|
||||||
|
http_response_data: List = .{},
|
||||||
|
http_response_header_done: List = .{},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Events = union(enum) {
|
||||||
|
page_remove: PageRemove,
|
||||||
|
page_created: *Page,
|
||||||
|
page_navigate: *const PageNavigate,
|
||||||
|
page_navigated: *const PageNavigated,
|
||||||
|
page_network_idle: *const PageNetworkIdle,
|
||||||
|
page_network_almost_idle: *const PageNetworkAlmostIdle,
|
||||||
|
page_frame_created: *const PageFrameCreated,
|
||||||
|
http_request_fail: *const RequestFail,
|
||||||
|
http_request_start: *const RequestStart,
|
||||||
|
http_request_intercept: *const RequestIntercept,
|
||||||
|
http_request_auth_required: *const RequestAuthRequired,
|
||||||
|
http_request_done: *const RequestDone,
|
||||||
|
http_response_data: *const ResponseData,
|
||||||
|
http_response_header_done: *const ResponseHeaderDone,
|
||||||
|
};
|
||||||
|
const EventType = std.meta.FieldEnum(Events);
|
||||||
|
|
||||||
|
pub const PageRemove = struct {};
|
||||||
|
|
||||||
|
pub const PageNavigate = struct {
|
||||||
|
req_id: u32,
|
||||||
|
frame_id: u32,
|
||||||
|
timestamp: u64,
|
||||||
|
url: [:0]const u8,
|
||||||
|
opts: Page.NavigateOpts,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PageNavigated = struct {
|
||||||
|
req_id: u32,
|
||||||
|
frame_id: u32,
|
||||||
|
timestamp: u64,
|
||||||
|
url: [:0]const u8,
|
||||||
|
opts: Page.NavigatedOpts,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PageNetworkIdle = struct {
|
||||||
|
req_id: u32,
|
||||||
|
frame_id: u32,
|
||||||
|
timestamp: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PageNetworkAlmostIdle = struct {
|
||||||
|
req_id: u32,
|
||||||
|
frame_id: u32,
|
||||||
|
timestamp: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PageFrameCreated = struct {
|
||||||
|
frame_id: u32,
|
||||||
|
parent_id: u32,
|
||||||
|
timestamp: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RequestStart = struct {
|
||||||
|
transfer: *Transfer,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RequestIntercept = struct {
|
||||||
|
transfer: *Transfer,
|
||||||
|
wait_for_interception: *bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RequestAuthRequired = struct {
|
||||||
|
transfer: *Transfer,
|
||||||
|
wait_for_interception: *bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ResponseData = struct {
|
||||||
|
data: []const u8,
|
||||||
|
transfer: *Transfer,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ResponseHeaderDone = struct {
|
||||||
|
transfer: *Transfer,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RequestDone = struct {
|
||||||
|
transfer: *Transfer,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RequestFail = struct {
|
||||||
|
transfer: *Transfer,
|
||||||
|
err: anyerror,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator) !*Notification {
|
||||||
|
const notification = try allocator.create(Notification);
|
||||||
|
errdefer allocator.destroy(notification);
|
||||||
|
|
||||||
|
notification.* = .{
|
||||||
|
.listeners = .{},
|
||||||
|
.event_listeners = .{},
|
||||||
|
.allocator = allocator,
|
||||||
|
.mem_pool = std.heap.MemoryPool(Listener).init(allocator),
|
||||||
|
};
|
||||||
|
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Notification) void {
|
||||||
|
const allocator = self.allocator;
|
||||||
|
|
||||||
|
var it = self.listeners.valueIterator();
|
||||||
|
while (it.next()) |listener| {
|
||||||
|
listener.deinit(allocator);
|
||||||
|
}
|
||||||
|
self.listeners.deinit(allocator);
|
||||||
|
self.mem_pool.deinit();
|
||||||
|
allocator.destroy(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(self: *Notification, comptime event: EventType, receiver: anytype, func: EventFunc(event)) !void {
|
||||||
|
var list = &@field(self.event_listeners, @tagName(event));
|
||||||
|
|
||||||
|
var listener = try self.mem_pool.create();
|
||||||
|
errdefer self.mem_pool.destroy(listener);
|
||||||
|
|
||||||
|
listener.* = .{
|
||||||
|
.node = .{},
|
||||||
|
.list = list,
|
||||||
|
.receiver = receiver,
|
||||||
|
.event = event,
|
||||||
|
.func = @ptrCast(func),
|
||||||
|
.struct_name = @typeName(@typeInfo(@TypeOf(receiver)).pointer.child),
|
||||||
|
};
|
||||||
|
|
||||||
|
const allocator = self.allocator;
|
||||||
|
const gop = try self.listeners.getOrPut(allocator, @intFromPtr(receiver));
|
||||||
|
if (gop.found_existing == false) {
|
||||||
|
gop.value_ptr.* = .{};
|
||||||
|
}
|
||||||
|
try gop.value_ptr.append(allocator, listener);
|
||||||
|
|
||||||
|
// we don't add this until we've successfully added the entry to
|
||||||
|
// self.listeners
|
||||||
|
list.append(&listener.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unregister(self: *Notification, comptime event: EventType, receiver: anytype) void {
|
||||||
|
var listeners = self.listeners.getPtr(@intFromPtr(receiver)) orelse return;
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < listeners.items.len) {
|
||||||
|
const listener = listeners.items[i];
|
||||||
|
if (listener.event != event) {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
listener.list.remove(&listener.node);
|
||||||
|
self.mem_pool.destroy(listener);
|
||||||
|
_ = listeners.swapRemove(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listeners.items.len == 0) {
|
||||||
|
listeners.deinit(self.allocator);
|
||||||
|
const removed = self.listeners.remove(@intFromPtr(receiver));
|
||||||
|
lp.assert(removed == true, "Notification.unregister", .{ .type = event });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unregisterAll(self: *Notification, receiver: *anyopaque) void {
|
||||||
|
var kv = self.listeners.fetchRemove(@intFromPtr(receiver)) orelse return;
|
||||||
|
for (kv.value.items) |listener| {
|
||||||
|
listener.list.remove(&listener.node);
|
||||||
|
self.mem_pool.destroy(listener);
|
||||||
|
}
|
||||||
|
kv.value.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
var node = list.first;
|
||||||
|
while (node) |n| {
|
||||||
|
const listener: *Listener = @fieldParentPtr("node", n);
|
||||||
|
const func: EventFunc(event) = @ptrCast(@alignCast(listener.func));
|
||||||
|
func(listener.receiver, data) catch |err| {
|
||||||
|
log.err(.app, "dispatch error", .{
|
||||||
|
.err = err,
|
||||||
|
.event = event,
|
||||||
|
.source = "notification",
|
||||||
|
.listener = listener.struct_name,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
node = n.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given an event type enum, returns the type of arg the event emits
|
||||||
|
fn ArgType(comptime event: Notification.EventType) type {
|
||||||
|
inline for (std.meta.fields(Notification.Events)) |f| {
|
||||||
|
if (std.mem.eql(u8, f.name, @tagName(event))) {
|
||||||
|
return f.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given an event type enum, returns the listening function type
|
||||||
|
fn EventFunc(comptime event: Notification.EventType) type {
|
||||||
|
return *const fn (*anyopaque, ArgType(event)) anyerror!void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A listener. This is 1 receiver, with its function, and the linked list
|
||||||
|
// node that goes in the appropriate EventListeners list.
|
||||||
|
const Listener = struct {
|
||||||
|
// the receiver of the event, i.e. the self parameter to `func`
|
||||||
|
receiver: *anyopaque,
|
||||||
|
|
||||||
|
// the function to call
|
||||||
|
func: *const anyopaque,
|
||||||
|
|
||||||
|
// For logging slightly better error
|
||||||
|
struct_name: []const u8,
|
||||||
|
|
||||||
|
event: Notification.EventType,
|
||||||
|
|
||||||
|
// intrusive linked list node
|
||||||
|
node: List.Node,
|
||||||
|
|
||||||
|
// The event list this listener belongs to.
|
||||||
|
// We need this in order to be able to remove the node from the list
|
||||||
|
list: *List,
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = std.testing;
|
||||||
|
test "Notification" {
|
||||||
|
var notifier = try Notification.init(testing.allocator);
|
||||||
|
defer notifier.deinit();
|
||||||
|
|
||||||
|
// noop
|
||||||
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.frame_id = 0,
|
||||||
|
.req_id = 1,
|
||||||
|
.timestamp = 4,
|
||||||
|
.url = undefined,
|
||||||
|
.opts = .{},
|
||||||
|
});
|
||||||
|
|
||||||
|
var tc = TestClient{};
|
||||||
|
|
||||||
|
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
||||||
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.frame_id = 0,
|
||||||
|
.req_id = 1,
|
||||||
|
.timestamp = 4,
|
||||||
|
.url = undefined,
|
||||||
|
.opts = .{},
|
||||||
|
});
|
||||||
|
try testing.expectEqual(4, tc.page_navigate);
|
||||||
|
|
||||||
|
notifier.unregisterAll(&tc);
|
||||||
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.frame_id = 0,
|
||||||
|
.req_id = 1,
|
||||||
|
.timestamp = 10,
|
||||||
|
.url = undefined,
|
||||||
|
.opts = .{},
|
||||||
|
});
|
||||||
|
try testing.expectEqual(4, tc.page_navigate);
|
||||||
|
|
||||||
|
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
||||||
|
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
|
||||||
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.frame_id = 0,
|
||||||
|
.req_id = 1,
|
||||||
|
.timestamp = 10,
|
||||||
|
.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(6, tc.page_navigated);
|
||||||
|
|
||||||
|
notifier.unregisterAll(&tc);
|
||||||
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.frame_id = 0,
|
||||||
|
.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(6, tc.page_navigated);
|
||||||
|
|
||||||
|
{
|
||||||
|
// unregister
|
||||||
|
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
||||||
|
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
|
||||||
|
notifier.dispatch(.page_navigate, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .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(1006, tc.page_navigated);
|
||||||
|
|
||||||
|
notifier.unregister(.page_navigate, &tc);
|
||||||
|
notifier.dispatch(.page_navigate, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .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(2006, tc.page_navigated);
|
||||||
|
|
||||||
|
notifier.unregister(.page_navigated, &tc);
|
||||||
|
notifier.dispatch(.page_navigate, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .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(2006, tc.page_navigated);
|
||||||
|
|
||||||
|
// already unregistered, try anyways
|
||||||
|
notifier.unregister(.page_navigated, &tc);
|
||||||
|
notifier.dispatch(.page_navigate, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .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(2006, tc.page_navigated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TestClient = struct {
|
||||||
|
page_navigate: u64 = 0,
|
||||||
|
page_navigated: u64 = 0,
|
||||||
|
|
||||||
|
fn pageNavigate(ptr: *anyopaque, data: *const Notification.PageNavigate) !void {
|
||||||
|
const self: *TestClient = @ptrCast(@alignCast(ptr));
|
||||||
|
self.page_navigate += data.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pageNavigated(ptr: *anyopaque, data: *const Notification.PageNavigated) !void {
|
||||||
|
const self: *TestClient = @ptrCast(@alignCast(ptr));
|
||||||
|
self.page_navigated += data.timestamp;
|
||||||
|
}
|
||||||
|
};
|
||||||
981
src/Server.zig
Normal file
981
src/Server.zig
Normal file
@@ -0,0 +1,981 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const lp = @import("lightpanda");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
const net = std.net;
|
||||||
|
const posix = std.posix;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
|
const log = @import("log.zig");
|
||||||
|
const App = @import("App.zig");
|
||||||
|
const Config = @import("Config.zig");
|
||||||
|
const CDP = @import("cdp/cdp.zig").CDP;
|
||||||
|
const Net = @import("Net.zig");
|
||||||
|
const Http = @import("http/Http.zig");
|
||||||
|
const HttpClient = @import("http/Client.zig");
|
||||||
|
|
||||||
|
const Server = @This();
|
||||||
|
|
||||||
|
app: *App,
|
||||||
|
shutdown: std.atomic.Value(bool) = .init(false),
|
||||||
|
allocator: Allocator,
|
||||||
|
listener: ?posix.socket_t,
|
||||||
|
json_version_response: []const u8,
|
||||||
|
|
||||||
|
// Thread management
|
||||||
|
active_threads: std.atomic.Value(u32) = .init(0),
|
||||||
|
clients: std.ArrayList(*Client) = .{},
|
||||||
|
client_mutex: std.Thread.Mutex = .{},
|
||||||
|
clients_pool: std.heap.MemoryPool(Client),
|
||||||
|
|
||||||
|
pub fn init(app: *App, address: net.Address) !Server {
|
||||||
|
const allocator = app.allocator;
|
||||||
|
const json_version_response = try buildJSONVersionResponse(allocator, address);
|
||||||
|
errdefer allocator.free(json_version_response);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.app = app,
|
||||||
|
.listener = null,
|
||||||
|
.allocator = allocator,
|
||||||
|
.json_version_response = json_version_response,
|
||||||
|
.clients_pool = std.heap.MemoryPool(Client).init(app.allocator),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interrupts the server so that main can complete normally and call all defer handlers.
|
||||||
|
pub fn stop(self: *Server) void {
|
||||||
|
if (self.shutdown.swap(true, .release)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown all active clients
|
||||||
|
{
|
||||||
|
self.client_mutex.lock();
|
||||||
|
defer self.client_mutex.unlock();
|
||||||
|
for (self.clients.items) |client| {
|
||||||
|
client.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux and BSD/macOS handle canceling a socket blocked on accept differently.
|
||||||
|
// For Linux, we use std.shutdown, which will cause accept to return error.SocketNotListening (EINVAL).
|
||||||
|
// For BSD, shutdown will return an error. Instead we call posix.close, which will result with error.ConnectionAborted (BADF).
|
||||||
|
if (self.listener) |listener| switch (builtin.target.os.tag) {
|
||||||
|
.linux => posix.shutdown(listener, .recv) catch |err| {
|
||||||
|
log.warn(.app, "listener shutdown", .{ .err = err });
|
||||||
|
},
|
||||||
|
.macos, .freebsd, .netbsd, .openbsd => {
|
||||||
|
self.listener = null;
|
||||||
|
posix.close(listener);
|
||||||
|
},
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Server) void {
|
||||||
|
if (!self.shutdown.load(.acquire)) {
|
||||||
|
self.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.joinThreads();
|
||||||
|
if (self.listener) |listener| {
|
||||||
|
posix.close(listener);
|
||||||
|
self.listener = null;
|
||||||
|
}
|
||||||
|
self.clients.deinit(self.allocator);
|
||||||
|
self.clients_pool.deinit();
|
||||||
|
self.allocator.free(self.json_version_response);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(self: *Server, address: net.Address, timeout_ms: u32) !void {
|
||||||
|
const flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC | posix.SOCK.NONBLOCK;
|
||||||
|
const listener = try posix.socket(address.any.family, flags, posix.IPPROTO.TCP);
|
||||||
|
self.listener = listener;
|
||||||
|
|
||||||
|
try posix.setsockopt(listener, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
|
||||||
|
if (@hasDecl(posix.TCP, "NODELAY")) {
|
||||||
|
try posix.setsockopt(listener, posix.IPPROTO.TCP, posix.TCP.NODELAY, &std.mem.toBytes(@as(c_int, 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
try posix.bind(listener, &address.any, address.getOsSockLen());
|
||||||
|
try posix.listen(listener, self.app.config.maxPendingConnections());
|
||||||
|
|
||||||
|
log.info(.app, "server running", .{ .address = address });
|
||||||
|
while (!self.shutdown.load(.acquire)) {
|
||||||
|
const socket = posix.accept(listener, null, null, posix.SOCK.NONBLOCK) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.SocketNotListening, error.ConnectionAborted => {
|
||||||
|
log.info(.app, "server stopped", .{});
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
error.WouldBlock => {
|
||||||
|
std.Thread.sleep(10 * std.time.ns_per_ms);
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
log.err(.app, "CDP accept", .{ .err = err });
|
||||||
|
std.Thread.sleep(std.time.ns_per_s);
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.spawnWorker(socket, timeout_ms) catch |err| {
|
||||||
|
log.err(.app, "CDP spawn", .{ .err = err });
|
||||||
|
posix.close(socket);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleConnection(self: *Server, socket: posix.socket_t, timeout_ms: u32) void {
|
||||||
|
defer posix.close(socket);
|
||||||
|
|
||||||
|
// Client is HUGE (> 512KB) because it has a large read buffer.
|
||||||
|
// V8 crashes if this is on the stack (likely related to its size).
|
||||||
|
const client = self.getClient() catch |err| {
|
||||||
|
log.err(.app, "CDP client create", .{ .err = err });
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer self.releaseClient(client);
|
||||||
|
|
||||||
|
client.* = Client.init(
|
||||||
|
socket,
|
||||||
|
self.allocator,
|
||||||
|
self.app,
|
||||||
|
self.json_version_response,
|
||||||
|
timeout_ms,
|
||||||
|
) catch |err| {
|
||||||
|
log.err(.app, "CDP client init", .{ .err = err });
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
self.registerClient(client);
|
||||||
|
defer self.unregisterClient(client);
|
||||||
|
|
||||||
|
// Check shutdown after registering to avoid missing stop() signal.
|
||||||
|
// If stop() already iterated over clients, this client won't receive stop()
|
||||||
|
// and would block joinThreads() indefinitely.
|
||||||
|
if (self.shutdown.load(.acquire)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getClient(self: *Server) !*Client {
|
||||||
|
self.client_mutex.lock();
|
||||||
|
defer self.client_mutex.unlock();
|
||||||
|
return self.clients_pool.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn releaseClient(self: *Server, client: *Client) void {
|
||||||
|
self.client_mutex.lock();
|
||||||
|
defer self.client_mutex.unlock();
|
||||||
|
self.clients_pool.destroy(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn registerClient(self: *Server, client: *Client) void {
|
||||||
|
self.client_mutex.lock();
|
||||||
|
defer self.client_mutex.unlock();
|
||||||
|
self.clients.append(self.allocator, client) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unregisterClient(self: *Server, client: *Client) void {
|
||||||
|
self.client_mutex.lock();
|
||||||
|
defer self.client_mutex.unlock();
|
||||||
|
for (self.clients.items, 0..) |c, i| {
|
||||||
|
if (c == client) {
|
||||||
|
_ = self.clients.swapRemove(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawnWorker(self: *Server, socket: posix.socket_t, timeout_ms: u32) !void {
|
||||||
|
if (self.shutdown.load(.acquire)) {
|
||||||
|
return error.ShuttingDown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomically increment active_threads only if below max_connections.
|
||||||
|
// Uses CAS loop to avoid race between checking the limit and incrementing.
|
||||||
|
//
|
||||||
|
// cmpxchgWeak may fail for two reasons:
|
||||||
|
// 1. Another thread changed the value (increment or decrement)
|
||||||
|
// 2. Spurious failure on some architectures (e.g. ARM)
|
||||||
|
//
|
||||||
|
// We use Weak instead of Strong because we need a retry loop anyway:
|
||||||
|
// if CAS fails because a thread finished (counter decreased), we should
|
||||||
|
// retry rather than return an error - there may now be room for a new connection.
|
||||||
|
//
|
||||||
|
// On failure, cmpxchgWeak returns the actual value, which we reuse to avoid
|
||||||
|
// an extra load on the next iteration.
|
||||||
|
const max_connections = self.app.config.maxConnections();
|
||||||
|
var current = self.active_threads.load(.monotonic);
|
||||||
|
while (current < max_connections) {
|
||||||
|
current = self.active_threads.cmpxchgWeak(current, current + 1, .monotonic, .monotonic) orelse break;
|
||||||
|
} else {
|
||||||
|
return error.MaxThreadsReached;
|
||||||
|
}
|
||||||
|
errdefer _ = self.active_threads.fetchSub(1, .monotonic);
|
||||||
|
|
||||||
|
const thread = try std.Thread.spawn(.{}, runWorker, .{ self, socket, timeout_ms });
|
||||||
|
thread.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runWorker(self: *Server, socket: posix.socket_t, timeout_ms: u32) void {
|
||||||
|
defer _ = self.active_threads.fetchSub(1, .monotonic);
|
||||||
|
handleConnection(self, socket, timeout_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn joinThreads(self: *Server) void {
|
||||||
|
while (self.active_threads.load(.monotonic) > 0) {
|
||||||
|
std.Thread.sleep(10 * std.time.ns_per_ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle exactly one TCP connection.
|
||||||
|
pub const Client = struct {
|
||||||
|
// The client is initially serving HTTP requests but, under normal circumstances
|
||||||
|
// should eventually be upgraded to a websocket connections
|
||||||
|
mode: union(enum) {
|
||||||
|
http: void,
|
||||||
|
cdp: CDP,
|
||||||
|
},
|
||||||
|
|
||||||
|
allocator: Allocator,
|
||||||
|
app: *App,
|
||||||
|
http: *HttpClient,
|
||||||
|
ws: Net.WsConnection,
|
||||||
|
|
||||||
|
fn init(
|
||||||
|
socket: posix.socket_t,
|
||||||
|
allocator: Allocator,
|
||||||
|
app: *App,
|
||||||
|
json_version_response: []const u8,
|
||||||
|
timeout_ms: u32,
|
||||||
|
) !Client {
|
||||||
|
var ws = try Net.WsConnection.init(socket, allocator, json_version_response, timeout_ms);
|
||||||
|
errdefer ws.deinit();
|
||||||
|
|
||||||
|
if (log.enabled(.app, .info)) {
|
||||||
|
const client_address = ws.getAddress() catch null;
|
||||||
|
log.info(.app, "client connected", .{ .ip = client_address });
|
||||||
|
}
|
||||||
|
|
||||||
|
const http = try app.http.createClient(allocator);
|
||||||
|
errdefer http.deinit();
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.app = app,
|
||||||
|
.http = http,
|
||||||
|
.ws = ws,
|
||||||
|
.mode = .{ .http = {} },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop(self: *Client) void {
|
||||||
|
self.ws.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(self: *Client) void {
|
||||||
|
switch (self.mode) {
|
||||||
|
.cdp => |*cdp| cdp.deinit(),
|
||||||
|
.http => {},
|
||||||
|
}
|
||||||
|
self.ws.deinit();
|
||||||
|
self.http.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(self: *Client) void {
|
||||||
|
const http = self.http;
|
||||||
|
http.cdp_client = .{
|
||||||
|
.socket = self.ws.socket,
|
||||||
|
.ctx = self,
|
||||||
|
.blocking_read_start = Client.blockingReadStart,
|
||||||
|
.blocking_read = Client.blockingRead,
|
||||||
|
.blocking_read_end = Client.blockingReadStop,
|
||||||
|
};
|
||||||
|
defer http.cdp_client = null;
|
||||||
|
|
||||||
|
self.httpLoop(http) catch |err| {
|
||||||
|
log.err(.app, "CDP client loop", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn httpLoop(self: *Client, http: *HttpClient) !void {
|
||||||
|
lp.assert(self.mode == .http, "Client.httpLoop invalid mode", .{});
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const status = http.tick(self.ws.timeout_ms) catch |err| {
|
||||||
|
log.err(.app, "http tick", .{ .err = err });
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (status != .cdp_socket) {
|
||||||
|
log.info(.app, "CDP timeout", .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.readSocket() == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.mode == .cdp) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cdp = &self.mode.cdp;
|
||||||
|
var last_message = timestamp(.monotonic);
|
||||||
|
var ms_remaining = self.ws.timeout_ms;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
switch (cdp.pageWait(ms_remaining)) {
|
||||||
|
.cdp_socket => {
|
||||||
|
if (self.readSocket() == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
last_message = timestamp(.monotonic);
|
||||||
|
ms_remaining = self.ws.timeout_ms;
|
||||||
|
},
|
||||||
|
.no_page => {
|
||||||
|
const status = http.tick(ms_remaining) catch |err| {
|
||||||
|
log.err(.app, "http tick", .{ .err = err });
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (status != .cdp_socket) {
|
||||||
|
log.info(.app, "CDP timeout", .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (self.readSocket() == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
last_message = timestamp(.monotonic);
|
||||||
|
ms_remaining = self.ws.timeout_ms;
|
||||||
|
},
|
||||||
|
.done => {
|
||||||
|
const elapsed = timestamp(.monotonic) - last_message;
|
||||||
|
if (elapsed > ms_remaining) {
|
||||||
|
log.info(.app, "CDP timeout", .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ms_remaining -= @intCast(elapsed);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blockingReadStart(ctx: *anyopaque) bool {
|
||||||
|
const self: *Client = @ptrCast(@alignCast(ctx));
|
||||||
|
self.ws.setBlocking(true) catch |err| {
|
||||||
|
log.warn(.app, "CDP blockingReadStart", .{ .err = err });
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blockingRead(ctx: *anyopaque) bool {
|
||||||
|
const self: *Client = @ptrCast(@alignCast(ctx));
|
||||||
|
return self.readSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blockingReadStop(ctx: *anyopaque) bool {
|
||||||
|
const self: *Client = @ptrCast(@alignCast(ctx));
|
||||||
|
self.ws.setBlocking(false) catch |err| {
|
||||||
|
log.warn(.app, "CDP blockingReadStop", .{ .err = err });
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readSocket(self: *Client) bool {
|
||||||
|
const n = self.ws.read() catch |err| {
|
||||||
|
log.warn(.app, "CDP read", .{ .err = err });
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (n == 0) {
|
||||||
|
log.info(.app, "CDP disconnect", .{});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.processData() catch false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processData(self: *Client) !bool {
|
||||||
|
switch (self.mode) {
|
||||||
|
.cdp => |*cdp| return self.processWebsocketMessage(cdp),
|
||||||
|
.http => return self.processHTTPRequest(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processHTTPRequest(self: *Client) !bool {
|
||||||
|
lp.assert(self.ws.reader.pos == 0, "Client.HTTP pos", .{ .pos = self.ws.reader.pos });
|
||||||
|
const request = self.ws.reader.buf[0..self.ws.reader.len];
|
||||||
|
|
||||||
|
if (request.len > Config.CDP_MAX_HTTP_REQUEST_SIZE) {
|
||||||
|
self.writeHTTPErrorResponse(413, "Request too large");
|
||||||
|
return error.RequestTooLarge;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we're only expecting [body-less] GET requests.
|
||||||
|
if (std.mem.endsWith(u8, request, "\r\n\r\n") == false) {
|
||||||
|
// we need more data, put any more data here
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the next incoming data can go to the front of our buffer
|
||||||
|
defer self.ws.reader.len = 0;
|
||||||
|
return self.handleHTTPRequest(request) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.NotFound => self.writeHTTPErrorResponse(404, "Not found"),
|
||||||
|
error.InvalidRequest => self.writeHTTPErrorResponse(400, "Invalid request"),
|
||||||
|
error.InvalidProtocol => self.writeHTTPErrorResponse(400, "Invalid HTTP protocol"),
|
||||||
|
error.MissingHeaders => self.writeHTTPErrorResponse(400, "Missing required header"),
|
||||||
|
error.InvalidUpgradeHeader => self.writeHTTPErrorResponse(400, "Unsupported upgrade type"),
|
||||||
|
error.InvalidVersionHeader => self.writeHTTPErrorResponse(400, "Invalid websocket version"),
|
||||||
|
error.InvalidConnectionHeader => self.writeHTTPErrorResponse(400, "Invalid connection header"),
|
||||||
|
else => {
|
||||||
|
log.err(.app, "server 500", .{ .err = err, .req = request[0..@min(100, request.len)] });
|
||||||
|
self.writeHTTPErrorResponse(500, "Internal Server Error");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleHTTPRequest(self: *Client, request: []u8) !bool {
|
||||||
|
if (request.len < 18) {
|
||||||
|
// 18 is [generously] the smallest acceptable HTTP request
|
||||||
|
return error.InvalidRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, request[0..4], "GET ") == false) {
|
||||||
|
return error.NotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url_end = std.mem.indexOfScalarPos(u8, request, 4, ' ') orelse {
|
||||||
|
return error.InvalidRequest;
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = request[4..url_end];
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, url, "/")) {
|
||||||
|
try self.upgradeConnection(request);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, url, "/json/version") or std.mem.eql(u8, url, "/json/version/")) {
|
||||||
|
try self.ws.send(self.ws.json_version_response);
|
||||||
|
// Chromedp (a Go driver) does an http request to /json/version
|
||||||
|
// then to / (websocket upgrade) using a different connection.
|
||||||
|
// Since we only allow 1 connection at a time, the 2nd one (the
|
||||||
|
// websocket upgrade) blocks until the first one times out.
|
||||||
|
// We can avoid that by closing the connection. json_version_response
|
||||||
|
// has a Connection: Close header too.
|
||||||
|
self.ws.shutdown();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.NotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upgradeConnection(self: *Client, request: []u8) !void {
|
||||||
|
try self.ws.upgrade(request);
|
||||||
|
self.mode = .{ .cdp = try CDP.init(self.app, self.http, self) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeHTTPErrorResponse(self: *Client, comptime status: u16, comptime body: []const u8) void {
|
||||||
|
self.ws.sendHttpError(status, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processWebsocketMessage(self: *Client, cdp: *CDP) !bool {
|
||||||
|
return self.ws.processMessages(cdp);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sendAllocator(self: *Client) Allocator {
|
||||||
|
return self.ws.send_arena.allocator();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sendJSON(self: *Client, message: anytype, opts: std.json.Stringify.Options) !void {
|
||||||
|
return self.ws.sendJSON(message, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sendJSONRaw(self: *Client, buf: std.ArrayList(u8)) !void {
|
||||||
|
return self.ws.sendJSONRaw(buf);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
// --------
|
||||||
|
|
||||||
|
fn buildJSONVersionResponse(
|
||||||
|
allocator: Allocator,
|
||||||
|
address: net.Address,
|
||||||
|
) ![]const u8 {
|
||||||
|
const body_format = "{{\"webSocketDebuggerUrl\": \"ws://{f}/\"}}";
|
||||||
|
const body_len = std.fmt.count(body_format, .{address});
|
||||||
|
|
||||||
|
// We send a Connection: Close (and actually close the connection)
|
||||||
|
// because chromedp (Go driver) sends a request to /json/version and then
|
||||||
|
// does an upgrade request, on a different connection. Since we only allow
|
||||||
|
// 1 connection at a time, the upgrade connection doesn't proceed until we
|
||||||
|
// timeout the /json/version. So, instead of waiting for that, we just
|
||||||
|
// always close HTTP requests.
|
||||||
|
const response_format =
|
||||||
|
"HTTP/1.1 200 OK\r\n" ++
|
||||||
|
"Content-Length: {d}\r\n" ++
|
||||||
|
"Connection: Close\r\n" ++
|
||||||
|
"Content-Type: application/json; charset=UTF-8\r\n\r\n" ++
|
||||||
|
body_format;
|
||||||
|
return try std.fmt.allocPrint(allocator, response_format, .{ body_len, address });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const timestamp = @import("datetime.zig").timestamp;
|
||||||
|
|
||||||
|
const testing = std.testing;
|
||||||
|
test "server: buildJSONVersionResponse" {
|
||||||
|
const address = try net.Address.parseIp4("127.0.0.1", 9001);
|
||||||
|
const res = try buildJSONVersionResponse(testing.allocator, address);
|
||||||
|
defer testing.allocator.free(res);
|
||||||
|
|
||||||
|
try testing.expectEqualStrings("HTTP/1.1 200 OK\r\n" ++
|
||||||
|
"Content-Length: 48\r\n" ++
|
||||||
|
"Connection: Close\r\n" ++
|
||||||
|
"Content-Type: application/json; charset=UTF-8\r\n\r\n" ++
|
||||||
|
"{\"webSocketDebuggerUrl\": \"ws://127.0.0.1:9001/\"}", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Client: http invalid request" {
|
||||||
|
var c = try createTestClient();
|
||||||
|
defer c.deinit();
|
||||||
|
|
||||||
|
const res = try c.httpRequest("GET /over/9000 HTTP/1.1\r\n" ++ "Header: " ++ ("a" ** 4100) ++ "\r\n\r\n");
|
||||||
|
try testing.expectEqualStrings("HTTP/1.1 413 \r\n" ++
|
||||||
|
"Connection: Close\r\n" ++
|
||||||
|
"Content-Length: 17\r\n\r\n" ++
|
||||||
|
"Request too large", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Client: http invalid handshake" {
|
||||||
|
try assertHTTPError(
|
||||||
|
400,
|
||||||
|
"Invalid request",
|
||||||
|
"\r\n\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
try assertHTTPError(
|
||||||
|
404,
|
||||||
|
"Not found",
|
||||||
|
"GET /over/9000 HTTP/1.1\r\n\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
try assertHTTPError(
|
||||||
|
404,
|
||||||
|
"Not found",
|
||||||
|
"POST / HTTP/1.1\r\n\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
try assertHTTPError(
|
||||||
|
400,
|
||||||
|
"Invalid HTTP protocol",
|
||||||
|
"GET / HTTP/1.0\r\n\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
try assertHTTPError(
|
||||||
|
400,
|
||||||
|
"Missing required header",
|
||||||
|
"GET / HTTP/1.1\r\n\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
try assertHTTPError(
|
||||||
|
400,
|
||||||
|
"Missing required header",
|
||||||
|
"GET / HTTP/1.1\r\nConnection: upgrade\r\n\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
try assertHTTPError(
|
||||||
|
400,
|
||||||
|
"Missing required header",
|
||||||
|
"GET / HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: websocket\r\n\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
try assertHTTPError(
|
||||||
|
400,
|
||||||
|
"Missing required header",
|
||||||
|
"GET / HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: websocket\r\nsec-websocket-version:13\r\n\r\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Client: http valid handshake" {
|
||||||
|
var c = try createTestClient();
|
||||||
|
defer c.deinit();
|
||||||
|
|
||||||
|
const request =
|
||||||
|
"GET / HTTP/1.1\r\n" ++
|
||||||
|
"Connection: upgrade\r\n" ++
|
||||||
|
"Upgrade: websocket\r\n" ++
|
||||||
|
"sec-websocket-version:13\r\n" ++
|
||||||
|
"sec-websocket-key: this is my key\r\n" ++
|
||||||
|
"Custom: Header-Value\r\n\r\n";
|
||||||
|
|
||||||
|
const res = try c.httpRequest(request);
|
||||||
|
try testing.expectEqualStrings("HTTP/1.1 101 Switching Protocols\r\n" ++
|
||||||
|
"Upgrade: websocket\r\n" ++
|
||||||
|
"Connection: upgrade\r\n" ++
|
||||||
|
"Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\r\n\r\n", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Client: read invalid websocket message" {
|
||||||
|
// 131 = 128 (fin) | 3 where 3 isn't a valid type
|
||||||
|
try assertWebSocketError(
|
||||||
|
1002,
|
||||||
|
&.{ 131, 128, 'm', 'a', 's', 'k' },
|
||||||
|
);
|
||||||
|
|
||||||
|
for ([_]u8{ 16, 32, 64 }) |rsv| {
|
||||||
|
// none of the reserve flags should be set
|
||||||
|
try assertWebSocketError(
|
||||||
|
1002,
|
||||||
|
&.{ rsv, 128, 'm', 'a', 's', 'k' },
|
||||||
|
);
|
||||||
|
|
||||||
|
// as a bitmask
|
||||||
|
try assertWebSocketError(
|
||||||
|
1002,
|
||||||
|
&.{ rsv + 4, 128, 'm', 'a', 's', 'k' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// client->server messages must be masked
|
||||||
|
try assertWebSocketError(
|
||||||
|
1002,
|
||||||
|
&.{ 129, 1, 'a' },
|
||||||
|
);
|
||||||
|
|
||||||
|
// control types (ping/ping/close) can't be > 125 bytes
|
||||||
|
for ([_]u8{ 136, 137, 138 }) |op| {
|
||||||
|
try assertWebSocketError(
|
||||||
|
1002,
|
||||||
|
&.{ op, 254, 1, 1 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// length of message is 0000 0810, i.e: 1024 * 512 + 265
|
||||||
|
try assertWebSocketError(1009, &.{ 129, 255, 0, 0, 0, 0, 0, 8, 1, 0, 'm', 'a', 's', 'k' });
|
||||||
|
|
||||||
|
// continuation type message must come after a normal message
|
||||||
|
// even when not a fin frame
|
||||||
|
try assertWebSocketError(
|
||||||
|
1002,
|
||||||
|
&.{ 0, 129, 'm', 'a', 's', 'k', 'd' },
|
||||||
|
);
|
||||||
|
|
||||||
|
// continuation type message must come after a normal message
|
||||||
|
// even as a fin frame
|
||||||
|
try assertWebSocketError(
|
||||||
|
1002,
|
||||||
|
&.{ 128, 129, 'm', 'a', 's', 'k', 'd' },
|
||||||
|
);
|
||||||
|
|
||||||
|
// text (non-fin) - text (non-fin)
|
||||||
|
try assertWebSocketError(
|
||||||
|
1002,
|
||||||
|
&.{ 1, 129, 'm', 'a', 's', 'k', 'd', 1, 128, 'k', 's', 'a', 'm' },
|
||||||
|
);
|
||||||
|
|
||||||
|
// text (non-fin) - text (fin) should always been continuation after non-fin
|
||||||
|
try assertWebSocketError(
|
||||||
|
1002,
|
||||||
|
&.{ 1, 129, 'm', 'a', 's', 'k', 'd', 129, 128, 'k', 's', 'a', 'm' },
|
||||||
|
);
|
||||||
|
|
||||||
|
// close must be fin
|
||||||
|
try assertWebSocketError(
|
||||||
|
1002,
|
||||||
|
&.{
|
||||||
|
8, 129, 'm', 'a', 's', 'k', 'd',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ping must be fin
|
||||||
|
try assertWebSocketError(
|
||||||
|
1002,
|
||||||
|
&.{
|
||||||
|
9, 129, 'm', 'a', 's', 'k', 'd',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// pong must be fin
|
||||||
|
try assertWebSocketError(
|
||||||
|
1002,
|
||||||
|
&.{
|
||||||
|
10, 129, 'm', 'a', 's', 'k', 'd',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Client: ping reply" {
|
||||||
|
try assertWebSocketMessage(
|
||||||
|
// fin | pong, len
|
||||||
|
&.{ 138, 0 },
|
||||||
|
|
||||||
|
// fin | ping, masked | len, 4-byte mask
|
||||||
|
&.{ 137, 128, 0, 0, 0, 0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
try assertWebSocketMessage(
|
||||||
|
// fin | pong, len, payload
|
||||||
|
&.{ 138, 5, 100, 96, 97, 109, 104 },
|
||||||
|
|
||||||
|
// fin | ping, masked | len, 4-byte mask, 5 byte payload
|
||||||
|
&.{ 137, 133, 0, 5, 7, 10, 100, 101, 102, 103, 104 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Client: close message" {
|
||||||
|
try assertWebSocketMessage(
|
||||||
|
// fin | close, len, close code (normal)
|
||||||
|
&.{ 136, 2, 3, 232 },
|
||||||
|
|
||||||
|
// fin | close, masked | len, 4-byte mask
|
||||||
|
&.{ 136, 128, 0, 0, 0, 0 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "server: 404" {
|
||||||
|
var c = try createTestClient();
|
||||||
|
defer c.deinit();
|
||||||
|
|
||||||
|
const res = try c.httpRequest("GET /unknown HTTP/1.1\r\n\r\n");
|
||||||
|
try testing.expectEqualStrings("HTTP/1.1 404 \r\n" ++
|
||||||
|
"Connection: Close\r\n" ++
|
||||||
|
"Content-Length: 9\r\n\r\n" ++
|
||||||
|
"Not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "server: get /json/version" {
|
||||||
|
const expected_response =
|
||||||
|
"HTTP/1.1 200 OK\r\n" ++
|
||||||
|
"Content-Length: 48\r\n" ++
|
||||||
|
"Connection: Close\r\n" ++
|
||||||
|
"Content-Type: application/json; charset=UTF-8\r\n\r\n" ++
|
||||||
|
"{\"webSocketDebuggerUrl\": \"ws://127.0.0.1:9583/\"}";
|
||||||
|
|
||||||
|
{
|
||||||
|
// twice on the same connection
|
||||||
|
var c = try createTestClient();
|
||||||
|
defer c.deinit();
|
||||||
|
|
||||||
|
const res1 = try c.httpRequest("GET /json/version HTTP/1.1\r\n\r\n");
|
||||||
|
try testing.expectEqualStrings(expected_response, res1);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// again on a new connection
|
||||||
|
var c = try createTestClient();
|
||||||
|
defer c.deinit();
|
||||||
|
|
||||||
|
const res1 = try c.httpRequest("GET /json/version HTTP/1.1\r\n\r\n");
|
||||||
|
try testing.expectEqualStrings(expected_response, res1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assertHTTPError(
|
||||||
|
comptime expected_status: u16,
|
||||||
|
comptime expected_body: []const u8,
|
||||||
|
input: []const u8,
|
||||||
|
) !void {
|
||||||
|
var c = try createTestClient();
|
||||||
|
defer c.deinit();
|
||||||
|
|
||||||
|
const res = try c.httpRequest(input);
|
||||||
|
const expected_response = std.fmt.comptimePrint(
|
||||||
|
"HTTP/1.1 {d} \r\nConnection: Close\r\nContent-Length: {d}\r\n\r\n{s}",
|
||||||
|
.{ expected_status, expected_body.len, expected_body },
|
||||||
|
);
|
||||||
|
|
||||||
|
try testing.expectEqualStrings(expected_response, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assertWebSocketError(close_code: u16, input: []const u8) !void {
|
||||||
|
var c = try createTestClient();
|
||||||
|
defer c.deinit();
|
||||||
|
|
||||||
|
try c.handshake();
|
||||||
|
try c.stream.writeAll(input);
|
||||||
|
|
||||||
|
const msg = try c.readWebsocketMessage() orelse return error.NoMessage;
|
||||||
|
defer if (msg.cleanup_fragment) {
|
||||||
|
c.reader.cleanup();
|
||||||
|
};
|
||||||
|
|
||||||
|
try testing.expectEqual(.close, msg.type);
|
||||||
|
try testing.expectEqual(2, msg.data.len);
|
||||||
|
try testing.expectEqual(close_code, std.mem.readInt(u16, msg.data[0..2], .big));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assertWebSocketMessage(expected: []const u8, input: []const u8) !void {
|
||||||
|
var c = try createTestClient();
|
||||||
|
defer c.deinit();
|
||||||
|
|
||||||
|
try c.handshake();
|
||||||
|
try c.stream.writeAll(input);
|
||||||
|
|
||||||
|
const msg = try c.readWebsocketMessage() orelse return error.NoMessage;
|
||||||
|
defer if (msg.cleanup_fragment) {
|
||||||
|
c.reader.cleanup();
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = c.reader.buf[0 .. msg.data.len + 2];
|
||||||
|
try testing.expectEqualSlices(u8, expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MockCDP = struct {
|
||||||
|
messages: std.ArrayList([]const u8) = .{},
|
||||||
|
|
||||||
|
allocator: Allocator = testing.allocator,
|
||||||
|
|
||||||
|
fn init(_: Allocator, client: anytype) MockCDP {
|
||||||
|
_ = client;
|
||||||
|
return .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(self: *MockCDP) void {
|
||||||
|
const allocator = self.allocator;
|
||||||
|
for (self.messages.items) |msg| {
|
||||||
|
allocator.free(msg);
|
||||||
|
}
|
||||||
|
self.messages.deinit(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleMessage(self: *MockCDP, message: []const u8) bool {
|
||||||
|
const owned = self.allocator.dupe(u8, message) catch unreachable;
|
||||||
|
self.messages.append(self.allocator, owned) catch unreachable;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn createTestClient() !TestClient {
|
||||||
|
const address = std.net.Address.initIp4([_]u8{ 127, 0, 0, 1 }, 9583);
|
||||||
|
const stream = try std.net.tcpConnectToAddress(address);
|
||||||
|
|
||||||
|
const timeout = std.mem.toBytes(posix.timeval{
|
||||||
|
.sec = 2,
|
||||||
|
.usec = 0,
|
||||||
|
});
|
||||||
|
try posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.RCVTIMEO, &timeout);
|
||||||
|
try posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout);
|
||||||
|
return .{
|
||||||
|
.stream = stream,
|
||||||
|
.reader = .{
|
||||||
|
.allocator = testing.allocator,
|
||||||
|
.buf = try testing.allocator.alloc(u8, 1024 * 16),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const TestClient = struct {
|
||||||
|
stream: std.net.Stream,
|
||||||
|
buf: [1024]u8 = undefined,
|
||||||
|
reader: Net.Reader(false),
|
||||||
|
|
||||||
|
fn deinit(self: *TestClient) void {
|
||||||
|
self.stream.close();
|
||||||
|
self.reader.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn httpRequest(self: *TestClient, req: []const u8) ![]const u8 {
|
||||||
|
try self.stream.writeAll(req);
|
||||||
|
|
||||||
|
var pos: usize = 0;
|
||||||
|
var total_length: ?usize = null;
|
||||||
|
while (true) {
|
||||||
|
pos += try self.stream.read(self.buf[pos..]);
|
||||||
|
if (pos == 0) {
|
||||||
|
return error.NoMoreData;
|
||||||
|
}
|
||||||
|
const response = self.buf[0..pos];
|
||||||
|
if (total_length == null) {
|
||||||
|
const header_end = std.mem.indexOf(u8, response, "\r\n\r\n") orelse continue;
|
||||||
|
const header = response[0 .. header_end + 4];
|
||||||
|
|
||||||
|
const cl = blk: {
|
||||||
|
const cl_header = "Content-Length: ";
|
||||||
|
const start = (std.mem.indexOf(u8, header, cl_header) orelse {
|
||||||
|
break :blk 0;
|
||||||
|
}) + cl_header.len;
|
||||||
|
|
||||||
|
const end = std.mem.indexOfScalarPos(u8, header, start, '\r') orelse {
|
||||||
|
return error.InvalidContentLength;
|
||||||
|
};
|
||||||
|
|
||||||
|
break :blk std.fmt.parseInt(usize, header[start..end], 10) catch {
|
||||||
|
return error.InvalidContentLength;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
total_length = cl + header.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total_length) |tl| {
|
||||||
|
if (pos == tl) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
if (pos > tl) {
|
||||||
|
return error.DataExceedsContentLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handshake(self: *TestClient) !void {
|
||||||
|
const request =
|
||||||
|
"GET / HTTP/1.1\r\n" ++
|
||||||
|
"Connection: upgrade\r\n" ++
|
||||||
|
"Upgrade: websocket\r\n" ++
|
||||||
|
"sec-websocket-version:13\r\n" ++
|
||||||
|
"sec-websocket-key: this is my key\r\n" ++
|
||||||
|
"Custom: Header-Value\r\n\r\n";
|
||||||
|
|
||||||
|
const res = try self.httpRequest(request);
|
||||||
|
try testing.expectEqualStrings("HTTP/1.1 101 Switching Protocols\r\n" ++
|
||||||
|
"Upgrade: websocket\r\n" ++
|
||||||
|
"Connection: upgrade\r\n" ++
|
||||||
|
"Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\r\n\r\n", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readWebsocketMessage(self: *TestClient) !?Net.Message {
|
||||||
|
while (true) {
|
||||||
|
const n = try self.stream.read(self.reader.readBuf());
|
||||||
|
if (n == 0) {
|
||||||
|
return error.Closed;
|
||||||
|
}
|
||||||
|
self.reader.len += n;
|
||||||
|
if (try self.reader.next()) |msg| {
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
107
src/Sighandler.zig
Normal file
107
src/Sighandler.zig
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
// 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/>.
|
||||||
|
|
||||||
|
//! This structure processes operating system signals (SIGINT, SIGTERM)
|
||||||
|
//! and runs callbacks to clean up the system gracefully.
|
||||||
|
//!
|
||||||
|
//! The structure does not clear the memory allocated in the arena,
|
||||||
|
//! clear the entire arena when exiting the program.
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const lp = @import("lightpanda");
|
||||||
|
|
||||||
|
const log = lp.log;
|
||||||
|
|
||||||
|
const SigHandler = @This();
|
||||||
|
|
||||||
|
arena: Allocator,
|
||||||
|
|
||||||
|
sigset: std.posix.sigset_t = undefined,
|
||||||
|
handle_thread: ?std.Thread = null,
|
||||||
|
|
||||||
|
attempt: u32 = 0,
|
||||||
|
listeners: std.ArrayList(Listener) = .empty,
|
||||||
|
|
||||||
|
pub const Listener = struct {
|
||||||
|
args: []const u8,
|
||||||
|
start: *const fn (context: *const anyopaque) void,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn install(self: *SigHandler) !void {
|
||||||
|
// Block SIGINT and SIGTERM for the current thread and all created from it
|
||||||
|
self.sigset = std.posix.sigemptyset();
|
||||||
|
std.posix.sigaddset(&self.sigset, std.posix.SIG.INT);
|
||||||
|
std.posix.sigaddset(&self.sigset, std.posix.SIG.TERM);
|
||||||
|
std.posix.sigaddset(&self.sigset, std.posix.SIG.QUIT);
|
||||||
|
std.posix.sigprocmask(std.posix.SIG.BLOCK, &self.sigset, null);
|
||||||
|
|
||||||
|
self.handle_thread = try std.Thread.spawn(.{ .allocator = self.arena }, SigHandler.sighandle, .{self});
|
||||||
|
self.handle_thread.?.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on(self: *SigHandler, func: anytype, args: std.meta.ArgsTuple(@TypeOf(func))) !void {
|
||||||
|
assert(@typeInfo(@TypeOf(func)).@"fn".return_type.? == void);
|
||||||
|
|
||||||
|
const Args = @TypeOf(args);
|
||||||
|
const TypeErased = struct {
|
||||||
|
fn start(context: *const anyopaque) void {
|
||||||
|
const args_casted: *const Args = @ptrCast(@alignCast(context));
|
||||||
|
@call(.auto, func, args_casted.*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buffer = try self.arena.alignedAlloc(u8, .of(Args), @sizeOf(Args));
|
||||||
|
errdefer self.arena.free(buffer);
|
||||||
|
|
||||||
|
const bytes: []const u8 = @ptrCast((&args)[0..1]);
|
||||||
|
@memcpy(buffer, bytes);
|
||||||
|
|
||||||
|
try self.listeners.append(self.arena, .{
|
||||||
|
.args = buffer,
|
||||||
|
.start = TypeErased.start,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sighandle(self: *SigHandler) noreturn {
|
||||||
|
while (true) {
|
||||||
|
var sig: c_int = 0;
|
||||||
|
|
||||||
|
const rc = std.c.sigwait(&self.sigset, &sig);
|
||||||
|
if (rc != 0) {
|
||||||
|
log.err(.app, "Unable to process signal {}", .{rc});
|
||||||
|
std.process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (sig) {
|
||||||
|
std.posix.SIG.INT, std.posix.SIG.TERM => {
|
||||||
|
if (self.attempt > 1) {
|
||||||
|
std.process.exit(1);
|
||||||
|
}
|
||||||
|
self.attempt += 1;
|
||||||
|
|
||||||
|
log.info(.app, "Received termination signal...", .{});
|
||||||
|
for (self.listeners.items) |*item| {
|
||||||
|
item.start(item.args.ptr);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
else => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,26 @@
|
|||||||
|
// 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 std = @import("std");
|
||||||
|
|
||||||
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,
|
||||||
|
|
||||||
@@ -10,16 +28,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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,12 +53,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;
|
||||||
@@ -61,6 +87,7 @@ fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !voi
|
|||||||
return err;
|
return err;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
self.handler(&req) catch |err| {
|
self.handler(&req) catch |err| {
|
||||||
std.debug.print("test http error '{s}': {}\n", .{ req.head.target, err });
|
std.debug.print("test http error '{s}': {}\n", .{ req.head.target, err });
|
||||||
try req.respond("server error", .{ .status = .internal_server_error });
|
try req.respond("server error", .{ .status = .internal_server_error });
|
||||||
@@ -113,6 +140,11 @@ fn getContentType(file_path: []const u8) []const u8 {
|
|||||||
return "text/xml";
|
return "text/xml";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (std.mem.endsWith(u8, file_path, ".mjs")) {
|
||||||
|
// mjs are ECMAScript modules
|
||||||
|
return "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
std.debug.print("TestHTTPServer asked to serve an unknown file type: {s}\n", .{file_path});
|
std.debug.print("TestHTTPServer asked to serve an unknown file type: {s}\n", .{file_path});
|
||||||
return "text/html";
|
return "text/html";
|
||||||
}
|
}
|
||||||
|
|||||||
115
src/app.zig
115
src/app.zig
@@ -1,115 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const log = @import("log.zig");
|
|
||||||
const Http = @import("http/Http.zig");
|
|
||||||
const Platform = @import("browser/js/Platform.zig");
|
|
||||||
|
|
||||||
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
|
||||||
const Notification = @import("notification.zig").Notification;
|
|
||||||
|
|
||||||
// Container for global state / objects that various parts of the system
|
|
||||||
// might need.
|
|
||||||
pub const App = struct {
|
|
||||||
http: Http,
|
|
||||||
config: Config,
|
|
||||||
platform: Platform,
|
|
||||||
allocator: Allocator,
|
|
||||||
telemetry: Telemetry,
|
|
||||||
app_dir_path: ?[]const u8,
|
|
||||||
notification: *Notification,
|
|
||||||
|
|
||||||
pub const RunMode = enum {
|
|
||||||
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);
|
|
||||||
errdefer allocator.destroy(app);
|
|
||||||
|
|
||||||
const notification = try Notification.init(allocator, null);
|
|
||||||
errdefer notification.deinit();
|
|
||||||
|
|
||||||
var 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 http.deinit();
|
|
||||||
|
|
||||||
const platform = try Platform.init();
|
|
||||||
errdefer platform.deinit();
|
|
||||||
|
|
||||||
const app_dir_path = getAndMakeAppDir(allocator);
|
|
||||||
|
|
||||||
app.* = .{
|
|
||||||
.http = http,
|
|
||||||
.allocator = allocator,
|
|
||||||
.telemetry = undefined,
|
|
||||||
.platform = platform,
|
|
||||||
.app_dir_path = app_dir_path,
|
|
||||||
.notification = notification,
|
|
||||||
.config = config,
|
|
||||||
};
|
|
||||||
|
|
||||||
app.telemetry = try Telemetry.init(app, config.run_mode);
|
|
||||||
errdefer app.telemetry.deinit();
|
|
||||||
|
|
||||||
try app.telemetry.register(app.notification);
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *App) void {
|
|
||||||
const allocator = self.allocator;
|
|
||||||
if (self.app_dir_path) |app_dir_path| {
|
|
||||||
allocator.free(app_dir_path);
|
|
||||||
}
|
|
||||||
self.telemetry.deinit();
|
|
||||||
self.notification.deinit();
|
|
||||||
self.http.deinit();
|
|
||||||
self.platform.deinit();
|
|
||||||
allocator.destroy(self);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn getAndMakeAppDir(allocator: Allocator) ?[]const u8 {
|
|
||||||
if (@import("builtin").is_test) {
|
|
||||||
return allocator.dupe(u8, "/tmp") catch unreachable;
|
|
||||||
}
|
|
||||||
const app_dir_path = std.fs.getAppDataDir(allocator, "lightpanda") catch |err| {
|
|
||||||
log.warn(.app, "get data dir", .{ .err = err });
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
std.fs.cwd().makePath(app_dir_path) catch |err| switch (err) {
|
|
||||||
error.PathAlreadyExists => return app_dir_path,
|
|
||||||
else => {
|
|
||||||
allocator.free(app_dir_path);
|
|
||||||
log.warn(.app, "create data dir", .{ .err = err, .path = app_dir_path });
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return app_dir_path;
|
|
||||||
}
|
|
||||||
115
src/browser/Browser.zig
Normal file
115
src/browser/Browser.zig
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
// 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 Allocator = std.mem.Allocator;
|
||||||
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
|
const js = @import("js/js.zig");
|
||||||
|
const log = @import("../log.zig");
|
||||||
|
const App = @import("../App.zig");
|
||||||
|
const HttpClient = @import("../http/Client.zig");
|
||||||
|
|
||||||
|
const ArenaPool = App.ArenaPool;
|
||||||
|
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
|
const Session = @import("Session.zig");
|
||||||
|
const Notification = @import("../Notification.zig");
|
||||||
|
|
||||||
|
// Browser is an instance of the browser.
|
||||||
|
// You can create multiple browser instances.
|
||||||
|
// A browser contains only one session.
|
||||||
|
const Browser = @This();
|
||||||
|
|
||||||
|
env: js.Env,
|
||||||
|
app: *App,
|
||||||
|
session: ?Session,
|
||||||
|
allocator: Allocator,
|
||||||
|
arena_pool: *ArenaPool,
|
||||||
|
http_client: *HttpClient,
|
||||||
|
|
||||||
|
const InitOpts = struct {
|
||||||
|
env: js.Env.InitOpts = .{},
|
||||||
|
http_client: *HttpClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(app: *App, opts: InitOpts) !Browser {
|
||||||
|
const allocator = app.allocator;
|
||||||
|
|
||||||
|
var env = try js.Env.init(app, opts.env);
|
||||||
|
errdefer env.deinit();
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.app = app,
|
||||||
|
.env = env,
|
||||||
|
.session = null,
|
||||||
|
.allocator = allocator,
|
||||||
|
.arena_pool = &app.arena_pool,
|
||||||
|
.http_client = opts.http_client,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Browser) void {
|
||||||
|
self.closeSession();
|
||||||
|
self.env.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn newSession(self: *Browser, notification: *Notification) !*Session {
|
||||||
|
self.closeSession();
|
||||||
|
self.session = @as(Session, undefined);
|
||||||
|
const session = &self.session.?;
|
||||||
|
try Session.init(session, self, notification);
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn closeSession(self: *Browser) void {
|
||||||
|
if (self.session) |*session| {
|
||||||
|
session.deinit();
|
||||||
|
self.session = null;
|
||||||
|
self.env.memoryPressureNotification(.critical);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runMicrotasks(self: *Browser) void {
|
||||||
|
self.env.runMicrotasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runMacrotasks(self: *Browser) !?u64 {
|
||||||
|
const env = &self.env;
|
||||||
|
|
||||||
|
const time_to_next = try self.env.runMacrotasks();
|
||||||
|
env.pumpMessageLoop();
|
||||||
|
|
||||||
|
// either of the above could have queued more microtasks
|
||||||
|
env.runMicrotasks();
|
||||||
|
|
||||||
|
return time_to_next;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn 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();
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
// Parses data:[<media-type>][;base64],<data>
|
|
||||||
pub fn parse(allocator: Allocator, src: []const u8) !?[]const u8 {
|
|
||||||
if (!std.mem.startsWith(u8, src, "data:")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uri = src[5..];
|
|
||||||
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
|
|
||||||
|
|
||||||
var data = uri[data_starts + 1 ..];
|
|
||||||
|
|
||||||
// Extract the encoding.
|
|
||||||
const metadata = uri[0..data_starts];
|
|
||||||
if (std.mem.endsWith(u8, metadata, ";base64")) {
|
|
||||||
const decoder = std.base64.standard.Decoder;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = @import("../testing.zig");
|
|
||||||
test "DataURI: parse valid" {
|
|
||||||
try test_valid("data:text/javascript; charset=utf-8;base64,Zm9v", "foo");
|
|
||||||
try test_valid("data:text/javascript; charset=utf-8;,foo", "foo");
|
|
||||||
try test_valid("data:,foo", "foo");
|
|
||||||
}
|
|
||||||
|
|
||||||
test "DataURI: parse invalid" {
|
|
||||||
try test_cannot_parse("atad:,foo");
|
|
||||||
try test_cannot_parse("data:foo");
|
|
||||||
try test_cannot_parse("data:");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_valid(uri: []const u8, expected: []const u8) !void {
|
|
||||||
defer testing.reset();
|
|
||||||
const data_uri = try parse(testing.arena_allocator, uri) orelse return error.TestFailed;
|
|
||||||
try testing.expectEqual(expected, data_uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_cannot_parse(uri: []const u8) !void {
|
|
||||||
try testing.expectEqual(null, parse(undefined, uri));
|
|
||||||
}
|
|
||||||
910
src/browser/EventManager.zig
Normal file
910
src/browser/EventManager.zig
Normal file
@@ -0,0 +1,910 @@
|
|||||||
|
// 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 builtin = @import("builtin");
|
||||||
|
|
||||||
|
const log = @import("../log.zig");
|
||||||
|
const String = @import("../string.zig").String;
|
||||||
|
|
||||||
|
const js = @import("js/js.zig");
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
const Event = @import("webapi/Event.zig");
|
||||||
|
const EventTarget = @import("webapi/EventTarget.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
page: *Page,
|
||||||
|
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),
|
||||||
|
ignore_list: std.ArrayList(*Listener),
|
||||||
|
list_pool: std.heap.MemoryPool(std.DoublyLinkedList),
|
||||||
|
lookup: std.HashMapUnmanaged(
|
||||||
|
EventKey,
|
||||||
|
*std.DoublyLinkedList,
|
||||||
|
EventKeyContext,
|
||||||
|
std.hash_map.default_max_load_percentage,
|
||||||
|
),
|
||||||
|
dispatch_depth: usize,
|
||||||
|
deferred_removals: std.ArrayList(struct { list: *std.DoublyLinkedList, listener: *Listener }),
|
||||||
|
|
||||||
|
pub fn init(arena: Allocator, page: *Page) EventManager {
|
||||||
|
return .{
|
||||||
|
.page = page,
|
||||||
|
.lookup = .{},
|
||||||
|
.arena = arena,
|
||||||
|
.ignore_list = .{},
|
||||||
|
.list_pool = .init(arena),
|
||||||
|
.listener_pool = .init(arena),
|
||||||
|
.dispatch_depth = 0,
|
||||||
|
.deferred_removals = .{},
|
||||||
|
.has_dom_load_listener = false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const RegisterOptions = struct {
|
||||||
|
once: bool = false,
|
||||||
|
capture: bool = false,
|
||||||
|
passive: bool = false,
|
||||||
|
signal: ?*@import("webapi/AbortSignal.zig") = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Callback = union(enum) {
|
||||||
|
function: js.Function,
|
||||||
|
object: js.Object,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, opts: RegisterOptions) !void {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
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 (opts.signal) |signal| {
|
||||||
|
if (signal.getAborted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// check for duplicate callbacks already registered
|
||||||
|
var node = gop.value_ptr.*.first;
|
||||||
|
while (node) |n| {
|
||||||
|
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||||
|
const is_duplicate = switch (callback) {
|
||||||
|
.object => |obj| listener.function.eqlObject(obj),
|
||||||
|
.function => |func| listener.function.eqlFunction(func),
|
||||||
|
};
|
||||||
|
if (is_duplicate and listener.capture == opts.capture) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node = n.next;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gop.value_ptr.* = try self.list_pool.create();
|
||||||
|
gop.value_ptr.*.* = .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
const func = switch (callback) {
|
||||||
|
.function => |f| Function{ .value = try f.persist() },
|
||||||
|
.object => |o| Function{ .object = try o.persist() },
|
||||||
|
};
|
||||||
|
|
||||||
|
const listener = try self.listener_pool.create();
|
||||||
|
listener.* = .{
|
||||||
|
.node = .{},
|
||||||
|
.once = opts.once,
|
||||||
|
.capture = opts.capture,
|
||||||
|
.passive = opts.passive,
|
||||||
|
.function = func,
|
||||||
|
.signal = opts.signal,
|
||||||
|
.typ = type_string,
|
||||||
|
};
|
||||||
|
// append the listener to the list of listeners for this target
|
||||||
|
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 {
|
||||||
|
const list = self.lookup.get(.{
|
||||||
|
.type_string = .wrap(typ),
|
||||||
|
.event_target = @intFromPtr(target),
|
||||||
|
}) orelse return;
|
||||||
|
if (findListener(list, callback, use_capture)) |listener| {
|
||||||
|
self.removeListener(list, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (target._type) {
|
||||||
|
.node => |node| try self.dispatchNode(node, event, opts),
|
||||||
|
else => try self.dispatchDirect(target, event, null, .{ .context = "dispatch" }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are a lot of events that can be attached via addEventListener or as
|
||||||
|
// a property, like the XHR events, or window.onload. You might think that the
|
||||||
|
// property is just a shortcut for calling addEventListener, but they are distinct.
|
||||||
|
// An event set via property cannot be removed by removeEventListener. If you
|
||||||
|
// set both the property and add a listener, they both execute.
|
||||||
|
const DispatchDirectOptions = struct {
|
||||||
|
context: []const u8,
|
||||||
|
inject_target: bool = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Direct dispatch for non-DOM targets (Window, XHR, AbortSignal) or DOM nodes with
|
||||||
|
// property handlers. No propagation - just calls the handler and registered listeners.
|
||||||
|
// Handler can be: null, ?js.Function.Global, ?js.Function.Temp, or js.Function
|
||||||
|
pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event, handler: anytype, comptime opts: DispatchDirectOptions) !void {
|
||||||
|
const page = self.page;
|
||||||
|
|
||||||
|
event.acquireRef();
|
||||||
|
defer event.deinit(false, page);
|
||||||
|
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
log.debug(.event, "dispatchDirect", .{ .type = event._type_string, .context = opts.context });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comptime opts.inject_target) {
|
||||||
|
event._target = target;
|
||||||
|
event._dispatch_target = target; // Store original target for composedPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
var was_dispatched = false;
|
||||||
|
|
||||||
|
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;
|
||||||
|
if (func.callWithThis(void, target, .{event})) {
|
||||||
|
was_dispatched = true;
|
||||||
|
} else |err| {
|
||||||
|
// a non-JS error
|
||||||
|
log.warn(.event, opts.context, .{ .err = err });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// listeners reigstered via addEventListener
|
||||||
|
const list = self.lookup.get(.{
|
||||||
|
.event_target = @intFromPtr(target),
|
||||||
|
.type_string = event._type_string,
|
||||||
|
}) orelse return;
|
||||||
|
|
||||||
|
// This is a slightly simplified version of what you'll find in dispatchPhase
|
||||||
|
// It is simpler because, for direct dispatching, we know there's no ancestors
|
||||||
|
// and only the single target phase.
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
while (node) |n| {
|
||||||
|
if (is_done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||||
|
is_done = (listener == last_listener);
|
||||||
|
node = n.next;
|
||||||
|
|
||||||
|
// Skip removed listeners
|
||||||
|
if (listener.removed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the listener has an aborted signal, remove it and skip
|
||||||
|
if (listener.signal) |signal| {
|
||||||
|
if (signal.getAborted()) {
|
||||||
|
self.removeListener(list, listener);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove "once" listeners BEFORE calling them so nested dispatches don't see them
|
||||||
|
if (listener.once) {
|
||||||
|
self.removeListener(list, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
was_dispatched = true;
|
||||||
|
event._current_target = target;
|
||||||
|
|
||||||
|
switch (listener.function) {
|
||||||
|
.value => |value| try ls.toLocal(value).callWithThis(void, target, .{event}),
|
||||||
|
.string => |string| {
|
||||||
|
const str = try page.call_arena.dupeZ(u8, string.str());
|
||||||
|
try ls.local.eval(str, null);
|
||||||
|
},
|
||||||
|
.object => |obj_global| {
|
||||||
|
const obj = ls.toLocal(obj_global);
|
||||||
|
if (try obj.getFunction("handleEvent")) |handleEvent| {
|
||||||
|
try handleEvent.callWithThis(void, obj, .{event});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event._stop_immediate_propagation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getFunction(handler: anytype, local: *const js.Local) ?js.Function {
|
||||||
|
const T = @TypeOf(handler);
|
||||||
|
const ti = @typeInfo(T);
|
||||||
|
|
||||||
|
if (ti == .null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (ti == .optional) {
|
||||||
|
return getFunction(handler orelse return null, local);
|
||||||
|
}
|
||||||
|
return switch (T) {
|
||||||
|
js.Function => handler,
|
||||||
|
js.Function.Temp => local.toLocal(handler),
|
||||||
|
js.Function.Global => local.toLocal(handler),
|
||||||
|
else => @compileError("handler must be null or \\??js.Function(\\.(Temp|Global))?"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts: DispatchOpts) !void {
|
||||||
|
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
||||||
|
|
||||||
|
{
|
||||||
|
const et = target.asEventTarget();
|
||||||
|
event._target = et;
|
||||||
|
event._dispatch_target = et; // Store original target for composedPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = self.page;
|
||||||
|
var was_handled = false;
|
||||||
|
|
||||||
|
defer if (was_handled) {
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
ls.local.runMicrotasks();
|
||||||
|
};
|
||||||
|
|
||||||
|
const activation_state = ActivationState.create(event, target, page);
|
||||||
|
|
||||||
|
// Defer runs even on early return - ensures event phase is reset
|
||||||
|
// and default actions execute (unless prevented)
|
||||||
|
defer {
|
||||||
|
event._event_phase = .none;
|
||||||
|
event._stop_propagation = false;
|
||||||
|
event._stop_immediate_propagation = false;
|
||||||
|
// Handle checkbox/radio activation rollback or commit
|
||||||
|
if (activation_state) |state| {
|
||||||
|
state.restore(event, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute default action if not prevented
|
||||||
|
if (event._prevent_default) {
|
||||||
|
// can't return in a defer (╯°□°)╯︵ ┻━┻
|
||||||
|
} else if (event._type_string.eql(comptime .wrap("click"))) {
|
||||||
|
page.handleClick(target) catch |err| {
|
||||||
|
log.warn(.event, "page.click", .{ .err = err });
|
||||||
|
};
|
||||||
|
} else if (event._type_string.eql(comptime .wrap("keydown"))) {
|
||||||
|
page.handleKeydown(target, event) catch |err| {
|
||||||
|
log.warn(.event, "page.keydown", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var path_len: usize = 0;
|
||||||
|
var path_buffer: [128]*EventTarget = undefined;
|
||||||
|
|
||||||
|
var node: ?*Node = target;
|
||||||
|
while (node) |n| {
|
||||||
|
if (path_len >= path_buffer.len) break;
|
||||||
|
path_buffer[path_len] = n.asEventTarget();
|
||||||
|
path_len += 1;
|
||||||
|
|
||||||
|
// Check if this node is a shadow root
|
||||||
|
if (n.is(ShadowRoot)) |shadow| {
|
||||||
|
event._needs_retargeting = true;
|
||||||
|
|
||||||
|
// If event is not composed, stop at shadow boundary
|
||||||
|
if (!event._composed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, jump to the shadow host and continue
|
||||||
|
node = shadow._host.asNode();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
node = n._parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Even though the window isn't part of the DOM, most events propagate
|
||||||
|
// through it in the capture phase (unless we stopped at a shadow boundary)
|
||||||
|
// The only explicit exception is "load"
|
||||||
|
if (event._type_string.eql(comptime .wrap("load")) == false) {
|
||||||
|
if (path_len < path_buffer.len) {
|
||||||
|
path_buffer[path_len] = page.window.asEventTarget();
|
||||||
|
path_len += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = path_buffer[0..path_len];
|
||||||
|
|
||||||
|
// Phase 1: Capturing phase (root → target, excluding target)
|
||||||
|
// This happens for all events, regardless of bubbling
|
||||||
|
event._event_phase = .capturing_phase;
|
||||||
|
var i: usize = path_len;
|
||||||
|
while (i > 1) {
|
||||||
|
i -= 1;
|
||||||
|
if (event._stop_propagation) return;
|
||||||
|
const current_target = path[i];
|
||||||
|
if (self.lookup.get(.{
|
||||||
|
.event_target = @intFromPtr(current_target),
|
||||||
|
.type_string = event._type_string,
|
||||||
|
})) |list| {
|
||||||
|
try self.dispatchPhase(list, current_target, event, &was_handled, comptime .init(true, opts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: At target
|
||||||
|
if (event._stop_propagation) return;
|
||||||
|
event._event_phase = .at_target;
|
||||||
|
const target_et = target.asEventTarget();
|
||||||
|
|
||||||
|
blk: {
|
||||||
|
// Get inline handler (e.g., onclick property) for this target
|
||||||
|
if (self.getInlineHandler(target_et, event)) |inline_handler| {
|
||||||
|
was_handled = true;
|
||||||
|
event._current_target = target_et;
|
||||||
|
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
self.page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
|
try ls.toLocal(inline_handler).callWithThis(void, target_et, .{event});
|
||||||
|
|
||||||
|
if (event._stop_propagation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event._stop_immediate_propagation) {
|
||||||
|
break :blk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.lookup.get(.{
|
||||||
|
.type_string = event._type_string,
|
||||||
|
.event_target = @intFromPtr(target_et),
|
||||||
|
})) |list| {
|
||||||
|
try self.dispatchPhase(list, target_et, event, &was_handled, comptime .init(null, opts));
|
||||||
|
if (event._stop_propagation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: Bubbling phase (target → root, excluding target)
|
||||||
|
// This only happens if the event bubbles
|
||||||
|
if (event._bubbles) {
|
||||||
|
event._event_phase = .bubbling_phase;
|
||||||
|
for (path[1..]) |current_target| {
|
||||||
|
if (event._stop_propagation) break;
|
||||||
|
if (self.lookup.get(.{
|
||||||
|
.type_string = event._type_string,
|
||||||
|
.event_target = @intFromPtr(current_target),
|
||||||
|
})) |list| {
|
||||||
|
try self.dispatchPhase(list, current_target, event, &was_handled, comptime .init(false, opts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DispatchPhaseOpts = struct {
|
||||||
|
capture_only: ?bool = null,
|
||||||
|
apply_ignore: bool = false,
|
||||||
|
|
||||||
|
fn init(capture_only: ?bool, opts: DispatchOpts) DispatchPhaseOpts {
|
||||||
|
return .{
|
||||||
|
.capture_only = capture_only,
|
||||||
|
.apply_ignore = opts.apply_ignore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime opts: DispatchPhaseOpts) !void {
|
||||||
|
const page = self.page;
|
||||||
|
|
||||||
|
// Track dispatch depth for deferred removal
|
||||||
|
self.dispatch_depth += 1;
|
||||||
|
defer {
|
||||||
|
const dispatch_depth = self.dispatch_depth;
|
||||||
|
// Only destroy deferred listeners when we exit the outermost dispatch
|
||||||
|
if (dispatch_depth == 1) {
|
||||||
|
for (self.deferred_removals.items) |removal| {
|
||||||
|
removal.list.remove(&removal.listener.node);
|
||||||
|
self.listener_pool.destroy(removal.listener);
|
||||||
|
}
|
||||||
|
self.deferred_removals.clearRetainingCapacity();
|
||||||
|
} else {
|
||||||
|
self.dispatch_depth = dispatch_depth - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the last listener in the list as sentinel - listeners added during dispatch will be after it
|
||||||
|
const last_node = list.last orelse return;
|
||||||
|
const last_listener: *Listener = @alignCast(@fieldParentPtr("node", last_node));
|
||||||
|
|
||||||
|
// Iterate through the list, stopping after we've encountered the last_listener
|
||||||
|
var node = list.first;
|
||||||
|
var is_done = false;
|
||||||
|
node_loop: while (node) |n| {
|
||||||
|
if (is_done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||||
|
is_done = (listener == last_listener);
|
||||||
|
node = n.next;
|
||||||
|
|
||||||
|
// Skip non-matching listeners
|
||||||
|
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;
|
||||||
|
event._current_target = current_target;
|
||||||
|
|
||||||
|
// Compute adjusted target for shadow DOM retargeting (only if needed)
|
||||||
|
const original_target = event._target;
|
||||||
|
if (event._needs_retargeting) {
|
||||||
|
event._target = getAdjustedTarget(original_target, current_target);
|
||||||
|
}
|
||||||
|
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
|
switch (listener.function) {
|
||||||
|
.value => |value| try ls.toLocal(value).callWithThis(void, current_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});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore original target (only if we changed it)
|
||||||
|
if (event._needs_retargeting) {
|
||||||
|
event._target = original_target;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event._stop_immediate_propagation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?js.Function.Global {
|
||||||
|
const global_event_handlers = @import("webapi/global_event_handlers.zig");
|
||||||
|
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 {
|
||||||
|
// If we're in a dispatch, defer removal to avoid invalidating iteration
|
||||||
|
if (self.dispatch_depth > 0) {
|
||||||
|
listener.removed = true;
|
||||||
|
self.deferred_removals.append(self.arena, .{ .list = list, .listener = listener }) catch unreachable;
|
||||||
|
} else {
|
||||||
|
// Outside dispatch, remove immediately
|
||||||
|
list.remove(&listener.node);
|
||||||
|
self.listener_pool.destroy(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findListener(list: *const std.DoublyLinkedList, callback: Callback, capture: bool) ?*Listener {
|
||||||
|
var node = list.first;
|
||||||
|
while (node) |n| {
|
||||||
|
node = n.next;
|
||||||
|
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||||
|
const matches = switch (callback) {
|
||||||
|
.object => |obj| listener.function.eqlObject(obj),
|
||||||
|
.function => |func| listener.function.eqlFunction(func),
|
||||||
|
};
|
||||||
|
if (!matches) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (listener.capture != capture) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return listener;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Listener = struct {
|
||||||
|
typ: String,
|
||||||
|
once: bool,
|
||||||
|
capture: bool,
|
||||||
|
passive: bool,
|
||||||
|
function: Function,
|
||||||
|
signal: ?*@import("webapi/AbortSignal.zig") = null,
|
||||||
|
node: std.DoublyLinkedList.Node,
|
||||||
|
removed: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Function = union(enum) {
|
||||||
|
value: js.Function.Global,
|
||||||
|
string: String,
|
||||||
|
object: js.Object.Global,
|
||||||
|
|
||||||
|
fn eqlFunction(self: Function, func: js.Function) bool {
|
||||||
|
return switch (self) {
|
||||||
|
.value => |v| v.isEqual(func),
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eqlObject(self: Function, obj: js.Object) bool {
|
||||||
|
return switch (self) {
|
||||||
|
.object => |o| return o.isEqual(obj),
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Computes the adjusted target for shadow DOM event retargeting
|
||||||
|
// Returns the lowest shadow-including ancestor of original_target that is
|
||||||
|
// also an ancestor-or-self of current_target
|
||||||
|
fn getAdjustedTarget(original_target: ?*EventTarget, current_target: *EventTarget) ?*EventTarget {
|
||||||
|
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
||||||
|
|
||||||
|
const orig_node = switch ((original_target orelse return null)._type) {
|
||||||
|
.node => |n| n,
|
||||||
|
else => return original_target,
|
||||||
|
};
|
||||||
|
const curr_node = switch (current_target._type) {
|
||||||
|
.node => |n| n,
|
||||||
|
else => return original_target,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Walk up from original target, checking if we can reach current target
|
||||||
|
var node: ?*Node = orig_node;
|
||||||
|
while (node) |n| {
|
||||||
|
// Check if current_target is an ancestor of n (or n itself)
|
||||||
|
if (isAncestorOrSelf(curr_node, n)) {
|
||||||
|
return n.asEventTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross shadow boundary if needed
|
||||||
|
if (n.is(ShadowRoot)) |shadow| {
|
||||||
|
node = shadow._host.asNode();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
node = n._parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return original_target;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if ancestor is an ancestor of (or the same as) node
|
||||||
|
// WITHOUT crossing shadow boundaries (just regular DOM tree)
|
||||||
|
fn isAncestorOrSelf(ancestor: *Node, node: *Node) bool {
|
||||||
|
if (ancestor == node) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var current: ?*Node = node._parent;
|
||||||
|
while (current) |n| {
|
||||||
|
if (n == ancestor) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current = n._parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
461
src/browser/Factory.zig
Normal file
461
src/browser/Factory.zig
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
// 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 reflect = @import("reflect.zig");
|
||||||
|
|
||||||
|
const log = @import("../log.zig");
|
||||||
|
const String = @import("../string.zig").String;
|
||||||
|
|
||||||
|
const SlabAllocator = @import("../slab.zig").SlabAllocator;
|
||||||
|
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
const Event = @import("webapi/Event.zig");
|
||||||
|
const UIEvent = @import("webapi/event/UIEvent.zig");
|
||||||
|
const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
const Document = @import("webapi/Document.zig");
|
||||||
|
const EventTarget = @import("webapi/EventTarget.zig");
|
||||||
|
const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.zig");
|
||||||
|
const Blob = @import("webapi/Blob.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();
|
||||||
|
|
||||||
|
_arena: Allocator,
|
||||||
|
_slab: SlabAllocator,
|
||||||
|
|
||||||
|
pub fn init(arena: Allocator) !*Factory {
|
||||||
|
const self = try arena.create(Factory);
|
||||||
|
self.* = .{
|
||||||
|
._arena = arena,
|
||||||
|
._slab = SlabAllocator.init(arena, 128),
|
||||||
|
};
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a root object
|
||||||
|
pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
|
return self.eventTargetWithAllocator(self._slab.allocator(), child);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eventTargetWithAllocator(_: *const Factory, allocator: Allocator, child: anytype) !*@TypeOf(child) {
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ EventTarget, @TypeOf(child) },
|
||||||
|
).allocate(allocator);
|
||||||
|
|
||||||
|
const event_ptr = chain.get(0);
|
||||||
|
event_ptr.* = .{
|
||||||
|
._type = unionInit(EventTarget.Type, chain.get(1)),
|
||||||
|
};
|
||||||
|
chain.setLeaf(1, child);
|
||||||
|
|
||||||
|
return chain.get(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn standaloneEventTarget(self: *Factory, child: anytype) !*EventTarget {
|
||||||
|
const allocator = self._slab.allocator();
|
||||||
|
const et = try allocator.create(EventTarget);
|
||||||
|
et.* = .{ ._type = unionInit(EventTarget.Type, child) };
|
||||||
|
return et;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a root object
|
||||||
|
pub fn event(_: *const Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ Event, @TypeOf(child) },
|
||||||
|
).allocate(arena);
|
||||||
|
|
||||||
|
// Special case: Event has a _type_string field, so we need manual setup
|
||||||
|
const event_ptr = chain.get(0);
|
||||||
|
event_ptr.* = try eventInit(arena, typ, chain.get(1));
|
||||||
|
chain.setLeaf(1, child);
|
||||||
|
|
||||||
|
return chain.get(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uiEvent(_: *const Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ Event, UIEvent, @TypeOf(child) },
|
||||||
|
).allocate(arena);
|
||||||
|
|
||||||
|
// Special case: Event has a _type_string field, so we need manual setup
|
||||||
|
const event_ptr = chain.get(0);
|
||||||
|
event_ptr.* = try eventInit(arena, typ, chain.get(1));
|
||||||
|
chain.setMiddle(1, UIEvent.Type);
|
||||||
|
chain.setLeaf(2, child);
|
||||||
|
|
||||||
|
return chain.get(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mouseEvent(_: *const Factory, arena: Allocator, typ: String, mouse: MouseEvent, child: anytype) !*@TypeOf(child) {
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ Event, UIEvent, MouseEvent, @TypeOf(child) },
|
||||||
|
).allocate(arena);
|
||||||
|
|
||||||
|
// Special case: Event has a _type_string field, so we need manual setup
|
||||||
|
const event_ptr = chain.get(0);
|
||||||
|
event_ptr.* = try eventInit(arena, typ, chain.get(1));
|
||||||
|
chain.setMiddle(1, UIEvent.Type);
|
||||||
|
|
||||||
|
// Set MouseEvent with all its fields
|
||||||
|
const mouse_ptr = chain.get(2);
|
||||||
|
mouse_ptr.* = mouse;
|
||||||
|
mouse_ptr._proto = chain.get(1);
|
||||||
|
mouse_ptr._type = unionInit(MouseEvent.Type, chain.get(3));
|
||||||
|
|
||||||
|
chain.setLeaf(3, child);
|
||||||
|
|
||||||
|
return chain.get(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn PrototypeChain(comptime types: []const type) type {
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
memory: []u8,
|
||||||
|
|
||||||
|
fn totalSize() usize {
|
||||||
|
var size: usize = 0;
|
||||||
|
for (types) |T| {
|
||||||
|
size = std.mem.alignForward(usize, size, @alignOf(T));
|
||||||
|
size += @sizeOf(T);
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maxAlign() std.mem.Alignment {
|
||||||
|
var alignment: std.mem.Alignment = .@"1";
|
||||||
|
|
||||||
|
for (types) |T| {
|
||||||
|
alignment = std.mem.Alignment.max(alignment, std.mem.Alignment.of(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
return alignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getType(comptime index: usize) type {
|
||||||
|
return types[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn allocate(allocator: std.mem.Allocator) !Self {
|
||||||
|
const size = comptime Self.totalSize();
|
||||||
|
const alignment = comptime Self.maxAlign();
|
||||||
|
|
||||||
|
const memory = try allocator.alignedAlloc(u8, alignment, size);
|
||||||
|
return .{ .memory = memory };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(self: *const Self, comptime index: usize) *getType(index) {
|
||||||
|
var offset: usize = 0;
|
||||||
|
inline for (types, 0..) |T, i| {
|
||||||
|
offset = std.mem.alignForward(usize, offset, @alignOf(T));
|
||||||
|
|
||||||
|
if (i == index) {
|
||||||
|
return @as(*T, @ptrCast(@alignCast(self.memory.ptr + offset)));
|
||||||
|
}
|
||||||
|
offset += @sizeOf(T);
|
||||||
|
}
|
||||||
|
unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(self: *const Self, comptime index: usize, value: getType(index)) void {
|
||||||
|
const ptr = self.get(index);
|
||||||
|
ptr.* = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setRoot(self: *const Self, comptime T: type) void {
|
||||||
|
const ptr = self.get(0);
|
||||||
|
ptr.* = .{ ._type = unionInit(T, self.get(1)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setMiddle(self: *const Self, comptime index: usize, comptime T: type) void {
|
||||||
|
assert(index >= 1);
|
||||||
|
assert(index < types.len);
|
||||||
|
|
||||||
|
const ptr = self.get(index);
|
||||||
|
ptr.* = .{ ._proto = self.get(index - 1), ._type = unionInit(T, self.get(index + 1)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setMiddleWithValue(self: *const Self, comptime index: usize, comptime T: type, value: anytype) void {
|
||||||
|
assert(index >= 1);
|
||||||
|
|
||||||
|
const ptr = self.get(index);
|
||||||
|
ptr.* = .{ ._proto = self.get(index - 1), ._type = unionInit(T, value) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setLeaf(self: *const Self, comptime index: usize, value: anytype) void {
|
||||||
|
assert(index >= 1);
|
||||||
|
|
||||||
|
const ptr = self.get(index);
|
||||||
|
ptr.* = value;
|
||||||
|
ptr._proto = self.get(index - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn AutoPrototypeChain(comptime types: []const type) type {
|
||||||
|
return struct {
|
||||||
|
fn create(allocator: std.mem.Allocator, leaf_value: anytype) !*@TypeOf(leaf_value) {
|
||||||
|
const chain = try PrototypeChain(types).allocate(allocator);
|
||||||
|
|
||||||
|
const RootType = types[0];
|
||||||
|
chain.setRoot(RootType.Type);
|
||||||
|
|
||||||
|
inline for (1..types.len - 1) |i| {
|
||||||
|
const MiddleType = types[i];
|
||||||
|
chain.setMiddle(i, MiddleType.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.setLeaf(types.len - 1, leaf_value);
|
||||||
|
return chain.get(types.len - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eventInit(arena: Allocator, typ: String, value: anytype) !Event {
|
||||||
|
// Round to 2ms for privacy (browsers do this)
|
||||||
|
const raw_timestamp = @import("../datetime.zig").milliTimestamp(.monotonic);
|
||||||
|
const time_stamp = (raw_timestamp / 2) * 2;
|
||||||
|
|
||||||
|
return .{
|
||||||
|
._rc = 0,
|
||||||
|
._arena = arena,
|
||||||
|
._type = unionInit(Event.Type, value),
|
||||||
|
._type_string = typ,
|
||||||
|
._time_stamp = time_stamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ Blob, @TypeOf(child) },
|
||||||
|
).allocate(allocator);
|
||||||
|
|
||||||
|
const blob_ptr = chain.get(0);
|
||||||
|
blob_ptr.* = .{
|
||||||
|
._type = unionInit(Blob.Type, chain.get(1)),
|
||||||
|
._slice = "",
|
||||||
|
._mime = "",
|
||||||
|
};
|
||||||
|
chain.setLeaf(1, child);
|
||||||
|
|
||||||
|
return chain.get(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn abstractRange(self: *Factory, child: anytype, page: *Page) !*@TypeOf(child) {
|
||||||
|
const allocator = self._slab.allocator();
|
||||||
|
const chain = try PrototypeChain(&.{ AbstractRange, @TypeOf(child) }).allocate(allocator);
|
||||||
|
|
||||||
|
const doc = page.document.asNode();
|
||||||
|
chain.set(0, AbstractRange{
|
||||||
|
._type = unionInit(AbstractRange.Type, chain.get(1)),
|
||||||
|
._end_offset = 0,
|
||||||
|
._start_offset = 0,
|
||||||
|
._end_container = doc,
|
||||||
|
._start_container = doc,
|
||||||
|
});
|
||||||
|
chain.setLeaf(1, child);
|
||||||
|
return chain.get(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn node(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
|
const allocator = self._slab.allocator();
|
||||||
|
return try AutoPrototypeChain(
|
||||||
|
&.{ EventTarget, Node, @TypeOf(child) },
|
||||||
|
).create(allocator, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn document(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
|
const allocator = self._slab.allocator();
|
||||||
|
return try AutoPrototypeChain(
|
||||||
|
&.{ EventTarget, Node, Document, @TypeOf(child) },
|
||||||
|
).create(allocator, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn documentFragment(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
|
const allocator = self._slab.allocator();
|
||||||
|
return try AutoPrototypeChain(
|
||||||
|
&.{ EventTarget, Node, Node.DocumentFragment, @TypeOf(child) },
|
||||||
|
).create(allocator, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn element(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
|
const allocator = self._slab.allocator();
|
||||||
|
return try AutoPrototypeChain(
|
||||||
|
&.{ EventTarget, Node, Element, @TypeOf(child) },
|
||||||
|
).create(allocator, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn htmlElement(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
|
const allocator = self._slab.allocator();
|
||||||
|
return try AutoPrototypeChain(
|
||||||
|
&.{ EventTarget, Node, Element, Element.Html, @TypeOf(child) },
|
||||||
|
).create(allocator, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn htmlMediaElement(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
|
const allocator = self._slab.allocator();
|
||||||
|
return try AutoPrototypeChain(
|
||||||
|
&.{ EventTarget, Node, Element, Element.Html, Element.Html.Media, @TypeOf(child) },
|
||||||
|
).create(allocator, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeOf(child) {
|
||||||
|
const allocator = self._slab.allocator();
|
||||||
|
const ChildT = @TypeOf(child);
|
||||||
|
|
||||||
|
if (ChildT == Element.Svg) {
|
||||||
|
return self.element(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ EventTarget, Node, Element, Element.Svg, ChildT },
|
||||||
|
).allocate(allocator);
|
||||||
|
|
||||||
|
chain.setRoot(EventTarget.Type);
|
||||||
|
chain.setMiddle(1, Node.Type);
|
||||||
|
chain.setMiddle(2, Element.Type);
|
||||||
|
|
||||||
|
// will never allocate, can't fail
|
||||||
|
const tag_name_str = String.init(self._arena, tag_name, .{}) catch unreachable;
|
||||||
|
|
||||||
|
// Manually set Element.Svg with the tag_name
|
||||||
|
chain.set(3, .{
|
||||||
|
._proto = chain.get(2),
|
||||||
|
._tag_name = tag_name_str,
|
||||||
|
._type = unionInit(Element.Svg.Type, chain.get(4)),
|
||||||
|
});
|
||||||
|
|
||||||
|
chain.setLeaf(4, child);
|
||||||
|
return chain.get(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn xhrEventTarget(_: *const Factory, allocator: Allocator, child: anytype) !*@TypeOf(child) {
|
||||||
|
return try AutoPrototypeChain(
|
||||||
|
&.{ EventTarget, XMLHttpRequestEventTarget, @TypeOf(child) },
|
||||||
|
).create(allocator, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn textTrackCue(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
|
const allocator = self._slab.allocator();
|
||||||
|
const TextTrackCue = @import("webapi/media/TextTrackCue.zig");
|
||||||
|
|
||||||
|
return try AutoPrototypeChain(
|
||||||
|
&.{ EventTarget, TextTrackCue, @TypeOf(child) },
|
||||||
|
).create(allocator, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn destroy(self: *Factory, value: anytype) void {
|
||||||
|
const S = reflect.Struct(@TypeOf(value));
|
||||||
|
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
// We should always destroy from the leaf down.
|
||||||
|
if (@hasDecl(S, "_prototype_root")) {
|
||||||
|
// A Event{._type == .generic} (or any other similar types)
|
||||||
|
// _should_ be destoyed directly. The _type = .generic is a pseudo
|
||||||
|
// child
|
||||||
|
if (S != Event or value._type != .generic) {
|
||||||
|
log.fatal(.bug, "factory.destroy.event", .{ .type = @typeName(S) });
|
||||||
|
unreachable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comptime @hasField(S, "_proto")) {
|
||||||
|
self.destroyChain(value, 0, std.mem.Alignment.@"1");
|
||||||
|
} else {
|
||||||
|
self.destroyStandalone(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn destroyStandalone(self: *Factory, value: anytype) void {
|
||||||
|
const allocator = self._slab.allocator();
|
||||||
|
allocator.destroy(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroyChain(
|
||||||
|
self: *Factory,
|
||||||
|
value: anytype,
|
||||||
|
old_size: usize,
|
||||||
|
old_align: std.mem.Alignment,
|
||||||
|
) void {
|
||||||
|
const S = reflect.Struct(@TypeOf(value));
|
||||||
|
const allocator = self._slab.allocator();
|
||||||
|
|
||||||
|
// aligns the old size to the alignment of this element
|
||||||
|
const current_size = std.mem.alignForward(usize, old_size, @alignOf(S));
|
||||||
|
const new_size = current_size + @sizeOf(S);
|
||||||
|
const new_align = std.mem.Alignment.max(old_align, std.mem.Alignment.of(S));
|
||||||
|
|
||||||
|
if (@hasField(S, "_proto")) {
|
||||||
|
self.destroyChain(value._proto, new_size, new_align);
|
||||||
|
} else {
|
||||||
|
// no proto so this is the head of the chain.
|
||||||
|
// we use this as the ptr to the start of the chain.
|
||||||
|
// and we have summed up the length.
|
||||||
|
assert(@hasDecl(S, "_prototype_root"));
|
||||||
|
|
||||||
|
const memory_ptr: [*]u8 = @ptrCast(@constCast(value));
|
||||||
|
const len = std.mem.alignForward(usize, new_size, new_align.toByteUnits());
|
||||||
|
allocator.rawFree(memory_ptr[0..len], new_align, @returnAddress());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createT(self: *Factory, comptime T: type) !*T {
|
||||||
|
const allocator = self._slab.allocator();
|
||||||
|
return try allocator.create(T);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(self: *Factory, value: anytype) !*@TypeOf(value) {
|
||||||
|
const ptr = try self.createT(@TypeOf(value));
|
||||||
|
ptr.* = value;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unionInit(comptime T: type, value: anytype) T {
|
||||||
|
const V = @TypeOf(value);
|
||||||
|
const field_name = comptime unionFieldName(T, V);
|
||||||
|
return @unionInit(T, field_name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// There can be friction between comptime and runtime. Comptime has to
|
||||||
|
// account for all possible types, even if some runtime flow makes certain
|
||||||
|
// cases impossible. At runtime, we always call `unionFieldName` with the
|
||||||
|
// correct struct or pointer type. But at comptime time, `unionFieldName`
|
||||||
|
// is called with both variants (S and *S). So we use reflect.Struct().
|
||||||
|
// This only works because we never have a union with a field S and another
|
||||||
|
// field *S.
|
||||||
|
fn unionFieldName(comptime T: type, comptime V: type) []const u8 {
|
||||||
|
inline for (@typeInfo(T).@"union".fields) |field| {
|
||||||
|
if (reflect.Struct(field.type) == reflect.Struct(V)) {
|
||||||
|
return field.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@compileError(@typeName(V) ++ " is not a valid type for " ++ @typeName(T) ++ ".type");
|
||||||
|
}
|
||||||
578
src/browser/Mime.zig
Normal file
578
src/browser/Mime.zig
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
// 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 Mime = @This();
|
||||||
|
content_type: ContentType,
|
||||||
|
params: []const u8 = "",
|
||||||
|
// IANA defines max. charset value length as 40.
|
||||||
|
// We keep 41 for null-termination since HTML parser expects in this format.
|
||||||
|
charset: [41]u8 = default_charset,
|
||||||
|
charset_len: usize = default_charset_len,
|
||||||
|
|
||||||
|
/// String "UTF-8" continued by null characters.
|
||||||
|
const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
|
||||||
|
const default_charset_len = 5;
|
||||||
|
|
||||||
|
/// Mime with unknown Content-Type, empty params and empty charset.
|
||||||
|
pub const unknown = Mime{ .content_type = .{ .unknown = {} } };
|
||||||
|
|
||||||
|
pub const ContentTypeEnum = enum {
|
||||||
|
text_xml,
|
||||||
|
text_html,
|
||||||
|
text_javascript,
|
||||||
|
text_plain,
|
||||||
|
text_css,
|
||||||
|
image_jpeg,
|
||||||
|
image_gif,
|
||||||
|
image_png,
|
||||||
|
image_webp,
|
||||||
|
application_json,
|
||||||
|
unknown,
|
||||||
|
other,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ContentType = union(ContentTypeEnum) {
|
||||||
|
text_xml: void,
|
||||||
|
text_html: void,
|
||||||
|
text_javascript: void,
|
||||||
|
text_plain: void,
|
||||||
|
text_css: void,
|
||||||
|
image_jpeg: void,
|
||||||
|
image_gif: void,
|
||||||
|
image_png: void,
|
||||||
|
image_webp: void,
|
||||||
|
application_json: void,
|
||||||
|
unknown: void,
|
||||||
|
other: struct { type: []const u8, sub_type: []const u8 },
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn contentTypeString(mime: *const Mime) []const u8 {
|
||||||
|
return switch (mime.content_type) {
|
||||||
|
.text_xml => "text/xml",
|
||||||
|
.text_html => "text/html",
|
||||||
|
.text_javascript => "application/javascript",
|
||||||
|
.text_plain => "text/plain",
|
||||||
|
.text_css => "text/css",
|
||||||
|
.image_jpeg => "image/jpeg",
|
||||||
|
.image_png => "image/png",
|
||||||
|
.image_gif => "image/gif",
|
||||||
|
.image_webp => "image/webp",
|
||||||
|
.application_json => "application/json",
|
||||||
|
else => "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the null-terminated charset value.
|
||||||
|
pub fn charsetStringZ(mime: *const Mime) [:0]const u8 {
|
||||||
|
return mime.charset[0..mime.charset_len :0];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn charsetString(mime: *const Mime) []const u8 {
|
||||||
|
return mime.charset[0..mime.charset_len];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes quotes of value if quotes are given.
|
||||||
|
///
|
||||||
|
/// Currently we don't validate the charset.
|
||||||
|
/// See section 2.3 Naming Requirements:
|
||||||
|
/// https://datatracker.ietf.org/doc/rfc2978/
|
||||||
|
fn parseCharset(value: []const u8) error{ CharsetTooBig, Invalid }![]const u8 {
|
||||||
|
// Cannot be larger than 40.
|
||||||
|
// https://datatracker.ietf.org/doc/rfc2978/
|
||||||
|
if (value.len > 40) return error.CharsetTooBig;
|
||||||
|
|
||||||
|
// If the first char is a quote, look for a pair.
|
||||||
|
if (value[0] == '"') {
|
||||||
|
if (value.len < 3 or value[value.len - 1] != '"') {
|
||||||
|
return error.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value[1 .. value.len - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// No quotes.
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(input: []u8) !Mime {
|
||||||
|
if (input.len > 255) {
|
||||||
|
return error.TooBig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zig's trim API is broken. The return type is always `[]const u8`,
|
||||||
|
// even if the input type is `[]u8`. @constCast is safe here.
|
||||||
|
var normalized = @constCast(std.mem.trim(u8, input, &std.ascii.whitespace));
|
||||||
|
_ = std.ascii.lowerString(normalized, normalized);
|
||||||
|
|
||||||
|
const content_type, const type_len = try parseContentType(normalized);
|
||||||
|
if (type_len >= normalized.len) {
|
||||||
|
return .{ .content_type = content_type };
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = trimLeft(normalized[type_len..]);
|
||||||
|
|
||||||
|
var charset: [41]u8 = default_charset;
|
||||||
|
var charset_len: usize = default_charset_len;
|
||||||
|
|
||||||
|
var it = std.mem.splitScalar(u8, params, ';');
|
||||||
|
while (it.next()) |attr| {
|
||||||
|
const i = std.mem.indexOfScalarPos(u8, attr, 0, '=') orelse continue;
|
||||||
|
const name = trimLeft(attr[0..i]);
|
||||||
|
|
||||||
|
const value = trimRight(attr[i + 1 ..]);
|
||||||
|
if (value.len == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attribute_name = std.meta.stringToEnum(enum {
|
||||||
|
charset,
|
||||||
|
}, name) orelse continue;
|
||||||
|
|
||||||
|
switch (attribute_name) {
|
||||||
|
.charset => {
|
||||||
|
if (value.len == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attribute_value = parseCharset(value) catch continue;
|
||||||
|
@memcpy(charset[0..attribute_value.len], attribute_value);
|
||||||
|
// Null-terminate right after attribute value.
|
||||||
|
charset[attribute_value.len] = 0;
|
||||||
|
charset_len = attribute_value.len;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.params = params,
|
||||||
|
.charset = charset,
|
||||||
|
.charset_len = charset_len,
|
||||||
|
.content_type = content_type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sniff(body: []const u8) ?Mime {
|
||||||
|
// 0x0C is form feed
|
||||||
|
const content = std.mem.trimLeft(u8, body, &.{ ' ', '\t', '\n', '\r', 0x0C });
|
||||||
|
if (content.len == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content[0] != '<') {
|
||||||
|
if (std.mem.startsWith(u8, content, &.{ 0xEF, 0xBB, 0xBF })) {
|
||||||
|
// UTF-8 BOM
|
||||||
|
return .{ .content_type = .{ .text_plain = {} } };
|
||||||
|
}
|
||||||
|
if (std.mem.startsWith(u8, content, &.{ 0xFE, 0xFF })) {
|
||||||
|
// UTF-16 big-endian BOM
|
||||||
|
return .{ .content_type = .{ .text_plain = {} } };
|
||||||
|
}
|
||||||
|
if (std.mem.startsWith(u8, content, &.{ 0xFF, 0xFE })) {
|
||||||
|
// UTF-16 little-endian BOM
|
||||||
|
return .{ .content_type = .{ .text_plain = {} } };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The longest prefix we have is "<!DOCTYPE HTML ", 15 bytes. If we're
|
||||||
|
// here, we already know content[0] == '<', so we can skip that. So 14
|
||||||
|
// bytes.
|
||||||
|
|
||||||
|
// +1 because we don't need the leading '<'
|
||||||
|
var buf: [14]u8 = undefined;
|
||||||
|
|
||||||
|
const stripped = content[1..];
|
||||||
|
const prefix_len = @min(stripped.len, buf.len);
|
||||||
|
const prefix = std.ascii.lowerString(&buf, stripped[0..prefix_len]);
|
||||||
|
|
||||||
|
// we already know it starts with a <
|
||||||
|
const known_prefixes = [_]struct { []const u8, ContentType }{
|
||||||
|
.{ "!doctype html", .{ .text_html = {} } },
|
||||||
|
.{ "html", .{ .text_html = {} } },
|
||||||
|
.{ "script", .{ .text_html = {} } },
|
||||||
|
.{ "iframe", .{ .text_html = {} } },
|
||||||
|
.{ "h1", .{ .text_html = {} } },
|
||||||
|
.{ "div", .{ .text_html = {} } },
|
||||||
|
.{ "font", .{ .text_html = {} } },
|
||||||
|
.{ "table", .{ .text_html = {} } },
|
||||||
|
.{ "a", .{ .text_html = {} } },
|
||||||
|
.{ "style", .{ .text_html = {} } },
|
||||||
|
.{ "title", .{ .text_html = {} } },
|
||||||
|
.{ "b", .{ .text_html = {} } },
|
||||||
|
.{ "body", .{ .text_html = {} } },
|
||||||
|
.{ "br", .{ .text_html = {} } },
|
||||||
|
.{ "p", .{ .text_html = {} } },
|
||||||
|
.{ "!--", .{ .text_html = {} } },
|
||||||
|
.{ "xml", .{ .text_xml = {} } },
|
||||||
|
};
|
||||||
|
inline for (known_prefixes) |kp| {
|
||||||
|
const known_prefix = kp.@"0";
|
||||||
|
if (std.mem.startsWith(u8, prefix, known_prefix) and prefix.len > known_prefix.len) {
|
||||||
|
const next = prefix[known_prefix.len];
|
||||||
|
// a "tag-terminating-byte"
|
||||||
|
if (next == ' ' or next == '>') {
|
||||||
|
return .{ .content_type = kp.@"1" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isHTML(self: *const Mime) bool {
|
||||||
|
return self.content_type == .text_html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we expect value to be lowercase
|
||||||
|
fn parseContentType(value: []const u8) !struct { ContentType, usize } {
|
||||||
|
const end = std.mem.indexOfScalarPos(u8, value, 0, ';') orelse value.len;
|
||||||
|
const type_name = trimRight(value[0..end]);
|
||||||
|
const attribute_start = end + 1;
|
||||||
|
|
||||||
|
if (std.meta.stringToEnum(enum {
|
||||||
|
@"text/xml",
|
||||||
|
@"text/html",
|
||||||
|
@"text/css",
|
||||||
|
@"text/plain",
|
||||||
|
|
||||||
|
@"text/javascript",
|
||||||
|
@"application/javascript",
|
||||||
|
@"application/x-javascript",
|
||||||
|
|
||||||
|
@"image/jpeg",
|
||||||
|
@"image/png",
|
||||||
|
@"image/gif",
|
||||||
|
@"image/webp",
|
||||||
|
|
||||||
|
@"application/json",
|
||||||
|
}, type_name)) |known_type| {
|
||||||
|
const ct: ContentType = switch (known_type) {
|
||||||
|
.@"text/xml" => .{ .text_xml = {} },
|
||||||
|
.@"text/html" => .{ .text_html = {} },
|
||||||
|
.@"text/javascript", .@"application/javascript", .@"application/x-javascript" => .{ .text_javascript = {} },
|
||||||
|
.@"text/plain" => .{ .text_plain = {} },
|
||||||
|
.@"text/css" => .{ .text_css = {} },
|
||||||
|
.@"image/jpeg" => .{ .image_jpeg = {} },
|
||||||
|
.@"image/png" => .{ .image_png = {} },
|
||||||
|
.@"image/gif" => .{ .image_gif = {} },
|
||||||
|
.@"image/webp" => .{ .image_webp = {} },
|
||||||
|
.@"application/json" => .{ .application_json = {} },
|
||||||
|
};
|
||||||
|
return .{ ct, attribute_start };
|
||||||
|
}
|
||||||
|
|
||||||
|
const separator = std.mem.indexOfScalarPos(u8, type_name, 0, '/') orelse return error.Invalid;
|
||||||
|
|
||||||
|
const main_type = value[0..separator];
|
||||||
|
const sub_type = trimRight(value[separator + 1 .. end]);
|
||||||
|
|
||||||
|
if (main_type.len == 0 or validType(main_type) == false) {
|
||||||
|
return error.Invalid;
|
||||||
|
}
|
||||||
|
if (sub_type.len == 0 or validType(sub_type) == false) {
|
||||||
|
return error.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{ .{ .other = .{
|
||||||
|
.type = main_type,
|
||||||
|
.sub_type = sub_type,
|
||||||
|
} }, attribute_start };
|
||||||
|
}
|
||||||
|
|
||||||
|
const VALID_CODEPOINTS = blk: {
|
||||||
|
var v: [256]bool = undefined;
|
||||||
|
for (0..256) |i| {
|
||||||
|
v[i] = std.ascii.isAlphanumeric(i);
|
||||||
|
}
|
||||||
|
for ("!#$%&\\*+-.^'_`|~") |b| {
|
||||||
|
v[b] = true;
|
||||||
|
}
|
||||||
|
break :blk v;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn validType(value: []const u8) bool {
|
||||||
|
for (value) |b| {
|
||||||
|
if (VALID_CODEPOINTS[b] == false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trimLeft(s: []const u8) []const u8 {
|
||||||
|
return std.mem.trimLeft(u8, s, &std.ascii.whitespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trimRight(s: []const u8) []const u8 {
|
||||||
|
return std.mem.trimRight(u8, s, &std.ascii.whitespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
test "Mime: invalid" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
const invalids = [_][]const u8{
|
||||||
|
"",
|
||||||
|
"text",
|
||||||
|
"text /html",
|
||||||
|
"text/ html",
|
||||||
|
"text / html",
|
||||||
|
"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; = ",
|
||||||
|
"text/html;=",
|
||||||
|
"text/html; charset=\"\"",
|
||||||
|
"text/html; charset=\"",
|
||||||
|
"text/html; charset=\"\\",
|
||||||
|
"text/html;\"",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (valid_with_malformed_params) |input| {
|
||||||
|
const mutable_input = try testing.arena_allocator.dupe(u8, input);
|
||||||
|
const mime = try Mime.parse(mutable_input);
|
||||||
|
try testing.expectEqual(.text_html, std.meta.activeTag(mime.content_type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Mime: parse common" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
try expect(.{ .content_type = .{ .text_xml = {} } }, "text/xml");
|
||||||
|
try expect(.{ .content_type = .{ .text_html = {} } }, "text/html");
|
||||||
|
try expect(.{ .content_type = .{ .text_plain = {} } }, "text/plain");
|
||||||
|
|
||||||
|
try expect(.{ .content_type = .{ .text_xml = {} } }, "text/xml;");
|
||||||
|
try expect(.{ .content_type = .{ .text_html = {} } }, "text/html;");
|
||||||
|
try expect(.{ .content_type = .{ .text_plain = {} } }, "text/plain;");
|
||||||
|
|
||||||
|
try expect(.{ .content_type = .{ .text_xml = {} } }, " \ttext/xml");
|
||||||
|
try expect(.{ .content_type = .{ .text_html = {} } }, "text/html ");
|
||||||
|
try expect(.{ .content_type = .{ .text_plain = {} } }, "text/plain \t\t");
|
||||||
|
|
||||||
|
try expect(.{ .content_type = .{ .text_xml = {} } }, "TEXT/xml");
|
||||||
|
try expect(.{ .content_type = .{ .text_html = {} } }, "text/Html");
|
||||||
|
try expect(.{ .content_type = .{ .text_plain = {} } }, "TEXT/PLAIN");
|
||||||
|
|
||||||
|
try expect(.{ .content_type = .{ .text_xml = {} } }, " TeXT/xml");
|
||||||
|
try expect(.{ .content_type = .{ .text_html = {} } }, "teXt/HtML ;");
|
||||||
|
try expect(.{ .content_type = .{ .text_plain = {} } }, "tExT/PlAiN;");
|
||||||
|
|
||||||
|
try expect(.{ .content_type = .{ .text_javascript = {} } }, "text/javascript");
|
||||||
|
try expect(.{ .content_type = .{ .text_javascript = {} } }, "Application/JavaScript");
|
||||||
|
try expect(.{ .content_type = .{ .text_javascript = {} } }, "application/x-javascript");
|
||||||
|
|
||||||
|
try expect(.{ .content_type = .{ .application_json = {} } }, "application/json");
|
||||||
|
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" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
const text_csv = Expectation{
|
||||||
|
.content_type = .{ .other = .{ .type = "text", .sub_type = "csv" } },
|
||||||
|
};
|
||||||
|
try expect(text_csv, "text/csv");
|
||||||
|
try expect(text_csv, "text/csv;");
|
||||||
|
try expect(text_csv, " text/csv\t ");
|
||||||
|
try expect(text_csv, " text/csv\t ;");
|
||||||
|
|
||||||
|
try expect(
|
||||||
|
.{ .content_type = .{ .other = .{ .type = "text", .sub_type = "csv" } } },
|
||||||
|
"Text/CSV",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Mime: parse charset" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
try expect(.{
|
||||||
|
.content_type = .{ .text_xml = {} },
|
||||||
|
.charset = "utf-8",
|
||||||
|
.params = "charset=utf-8",
|
||||||
|
}, "text/xml; charset=utf-8");
|
||||||
|
|
||||||
|
try expect(.{
|
||||||
|
.content_type = .{ .text_xml = {} },
|
||||||
|
.charset = "utf-8",
|
||||||
|
.params = "charset=\"utf-8\"",
|
||||||
|
}, "text/xml;charset=\"UTF-8\"");
|
||||||
|
|
||||||
|
try expect(.{
|
||||||
|
.content_type = .{ .text_html = {} },
|
||||||
|
.charset = "iso-8859-1",
|
||||||
|
.params = "charset=\"iso-8859-1\"",
|
||||||
|
}, "text/html; charset=\"iso-8859-1\"");
|
||||||
|
|
||||||
|
try expect(.{
|
||||||
|
.content_type = .{ .text_html = {} },
|
||||||
|
.charset = "iso-8859-1",
|
||||||
|
.params = "charset=\"iso-8859-1\"",
|
||||||
|
}, "text/html; charset=\"ISO-8859-1\"");
|
||||||
|
|
||||||
|
try expect(.{
|
||||||
|
.content_type = .{ .text_xml = {} },
|
||||||
|
.charset = "custom-non-standard-charset-value",
|
||||||
|
.params = "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" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
const assert = struct {
|
||||||
|
fn assert(expected: bool, input: []const u8) !void {
|
||||||
|
const mutable_input = try testing.arena_allocator.dupe(u8, input);
|
||||||
|
var mime = try Mime.parse(mutable_input);
|
||||||
|
try testing.expectEqual(expected, mime.isHTML());
|
||||||
|
}
|
||||||
|
}.assert;
|
||||||
|
try assert(true, "text/html");
|
||||||
|
try assert(true, "text/html;");
|
||||||
|
try assert(true, "text/html; charset=utf-8");
|
||||||
|
try assert(false, "text/htm"); // htm not html
|
||||||
|
try assert(false, "text/plain");
|
||||||
|
try assert(false, "over/9000");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Mime: sniff" {
|
||||||
|
try testing.expectEqual(null, Mime.sniff(""));
|
||||||
|
try testing.expectEqual(null, Mime.sniff("<htm"));
|
||||||
|
try testing.expectEqual(null, Mime.sniff("<html!"));
|
||||||
|
try testing.expectEqual(null, Mime.sniff("<a_"));
|
||||||
|
try testing.expectEqual(null, Mime.sniff("<!doctype html"));
|
||||||
|
try testing.expectEqual(null, Mime.sniff("<!doctype html>"));
|
||||||
|
try testing.expectEqual(null, Mime.sniff("\n <!doctype html>"));
|
||||||
|
try testing.expectEqual(null, Mime.sniff("\n \t <font/>"));
|
||||||
|
|
||||||
|
const expectHTML = struct {
|
||||||
|
fn expect(input: []const u8) !void {
|
||||||
|
try testing.expectEqual(.text_html, std.meta.activeTag(Mime.sniff(input).?.content_type));
|
||||||
|
}
|
||||||
|
}.expect;
|
||||||
|
|
||||||
|
try expectHTML("<!doctype html ");
|
||||||
|
try expectHTML("\n \t <!DOCTYPE HTML ");
|
||||||
|
|
||||||
|
try expectHTML("<html ");
|
||||||
|
try expectHTML("\n \t <HtmL> even more stufff");
|
||||||
|
|
||||||
|
try expectHTML("<script>");
|
||||||
|
try expectHTML("\n \t <SCRIpt >alert(document.cookies)</script>");
|
||||||
|
|
||||||
|
try expectHTML("<iframe>");
|
||||||
|
try expectHTML(" \t <ifRAME >");
|
||||||
|
|
||||||
|
try expectHTML("<h1>");
|
||||||
|
try expectHTML(" <H1>");
|
||||||
|
|
||||||
|
try expectHTML("<div>");
|
||||||
|
try expectHTML("\n\r\r <DiV>");
|
||||||
|
|
||||||
|
try expectHTML("<font>");
|
||||||
|
try expectHTML(" <fonT>");
|
||||||
|
|
||||||
|
try expectHTML("<table>");
|
||||||
|
try expectHTML("\t\t<TAblE>");
|
||||||
|
|
||||||
|
try expectHTML("<a>");
|
||||||
|
try expectHTML("\n\n<A>");
|
||||||
|
|
||||||
|
try expectHTML("<style>");
|
||||||
|
try expectHTML(" \n\t <STyLE>");
|
||||||
|
|
||||||
|
try expectHTML("<title>");
|
||||||
|
try expectHTML(" \n\t <TITLE>");
|
||||||
|
|
||||||
|
try expectHTML("<b>");
|
||||||
|
try expectHTML(" \n\t <B>");
|
||||||
|
|
||||||
|
try expectHTML("<body>");
|
||||||
|
try expectHTML(" \n\t <BODY>");
|
||||||
|
|
||||||
|
try expectHTML("<br>");
|
||||||
|
try expectHTML(" \n\t <BR>");
|
||||||
|
|
||||||
|
try expectHTML("<p>");
|
||||||
|
try expectHTML(" \n\t <P>");
|
||||||
|
|
||||||
|
try expectHTML("<!-->");
|
||||||
|
try expectHTML(" \n\t <!-->");
|
||||||
|
}
|
||||||
|
|
||||||
|
const Expectation = struct {
|
||||||
|
content_type: Mime.ContentType,
|
||||||
|
params: []const u8 = "",
|
||||||
|
charset: ?[]const u8 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn expect(expected: Expectation, input: []const u8) !void {
|
||||||
|
const mutable_input = try testing.arena_allocator.dupe(u8, input);
|
||||||
|
|
||||||
|
const actual = try Mime.parse(mutable_input);
|
||||||
|
try testing.expectEqual(
|
||||||
|
std.meta.activeTag(expected.content_type),
|
||||||
|
std.meta.activeTag(actual.content_type),
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (expected.content_type) {
|
||||||
|
.other => |e| {
|
||||||
|
const a = actual.content_type.other;
|
||||||
|
try testing.expectEqual(e.type, a.type);
|
||||||
|
try testing.expectEqual(e.sub_type, a.sub_type);
|
||||||
|
},
|
||||||
|
else => {}, // already asserted above
|
||||||
|
}
|
||||||
|
|
||||||
|
try testing.expectEqual(expected.params, actual.params);
|
||||||
|
|
||||||
|
if (expected.charset) |ec| {
|
||||||
|
// We remove the null characters for testing purposes here.
|
||||||
|
try testing.expectEqual(ec, actual.charsetString());
|
||||||
|
} else {
|
||||||
|
const m: Mime = .unknown;
|
||||||
|
try testing.expectEqual(m.charsetStringZ(), actual.charsetStringZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
3225
src/browser/Page.zig
Normal file
3225
src/browser/Page.zig
Normal file
File diff suppressed because it is too large
Load Diff
1003
src/browser/Robots.zig
Normal file
1003
src/browser/Robots.zig
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,163 +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 Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const Scheduler = @This();
|
|
||||||
|
|
||||||
high_priority: Queue,
|
|
||||||
|
|
||||||
// For repeating tasks. We only want to run these if there are other things to
|
|
||||||
// do. We don't, for example, want a window.setInterval or the page.runMicrotasks
|
|
||||||
// to block the page.wait.
|
|
||||||
low_priority: Queue,
|
|
||||||
|
|
||||||
// we expect allocator to be the page arena, hence we never call high_priority.deinit
|
|
||||||
pub fn init(allocator: Allocator) Scheduler {
|
|
||||||
return .{
|
|
||||||
.high_priority = Queue.init(allocator, {}),
|
|
||||||
.low_priority = Queue.init(allocator, {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(self: *Scheduler) void {
|
|
||||||
// Our allocator is the page arena, it's been reset. We cannot use
|
|
||||||
// clearAndRetainCapacity, since that space is no longer ours
|
|
||||||
self.high_priority.clearAndFree();
|
|
||||||
self.low_priority.clearAndFree();
|
|
||||||
}
|
|
||||||
|
|
||||||
const AddOpts = struct {
|
|
||||||
name: []const u8 = "",
|
|
||||||
low_priority: bool = false,
|
|
||||||
};
|
|
||||||
pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: AddOpts) !void {
|
|
||||||
var low_priority = opts.low_priority;
|
|
||||||
if (ms > 5_000) {
|
|
||||||
// we don't want tasks in the far future to block page.wait from
|
|
||||||
// completing. However, if page.wait is called multiple times (maybe
|
|
||||||
// a CDP driver is wait for something to happen), then we do want
|
|
||||||
// to [eventually] run these when their time is up.
|
|
||||||
low_priority = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var q = if (low_priority) &self.low_priority else &self.high_priority;
|
|
||||||
return q.add(.{
|
|
||||||
.ms = std.time.milliTimestamp() + ms,
|
|
||||||
.ctx = ctx,
|
|
||||||
.func = func,
|
|
||||||
.name = opts.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(self: *Scheduler) !?i32 {
|
|
||||||
_ = try self.runQueue(&self.low_priority);
|
|
||||||
return self.runQueue(&self.high_priority);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn runQueue(self: *Scheduler, queue: *Queue) !?i32 {
|
|
||||||
// this is O(1)
|
|
||||||
if (queue.count() == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = std.time.milliTimestamp();
|
|
||||||
|
|
||||||
var next = queue.peek();
|
|
||||||
while (next) |task| {
|
|
||||||
const time_to_next = task.ms - now;
|
|
||||||
if (time_to_next > 0) {
|
|
||||||
// @intCast is petty safe since we limit tasks to just 5 seconds
|
|
||||||
// in the future
|
|
||||||
return @intCast(time_to_next);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (task.func(task.ctx)) |repeat_delay| {
|
|
||||||
// if we do (now + 0) then our WHILE loop will run endlessly.
|
|
||||||
// no task should ever return 0
|
|
||||||
std.debug.assert(repeat_delay != 0);
|
|
||||||
|
|
||||||
var copy = task;
|
|
||||||
copy.ms = now + repeat_delay;
|
|
||||||
try self.low_priority.add(copy);
|
|
||||||
}
|
|
||||||
_ = queue.remove();
|
|
||||||
next = queue.peek();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Task = struct {
|
|
||||||
ms: i64,
|
|
||||||
func: Func,
|
|
||||||
ctx: *anyopaque,
|
|
||||||
name: []const u8,
|
|
||||||
|
|
||||||
const Func = *const fn (ctx: *anyopaque) ?u32;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Queue = std.PriorityQueue(Task, void, struct {
|
|
||||||
fn compare(_: void, a: Task, b: Task) std.math.Order {
|
|
||||||
return std.math.order(a.ms, b.ms);
|
|
||||||
}
|
|
||||||
}.compare);
|
|
||||||
|
|
||||||
const testing = @import("../testing.zig");
|
|
||||||
test "Scheduler" {
|
|
||||||
defer testing.reset();
|
|
||||||
|
|
||||||
var task = TestTask{ .allocator = testing.arena_allocator };
|
|
||||||
|
|
||||||
var s = Scheduler.init(testing.arena_allocator);
|
|
||||||
try testing.expectEqual(null, s.run());
|
|
||||||
try testing.expectEqual(0, task.calls.items.len);
|
|
||||||
|
|
||||||
try s.add(&task, TestTask.run1, 3, .{});
|
|
||||||
|
|
||||||
try testing.expectDelta(3, try s.run(), 1);
|
|
||||||
try testing.expectEqual(0, task.calls.items.len);
|
|
||||||
|
|
||||||
std.Thread.sleep(std.time.ns_per_ms * 5);
|
|
||||||
try testing.expectEqual(null, s.run());
|
|
||||||
try testing.expectEqualSlices(u32, &.{1}, task.calls.items);
|
|
||||||
|
|
||||||
try s.add(&task, TestTask.run2, 3, .{});
|
|
||||||
try s.add(&task, TestTask.run1, 2, .{});
|
|
||||||
|
|
||||||
std.Thread.sleep(std.time.ns_per_ms * 5);
|
|
||||||
try testing.expectDelta(null, try s.run(), 1);
|
|
||||||
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
|
|
||||||
}
|
|
||||||
|
|
||||||
const TestTask = struct {
|
|
||||||
allocator: Allocator,
|
|
||||||
calls: std.ArrayListUnmanaged(u32) = .{},
|
|
||||||
|
|
||||||
fn run1(ctx: *anyopaque) ?u32 {
|
|
||||||
var self: *TestTask = @ptrCast(@alignCast(ctx));
|
|
||||||
self.calls.append(self.allocator, 1) catch unreachable;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run2(ctx: *anyopaque) ?u32 {
|
|
||||||
var self: *TestTask = @ptrCast(@alignCast(ctx));
|
|
||||||
self.calls.append(self.allocator, 2) catch unreachable;
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
384
src/browser/Session.zig
Normal file
384
src/browser/Session.zig
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const lp = @import("lightpanda");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
const log = @import("../log.zig");
|
||||||
|
|
||||||
|
const js = @import("js/js.zig");
|
||||||
|
const storage = @import("webapi/storage/storage.zig");
|
||||||
|
const Navigation = @import("webapi/navigation/Navigation.zig");
|
||||||
|
const History = @import("webapi/History.zig");
|
||||||
|
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
const Browser = @import("Browser.zig");
|
||||||
|
const Notification = @import("../Notification.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
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
|
||||||
|
// deinit a page before running another one.
|
||||||
|
const Session = @This();
|
||||||
|
|
||||||
|
browser: *Browser,
|
||||||
|
notification: *Notification,
|
||||||
|
|
||||||
|
// Used to create our Inspector and in the BrowserContext.
|
||||||
|
arena: Allocator,
|
||||||
|
|
||||||
|
cookie_jar: storage.Cookie.Jar,
|
||||||
|
storage_shed: storage.Shed,
|
||||||
|
|
||||||
|
history: History,
|
||||||
|
navigation: Navigation,
|
||||||
|
|
||||||
|
page: ?Page,
|
||||||
|
|
||||||
|
frame_id_gen: u32,
|
||||||
|
|
||||||
|
pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void {
|
||||||
|
const allocator = browser.app.allocator;
|
||||||
|
const arena = try browser.arena_pool.acquire();
|
||||||
|
errdefer browser.arena_pool.release(arena);
|
||||||
|
|
||||||
|
self.* = .{
|
||||||
|
.page = null,
|
||||||
|
.arena = arena,
|
||||||
|
.history = .{},
|
||||||
|
.frame_id_gen = 0,
|
||||||
|
// The prototype (EventTarget) for Navigation is created when a Page is created.
|
||||||
|
.navigation = .{ ._proto = undefined },
|
||||||
|
.storage_shed = .{},
|
||||||
|
.browser = browser,
|
||||||
|
.notification = notification,
|
||||||
|
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Session) void {
|
||||||
|
if (self.page != null) {
|
||||||
|
self.removePage();
|
||||||
|
}
|
||||||
|
const browser = self.browser;
|
||||||
|
|
||||||
|
self.cookie_jar.deinit();
|
||||||
|
self.storage_shed.deinit(browser.app.allocator);
|
||||||
|
browser.arena_pool.release(self.arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: the caller is not the owner of the returned value,
|
||||||
|
// the pointer on Page is just returned as a convenience
|
||||||
|
pub fn createPage(self: *Session) !*Page {
|
||||||
|
lp.assert(self.page == null, "Session.createPage - page not null", .{});
|
||||||
|
|
||||||
|
self.page = @as(Page, undefined);
|
||||||
|
const page = &self.page.?;
|
||||||
|
try Page.init(page, self.nextFrameId(), self, null);
|
||||||
|
|
||||||
|
// Creates a new NavigationEventTarget for this page.
|
||||||
|
try self.navigation.onNewPage(page);
|
||||||
|
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
log.debug(.browser, "create page", .{});
|
||||||
|
}
|
||||||
|
// start JS env
|
||||||
|
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
||||||
|
self.notification.dispatch(.page_created, page);
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
self.notification.dispatch(.page_remove, .{});
|
||||||
|
lp.assert(self.page != null, "Session.removePage - page is null", .{});
|
||||||
|
|
||||||
|
self.page.?.deinit();
|
||||||
|
self.page = null;
|
||||||
|
|
||||||
|
self.navigation.onRemovePage();
|
||||||
|
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
log.debug(.browser, "remove page", .{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replacePage(self: *Session) !*Page {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
log.debug(.browser, "replace page", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.assert(self.page != null, "Session.replacePage null page", .{});
|
||||||
|
|
||||||
|
var current = self.page.?;
|
||||||
|
const frame_id = current._frame_id;
|
||||||
|
const parent = current.parent;
|
||||||
|
current.deinit();
|
||||||
|
|
||||||
|
self.browser.env.memoryPressureNotification(.moderate);
|
||||||
|
|
||||||
|
self.page = @as(Page, undefined);
|
||||||
|
const page = &self.page.?;
|
||||||
|
try Page.init(page, frame_id, self, parent);
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn currentPage(self: *Session) ?*Page {
|
||||||
|
return &(self.page orelse return null);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const WaitResult = enum {
|
||||||
|
done,
|
||||||
|
no_page,
|
||||||
|
cdp_socket,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn findPage(self: *Session, frame_id: u32) ?*Page {
|
||||||
|
const page = self.currentPage() orelse return null;
|
||||||
|
return if (page._frame_id == frame_id) page else null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
||||||
|
var page = &(self.page orelse return .no_page);
|
||||||
|
while (true) {
|
||||||
|
const wait_result = self._wait(page, wait_ms) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.JsError => {}, // already logged (with hopefully more context)
|
||||||
|
else => log.err(.browser, "session wait", .{
|
||||||
|
.err = err,
|
||||||
|
.url = page.url,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
return .done;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (wait_result) {
|
||||||
|
.done => {
|
||||||
|
if (page._queued_navigation == null) {
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
page = self.processScheduledNavigation(page) catch return .done;
|
||||||
|
},
|
||||||
|
else => |result| return result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
||||||
|
var timer = try std.time.Timer.start();
|
||||||
|
var ms_remaining = wait_ms;
|
||||||
|
|
||||||
|
const browser = self.browser;
|
||||||
|
var http_client = browser.http_client;
|
||||||
|
|
||||||
|
// I'd like the page to know NOTHING about cdp_socket / CDP, but the
|
||||||
|
// fact is that the behavior of wait changes depending on whether or
|
||||||
|
// not we're using CDP.
|
||||||
|
// If we aren't using CDP, as soon as we think there's nothing left
|
||||||
|
// to do, we can exit - we'de done.
|
||||||
|
// But if we are using CDP, we should wait for the whole `wait_ms`
|
||||||
|
// because the http_click.tick() also monitors the CDP socket. And while
|
||||||
|
// we could let CDP poll http (like it does for HTTP requests), the fact
|
||||||
|
// is that we know more about the timing of stuff (e.g. how long to
|
||||||
|
// poll/sleep) in the page.
|
||||||
|
const exit_when_done = http_client.cdp_client == null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
switch (page._parse_state) {
|
||||||
|
.pre, .raw, .text, .image => {
|
||||||
|
// The main page hasn't started/finished navigating.
|
||||||
|
// There's no JS to run, and no reason to run the scheduler.
|
||||||
|
if (http_client.active == 0 and exit_when_done) {
|
||||||
|
// haven't started navigating, I guess.
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
// Either we have active http connections, or we're in CDP
|
||||||
|
// mode with an extra socket. Either way, we're waiting
|
||||||
|
// for http traffic
|
||||||
|
if (try http_client.tick(@intCast(ms_remaining)) == .cdp_socket) {
|
||||||
|
// exit_when_done is explicitly set when there isn't
|
||||||
|
// an extra socket, so it should not be possibl to
|
||||||
|
// get an cdp_socket message when exit_when_done
|
||||||
|
// is true.
|
||||||
|
if (IS_DEBUG) {
|
||||||
|
std.debug.assert(exit_when_done == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// data on a socket we aren't handling, return to caller
|
||||||
|
return .cdp_socket;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.html, .complete => {
|
||||||
|
if (page._queued_navigation != null) {
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The HTML page was parsed. We now either have JS scripts to
|
||||||
|
// download, or scheduled tasks to execute, or both.
|
||||||
|
|
||||||
|
// scheduler.run could trigger new http transfers, so do not
|
||||||
|
// store http_client.active BEFORE this call and then use
|
||||||
|
// it AFTER.
|
||||||
|
const ms_to_next_task = try browser.runMacrotasks();
|
||||||
|
|
||||||
|
// Each call to this runs scheduled load events.
|
||||||
|
try page.dispatchLoad();
|
||||||
|
|
||||||
|
const http_active = http_client.active;
|
||||||
|
const total_network_activity = http_active + http_client.intercepted;
|
||||||
|
if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
|
||||||
|
page.notifyNetworkAlmostIdle();
|
||||||
|
}
|
||||||
|
if (page._notified_network_idle.check(total_network_activity == 0)) {
|
||||||
|
page.notifyNetworkIdle();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (http_active == 0 and exit_when_done) {
|
||||||
|
// we don't need to consider http_client.intercepted here
|
||||||
|
// because exit_when_done is true, and that can only be
|
||||||
|
// the case when interception isn't possible.
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(http_client.intercepted == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ms: u64 = ms_to_next_task orelse blk: {
|
||||||
|
if (wait_ms - ms_remaining < 100) {
|
||||||
|
if (comptime builtin.is_test) {
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
// Look, we want to exit ASAP, but we don't want
|
||||||
|
// to exit so fast that we've run none of the
|
||||||
|
// background jobs.
|
||||||
|
break :blk 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (browser.hasBackgroundTasks()) {
|
||||||
|
// _we_ have nothing to run, but v8 is working on
|
||||||
|
// background tasks. We'll wait for them.
|
||||||
|
browser.waitForBackgroundTasks();
|
||||||
|
break :blk 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No http transfers, no cdp extra socket, no
|
||||||
|
// scheduled tasks, we're done.
|
||||||
|
return .done;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ms > ms_remaining) {
|
||||||
|
// Same as above, except we have a scheduled task,
|
||||||
|
// it just happens to be too far into the future
|
||||||
|
// compared to how long we were told to wait.
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a task to run in the not-so-distant future.
|
||||||
|
// You might think we can just sleep until that task is
|
||||||
|
// ready, but we should continue to run lowPriority tasks
|
||||||
|
// in the meantime, and that could unblock things. So
|
||||||
|
// we'll just sleep for a bit, and then restart our wait
|
||||||
|
// loop to see if anything new can be processed.
|
||||||
|
std.Thread.sleep(std.time.ns_per_ms * @as(u64, @intCast(@min(ms, 20))));
|
||||||
|
} else {
|
||||||
|
// We're here because we either have active HTTP
|
||||||
|
// connections, or exit_when_done == false (aka, there's
|
||||||
|
// an cdp_socket registered with the http client).
|
||||||
|
// We should continue to run lowPriority tasks, so we
|
||||||
|
// minimize how long we'll poll for network I/O.
|
||||||
|
var ms_to_wait = @min(200, ms_to_next_task orelse 200);
|
||||||
|
if (ms_to_wait > 10 and browser.hasBackgroundTasks()) {
|
||||||
|
// if we have background tasks, we don't want to wait too
|
||||||
|
// long for a message from the client. We want to go back
|
||||||
|
// to the top of the loop and run macrotasks.
|
||||||
|
ms_to_wait = 10;
|
||||||
|
}
|
||||||
|
if (try http_client.tick(@min(ms_remaining, ms_to_wait)) == .cdp_socket) {
|
||||||
|
// data on a socket we aren't handling, return to caller
|
||||||
|
return .cdp_socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.err => |err| {
|
||||||
|
page._parse_state = .{ .raw_done = @errorName(err) };
|
||||||
|
return err;
|
||||||
|
},
|
||||||
|
.raw_done => {
|
||||||
|
if (exit_when_done) {
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
// we _could_ http_client.tick(ms_to_wait), but this has
|
||||||
|
// the same result, and I feel is more correct.
|
||||||
|
return .no_page;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const ms_elapsed = timer.lap() / 1_000_000;
|
||||||
|
if (ms_elapsed >= ms_remaining) {
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
ms_remaining -= @intCast(ms_elapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processScheduledNavigation(self: *Session, current_page: *Page) !*Page {
|
||||||
|
const browser = self.browser;
|
||||||
|
|
||||||
|
const qn = current_page._queued_navigation.?;
|
||||||
|
// take ownership of the page's queued navigation
|
||||||
|
current_page._queued_navigation = null;
|
||||||
|
defer browser.arena_pool.release(qn.arena);
|
||||||
|
|
||||||
|
const frame_id, const parent = blk: {
|
||||||
|
const page = &self.page.?;
|
||||||
|
const frame_id = page._frame_id;
|
||||||
|
const parent = page.parent;
|
||||||
|
|
||||||
|
browser.http_client.abort();
|
||||||
|
self.removePage();
|
||||||
|
|
||||||
|
break :blk .{ frame_id, parent };
|
||||||
|
};
|
||||||
|
|
||||||
|
self.page = @as(Page, undefined);
|
||||||
|
const page = &self.page.?;
|
||||||
|
try Page.init(page, frame_id, self, parent);
|
||||||
|
|
||||||
|
// Creates a new NavigationEventTarget for this page.
|
||||||
|
try self.navigation.onNewPage(page);
|
||||||
|
|
||||||
|
// start JS env
|
||||||
|
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
||||||
|
self.notification.dispatch(.page_created, page);
|
||||||
|
|
||||||
|
page.navigate(qn.url, qn.opts) catch |err| {
|
||||||
|
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nextFrameId(self: *Session) u32 {
|
||||||
|
const id = self.frame_id_gen +% 1;
|
||||||
|
self.frame_id_gen = id;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const log = @import("../log.zig");
|
|
||||||
const parser = @import("netsurf.zig");
|
|
||||||
const collection = @import("dom/html_collection.zig");
|
|
||||||
|
|
||||||
const Page = @import("page.zig").Page;
|
|
||||||
|
|
||||||
const SlotChangeMonitor = @This();
|
|
||||||
|
|
||||||
page: *Page,
|
|
||||||
event_node: parser.EventNode,
|
|
||||||
slots_changed: std.ArrayList(*parser.Slot),
|
|
||||||
|
|
||||||
// Monitors the document in order to trigger slotchange events.
|
|
||||||
pub fn init(page: *Page) !*SlotChangeMonitor {
|
|
||||||
// on the heap, we need a stable address for event_node
|
|
||||||
const self = try page.arena.create(SlotChangeMonitor);
|
|
||||||
self.* = .{
|
|
||||||
.page = page,
|
|
||||||
.slots_changed = .empty,
|
|
||||||
.event_node = .{ .func = mutationCallback },
|
|
||||||
};
|
|
||||||
const root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document));
|
|
||||||
|
|
||||||
_ = try parser.eventTargetAddEventListener(
|
|
||||||
parser.toEventTarget(parser.Node, root),
|
|
||||||
"DOMNodeInserted",
|
|
||||||
&self.event_node,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
_ = try parser.eventTargetAddEventListener(
|
|
||||||
parser.toEventTarget(parser.Node, root),
|
|
||||||
"DOMNodeRemoved",
|
|
||||||
&self.event_node,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
_ = try parser.eventTargetAddEventListener(
|
|
||||||
parser.toEventTarget(parser.Node, root),
|
|
||||||
"DOMAttrModified",
|
|
||||||
&self.event_node,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given a element, finds its slot, if any.
|
|
||||||
pub fn findSlot(element: *parser.Element, page: *const Page) !?*parser.Slot {
|
|
||||||
const target_name = (try parser.elementGetAttribute(element, "slot")) orelse return null;
|
|
||||||
return findNamedSlot(element, target_name, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given an element and a name, find the slo, if any. This is only useful for
|
|
||||||
// MutationEvents where findSlot is unreliable because parser.elementGetAttribute(element, "slot")
|
|
||||||
// could return the new or old value.
|
|
||||||
fn findNamedSlot(element: *parser.Element, target_name: []const u8, page: *const Page) !?*parser.Slot {
|
|
||||||
// I believe elements need to be added as direct descendents of the host,
|
|
||||||
// so we don't need to go find the host, we just grab the parent.
|
|
||||||
const host = parser.nodeParentNode(@ptrCast(element)) orelse return null;
|
|
||||||
const state = page.getNodeState(host) orelse return null;
|
|
||||||
const shadow_root = state.shadow_root orelse return null;
|
|
||||||
|
|
||||||
// if we're here, we found a host, now find the slot
|
|
||||||
var nodes = collection.HTMLCollectionByTagName(
|
|
||||||
@ptrCast(@alignCast(shadow_root.proto)),
|
|
||||||
"slot",
|
|
||||||
.{ .include_root = false },
|
|
||||||
);
|
|
||||||
for (0..1000) |i| {
|
|
||||||
const n = (try nodes.item(@intCast(i))) orelse return null;
|
|
||||||
const slot_name = (try parser.elementGetAttribute(@ptrCast(n), "name")) orelse "";
|
|
||||||
if (std.mem.eql(u8, target_name, slot_name)) {
|
|
||||||
return @ptrCast(n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event callback from the mutation event, signaling either the addition of
|
|
||||||
// a node, removal of a node, or a change in attribute
|
|
||||||
fn mutationCallback(en: *parser.EventNode, event: *parser.Event) void {
|
|
||||||
const mutation_event = parser.eventToMutationEvent(event);
|
|
||||||
const self: *SlotChangeMonitor = @fieldParentPtr("event_node", en);
|
|
||||||
self._mutationCallback(mutation_event) catch |err| {
|
|
||||||
log.err(.web_api, "slot change callback", .{ .err = err });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _mutationCallback(self: *SlotChangeMonitor, event: *parser.MutationEvent) !void {
|
|
||||||
const event_type = parser.eventType(@ptrCast(event));
|
|
||||||
if (std.mem.eql(u8, event_type, "DOMNodeInserted")) {
|
|
||||||
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
|
|
||||||
return self.nodeAddedOrRemoved(@ptrCast(event_target));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, event_type, "DOMNodeRemoved")) {
|
|
||||||
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
|
|
||||||
return self.nodeAddedOrRemoved(@ptrCast(event_target));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, event_type, "DOMAttrModified")) {
|
|
||||||
const attribute_name = try parser.mutationEventAttributeName(event);
|
|
||||||
if (std.mem.eql(u8, attribute_name, "slot") == false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const new_value = parser.mutationEventNewValue(event);
|
|
||||||
const prev_value = parser.mutationEventPrevValue(event);
|
|
||||||
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
|
|
||||||
return self.nodeAttributeChanged(@ptrCast(event_target), new_value, prev_value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A node was removed or added. If it's an element, and if it has a slot attribute
|
|
||||||
// then we'll dispatch a slotchange event.
|
|
||||||
fn nodeAddedOrRemoved(self: *SlotChangeMonitor, node: *parser.Node) !void {
|
|
||||||
if (parser.nodeType(node) != .element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const el: *parser.Element = @ptrCast(node);
|
|
||||||
if (try findSlot(el, self.page)) |slot| {
|
|
||||||
return self.scheduleSlotChange(slot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// An attribute was modified. If the attribute is "slot", then we'll trigger 1
|
|
||||||
// slotchange for the old slot (if there was one) and 1 slotchange for the new
|
|
||||||
// one (if there is one)
|
|
||||||
fn nodeAttributeChanged(self: *SlotChangeMonitor, node: *parser.Node, new_value: ?[]const u8, prev_value: ?[]const u8) !void {
|
|
||||||
if (parser.nodeType(node) != .element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const el: *parser.Element = @ptrCast(node);
|
|
||||||
if (try findNamedSlot(el, prev_value orelse "", self.page)) |slot| {
|
|
||||||
try self.scheduleSlotChange(slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try findNamedSlot(el, new_value orelse "", self.page)) |slot| {
|
|
||||||
try self.scheduleSlotChange(slot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OK. Our MutationEvent is not a MutationObserver - it's an older, deprecated
|
|
||||||
// API. It gets dispatched in the middle of the change. While I'm sure it has
|
|
||||||
// some rules, from our point of view, it fires too early. DOMAttrModified fires
|
|
||||||
// before the attribute is actually updated and DOMNodeRemoved before the node
|
|
||||||
// is actually removed. This is a problem if the callback will call
|
|
||||||
// `slot.assignedNodes`, since that won't return the new state.
|
|
||||||
// So, we use the page schedule to schedule the dispatching of the slotchange
|
|
||||||
// event.
|
|
||||||
fn scheduleSlotChange(self: *SlotChangeMonitor, slot: *parser.Slot) !void {
|
|
||||||
for (self.slots_changed.items) |changed| {
|
|
||||||
if (slot == changed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.slots_changed.append(self.page.arena, slot);
|
|
||||||
if (self.slots_changed.items.len == 1) {
|
|
||||||
// first item added, schedule the callback
|
|
||||||
try self.page.scheduler.add(self, scheduleCallback, 0, .{ .name = "slot change" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Callback from the schedule. Time to dispatch the slotchange event
|
|
||||||
fn scheduleCallback(ctx: *anyopaque) ?u32 {
|
|
||||||
var self: *SlotChangeMonitor = @ptrCast(@alignCast(ctx));
|
|
||||||
self._scheduleCallback() catch |err| {
|
|
||||||
log.err(.app, "slot change schedule", .{ .err = err });
|
|
||||||
};
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _scheduleCallback(self: *SlotChangeMonitor) !void {
|
|
||||||
for (self.slots_changed.items) |slot| {
|
|
||||||
const event = try parser.eventCreate();
|
|
||||||
defer parser.eventDestroy(event);
|
|
||||||
try parser.eventInit(event, "slotchange", .{});
|
|
||||||
_ = try parser.eventTargetDispatchEvent(
|
|
||||||
parser.toEventTarget(parser.Element, @ptrCast(@alignCast(slot))),
|
|
||||||
event,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
self.slots_changed.clearRetainingCapacity();
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Sometimes we need to extend libdom. For example, its HTMLDocument doesn't
|
|
||||||
// have a readyState. We have a couple different options, such as making the
|
|
||||||
// correction in libdom directly. Another option stems from the fact that every
|
|
||||||
// libdom node has an opaque embedder_data field. This is the struct that we
|
|
||||||
// lazily load into that field.
|
|
||||||
//
|
|
||||||
// It didn't originally start off as a collection of every single extension, but
|
|
||||||
// this quickly proved necessary, since different fields are needed on the same
|
|
||||||
// data at different levels of the prototype chain. This isn't memory efficient.
|
|
||||||
|
|
||||||
const js = @import("js/js.zig");
|
|
||||||
const parser = @import("netsurf.zig");
|
|
||||||
const DataSet = @import("html/DataSet.zig");
|
|
||||||
const ShadowRoot = @import("dom/shadow_root.zig").ShadowRoot;
|
|
||||||
const StyleSheet = @import("cssom/StyleSheet.zig");
|
|
||||||
const CSSStyleDeclaration = @import("cssom/CSSStyleDeclaration.zig");
|
|
||||||
|
|
||||||
// for HTMLScript (but probably needs to be added to more)
|
|
||||||
onload: ?js.Function = null,
|
|
||||||
onerror: ?js.Function = null,
|
|
||||||
|
|
||||||
// for HTMLElement
|
|
||||||
style: CSSStyleDeclaration = .empty,
|
|
||||||
dataset: ?DataSet = null,
|
|
||||||
template_content: ?*parser.DocumentFragment = null,
|
|
||||||
|
|
||||||
// For dom/element
|
|
||||||
shadow_root: ?*ShadowRoot = null,
|
|
||||||
|
|
||||||
// for html/document
|
|
||||||
ready_state: ReadyState = .loading,
|
|
||||||
|
|
||||||
// for html/HTMLStyleElement
|
|
||||||
style_sheet: ?*StyleSheet = null,
|
|
||||||
|
|
||||||
// for dom/document
|
|
||||||
active_element: ?*parser.Element = null,
|
|
||||||
adopted_style_sheets: ?js.Object = null,
|
|
||||||
|
|
||||||
// for HTMLSelectElement
|
|
||||||
// By default, if no option is explicitly selected, the first option should
|
|
||||||
// be selected. However, libdom doesn't do this, and it sets the
|
|
||||||
// selectedIndex to -1, which is a valid value for "nothing selected".
|
|
||||||
// Therefore, when libdom says the selectedIndex == -1, we don't know if
|
|
||||||
// it means that nothing is selected, or if the first option is selected by
|
|
||||||
// default.
|
|
||||||
// There are cases where this won't work, but when selectedIndex is
|
|
||||||
// explicitly set, we set this boolean flag. Then, when we're getting then
|
|
||||||
// selectedIndex, if this flag is == false, which is to say that if
|
|
||||||
// selectedIndex hasn't been explicitly set AND if we have at least 1 option
|
|
||||||
// AND if it isn't a multi select, we can make the 1st item selected by
|
|
||||||
// default (by returning selectedIndex == 0).
|
|
||||||
explicit_index_set: bool = false,
|
|
||||||
|
|
||||||
const ReadyState = enum {
|
|
||||||
loading,
|
|
||||||
interactive,
|
|
||||||
complete,
|
|
||||||
};
|
|
||||||
1327
src/browser/URL.zig
Normal file
1327
src/browser/URL.zig
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,119 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
|
||||||
|
|
||||||
const js = @import("js/js.zig");
|
|
||||||
const State = @import("State.zig");
|
|
||||||
const App = @import("../app.zig").App;
|
|
||||||
const Session = @import("session.zig").Session;
|
|
||||||
const Notification = @import("../notification.zig").Notification;
|
|
||||||
|
|
||||||
const log = @import("../log.zig");
|
|
||||||
const HttpClient = @import("../http/Client.zig");
|
|
||||||
|
|
||||||
// Browser is an instance of the browser.
|
|
||||||
// You can create multiple browser instances.
|
|
||||||
// A browser contains only one session.
|
|
||||||
pub const Browser = struct {
|
|
||||||
env: *js.Env,
|
|
||||||
app: *App,
|
|
||||||
session: ?Session,
|
|
||||||
allocator: Allocator,
|
|
||||||
http_client: *HttpClient,
|
|
||||||
call_arena: ArenaAllocator,
|
|
||||||
page_arena: ArenaAllocator,
|
|
||||||
session_arena: ArenaAllocator,
|
|
||||||
transfer_arena: ArenaAllocator,
|
|
||||||
notification: *Notification,
|
|
||||||
state_pool: std.heap.MemoryPool(State),
|
|
||||||
|
|
||||||
pub fn init(app: *App) !Browser {
|
|
||||||
const allocator = app.allocator;
|
|
||||||
|
|
||||||
const env = try js.Env.init(allocator, &app.platform, .{});
|
|
||||||
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 .{
|
|
||||||
.app = app,
|
|
||||||
.env = env,
|
|
||||||
.session = null,
|
|
||||||
.allocator = allocator,
|
|
||||||
.notification = notification,
|
|
||||||
.http_client = app.http.client,
|
|
||||||
.call_arena = ArenaAllocator.init(allocator),
|
|
||||||
.page_arena = ArenaAllocator.init(allocator),
|
|
||||||
.session_arena = ArenaAllocator.init(allocator),
|
|
||||||
.transfer_arena = ArenaAllocator.init(allocator),
|
|
||||||
.state_pool = std.heap.MemoryPool(State).init(allocator),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Browser) void {
|
|
||||||
self.closeSession();
|
|
||||||
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();
|
|
||||||
self.state_pool.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn newSession(self: *Browser) !*Session {
|
|
||||||
self.closeSession();
|
|
||||||
self.session = @as(Session, undefined);
|
|
||||||
const session = &self.session.?;
|
|
||||||
try Session.init(session, self);
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn closeSession(self: *Browser) void {
|
|
||||||
if (self.session) |*session| {
|
|
||||||
session.deinit();
|
|
||||||
self.session = null;
|
|
||||||
_ = self.session_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
|
||||||
self.env.lowMemoryNotification();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn runMicrotasks(self: *const Browser) void {
|
|
||||||
self.env.runMicrotasks();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn runMessageLoop(self: *const Browser) void {
|
|
||||||
while (self.env.pumpMessageLoop()) {
|
|
||||||
log.debug(.browser, "pumpMessageLoop", .{});
|
|
||||||
}
|
|
||||||
self.env.runIdleTasks();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../testing.zig");
|
|
||||||
test "Browser" {
|
|
||||||
try testing.htmlRunner("browser.html");
|
|
||||||
}
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const builtin = @import("builtin");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
pub const Console = struct {
|
|
||||||
// TODO: configurable writer
|
|
||||||
timers: std.StringHashMapUnmanaged(u32) = .{},
|
|
||||||
counts: std.StringHashMapUnmanaged(u32) = .{},
|
|
||||||
|
|
||||||
pub fn _lp(values: []js.Object, page: *Page) !void {
|
|
||||||
if (values.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _log(values: []js.Object, page: *Page) !void {
|
|
||||||
if (values.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.info(.console, "info", .{ .args = try serializeValues(values, page) });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _info(values: []js.Object, page: *Page) !void {
|
|
||||||
return _log(values, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _debug(values: []js.Object, page: *Page) !void {
|
|
||||||
if (values.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.debug(.console, "debug", .{ .args = try serializeValues(values, page) });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _warn(values: []js.Object, page: *Page) !void {
|
|
||||||
if (values.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.warn(.console, "warn", .{ .args = try serializeValues(values, page) });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _error(values: []js.Object, page: *Page) !void {
|
|
||||||
if (values.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.warn(.console, "error", .{
|
|
||||||
.args = try serializeValues(values, page),
|
|
||||||
.stack = page.stackTrace() catch "???",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _trace(values: []js.Object, page: *Page) !void {
|
|
||||||
if (values.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.debug(.console, "debug", .{
|
|
||||||
.stack = page.js.stackTrace() catch "???",
|
|
||||||
.args = try serializeValues(values, page),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _clear() void {}
|
|
||||||
|
|
||||||
pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void {
|
|
||||||
const label = label_ orelse "default";
|
|
||||||
const gop = try self.counts.getOrPut(page.arena, label);
|
|
||||||
|
|
||||||
var current: u32 = 0;
|
|
||||||
if (gop.found_existing) {
|
|
||||||
current = gop.value_ptr.*;
|
|
||||||
} else {
|
|
||||||
gop.key_ptr.* = try page.arena.dupe(u8, label);
|
|
||||||
}
|
|
||||||
|
|
||||||
const count = current + 1;
|
|
||||||
gop.value_ptr.* = count;
|
|
||||||
|
|
||||||
log.info(.console, "count", .{ .label = label, .count = count });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _countReset(self: *Console, label_: ?[]const u8) !void {
|
|
||||||
const label = label_ orelse "default";
|
|
||||||
const kv = self.counts.fetchRemove(label) orelse {
|
|
||||||
log.info(.console, "invalid counter", .{ .label = label });
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
log.info(.console, "count reset", .{ .label = label, .count = kv.value });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _time(self: *Console, label_: ?[]const u8, page: *Page) !void {
|
|
||||||
const label = label_ orelse "default";
|
|
||||||
const gop = try self.timers.getOrPut(page.arena, label);
|
|
||||||
|
|
||||||
if (gop.found_existing) {
|
|
||||||
log.info(.console, "duplicate timer", .{ .label = label });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
gop.key_ptr.* = try page.arena.dupe(u8, label);
|
|
||||||
gop.value_ptr.* = timestamp();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _timeLog(self: *Console, label_: ?[]const u8) void {
|
|
||||||
const elapsed = timestamp();
|
|
||||||
const label = label_ orelse "default";
|
|
||||||
const start = self.timers.get(label) orelse {
|
|
||||||
log.info(.console, "invalid timer", .{ .label = label });
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
log.info(.console, "timer", .{ .label = label, .elapsed = elapsed - start });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _timeStop(self: *Console, label_: ?[]const u8) void {
|
|
||||||
const elapsed = timestamp();
|
|
||||||
const label = label_ orelse "default";
|
|
||||||
const kv = self.timers.fetchRemove(label) orelse {
|
|
||||||
log.info(.console, "invalid timer", .{ .label = label });
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _assert(assertion: js.Object, values: []js.Object, page: *Page) !void {
|
|
||||||
if (assertion.isTruthy()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var serialized_values: []const u8 = "";
|
|
||||||
if (values.len > 0) {
|
|
||||||
serialized_values = try serializeValues(values, page);
|
|
||||||
}
|
|
||||||
log.info(.console, "assertion failed", .{ .values = serialized_values });
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serializeValues(values: []js.Object, page: *Page) ![]const u8 {
|
|
||||||
if (values.len == 0) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const arena = page.call_arena;
|
|
||||||
const separator = log.separator();
|
|
||||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
|
||||||
|
|
||||||
for (values, 1..) |value, i| {
|
|
||||||
try arr.appendSlice(arena, separator);
|
|
||||||
try arr.writer(arena).print("{d}: ", .{i});
|
|
||||||
const serialized = if (builtin.mode == .Debug) value.toDetailString() else value.toString();
|
|
||||||
try arr.appendSlice(arena, try serialized);
|
|
||||||
}
|
|
||||||
return arr.items;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn timestamp() u32 {
|
|
||||||
return @import("../../datetime.zig").timestamp();
|
|
||||||
}
|
|
||||||
295
src/browser/css/Parser.zig
Normal file
295
src/browser/css/Parser.zig
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
// 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 Tokenizer = @import("Tokenizer.zig");
|
||||||
|
|
||||||
|
pub const Declaration = struct {
|
||||||
|
name: []const u8,
|
||||||
|
value: []const u8,
|
||||||
|
important: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TokenSpan = struct {
|
||||||
|
token: Tokenizer.Token,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TokenStream = struct {
|
||||||
|
tokenizer: Tokenizer,
|
||||||
|
peeked: ?TokenSpan = null,
|
||||||
|
|
||||||
|
fn init(input: []const u8) TokenStream {
|
||||||
|
return .{ .tokenizer = .{ .input = input } };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nextRaw(self: *TokenStream) ?TokenSpan {
|
||||||
|
const start = self.tokenizer.position;
|
||||||
|
const token = self.tokenizer.next() orelse return null;
|
||||||
|
const end = self.tokenizer.position;
|
||||||
|
return .{ .token = token, .start = start, .end = end };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(self: *TokenStream) ?TokenSpan {
|
||||||
|
if (self.peeked) |token| {
|
||||||
|
self.peeked = null;
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
return self.nextRaw();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek(self: *TokenStream) ?TokenSpan {
|
||||||
|
if (self.peeked == null) {
|
||||||
|
self.peeked = self.nextRaw();
|
||||||
|
}
|
||||||
|
return self.peeked;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn parseDeclarationsList(input: []const u8) DeclarationsIterator {
|
||||||
|
return DeclarationsIterator.init(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const DeclarationsIterator = struct {
|
||||||
|
input: []const u8,
|
||||||
|
stream: TokenStream,
|
||||||
|
|
||||||
|
pub fn init(input: []const u8) DeclarationsIterator {
|
||||||
|
return .{
|
||||||
|
.input = input,
|
||||||
|
.stream = TokenStream.init(input),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(self: *DeclarationsIterator) ?Declaration {
|
||||||
|
while (true) {
|
||||||
|
self.skipTriviaAndSemicolons();
|
||||||
|
const peeked = self.stream.peek() orelse return null;
|
||||||
|
|
||||||
|
switch (peeked.token) {
|
||||||
|
.at_keyword => {
|
||||||
|
_ = self.stream.next();
|
||||||
|
self.skipAtRule();
|
||||||
|
},
|
||||||
|
.ident => |name| {
|
||||||
|
_ = self.stream.next();
|
||||||
|
if (self.consumeDeclaration(name)) |declaration| {
|
||||||
|
return declaration;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
_ = self.stream.next();
|
||||||
|
self.skipInvalidDeclaration();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consumeDeclaration(self: *DeclarationsIterator, name: []const u8) ?Declaration {
|
||||||
|
self.skipTrivia();
|
||||||
|
|
||||||
|
const colon = self.stream.next() orelse return null;
|
||||||
|
if (!isColon(colon.token)) {
|
||||||
|
self.skipInvalidDeclaration();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = self.consumeValue() orelse return null;
|
||||||
|
return .{
|
||||||
|
.name = name,
|
||||||
|
.value = value.value,
|
||||||
|
.important = value.important,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ValueResult = struct {
|
||||||
|
value: []const u8,
|
||||||
|
important: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn consumeValue(self: *DeclarationsIterator) ?ValueResult {
|
||||||
|
self.skipTrivia();
|
||||||
|
|
||||||
|
var depth: usize = 0;
|
||||||
|
var start: ?usize = null;
|
||||||
|
var last_sig: ?TokenSpan = null;
|
||||||
|
var prev_sig: ?TokenSpan = null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const peeked = self.stream.peek() orelse break;
|
||||||
|
if (isSemicolon(peeked.token) and depth == 0) {
|
||||||
|
_ = self.stream.next();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = self.stream.next() orelse break;
|
||||||
|
if (isWhitespaceOrComment(span.token)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start == null) start = span.start;
|
||||||
|
prev_sig = last_sig;
|
||||||
|
last_sig = span;
|
||||||
|
updateDepth(span.token, &depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
const value_start = start orelse return null;
|
||||||
|
const last = last_sig orelse return null;
|
||||||
|
|
||||||
|
var important = false;
|
||||||
|
var end_pos = last.end;
|
||||||
|
|
||||||
|
if (isImportantPair(prev_sig, last)) {
|
||||||
|
important = true;
|
||||||
|
const bang = prev_sig orelse return null;
|
||||||
|
if (value_start >= bang.start) return null;
|
||||||
|
end_pos = bang.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value_slice = self.input[value_start..end_pos];
|
||||||
|
value_slice = std.mem.trim(u8, value_slice, &std.ascii.whitespace);
|
||||||
|
if (value_slice.len == 0) return null;
|
||||||
|
|
||||||
|
return .{ .value = value_slice, .important = important };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skipTrivia(self: *DeclarationsIterator) void {
|
||||||
|
while (self.stream.peek()) |peeked| {
|
||||||
|
if (!isWhitespaceOrComment(peeked.token)) break;
|
||||||
|
_ = self.stream.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skipTriviaAndSemicolons(self: *DeclarationsIterator) void {
|
||||||
|
while (self.stream.peek()) |peeked| {
|
||||||
|
if (isWhitespaceOrComment(peeked.token) or isSemicolon(peeked.token)) {
|
||||||
|
_ = self.stream.next();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skipAtRule(self: *DeclarationsIterator) void {
|
||||||
|
var depth: usize = 0;
|
||||||
|
var saw_block = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const peeked = self.stream.peek() orelse return;
|
||||||
|
if (!saw_block and isSemicolon(peeked.token) and depth == 0) {
|
||||||
|
_ = self.stream.next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = self.stream.next() orelse return;
|
||||||
|
if (isWhitespaceOrComment(span.token)) continue;
|
||||||
|
|
||||||
|
if (isBlockStart(span.token)) {
|
||||||
|
depth += 1;
|
||||||
|
saw_block = true;
|
||||||
|
} else if (isBlockEnd(span.token)) {
|
||||||
|
if (depth > 0) depth -= 1;
|
||||||
|
if (saw_block and depth == 0) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skipInvalidDeclaration(self: *DeclarationsIterator) void {
|
||||||
|
var depth: usize = 0;
|
||||||
|
|
||||||
|
while (self.stream.peek()) |peeked| {
|
||||||
|
if (isSemicolon(peeked.token) and depth == 0) {
|
||||||
|
_ = self.stream.next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = self.stream.next() orelse return;
|
||||||
|
if (isWhitespaceOrComment(span.token)) continue;
|
||||||
|
updateDepth(span.token, &depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn isWhitespaceOrComment(token: Tokenizer.Token) bool {
|
||||||
|
return switch (token) {
|
||||||
|
.white_space, .comment => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isSemicolon(token: Tokenizer.Token) bool {
|
||||||
|
return switch (token) {
|
||||||
|
.semicolon => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isColon(token: Tokenizer.Token) bool {
|
||||||
|
return switch (token) {
|
||||||
|
.colon => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isBlockStart(token: Tokenizer.Token) bool {
|
||||||
|
return switch (token) {
|
||||||
|
.curly_bracket_block, .square_bracket_block, .parenthesis_block, .function => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isBlockEnd(token: Tokenizer.Token) bool {
|
||||||
|
return switch (token) {
|
||||||
|
.close_curly_bracket, .close_parenthesis, .close_square_bracket => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn updateDepth(token: Tokenizer.Token, depth: *usize) void {
|
||||||
|
if (isBlockStart(token)) {
|
||||||
|
depth.* += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBlockEnd(token)) {
|
||||||
|
if (depth.* > 0) depth.* -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isImportantPair(prev_sig: ?TokenSpan, last_sig: TokenSpan) bool {
|
||||||
|
if (!isIdentImportant(last_sig.token)) return false;
|
||||||
|
const prev = prev_sig orelse return false;
|
||||||
|
return isBang(prev.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isIdentImportant(token: Tokenizer.Token) bool {
|
||||||
|
return switch (token) {
|
||||||
|
.ident => |name| std.ascii.eqlIgnoreCase(name, "important"),
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isBang(token: Tokenizer.Token) bool {
|
||||||
|
return switch (token) {
|
||||||
|
.delim => |c| c == '!',
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
# css
|
|
||||||
|
|
||||||
Lightpanda css implements CSS selectors parsing and matching in Zig.
|
|
||||||
This package is a port of the Go lib [andybalholm/cascadia](https://github.com/andybalholm/cascadia).
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Query parser
|
|
||||||
|
|
||||||
```zig
|
|
||||||
const css = @import("css.zig");
|
|
||||||
|
|
||||||
const selector = try css.parse(alloc, "h1", .{});
|
|
||||||
defer selector.deinit(alloc);
|
|
||||||
```
|
|
||||||
|
|
||||||
### DOM tree match
|
|
||||||
|
|
||||||
The lib expects a `Node` interface implementation to match your DOM tree.
|
|
||||||
|
|
||||||
```zig
|
|
||||||
pub const Node = struct {
|
|
||||||
pub fn firstChild(_: Node) !?Node {
|
|
||||||
return error.TODO;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lastChild(_: Node) !?Node {
|
|
||||||
return error.TODO;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn nextSibling(_: Node) !?Node {
|
|
||||||
return error.TODO;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prevSibling(_: Node) !?Node {
|
|
||||||
return error.TODO;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parent(_: Node) !?Node {
|
|
||||||
return error.TODO;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isElement(_: Node) bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isDocument(_: Node) bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isComment(_: Node) bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isText(_: Node) bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isEmptyText(_: Node) !bool {
|
|
||||||
return error.TODO;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tag(_: Node) ![]const u8 {
|
|
||||||
return error.TODO;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn attr(_: Node, _: []const u8) !?[]const u8 {
|
|
||||||
return error.TODO;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eql(_: Node, _: Node) bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
You also need do define a `Matcher` implementing a `match` function to
|
|
||||||
accumulate the results.
|
|
||||||
|
|
||||||
```zig
|
|
||||||
const Matcher = struct {
|
|
||||||
const Nodes = std.ArrayList(Node);
|
|
||||||
|
|
||||||
nodes: Nodes,
|
|
||||||
|
|
||||||
fn init(alloc: std.mem.Allocator) Matcher {
|
|
||||||
return .{ .nodes = Nodes.init(alloc) };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(m: *Matcher) void {
|
|
||||||
m.nodes.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match(m: *Matcher, n: Node) !void {
|
|
||||||
try m.nodes.append(n);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you can use the lib itself.
|
|
||||||
|
|
||||||
```zig
|
|
||||||
var matcher = Matcher.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
try css.matchAll(selector, node, &matcher);
|
|
||||||
_ = try css.matchFirst(selector, node, &matcher); // returns true if a node matched.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
* [x] parse query selector
|
|
||||||
* [x] `matchAll`
|
|
||||||
* [x] `matchFirst`
|
|
||||||
* [ ] specificity
|
|
||||||
|
|
||||||
### Selectors implemented
|
|
||||||
|
|
||||||
#### Selectors
|
|
||||||
|
|
||||||
* [x] Class selectors
|
|
||||||
* [x] Id selectors
|
|
||||||
* [x] Type selectors
|
|
||||||
* [x] Universal selectors
|
|
||||||
* [ ] Nesting selectors
|
|
||||||
|
|
||||||
#### Combinators
|
|
||||||
|
|
||||||
* [x] Child combinator
|
|
||||||
* [ ] Column combinator
|
|
||||||
* [x] Descendant combinator
|
|
||||||
* [ ] Namespace combinator
|
|
||||||
* [x] Next-sibling combinator
|
|
||||||
* [x] Selector list combinator
|
|
||||||
* [x] Subsequent-sibling combinator
|
|
||||||
|
|
||||||
#### Attribute
|
|
||||||
|
|
||||||
* [x] `[attr]`
|
|
||||||
* [x] `[attr=value]`
|
|
||||||
* [x] `[attr|=value]`
|
|
||||||
* [x] `[attr^=value]`
|
|
||||||
* [x] `[attr$=value]`
|
|
||||||
* [ ] `[attr*=value]`
|
|
||||||
* [x] `[attr operator value i]`
|
|
||||||
* [ ] `[attr operator value s]`
|
|
||||||
|
|
||||||
#### Pseudo classes
|
|
||||||
|
|
||||||
* [ ] `:active`
|
|
||||||
* [ ] `:any-link`
|
|
||||||
* [ ] `:autofill`
|
|
||||||
* [ ] `:blank Experimental`
|
|
||||||
* [x] `:checked`
|
|
||||||
* [ ] `:current Experimental`
|
|
||||||
* [ ] `:default`
|
|
||||||
* [ ] `:defined`
|
|
||||||
* [ ] `:dir() Experimental`
|
|
||||||
* [x] `:disabled`
|
|
||||||
* [x] `:empty`
|
|
||||||
* [x] `:enabled`
|
|
||||||
* [ ] `:first`
|
|
||||||
* [x] `:first-child`
|
|
||||||
* [x] `:first-of-type`
|
|
||||||
* [ ] `:focus`
|
|
||||||
* [ ] `:focus-visible`
|
|
||||||
* [ ] `:focus-within`
|
|
||||||
* [ ] `:fullscreen`
|
|
||||||
* [ ] `:future Experimental`
|
|
||||||
* [x] `:has() Experimental`
|
|
||||||
* [ ] `:host`
|
|
||||||
* [ ] `:host()`
|
|
||||||
* [ ] `:host-context() Experimental`
|
|
||||||
* [ ] `:hover`
|
|
||||||
* [ ] `:indeterminate`
|
|
||||||
* [ ] `:in-range`
|
|
||||||
* [ ] `:invalid`
|
|
||||||
* [ ] `:is()`
|
|
||||||
* [x] `:lang()`
|
|
||||||
* [x] `:last-child`
|
|
||||||
* [x] `:last-of-type`
|
|
||||||
* [ ] `:left`
|
|
||||||
* [x] `:link`
|
|
||||||
* [ ] `:local-link Experimental`
|
|
||||||
* [ ] `:modal`
|
|
||||||
* [x] `:not()`
|
|
||||||
* [x] `:nth-child()`
|
|
||||||
* [x] `:nth-last-child()`
|
|
||||||
* [x] `:nth-last-of-type()`
|
|
||||||
* [x] `:nth-of-type()`
|
|
||||||
* [x] `:only-child`
|
|
||||||
* [x] `:only-of-type`
|
|
||||||
* [ ] `:optional`
|
|
||||||
* [ ] `:out-of-range`
|
|
||||||
* [ ] `:past Experimental`
|
|
||||||
* [ ] `:paused`
|
|
||||||
* [ ] `:picture-in-picture`
|
|
||||||
* [ ] `:placeholder-shown`
|
|
||||||
* [ ] `:playing`
|
|
||||||
* [ ] `:read-only`
|
|
||||||
* [ ] `:read-write`
|
|
||||||
* [ ] `:required`
|
|
||||||
* [ ] `:right`
|
|
||||||
* [x] `:root`
|
|
||||||
* [ ] `:scope`
|
|
||||||
* [ ] `:state() Experimental`
|
|
||||||
* [ ] `:target`
|
|
||||||
* [ ] `:target-within Experimental`
|
|
||||||
* [ ] `:user-invalid Experimental`
|
|
||||||
* [ ] `:valid`
|
|
||||||
* [ ] `:visited`
|
|
||||||
* [ ] `:where()`
|
|
||||||
* [ ] `:contains()`
|
|
||||||
* [ ] `:containsown()`
|
|
||||||
* [ ] `:matched()`
|
|
||||||
* [ ] `:matchesown()`
|
|
||||||
* [x] `:root`
|
|
||||||
|
|
||||||
824
src/browser/css/Tokenizer.zig
Normal file
824
src/browser/css/Tokenizer.zig
Normal file
@@ -0,0 +1,824 @@
|
|||||||
|
// 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/>.
|
||||||
|
|
||||||
|
//! This file implements the tokenization step defined in the CSS Syntax Module Level 3 specification.
|
||||||
|
//!
|
||||||
|
//! The algorithm accepts a valid UTF-8 string and returns a stream of tokens.
|
||||||
|
//! The tokenization step never fails, even for complete gibberish.
|
||||||
|
//! Validity must then be checked by the parser.
|
||||||
|
//!
|
||||||
|
//! NOTE: The tokenizer is not thread-safe and does not own any memory, and does not check the validity of utf8.
|
||||||
|
//!
|
||||||
|
//! See spec for more info: https://drafts.csswg.org/css-syntax/#tokenization
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
const Tokenizer = @This();
|
||||||
|
|
||||||
|
pub const Token = union(enum) {
|
||||||
|
/// A `<ident-token>`
|
||||||
|
ident: []const u8,
|
||||||
|
|
||||||
|
/// A `<function-token>`
|
||||||
|
///
|
||||||
|
/// The value (name) does not include the `(` marker.
|
||||||
|
function: []const u8,
|
||||||
|
|
||||||
|
/// A `<at-keyword-token>`
|
||||||
|
///
|
||||||
|
/// The value does not include the `@` marker.
|
||||||
|
at_keyword: []const u8,
|
||||||
|
|
||||||
|
/// A `<hash-token>` with the type flag set to "id"
|
||||||
|
///
|
||||||
|
/// The value does not include the `#` marker.
|
||||||
|
id_hash: []const u8, // Hash that is a valid ID selector.
|
||||||
|
|
||||||
|
/// A `<hash-token>` with the type flag set to "unrestricted"
|
||||||
|
///
|
||||||
|
/// The value does not include the `#` marker.
|
||||||
|
unrestricted_hash: []const u8,
|
||||||
|
|
||||||
|
/// A `<string-token>`
|
||||||
|
///
|
||||||
|
/// The value does not include the quotes.
|
||||||
|
string: []const u8,
|
||||||
|
|
||||||
|
/// A `<bad-string-token>`
|
||||||
|
///
|
||||||
|
/// This token always indicates a parse error.
|
||||||
|
bad_string: []const u8,
|
||||||
|
|
||||||
|
/// A `<url-token>`
|
||||||
|
///
|
||||||
|
/// The value does not include the `url(` `)` markers. Note that `url( <string-token> )` is represented by a
|
||||||
|
/// `Function` token.
|
||||||
|
url: []const u8,
|
||||||
|
|
||||||
|
/// A `<bad-url-token>`
|
||||||
|
///
|
||||||
|
/// This token always indicates a parse error.
|
||||||
|
bad_url: []const u8,
|
||||||
|
|
||||||
|
/// A `<delim-token>`
|
||||||
|
delim: u8,
|
||||||
|
|
||||||
|
/// A `<number-token>`
|
||||||
|
number: struct {
|
||||||
|
/// Whether the number had a `+` or `-` sign.
|
||||||
|
///
|
||||||
|
/// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.)
|
||||||
|
has_sign: bool,
|
||||||
|
|
||||||
|
/// If the origin source did not include a fractional part, the value as an integer.
|
||||||
|
int_value: ?i32,
|
||||||
|
|
||||||
|
/// The value as a float
|
||||||
|
value: f32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A `<percentage-token>`
|
||||||
|
percentage: struct {
|
||||||
|
/// Whether the number had a `+` or `-` sign.
|
||||||
|
has_sign: bool,
|
||||||
|
|
||||||
|
/// If the origin source did not include a fractional part, the value as an integer.
|
||||||
|
/// It is **not** divided by 100.
|
||||||
|
int_value: ?i32,
|
||||||
|
|
||||||
|
/// The value as a float, divided by 100 so that the nominal range is 0.0 to 1.0.
|
||||||
|
unit_value: f32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A `<dimension-token>`
|
||||||
|
dimension: struct {
|
||||||
|
/// Whether the number had a `+` or `-` sign.
|
||||||
|
///
|
||||||
|
/// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.)
|
||||||
|
has_sign: bool,
|
||||||
|
|
||||||
|
/// If the origin source did not include a fractional part, the value as an integer.
|
||||||
|
int_value: ?i32,
|
||||||
|
|
||||||
|
/// The value as a float
|
||||||
|
value: f32,
|
||||||
|
|
||||||
|
/// The unit, e.g. "px" in `12px`
|
||||||
|
unit: []const u8,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A `<unicode-range-token>`
|
||||||
|
unicode_range: struct { bgn: u32, end: i32 },
|
||||||
|
|
||||||
|
/// A `<whitespace-token>`
|
||||||
|
white_space: []const u8,
|
||||||
|
|
||||||
|
/// A `<!--` `<CDO-token>`
|
||||||
|
cdo,
|
||||||
|
|
||||||
|
/// A `-->` `<CDC-token>`
|
||||||
|
cdc,
|
||||||
|
|
||||||
|
/// A `:` `<colon-token>`
|
||||||
|
colon, // :
|
||||||
|
|
||||||
|
/// A `;` `<semicolon-token>`
|
||||||
|
semicolon, // ;
|
||||||
|
|
||||||
|
/// A `,` `<comma-token>`
|
||||||
|
comma, // ,
|
||||||
|
|
||||||
|
/// A `<[-token>`
|
||||||
|
square_bracket_block,
|
||||||
|
|
||||||
|
/// A `<]-token>`
|
||||||
|
///
|
||||||
|
/// When obtained from one of the `Parser::next*` methods,
|
||||||
|
/// this token is always unmatched and indicates a parse error.
|
||||||
|
close_square_bracket,
|
||||||
|
|
||||||
|
/// A `<(-token>`
|
||||||
|
parenthesis_block,
|
||||||
|
|
||||||
|
/// A `<)-token>`
|
||||||
|
///
|
||||||
|
/// When obtained from one of the `Parser::next*` methods,
|
||||||
|
/// this token is always unmatched and indicates a parse error.
|
||||||
|
close_parenthesis,
|
||||||
|
|
||||||
|
/// A `<{-token>`
|
||||||
|
curly_bracket_block,
|
||||||
|
|
||||||
|
/// A `<}-token>`
|
||||||
|
///
|
||||||
|
/// When obtained from one of the `Parser::next*` methods,
|
||||||
|
/// this token is always unmatched and indicates a parse error.
|
||||||
|
close_curly_bracket,
|
||||||
|
|
||||||
|
/// A comment.
|
||||||
|
///
|
||||||
|
/// The CSS Syntax spec does not generate tokens for comments,
|
||||||
|
/// But we do for simplicity of the interface.
|
||||||
|
///
|
||||||
|
/// The value does not include the `/*` `*/` markers.
|
||||||
|
comment: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
input: []const u8,
|
||||||
|
|
||||||
|
/// Counted in bytes, not code points. From 0.
|
||||||
|
position: usize = 0,
|
||||||
|
|
||||||
|
// If true, the input has at least `n` bytes left *after* the current one.
|
||||||
|
// That is, `Lexer.byteAt(n)` will not panic.
|
||||||
|
fn hasAtLeast(self: *const Tokenizer, n: usize) bool {
|
||||||
|
return self.position + n < self.input.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isEof(self: *const Tokenizer) bool {
|
||||||
|
return !self.hasAtLeast(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn byteAt(self: *const Tokenizer, offset: usize) u8 {
|
||||||
|
return self.input[self.position + offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes non-EOF
|
||||||
|
fn nextByteUnchecked(self: *const Tokenizer) u8 {
|
||||||
|
return self.byteAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nextByte(self: *const Tokenizer) ?u8 {
|
||||||
|
return if (self.isEof())
|
||||||
|
null
|
||||||
|
else
|
||||||
|
self.input[self.position];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn startsWith(self: *const Tokenizer, needle: []const u8) bool {
|
||||||
|
return std.mem.startsWith(u8, self.input[self.position..], needle);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slice(self: *const Tokenizer, start: usize, end: usize) []const u8 {
|
||||||
|
return self.input[start..end];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sliceFrom(self: *const Tokenizer, start_pos: usize) []const u8 {
|
||||||
|
return self.slice(start_pos, self.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance over N bytes in the input. This function can advance
|
||||||
|
// over ASCII bytes (excluding newlines), or UTF-8 sequence
|
||||||
|
// leaders (excluding leaders for 4-byte sequences).
|
||||||
|
fn advance(self: *Tokenizer, n: usize) void {
|
||||||
|
if (builtin.mode == .Debug) {
|
||||||
|
// Each byte must either be an ASCII byte or a sequence leader,
|
||||||
|
// but not a 4-byte leader; also newlines are rejected.
|
||||||
|
for (0..n) |i| {
|
||||||
|
const b = self.byteAt(i);
|
||||||
|
assert(b != '\r' and b != '\n' and b != '\x0C');
|
||||||
|
assert(b <= 0x7F or (b & 0xF0 != 0xF0 and b & 0xC0 != 0x80));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.position += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hasNewlineAt(self: *const Tokenizer, offset: usize) bool {
|
||||||
|
if (!self.hasAtLeast(offset)) return false;
|
||||||
|
|
||||||
|
return switch (self.byteAt(offset)) {
|
||||||
|
'\n', '\r', '\x0C' => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hasNonAsciiAt(self: *const Tokenizer, offset: usize) bool {
|
||||||
|
if (!self.hasAtLeast(offset)) return false;
|
||||||
|
|
||||||
|
const byte = self.byteAt(offset);
|
||||||
|
const len_utf8 = std.unicode.utf8ByteSequenceLength(byte) catch return false;
|
||||||
|
|
||||||
|
if (!self.hasAtLeast(offset + len_utf8 - 1)) return false;
|
||||||
|
|
||||||
|
const start = self.position + offset;
|
||||||
|
const bytes = self.slice(start, start + len_utf8);
|
||||||
|
|
||||||
|
const codepoint = std.unicode.utf8Decode(bytes) catch return false;
|
||||||
|
|
||||||
|
// https://drafts.csswg.org/css-syntax/#non-ascii-ident-code-point
|
||||||
|
return switch (codepoint) {
|
||||||
|
'\u{00B7}', '\u{200C}', '\u{200D}', '\u{203F}', '\u{2040}' => true,
|
||||||
|
'\u{00C0}'...'\u{00D6}' => true,
|
||||||
|
'\u{00D8}'...'\u{00F6}' => true,
|
||||||
|
'\u{00F8}'...'\u{037D}' => true,
|
||||||
|
'\u{037F}'...'\u{1FFF}' => true,
|
||||||
|
'\u{2070}'...'\u{218F}' => true,
|
||||||
|
'\u{2C00}'...'\u{2FEF}' => true,
|
||||||
|
'\u{3001}'...'\u{D7FF}' => true,
|
||||||
|
'\u{F900}'...'\u{FDCF}' => true,
|
||||||
|
'\u{FDF0}'...'\u{FFFD}' => true,
|
||||||
|
else => codepoint >= '\u{10000}',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isIdentStart(self: *Tokenizer) bool {
|
||||||
|
if (self.isEof()) return false;
|
||||||
|
|
||||||
|
var b = self.nextByteUnchecked();
|
||||||
|
if (b == '-') {
|
||||||
|
b = if (self.hasAtLeast(1)) self.byteAt(1) else return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (b) {
|
||||||
|
'a'...'z', 'A'...'Z', '_', 0x0 => true,
|
||||||
|
'\\' => !self.hasNewlineAt(1),
|
||||||
|
else => b > 0x7F, // not is ascii
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consumeChar(self: *Tokenizer) void {
|
||||||
|
const byte = self.nextByteUnchecked();
|
||||||
|
const len_utf8 = std.unicode.utf8ByteSequenceLength(byte) catch 1;
|
||||||
|
self.position += len_utf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given that a newline has been seen, advance over the newline
|
||||||
|
// and update the state.
|
||||||
|
fn consumeNewline(self: *Tokenizer) void {
|
||||||
|
const byte = self.nextByteUnchecked();
|
||||||
|
assert(byte == '\r' or byte == '\n' or byte == '\x0C');
|
||||||
|
|
||||||
|
self.position += 1;
|
||||||
|
if (byte == '\r' and self.nextByte() == '\n') {
|
||||||
|
self.position += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consumeWhiteSpace(self: *Tokenizer, newline: bool) Token {
|
||||||
|
const start_position = self.position;
|
||||||
|
if (newline) {
|
||||||
|
self.consumeNewline();
|
||||||
|
} else {
|
||||||
|
self.advance(1);
|
||||||
|
}
|
||||||
|
while (!self.isEof()) {
|
||||||
|
const b = self.nextByteUnchecked();
|
||||||
|
switch (b) {
|
||||||
|
' ', '\t' => {
|
||||||
|
self.advance(1);
|
||||||
|
},
|
||||||
|
'\n', '\x0C', '\r' => {
|
||||||
|
self.consumeNewline();
|
||||||
|
},
|
||||||
|
else => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .{ .white_space = self.sliceFrom(start_position) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consumeComment(self: *Tokenizer) []const u8 {
|
||||||
|
self.advance(2); // consume "/*"
|
||||||
|
const start_position = self.position;
|
||||||
|
while (!self.isEof()) {
|
||||||
|
switch (self.nextByteUnchecked()) {
|
||||||
|
'*' => {
|
||||||
|
const end_position = self.position;
|
||||||
|
self.advance(1);
|
||||||
|
if (self.nextByte() == '/') {
|
||||||
|
self.advance(1);
|
||||||
|
return self.slice(start_position, end_position);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'\n', '\x0C', '\r' => {
|
||||||
|
self.consumeNewline();
|
||||||
|
},
|
||||||
|
0x0 => self.advance(1),
|
||||||
|
else => self.consumeChar(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self.sliceFrom(start_position);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn byteToHexDigit(b: u8) ?u32 {
|
||||||
|
return switch (b) {
|
||||||
|
'0'...'9' => b - '0',
|
||||||
|
'a'...'f' => b - 'a' + 10,
|
||||||
|
'A'...'F' => b - 'A' + 10,
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn byteToDecimalDigit(b: u8) ?u32 {
|
||||||
|
return if (std.ascii.isDigit(b)) b - '0' else null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// (value, number of digits up to 6)
|
||||||
|
fn consumeHexDigits(self: *Tokenizer) void {
|
||||||
|
var value: u32 = 0;
|
||||||
|
var digits: u32 = 0;
|
||||||
|
|
||||||
|
while (digits < 6 and !self.isEof()) {
|
||||||
|
if (byteToHexDigit(self.nextByteUnchecked())) |digit| {
|
||||||
|
value = value * 16 + digit;
|
||||||
|
digits += 1;
|
||||||
|
self.advance(1);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = &value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes that the U+005C REVERSE SOLIDUS (\) has already been consumed
|
||||||
|
// and that the next input character has already been verified
|
||||||
|
// to not be a newline.
|
||||||
|
fn consumeEscape(self: *Tokenizer) void {
|
||||||
|
if (self.isEof())
|
||||||
|
return; // Escaped EOF
|
||||||
|
|
||||||
|
switch (self.nextByteUnchecked()) {
|
||||||
|
'0'...'9', 'A'...'F', 'a'...'f' => {
|
||||||
|
consumeHexDigits(self);
|
||||||
|
|
||||||
|
if (!self.isEof()) {
|
||||||
|
switch (self.nextByteUnchecked()) {
|
||||||
|
' ', '\t' => {
|
||||||
|
self.advance(1);
|
||||||
|
},
|
||||||
|
'\n', '\x0C', '\r' => {
|
||||||
|
self.consumeNewline();
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => self.consumeChar(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// https://drafts.csswg.org/css-syntax/#consume-string-token
|
||||||
|
fn consumeString(self: *Tokenizer, single_quote: bool) Token {
|
||||||
|
self.advance(1); // Skip the initial quote
|
||||||
|
|
||||||
|
// start_pos is at code point boundary, after " or '
|
||||||
|
const start_pos = self.position;
|
||||||
|
|
||||||
|
while (!self.isEof()) {
|
||||||
|
switch (self.nextByteUnchecked()) {
|
||||||
|
'"' => {
|
||||||
|
if (!single_quote) {
|
||||||
|
const value = self.sliceFrom(start_pos);
|
||||||
|
self.advance(1);
|
||||||
|
return .{ .string = value };
|
||||||
|
}
|
||||||
|
self.advance(1);
|
||||||
|
},
|
||||||
|
'\'' => {
|
||||||
|
if (single_quote) {
|
||||||
|
const value = self.sliceFrom(start_pos);
|
||||||
|
self.advance(1);
|
||||||
|
return .{ .string = value };
|
||||||
|
}
|
||||||
|
self.advance(1);
|
||||||
|
},
|
||||||
|
'\n', '\r', '\x0C' => {
|
||||||
|
return .{ .bad_string = self.sliceFrom(start_pos) };
|
||||||
|
},
|
||||||
|
'\\' => {
|
||||||
|
self.advance(1);
|
||||||
|
if (self.isEof())
|
||||||
|
continue; // escaped EOF, do nothing.
|
||||||
|
|
||||||
|
switch (self.nextByteUnchecked()) {
|
||||||
|
// Escaped newline
|
||||||
|
'\n', '\x0C', '\r' => self.consumeNewline(),
|
||||||
|
|
||||||
|
// Spec calls for replacing escape sequences with characters,
|
||||||
|
// but this would require allocating a new string.
|
||||||
|
// Therefore, we leave it as is and let the parser handle the escaping.
|
||||||
|
else => self.consumeEscape(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => self.consumeChar(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{ .string = self.sliceFrom(start_pos) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consumeName(self: *Tokenizer) []const u8 {
|
||||||
|
// start_pos is the end of the previous token, therefore at a code point boundary
|
||||||
|
const start_pos = self.position;
|
||||||
|
|
||||||
|
while (!self.isEof()) {
|
||||||
|
switch (self.nextByteUnchecked()) {
|
||||||
|
'a'...'z', 'A'...'Z', '0'...'9', '_', '-' => self.advance(1),
|
||||||
|
'\\' => {
|
||||||
|
if (self.hasNewlineAt(1)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.advance(1);
|
||||||
|
self.consumeEscape();
|
||||||
|
},
|
||||||
|
0x0 => self.advance(1),
|
||||||
|
'\x80'...'\xBF', '\xC0'...'\xEF', '\xF0'...'\xFF' => {
|
||||||
|
// This byte *is* part of a multi-byte code point,
|
||||||
|
// we’ll end up copying the whole code point before this loop does something else.
|
||||||
|
self.advance(1);
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
if (self.hasNonAsciiAt(0)) {
|
||||||
|
self.consumeChar();
|
||||||
|
} else {
|
||||||
|
break; // ASCII
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.sliceFrom(start_pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consumeMark(self: *Tokenizer) Token {
|
||||||
|
const byte = self.nextByteUnchecked();
|
||||||
|
self.advance(1);
|
||||||
|
return switch (byte) {
|
||||||
|
',' => .comma,
|
||||||
|
':' => .colon,
|
||||||
|
';' => .semicolon,
|
||||||
|
'(' => .parenthesis_block,
|
||||||
|
')' => .close_parenthesis,
|
||||||
|
'{' => .curly_bracket_block,
|
||||||
|
'}' => .close_curly_bracket,
|
||||||
|
'[' => .square_bracket_block,
|
||||||
|
']' => .close_square_bracket,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consumeNumeric(self: *Tokenizer) Token {
|
||||||
|
// Parse [+-]?\d*(\.\d+)?([eE][+-]?\d+)?
|
||||||
|
// But this is always called so that there is at least one digit in \d*(\.\d+)?
|
||||||
|
|
||||||
|
// Do all the math in f64 so that large numbers overflow to +/-inf
|
||||||
|
// and i32::{MIN, MAX} are within range.
|
||||||
|
|
||||||
|
var sign: f64 = 1.0;
|
||||||
|
var has_sign = false;
|
||||||
|
switch (self.nextByteUnchecked()) {
|
||||||
|
'+' => {
|
||||||
|
has_sign = true;
|
||||||
|
},
|
||||||
|
'-' => {
|
||||||
|
has_sign = true;
|
||||||
|
sign = -1.0;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
if (has_sign) {
|
||||||
|
self.advance(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_integer = true;
|
||||||
|
var integral_part: f64 = 0.0;
|
||||||
|
var fractional_part: f64 = 0.0;
|
||||||
|
|
||||||
|
while (!self.isEof()) {
|
||||||
|
if (byteToDecimalDigit(self.nextByteUnchecked())) |digit| {
|
||||||
|
integral_part = integral_part * 10.0 + @as(f64, @floatFromInt(digit));
|
||||||
|
self.advance(1);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.hasAtLeast(1) and self.nextByteUnchecked() == '.' and std.ascii.isDigit(self.byteAt(1))) {
|
||||||
|
is_integer = false;
|
||||||
|
self.advance(1); // Consume '.'
|
||||||
|
|
||||||
|
var factor: f64 = 0.1;
|
||||||
|
while (!self.isEof()) {
|
||||||
|
if (byteToDecimalDigit(self.nextByteUnchecked())) |digit| {
|
||||||
|
fractional_part += @as(f64, @floatFromInt(digit)) * factor;
|
||||||
|
factor *= 0.1;
|
||||||
|
self.advance(1);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = sign * (integral_part + fractional_part);
|
||||||
|
|
||||||
|
blk: {
|
||||||
|
const e = self.nextByte() orelse break :blk;
|
||||||
|
if (e != 'e' and e != 'E') break :blk;
|
||||||
|
|
||||||
|
var mul: f64 = 1.0;
|
||||||
|
|
||||||
|
if (self.hasAtLeast(2) and (self.byteAt(1) == '+' or self.byteAt(1) == '-') and std.ascii.isDigit(self.byteAt(2))) {
|
||||||
|
mul = switch (self.byteAt(1)) {
|
||||||
|
'-' => -1.0,
|
||||||
|
'+' => 1.0,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.advance(2);
|
||||||
|
} else if (self.hasAtLeast(2) and std.ascii.isDigit(self.byteAt(2))) {
|
||||||
|
self.advance(1);
|
||||||
|
} else {
|
||||||
|
break :blk;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_integer = false;
|
||||||
|
|
||||||
|
var exponent: f64 = 0.0;
|
||||||
|
while (!self.isEof()) {
|
||||||
|
if (byteToDecimalDigit(self.nextByteUnchecked())) |digit| {
|
||||||
|
exponent = exponent * 10.0 + @as(f64, @floatFromInt(digit));
|
||||||
|
self.advance(1);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value *= std.math.pow(f64, 10.0, mul * exponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int_value: ?i32 = if (is_integer) blk: {
|
||||||
|
if (value >= std.math.maxInt(i32)) {
|
||||||
|
break :blk std.math.maxInt(i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value <= std.math.minInt(i32)) {
|
||||||
|
break :blk std.math.minInt(i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk @as(i32, @intFromFloat(value));
|
||||||
|
} else null;
|
||||||
|
|
||||||
|
if (!self.isEof() and self.nextByteUnchecked() == '%') {
|
||||||
|
self.advance(1);
|
||||||
|
|
||||||
|
return .{ .percentage = .{
|
||||||
|
.has_sign = has_sign,
|
||||||
|
.int_value = int_value,
|
||||||
|
.unit_value = @as(f32, @floatCast(value / 100.0)),
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIdentStart(self)) {
|
||||||
|
return .{ .dimension = .{
|
||||||
|
.has_sign = has_sign,
|
||||||
|
.int_value = int_value,
|
||||||
|
.value = @as(f32, @floatCast(value)),
|
||||||
|
.unit = consumeName(self),
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{ .number = .{
|
||||||
|
.has_sign = has_sign,
|
||||||
|
.int_value = int_value,
|
||||||
|
.value = @as(f32, @floatCast(value)),
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consumeUnquotedUrl(self: *Tokenizer) ?Token {
|
||||||
|
// TODO: true url parser
|
||||||
|
if (self.nextByte()) |it| {
|
||||||
|
return self.consumeString(it == '\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consumeIdentLike(self: *Tokenizer) Token {
|
||||||
|
const value = self.consumeName();
|
||||||
|
|
||||||
|
if (!self.isEof() and self.nextByteUnchecked() == '(') {
|
||||||
|
self.advance(1);
|
||||||
|
|
||||||
|
if (std.ascii.eqlIgnoreCase(value, "url")) {
|
||||||
|
if (self.consumeUnquotedUrl()) |result| {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{ .function = value };
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{ .ident = value };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(self: *Tokenizer) ?Token {
|
||||||
|
if (self.isEof()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const b = self.nextByteUnchecked();
|
||||||
|
return switch (b) {
|
||||||
|
// Consume comments
|
||||||
|
'/' => {
|
||||||
|
if (self.startsWith("/*")) {
|
||||||
|
return .{ .comment = self.consumeComment() };
|
||||||
|
} else {
|
||||||
|
self.advance(1);
|
||||||
|
return .{ .delim = '/' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Consume marks
|
||||||
|
'(', ')', '{', '}', '[', ']', ',', ':', ';' => {
|
||||||
|
return self.consumeMark();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Consume as much whitespace as possible. Return a <whitespace-token>.
|
||||||
|
' ', '\t' => self.consumeWhiteSpace(false),
|
||||||
|
'\n', '\x0C', '\r' => self.consumeWhiteSpace(true),
|
||||||
|
|
||||||
|
// Consume a string token and return it.
|
||||||
|
'"' => self.consumeString(false),
|
||||||
|
'\'' => self.consumeString(true),
|
||||||
|
|
||||||
|
'0'...'9' => self.consumeNumeric(),
|
||||||
|
'a'...'z', 'A'...'Z', '_', 0x0 => self.consumeIdentLike(),
|
||||||
|
|
||||||
|
'+' => {
|
||||||
|
if ((self.hasAtLeast(1) and std.ascii.isDigit(self.byteAt(1))) or
|
||||||
|
(self.hasAtLeast(2) and self.byteAt(1) == '.' and std.ascii.isDigit(self.byteAt(2))))
|
||||||
|
{
|
||||||
|
return self.consumeNumeric();
|
||||||
|
}
|
||||||
|
self.advance(1);
|
||||||
|
return .{ .delim = '+' };
|
||||||
|
},
|
||||||
|
'-' => {
|
||||||
|
if ((self.hasAtLeast(1) and std.ascii.isDigit(self.byteAt(1))) or
|
||||||
|
(self.hasAtLeast(2) and self.byteAt(1) == '.' and std.ascii.isDigit(self.byteAt(2))))
|
||||||
|
{
|
||||||
|
return self.consumeNumeric();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.startsWith("-->")) {
|
||||||
|
self.advance(3);
|
||||||
|
return .cdc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIdentStart(self)) {
|
||||||
|
return self.consumeIdentLike();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.advance(1);
|
||||||
|
return .{ .delim = '-' };
|
||||||
|
},
|
||||||
|
'.' => {
|
||||||
|
if (self.hasAtLeast(1) and std.ascii.isDigit(self.byteAt(1))) {
|
||||||
|
return self.consumeNumeric();
|
||||||
|
}
|
||||||
|
self.advance(1);
|
||||||
|
return .{ .delim = '.' };
|
||||||
|
},
|
||||||
|
|
||||||
|
// Consume hash token
|
||||||
|
'#' => {
|
||||||
|
self.advance(1);
|
||||||
|
if (self.isIdentStart()) {
|
||||||
|
return .{ .id_hash = self.consumeName() };
|
||||||
|
}
|
||||||
|
if (self.nextByte()) |it| {
|
||||||
|
switch (it) {
|
||||||
|
// Any other valid case here already resulted in IDHash.
|
||||||
|
'0'...'9', '-' => return .{ .unrestricted_hash = self.consumeName() },
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .{ .delim = '#' };
|
||||||
|
},
|
||||||
|
|
||||||
|
// Consume at-rules
|
||||||
|
'@' => {
|
||||||
|
self.advance(1);
|
||||||
|
return if (isIdentStart(self))
|
||||||
|
.{ .at_keyword = consumeName(self) }
|
||||||
|
else
|
||||||
|
.{ .delim = '@' };
|
||||||
|
},
|
||||||
|
|
||||||
|
'<' => {
|
||||||
|
if (self.startsWith("<!--")) {
|
||||||
|
self.advance(4);
|
||||||
|
return .cdo;
|
||||||
|
} else {
|
||||||
|
self.advance(1);
|
||||||
|
return .{ .delim = '<' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'\\' => {
|
||||||
|
if (!self.hasNewlineAt(1)) {
|
||||||
|
return self.consumeIdentLike();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.advance(1);
|
||||||
|
return .{ .delim = '\\' };
|
||||||
|
},
|
||||||
|
|
||||||
|
else => {
|
||||||
|
if (b > 0x7F) { // not is ascii
|
||||||
|
return self.consumeIdentLike();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.advance(1);
|
||||||
|
return .{ .delim = b };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
fn expectTokensEqual(input: []const u8, tokens: []const Token) !void {
|
||||||
|
var lexer = Tokenizer{ .input = input };
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (lexer.next()) |token| : (i += 1) {
|
||||||
|
assert(i < tokens.len);
|
||||||
|
try testing.expectEqualDeep(tokens[i], token);
|
||||||
|
}
|
||||||
|
|
||||||
|
try testing.expectEqual(i, tokens.len);
|
||||||
|
try testing.expectEqualDeep(null, lexer.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "smoke" {
|
||||||
|
try expectTokensEqual(
|
||||||
|
\\.lightpanda {color:red;}
|
||||||
|
, &.{
|
||||||
|
.{ .delim = '.' },
|
||||||
|
.{ .ident = "lightpanda" },
|
||||||
|
.{ .white_space = " " },
|
||||||
|
.curly_bracket_block,
|
||||||
|
.{ .ident = "color" },
|
||||||
|
.colon,
|
||||||
|
.{ .ident = "red" },
|
||||||
|
.semicolon,
|
||||||
|
.close_curly_bracket,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// CSS Selector parser and query
|
|
||||||
// This package is a rewrite in Zig of Cascadia CSS Selector parser.
|
|
||||||
// see https://github.com/andybalholm/cascadia
|
|
||||||
const std = @import("std");
|
|
||||||
const Selector = @import("selector.zig").Selector;
|
|
||||||
const parser = @import("parser.zig");
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
Css,
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/CSS
|
|
||||||
pub const Css = struct {
|
|
||||||
_not_empty: bool = true,
|
|
||||||
|
|
||||||
pub fn _supports(_: *Css, _: []const u8, _: ?[]const u8) bool {
|
|
||||||
// TODO: Actually respond with which CSS features we support.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// parse parse a selector string and returns the parsed result or an error.
|
|
||||||
pub fn parse(alloc: std.mem.Allocator, s: []const u8, opts: parser.ParseOptions) parser.ParseError!Selector {
|
|
||||||
var p = parser.Parser{ .s = s, .i = 0, .opts = opts };
|
|
||||||
return p.parse(alloc);
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchFirst call m.match with the first node that matches the selector s, from the
|
|
||||||
// descendants of n and returns true. If none matches, it returns false.
|
|
||||||
pub fn matchFirst(s: *const Selector, node: anytype, m: anytype) !bool {
|
|
||||||
var child = node.firstChild();
|
|
||||||
while (child) |c| {
|
|
||||||
if (try s.match(c)) {
|
|
||||||
try m.match(c);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try matchFirst(s, c, m)) return true;
|
|
||||||
child = c.nextSibling();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchAll call m.match with the all the nodes that matches the selector s, from the
|
|
||||||
// descendants of n.
|
|
||||||
pub fn matchAll(s: *const Selector, node: anytype, m: anytype) !void {
|
|
||||||
var child = node.firstChild();
|
|
||||||
while (child) |c| {
|
|
||||||
if (try s.match(c)) try m.match(c);
|
|
||||||
try matchAll(s, c, m);
|
|
||||||
child = c.nextSibling();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "parse" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
const testcases = [_][]const u8{
|
|
||||||
"address",
|
|
||||||
"*",
|
|
||||||
"#foo",
|
|
||||||
"li#t1",
|
|
||||||
"*#t4",
|
|
||||||
".t1",
|
|
||||||
"p.t1",
|
|
||||||
"div.teST",
|
|
||||||
".t1.fail",
|
|
||||||
"p.t1.t2",
|
|
||||||
"p.--t1",
|
|
||||||
"p.--t1.--t2",
|
|
||||||
"p[title]",
|
|
||||||
"div[class=\"red\" i]",
|
|
||||||
"address[title=\"foo\"]",
|
|
||||||
"address[title=\"FoOIgnoRECaSe\" i]",
|
|
||||||
"address[title!=\"foo\"]",
|
|
||||||
"address[title!=\"foo\" i]",
|
|
||||||
"p[title!=\"FooBarUFoo\" i]",
|
|
||||||
"[ \t title ~= foo ]",
|
|
||||||
"p[title~=\"FOO\" i]",
|
|
||||||
"p[title~=toofoo i]",
|
|
||||||
"[title~=\"hello world\"]",
|
|
||||||
"[title~=\"hello\" i]",
|
|
||||||
"[title~=\"hello\" I]",
|
|
||||||
"[lang|=\"en\"]",
|
|
||||||
"[lang|=\"EN\" i]",
|
|
||||||
"[lang|=\"EN\" i]",
|
|
||||||
"[title^=\"foo\"]",
|
|
||||||
"[title^=\"foo\" i]",
|
|
||||||
"[title$=\"bar\"]",
|
|
||||||
"[title$=\"BAR\" i]",
|
|
||||||
"[title*=\"bar\"]",
|
|
||||||
"[title*=\"BaRu\" i]",
|
|
||||||
"[title*=\"BaRu\" I]",
|
|
||||||
"p[class$=\" \"]",
|
|
||||||
"p[class$=\"\"]",
|
|
||||||
"p[class^=\" \"]",
|
|
||||||
"p[class^=\"\"]",
|
|
||||||
"p[class*=\" \"]",
|
|
||||||
"p[class*=\"\"]",
|
|
||||||
"input[name=Sex][value=F]",
|
|
||||||
"table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]",
|
|
||||||
".t1:not(.t2)",
|
|
||||||
"div:not(.t1)",
|
|
||||||
"div:not([class=\"t2\"])",
|
|
||||||
"li:nth-child(odd)",
|
|
||||||
"li:nth-child(even)",
|
|
||||||
"li:nth-child(-n+2)",
|
|
||||||
"li:nth-child(3n+1)",
|
|
||||||
"li:nth-last-child(odd)",
|
|
||||||
"li:nth-last-child(even)",
|
|
||||||
"li:nth-last-child(-n+2)",
|
|
||||||
"li:nth-last-child(3n+1)",
|
|
||||||
"span:first-child",
|
|
||||||
"span:last-child",
|
|
||||||
"p:nth-of-type(2)",
|
|
||||||
"p:nth-last-of-type(2)",
|
|
||||||
"p:last-of-type",
|
|
||||||
"p:first-of-type",
|
|
||||||
"p:only-child",
|
|
||||||
"p:only-of-type",
|
|
||||||
":empty",
|
|
||||||
"div p",
|
|
||||||
"div table p",
|
|
||||||
"div > p",
|
|
||||||
"p ~ p",
|
|
||||||
"p + p",
|
|
||||||
"li, p",
|
|
||||||
"p +/*This is a comment*/ p",
|
|
||||||
"p:contains(\"that wraps\")",
|
|
||||||
"p:containsOwn(\"that wraps\")",
|
|
||||||
":containsOwn(\"inner\")",
|
|
||||||
"p:containsOwn(\"block\")",
|
|
||||||
"div:has(#p1)",
|
|
||||||
"div:has(:containsOwn(\"2\"))",
|
|
||||||
"body :has(:containsOwn(\"2\"))",
|
|
||||||
"body :haschild(:containsOwn(\"2\"))",
|
|
||||||
"p:matches([\\d])",
|
|
||||||
"p:matches([a-z])",
|
|
||||||
"p:matches([a-zA-Z])",
|
|
||||||
"p:matches([^\\d])",
|
|
||||||
"p:matches(^(0|a))",
|
|
||||||
"p:matches(^\\d+$)",
|
|
||||||
"p:not(:matches(^\\d+$))",
|
|
||||||
"div :matchesOwn(^\\d+$)",
|
|
||||||
"[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])",
|
|
||||||
"[href#=(^https:\\/\\/[^\\/]*\\/?news)]",
|
|
||||||
":input",
|
|
||||||
":root",
|
|
||||||
"*:root",
|
|
||||||
"html:nth-child(1)",
|
|
||||||
"*:root:first-child",
|
|
||||||
"*:root:nth-child(1)",
|
|
||||||
"a:not(:root)",
|
|
||||||
"body > *:nth-child(3n+2)",
|
|
||||||
"input:disabled",
|
|
||||||
":disabled",
|
|
||||||
":enabled",
|
|
||||||
"div.class1, div.class2",
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
const s = parse(alloc, tc, .{}) catch |e| {
|
|
||||||
std.debug.print("query {s}", .{tc});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
defer s.deinit(alloc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: CSS" {
|
|
||||||
try testing.htmlRunner("css.html");
|
|
||||||
}
|
|
||||||
@@ -1,423 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const css = @import("css.zig");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
// Node implementation with Netsurf Libdom C lib.
|
|
||||||
pub const Node = struct {
|
|
||||||
node: *parser.Node,
|
|
||||||
|
|
||||||
pub fn firstChild(n: Node) ?Node {
|
|
||||||
const c = parser.nodeFirstChild(n.node);
|
|
||||||
if (c) |cc| return .{ .node = cc };
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lastChild(n: Node) ?Node {
|
|
||||||
const c = parser.nodeLastChild(n.node);
|
|
||||||
if (c) |cc| return .{ .node = cc };
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn nextSibling(n: Node) ?Node {
|
|
||||||
const c = parser.nodeNextSibling(n.node);
|
|
||||||
if (c) |cc| return .{ .node = cc };
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prevSibling(n: Node) ?Node {
|
|
||||||
const c = parser.nodePreviousSibling(n.node);
|
|
||||||
if (c) |cc| return .{ .node = cc };
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parent(n: Node) ?Node {
|
|
||||||
const c = parser.nodeParentNode(n.node);
|
|
||||||
if (c) |cc| return .{ .node = cc };
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isElement(n: Node) bool {
|
|
||||||
return parser.nodeType(n.node) == .element;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isDocument(n: Node) bool {
|
|
||||||
return parser.nodeType(n.node) == .document;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isComment(n: Node) bool {
|
|
||||||
return parser.nodeType(n.node) == .comment;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isText(n: Node) bool {
|
|
||||||
return parser.nodeType(n.node) == .text;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn text(n: Node) ?[]const u8 {
|
|
||||||
const data = parser.nodeTextContent(n.node);
|
|
||||||
if (data == null) return null;
|
|
||||||
if (data.?.len == 0) return null;
|
|
||||||
|
|
||||||
return std.mem.trim(u8, data.?, &std.ascii.whitespace);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isEmptyText(n: Node) bool {
|
|
||||||
const data = parser.nodeTextContent(n.node);
|
|
||||||
if (data == null) return true;
|
|
||||||
if (data.?.len == 0) return true;
|
|
||||||
|
|
||||||
return std.mem.trim(u8, data.?, &std.ascii.whitespace).len == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tag(n: Node) ![]const u8 {
|
|
||||||
return parser.nodeName(n.node);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn attr(n: Node, key: []const u8) !?[]const u8 {
|
|
||||||
if (!n.isElement()) return null;
|
|
||||||
return try parser.elementGetAttribute(parser.nodeToElement(n.node), key);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eql(a: Node, b: Node) bool {
|
|
||||||
return a.node == b.node;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const MatcherTest = struct {
|
|
||||||
const Nodes = std.ArrayListUnmanaged(Node);
|
|
||||||
|
|
||||||
nodes: Nodes,
|
|
||||||
allocator: Allocator,
|
|
||||||
|
|
||||||
fn init(allocator: Allocator) MatcherTest {
|
|
||||||
return .{
|
|
||||||
.nodes = .empty,
|
|
||||||
.allocator = allocator,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(m: *MatcherTest) void {
|
|
||||||
m.nodes.deinit(m.allocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(m: *MatcherTest) void {
|
|
||||||
m.nodes.clearRetainingCapacity();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match(m: *MatcherTest, n: Node) !void {
|
|
||||||
try m.nodes.append(m.allocator, n);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
test "Browser.CSS.Libdom: matchFirst" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
parser.init();
|
|
||||||
defer parser.deinit();
|
|
||||||
|
|
||||||
var matcher = MatcherTest.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
q: []const u8,
|
|
||||||
html: []const u8,
|
|
||||||
exp: usize,
|
|
||||||
}{
|
|
||||||
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
|
||||||
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
|
||||||
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
|
||||||
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
|
||||||
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
|
||||||
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
|
||||||
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
|
||||||
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
|
||||||
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
|
||||||
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
|
||||||
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
|
||||||
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
|
||||||
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
|
||||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
|
||||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
|
||||||
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
|
||||||
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
|
||||||
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
|
||||||
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
|
||||||
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
|
||||||
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
|
||||||
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
|
||||||
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
|
||||||
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
|
||||||
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
|
||||||
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 1 },
|
|
||||||
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 1 },
|
|
||||||
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 1 },
|
|
||||||
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
.{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
|
||||||
.{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
.{ .q = ":containsOwn(\"Inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
|
||||||
.{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
// .{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
|
||||||
.{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
|
||||||
.{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
|
||||||
.{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 1 },
|
|
||||||
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
|
||||||
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
|
||||||
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 1 },
|
|
||||||
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 1 },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
matcher.reset();
|
|
||||||
|
|
||||||
const doc = try parser.documentHTMLParseFromStr(tc.html);
|
|
||||||
defer parser.documentHTMLClose(doc) catch {};
|
|
||||||
|
|
||||||
const s = css.parse(alloc, tc.q, .{}) catch |e| {
|
|
||||||
std.debug.print("parse, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
defer s.deinit(alloc);
|
|
||||||
|
|
||||||
const node = Node{ .node = parser.documentHTMLToNode(doc) };
|
|
||||||
|
|
||||||
_ = css.matchFirst(&s, node, &matcher) catch |e| {
|
|
||||||
std.debug.print("match, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("expectation, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser.CSS.Libdom: matchAll" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
parser.init();
|
|
||||||
defer parser.deinit();
|
|
||||||
|
|
||||||
var matcher = MatcherTest.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
q: []const u8,
|
|
||||||
html: []const u8,
|
|
||||||
exp: usize,
|
|
||||||
}{
|
|
||||||
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
|
||||||
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 3 },
|
|
||||||
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 3 },
|
|
||||||
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
|
||||||
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
|
||||||
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
|
||||||
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
|
||||||
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
|
||||||
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
|
||||||
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
|
||||||
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 2 },
|
|
||||||
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 2 },
|
|
||||||
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
|
||||||
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
|
||||||
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
|
||||||
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
|
||||||
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
|
||||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
|
||||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
|
||||||
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
|
||||||
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
|
||||||
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
|
||||||
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
|
||||||
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
|
||||||
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
|
||||||
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
|
||||||
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
|
||||||
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
|
||||||
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
|
||||||
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 3 },
|
|
||||||
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 2 },
|
|
||||||
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 2 },
|
|
||||||
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 2 },
|
|
||||||
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 3 },
|
|
||||||
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
.{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
|
||||||
.{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
.{ .q = ":containsOwn(\"Inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
|
||||||
.{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
.{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
|
||||||
.{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
|
||||||
.{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 2 },
|
|
||||||
.{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
|
||||||
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
|
||||||
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
|
||||||
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 3 },
|
|
||||||
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
|
||||||
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 2 },
|
|
||||||
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 2 },
|
|
||||||
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
|
||||||
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 5 },
|
|
||||||
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 2 },
|
|
||||||
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 2 },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
matcher.reset();
|
|
||||||
|
|
||||||
const doc = try parser.documentHTMLParseFromStr(tc.html);
|
|
||||||
defer parser.documentHTMLClose(doc) catch {};
|
|
||||||
|
|
||||||
const s = css.parse(alloc, tc.q, .{}) catch |e| {
|
|
||||||
std.debug.print("parse, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
defer s.deinit(alloc);
|
|
||||||
|
|
||||||
const node = Node{ .node = parser.documentHTMLToNode(doc) };
|
|
||||||
|
|
||||||
_ = css.matchAll(&s, node, &matcher) catch |e| {
|
|
||||||
std.debug.print("match, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("expectation, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,996 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// CSS Selector parser
|
|
||||||
// This file is a rewrite in Zig of Cascadia CSS Selector parser.
|
|
||||||
// see https://github.com/andybalholm/cascadia
|
|
||||||
// see https://github.com/andybalholm/cascadia/blob/master/parser.go
|
|
||||||
const std = @import("std");
|
|
||||||
const ascii = std.ascii;
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const selector = @import("selector.zig");
|
|
||||||
const Selector = selector.Selector;
|
|
||||||
const PseudoClass = selector.PseudoClass;
|
|
||||||
const AttributeOP = selector.AttributeOP;
|
|
||||||
const Combinator = selector.Combinator;
|
|
||||||
|
|
||||||
const REPLACEMENT_CHARACTER = &.{ 239, 191, 189 };
|
|
||||||
|
|
||||||
pub const ParseError = error{
|
|
||||||
ExpectedSelector,
|
|
||||||
ExpectedIdentifier,
|
|
||||||
ExpectedName,
|
|
||||||
ExpectedIDSelector,
|
|
||||||
ExpectedClassSelector,
|
|
||||||
ExpectedAttributeSelector,
|
|
||||||
ExpectedString,
|
|
||||||
ExpectedRegexp,
|
|
||||||
ExpectedPseudoClassSelector,
|
|
||||||
ExpectedParenthesis,
|
|
||||||
ExpectedParenthesisClose,
|
|
||||||
ExpectedNthExpression,
|
|
||||||
ExpectedInteger,
|
|
||||||
InvalidEscape,
|
|
||||||
EscapeLineEndingOutsideString,
|
|
||||||
InvalidUnicode,
|
|
||||||
UnicodeIsNotHandled,
|
|
||||||
WriteError,
|
|
||||||
PseudoElementNotAtSelectorEnd,
|
|
||||||
PseudoElementNotUnique,
|
|
||||||
PseudoElementDisabled,
|
|
||||||
InvalidAttributeOperator,
|
|
||||||
InvalidAttributeSelector,
|
|
||||||
InvalidString,
|
|
||||||
InvalidRegexp,
|
|
||||||
InvalidPseudoClassSelector,
|
|
||||||
EmptyPseudoClassSelector,
|
|
||||||
InvalidPseudoClass,
|
|
||||||
InvalidPseudoElement,
|
|
||||||
UnmatchParenthesis,
|
|
||||||
NotHandled,
|
|
||||||
UnknownPseudoSelector,
|
|
||||||
InvalidNthExpression,
|
|
||||||
} || PseudoClass.Error || Combinator.Error || std.mem.Allocator.Error;
|
|
||||||
|
|
||||||
pub const ParseOptions = struct {
|
|
||||||
accept_pseudo_elts: bool = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Parser = struct {
|
|
||||||
s: []const u8, // string to parse
|
|
||||||
i: usize = 0, // current position
|
|
||||||
|
|
||||||
opts: ParseOptions,
|
|
||||||
|
|
||||||
pub fn parse(p: *Parser, allocator: Allocator) ParseError!Selector {
|
|
||||||
return p.parseSelectorGroup(allocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
// skipWhitespace consumes whitespace characters and comments.
|
|
||||||
// It returns true if there was actually anything to skip.
|
|
||||||
fn skipWhitespace(p: *Parser) bool {
|
|
||||||
var i = p.i;
|
|
||||||
while (i < p.s.len) {
|
|
||||||
const c = p.s[i];
|
|
||||||
// Whitespaces.
|
|
||||||
if (ascii.isWhitespace(c)) {
|
|
||||||
i += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comments.
|
|
||||||
if (c == '/') {
|
|
||||||
if (std.mem.startsWith(u8, p.s[i..], "/*")) {
|
|
||||||
if (std.mem.indexOf(u8, p.s[i..], "*/")) |end| {
|
|
||||||
i += end + "*/".len;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i > p.i) {
|
|
||||||
p.i = i;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseSimpleSelectorSequence parses a selector sequence that applies to
|
|
||||||
// a single element.
|
|
||||||
fn parseSimpleSelectorSequence(p: *Parser, allocator: Allocator) ParseError!Selector {
|
|
||||||
if (p.i >= p.s.len) {
|
|
||||||
return ParseError.ExpectedSelector;
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf: std.ArrayListUnmanaged(Selector) = .empty;
|
|
||||||
defer buf.deinit(allocator);
|
|
||||||
|
|
||||||
switch (p.s[p.i]) {
|
|
||||||
'*' => {
|
|
||||||
// It's the universal selector. Just skip over it, since it
|
|
||||||
// doesn't affect the meaning.
|
|
||||||
p.i += 1;
|
|
||||||
|
|
||||||
// other version of universal selector
|
|
||||||
if (p.i + 2 < p.s.len and std.mem.eql(u8, "|*", p.s[p.i .. p.i + 2])) {
|
|
||||||
p.i += 2;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'#', '.', '[', ':' => {
|
|
||||||
// There's no type selector. Wait to process the other till the
|
|
||||||
// main loop.
|
|
||||||
},
|
|
||||||
else => try buf.append(allocator, try p.parseTypeSelector(allocator)),
|
|
||||||
}
|
|
||||||
|
|
||||||
var pseudo_elt: ?PseudoClass = null;
|
|
||||||
|
|
||||||
loop: while (p.i < p.s.len) {
|
|
||||||
var ns: Selector = switch (p.s[p.i]) {
|
|
||||||
'#' => try p.parseIDSelector(allocator),
|
|
||||||
'.' => try p.parseClassSelector(allocator),
|
|
||||||
'[' => try p.parseAttributeSelector(allocator),
|
|
||||||
':' => try p.parsePseudoclassSelector(allocator),
|
|
||||||
else => break :loop,
|
|
||||||
};
|
|
||||||
errdefer ns.deinit(allocator);
|
|
||||||
|
|
||||||
// From https://drafts.csswg.org/selectors-3/#pseudo-elements :
|
|
||||||
// "Only one pseudo-element may appear per selector, and if present
|
|
||||||
// it must appear after the sequence of simple selectors that
|
|
||||||
// represents the subjects of the selector.""
|
|
||||||
switch (ns) {
|
|
||||||
.pseudo_element => |e| {
|
|
||||||
// We found a pseudo-element.
|
|
||||||
// Only one pseudo-element is accepted per selector.
|
|
||||||
if (pseudo_elt != null) return ParseError.PseudoElementNotUnique;
|
|
||||||
if (!p.opts.accept_pseudo_elts) return ParseError.PseudoElementDisabled;
|
|
||||||
|
|
||||||
pseudo_elt = e;
|
|
||||||
ns.deinit(allocator);
|
|
||||||
},
|
|
||||||
else => {
|
|
||||||
if (pseudo_elt != null) return ParseError.PseudoElementNotAtSelectorEnd;
|
|
||||||
try buf.append(allocator, ns);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no need wrap the selectors in compoundSelector
|
|
||||||
if (buf.items.len == 1 and pseudo_elt == null) {
|
|
||||||
return buf.items[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.compound = .{ .selectors = try buf.toOwnedSlice(allocator), .pseudo_elt = pseudo_elt },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseTypeSelector parses a type selector (one that matches by tag name).
|
|
||||||
fn parseTypeSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
|
||||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
|
||||||
defer buf.deinit(allocator);
|
|
||||||
try p.parseIdentifier(buf.writer(allocator));
|
|
||||||
|
|
||||||
return .{ .tag = try buf.toOwnedSlice(allocator) };
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseIdentifier parses an identifier.
|
|
||||||
fn parseIdentifier(p: *Parser, w: anytype) ParseError!void {
|
|
||||||
const prefix = '-';
|
|
||||||
var numPrefix: usize = 0;
|
|
||||||
|
|
||||||
while (p.s.len > p.i and p.s[p.i] == prefix) {
|
|
||||||
p.i += 1;
|
|
||||||
numPrefix += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p.s.len <= p.i) {
|
|
||||||
return ParseError.ExpectedSelector;
|
|
||||||
}
|
|
||||||
|
|
||||||
const c = p.s[p.i];
|
|
||||||
if (!(nameStart(c) or c == '\\')) {
|
|
||||||
return ParseError.ExpectedSelector;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ii: usize = 0;
|
|
||||||
while (ii < numPrefix) {
|
|
||||||
w.writeByte(prefix) catch return ParseError.WriteError;
|
|
||||||
ii += 1;
|
|
||||||
}
|
|
||||||
try parseName(p, w);
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseName parses a name (which is like an identifier, but doesn't have
|
|
||||||
// extra restrictions on the first character).
|
|
||||||
fn parseName(p: *Parser, w: anytype) ParseError!void {
|
|
||||||
const sel = p.s;
|
|
||||||
const sel_len = sel.len;
|
|
||||||
|
|
||||||
var i = p.i;
|
|
||||||
var ok = false;
|
|
||||||
|
|
||||||
while (i < sel_len) {
|
|
||||||
const c = sel[i];
|
|
||||||
|
|
||||||
if (nameChar(c)) {
|
|
||||||
const start = i;
|
|
||||||
while (i < sel_len and nameChar(sel[i])) i += 1;
|
|
||||||
w.writeAll(sel[start..i]) catch return ParseError.WriteError;
|
|
||||||
ok = true;
|
|
||||||
} else if (c == '\\') {
|
|
||||||
p.i = i;
|
|
||||||
try p.parseEscape(w);
|
|
||||||
i = p.i;
|
|
||||||
ok = true;
|
|
||||||
} else if (c == 0) {
|
|
||||||
w.writeAll(REPLACEMENT_CHARACTER) catch return ParseError.WriteError;
|
|
||||||
i += 1;
|
|
||||||
if (i == sel_len) {
|
|
||||||
ok = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ok) return ParseError.ExpectedName;
|
|
||||||
p.i = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseEscape parses a backslash escape.
|
|
||||||
// The returned string is owned by the caller.
|
|
||||||
fn parseEscape(p: *Parser, w: anytype) ParseError!void {
|
|
||||||
const sel = p.s;
|
|
||||||
const sel_len = sel.len;
|
|
||||||
|
|
||||||
if (sel_len < p.i + 2 or sel[p.i] != '\\') {
|
|
||||||
p.i += 1;
|
|
||||||
w.writeAll(REPLACEMENT_CHARACTER) catch return ParseError.WriteError;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = p.i + 1;
|
|
||||||
const c = sel[start];
|
|
||||||
|
|
||||||
// unicode escape (hex)
|
|
||||||
if (ascii.isHex(c)) {
|
|
||||||
var i: usize = start;
|
|
||||||
while (i < start + 6 and i < sel_len and ascii.isHex(sel[i])) {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const v = std.fmt.parseUnsigned(u21, sel[start..i], 16) catch {
|
|
||||||
p.i = i;
|
|
||||||
w.writeAll(REPLACEMENT_CHARACTER) catch return ParseError.WriteError;
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (sel_len >= i) {
|
|
||||||
if (sel_len > i) {
|
|
||||||
switch (sel[i]) {
|
|
||||||
'\r' => {
|
|
||||||
i += 1;
|
|
||||||
if (sel_len > i and sel[i] == '\n') i += 1;
|
|
||||||
},
|
|
||||||
' ', '\t', '\n', std.ascii.control_code.ff => i += 1,
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.i = i;
|
|
||||||
if (v == 0) {
|
|
||||||
w.writeAll(REPLACEMENT_CHARACTER) catch return ParseError.WriteError;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var buf: [4]u8 = undefined;
|
|
||||||
const ln = std.unicode.utf8Encode(v, &buf) catch {
|
|
||||||
w.writeAll(REPLACEMENT_CHARACTER) catch return ParseError.WriteError;
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
w.writeAll(buf[0..ln]) catch return ParseError.WriteError;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the literal character after the backslash.
|
|
||||||
p.i += 2;
|
|
||||||
w.writeByte(sel[start]) catch return ParseError.WriteError;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseIDSelector parses a selector that matches by id attribute.
|
|
||||||
fn parseIDSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedIDSelector;
|
|
||||||
if (p.s[p.i] != '#') return ParseError.ExpectedIDSelector;
|
|
||||||
|
|
||||||
p.i += 1;
|
|
||||||
|
|
||||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
|
||||||
defer buf.deinit(allocator);
|
|
||||||
|
|
||||||
try p.parseName(buf.writer(allocator));
|
|
||||||
return .{ .id = try buf.toOwnedSlice(allocator) };
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseClassSelector parses a selector that matches by class attribute.
|
|
||||||
fn parseClassSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedClassSelector;
|
|
||||||
if (p.s[p.i] != '.') return ParseError.ExpectedClassSelector;
|
|
||||||
|
|
||||||
p.i += 1;
|
|
||||||
|
|
||||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
|
||||||
defer buf.deinit(allocator);
|
|
||||||
|
|
||||||
try p.parseIdentifier(buf.writer(allocator));
|
|
||||||
return .{ .class = try buf.toOwnedSlice(allocator) };
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseAttributeSelector parses a selector that matches by attribute value.
|
|
||||||
fn parseAttributeSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
|
||||||
if (p.s[p.i] != '[') return ParseError.ExpectedAttributeSelector;
|
|
||||||
|
|
||||||
p.i += 1;
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
|
|
||||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
|
||||||
defer buf.deinit(allocator);
|
|
||||||
|
|
||||||
try p.parseIdentifier(buf.writer(allocator));
|
|
||||||
const key = try buf.toOwnedSlice(allocator);
|
|
||||||
errdefer allocator.free(key);
|
|
||||||
|
|
||||||
lowerstr(key);
|
|
||||||
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
|
||||||
if (p.s[p.i] == ']') {
|
|
||||||
p.i += 1;
|
|
||||||
return .{ .attribute = .{ .key = key } };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p.i + 2 >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
|
||||||
|
|
||||||
const op = try parseAttributeOP(p.s[p.i .. p.i + 2]);
|
|
||||||
p.i += op.len();
|
|
||||||
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
|
||||||
|
|
||||||
buf.clearRetainingCapacity();
|
|
||||||
var is_val: bool = undefined;
|
|
||||||
if (op == .regexp) {
|
|
||||||
is_val = false;
|
|
||||||
try p.parseRegex(buf.writer(allocator));
|
|
||||||
} else {
|
|
||||||
is_val = true;
|
|
||||||
switch (p.s[p.i]) {
|
|
||||||
'\'', '"' => try p.parseString(buf.writer(allocator)),
|
|
||||||
else => try p.parseIdentifier(buf.writer(allocator)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
|
||||||
|
|
||||||
// check if the attribute contains an ignore case flag
|
|
||||||
var ci = false;
|
|
||||||
if (p.s[p.i] == 'i' or p.s[p.i] == 'I') {
|
|
||||||
ci = true;
|
|
||||||
p.i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
|
||||||
|
|
||||||
if (p.s[p.i] != ']') return ParseError.InvalidAttributeSelector;
|
|
||||||
p.i += 1;
|
|
||||||
|
|
||||||
return .{ .attribute = .{
|
|
||||||
.key = key,
|
|
||||||
.val = if (is_val) try buf.toOwnedSlice(allocator) else null,
|
|
||||||
.regexp = if (!is_val) try buf.toOwnedSlice(allocator) else null,
|
|
||||||
.op = op,
|
|
||||||
.ci = ci,
|
|
||||||
} };
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseString parses a single- or double-quoted string.
|
|
||||||
fn parseString(p: *Parser, writer: anytype) ParseError!void {
|
|
||||||
const sel = p.s;
|
|
||||||
const sel_len = sel.len;
|
|
||||||
|
|
||||||
var i = p.i;
|
|
||||||
if (sel_len < i + 2) return ParseError.ExpectedString;
|
|
||||||
|
|
||||||
const quote = sel[i];
|
|
||||||
i += 1;
|
|
||||||
|
|
||||||
loop: while (i < sel_len) {
|
|
||||||
switch (sel[i]) {
|
|
||||||
'\\' => {
|
|
||||||
if (sel_len > i + 1) {
|
|
||||||
const c = sel[i + 1];
|
|
||||||
switch (c) {
|
|
||||||
'\r' => {
|
|
||||||
if (sel_len > i + 2 and sel[i + 2] == '\n') {
|
|
||||||
i += 3;
|
|
||||||
continue :loop;
|
|
||||||
}
|
|
||||||
i += 2;
|
|
||||||
continue :loop;
|
|
||||||
},
|
|
||||||
'\n', std.ascii.control_code.ff => {
|
|
||||||
i += 2;
|
|
||||||
continue :loop;
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.i = i;
|
|
||||||
try p.parseEscape(writer);
|
|
||||||
i = p.i;
|
|
||||||
},
|
|
||||||
'\r', '\n', std.ascii.control_code.ff => return ParseError.InvalidString,
|
|
||||||
else => |c| {
|
|
||||||
if (c == quote) break :loop;
|
|
||||||
const start = i;
|
|
||||||
while (i < sel_len) {
|
|
||||||
const cc = sel[i];
|
|
||||||
if (cc == quote or cc == '\\' or c == '\r' or c == '\n' or c == std.ascii.control_code.ff) break;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
writer.writeAll(sel[start..i]) catch return ParseError.WriteError;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i >= sel_len) return ParseError.InvalidString;
|
|
||||||
|
|
||||||
// Consume the final quote.
|
|
||||||
i += 1;
|
|
||||||
p.i = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseRegex parses a regular expression; the end is defined by encountering an
|
|
||||||
// unmatched closing ')' or ']' which is not consumed
|
|
||||||
fn parseRegex(p: *Parser, writer: anytype) ParseError!void {
|
|
||||||
var i = p.i;
|
|
||||||
if (p.s.len < i + 2) return ParseError.ExpectedRegexp;
|
|
||||||
|
|
||||||
// number of open parens or brackets;
|
|
||||||
// when it becomes negative, finished parsing regex
|
|
||||||
var open: isize = 0;
|
|
||||||
|
|
||||||
loop: while (i < p.s.len) {
|
|
||||||
switch (p.s[i]) {
|
|
||||||
'(', '[' => open += 1,
|
|
||||||
')', ']' => {
|
|
||||||
open -= 1;
|
|
||||||
if (open < 0) break :loop;
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i >= p.s.len) return ParseError.InvalidRegexp;
|
|
||||||
writer.writeAll(p.s[p.i..i]) catch return ParseError.WriteError;
|
|
||||||
p.i = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parsePseudoclassSelector parses a pseudoclass selector like :not(p) or a pseudo-element
|
|
||||||
// For backwards compatibility, both ':' and '::' prefix are allowed for pseudo-elements.
|
|
||||||
// https://drafts.csswg.org/selectors-3/#pseudo-elements
|
|
||||||
fn parsePseudoclassSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedPseudoClassSelector;
|
|
||||||
if (p.s[p.i] != ':') return ParseError.ExpectedPseudoClassSelector;
|
|
||||||
|
|
||||||
p.i += 1;
|
|
||||||
|
|
||||||
var must_pseudo_elt: bool = false;
|
|
||||||
if (p.i >= p.s.len) return ParseError.EmptyPseudoClassSelector;
|
|
||||||
if (p.s[p.i] == ':') { // we found a pseudo-element
|
|
||||||
must_pseudo_elt = true;
|
|
||||||
p.i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
|
||||||
defer buf.deinit(allocator);
|
|
||||||
|
|
||||||
try p.parseIdentifier(buf.writer(allocator));
|
|
||||||
|
|
||||||
const pseudo_class = try PseudoClass.parse(buf.items);
|
|
||||||
|
|
||||||
// reset the buffer to reuse it.
|
|
||||||
buf.clearRetainingCapacity();
|
|
||||||
|
|
||||||
if (must_pseudo_elt and !pseudo_class.isPseudoElement()) return ParseError.InvalidPseudoElement;
|
|
||||||
|
|
||||||
switch (pseudo_class) {
|
|
||||||
.not, .has, .haschild => {
|
|
||||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
|
||||||
|
|
||||||
const sel = try p.parseSelectorGroup(allocator);
|
|
||||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
|
||||||
|
|
||||||
const s = try allocator.create(Selector);
|
|
||||||
errdefer allocator.destroy(s);
|
|
||||||
s.* = sel;
|
|
||||||
|
|
||||||
return .{ .pseudo_class_relative = .{ .pseudo_class = pseudo_class, .match = s } };
|
|
||||||
},
|
|
||||||
.contains, .containsown => {
|
|
||||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
|
||||||
if (p.i == p.s.len) return ParseError.UnmatchParenthesis;
|
|
||||||
|
|
||||||
switch (p.s[p.i]) {
|
|
||||||
'\'', '"' => try p.parseString(buf.writer(allocator)),
|
|
||||||
else => try p.parseString(buf.writer(allocator)),
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
if (p.i >= p.s.len) return ParseError.InvalidPseudoClass;
|
|
||||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
|
||||||
|
|
||||||
const val = try buf.toOwnedSlice(allocator);
|
|
||||||
errdefer allocator.free(val);
|
|
||||||
|
|
||||||
return .{ .pseudo_class_contains = .{ .own = pseudo_class == .containsown, .val = val } };
|
|
||||||
},
|
|
||||||
.matches, .matchesown => {
|
|
||||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
|
||||||
|
|
||||||
try p.parseRegex(buf.writer(allocator));
|
|
||||||
if (p.i >= p.s.len) return ParseError.InvalidPseudoClassSelector;
|
|
||||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
|
||||||
|
|
||||||
return .{ .pseudo_class_regexp = .{ .own = pseudo_class == .matchesown, .regexp = try buf.toOwnedSlice(allocator) } };
|
|
||||||
},
|
|
||||||
.nth_child, .nth_last_child, .nth_of_type, .nth_last_of_type => {
|
|
||||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
|
||||||
const nth = try p.parseNth(allocator);
|
|
||||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
|
||||||
|
|
||||||
const last = pseudo_class == .nth_last_child or pseudo_class == .nth_last_of_type;
|
|
||||||
const of_type = pseudo_class == .nth_of_type or pseudo_class == .nth_last_of_type;
|
|
||||||
return .{ .pseudo_class_nth = .{ .a = nth[0], .b = nth[1], .of_type = of_type, .last = last } };
|
|
||||||
},
|
|
||||||
.first_child => return .{ .pseudo_class_nth = .{ .a = 0, .b = 1, .of_type = false, .last = false } },
|
|
||||||
.last_child => return .{ .pseudo_class_nth = .{ .a = 0, .b = 1, .of_type = false, .last = true } },
|
|
||||||
.first_of_type => return .{ .pseudo_class_nth = .{ .a = 0, .b = 1, .of_type = true, .last = false } },
|
|
||||||
.last_of_type => return .{ .pseudo_class_nth = .{ .a = 0, .b = 1, .of_type = true, .last = true } },
|
|
||||||
.only_child => return .{ .pseudo_class_only_child = false },
|
|
||||||
.only_of_type => return .{ .pseudo_class_only_child = true },
|
|
||||||
.input, .empty, .root, .link => return .{ .pseudo_class = pseudo_class },
|
|
||||||
.enabled, .disabled, .checked => return .{ .pseudo_class = pseudo_class },
|
|
||||||
.visible => return .{ .pseudo_class = pseudo_class },
|
|
||||||
.lang => {
|
|
||||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
|
||||||
if (p.i == p.s.len) return ParseError.UnmatchParenthesis;
|
|
||||||
|
|
||||||
try p.parseIdentifier(buf.writer(allocator));
|
|
||||||
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
if (p.i >= p.s.len) return ParseError.InvalidPseudoClass;
|
|
||||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
|
||||||
|
|
||||||
const val = try buf.toOwnedSlice(allocator);
|
|
||||||
errdefer allocator.free(val);
|
|
||||||
lowerstr(val);
|
|
||||||
|
|
||||||
return .{ .pseudo_class_lang = val };
|
|
||||||
},
|
|
||||||
.visited, .hover, .active, .focus, .target => {
|
|
||||||
// Not applicable in a static context: never match.
|
|
||||||
return .{ .never_match = pseudo_class };
|
|
||||||
},
|
|
||||||
.after, .backdrop, .before, .cue, .first_letter => return .{ .pseudo_element = pseudo_class },
|
|
||||||
.first_line, .grammar_error, .marker, .placeholder => return .{ .pseudo_element = pseudo_class },
|
|
||||||
.selection, .spelling_error => return .{ .pseudo_element = pseudo_class },
|
|
||||||
.modal, .popover_open => return .{ .pseudo_element = pseudo_class },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// consumeParenthesis consumes an opening parenthesis and any following
|
|
||||||
// whitespace. It returns true if there was actually a parenthesis to skip.
|
|
||||||
fn consumeParenthesis(p: *Parser) bool {
|
|
||||||
if (p.i < p.s.len and p.s[p.i] == '(') {
|
|
||||||
p.i += 1;
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseSelectorGroup parses a group of selectors, separated by commas.
|
|
||||||
fn parseSelectorGroup(p: *Parser, allocator: Allocator) ParseError!Selector {
|
|
||||||
const s = try p.parseSelector(allocator);
|
|
||||||
|
|
||||||
var buf: std.ArrayListUnmanaged(Selector) = .empty;
|
|
||||||
defer buf.deinit(allocator);
|
|
||||||
|
|
||||||
try buf.append(allocator, s);
|
|
||||||
|
|
||||||
while (p.i < p.s.len) {
|
|
||||||
if (p.s[p.i] != ',') break;
|
|
||||||
p.i += 1;
|
|
||||||
const ss = try p.parseSelector(allocator);
|
|
||||||
try buf.append(allocator, ss);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buf.items.len == 1) {
|
|
||||||
return buf.items[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{ .group = try buf.toOwnedSlice(allocator) };
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseSelector parses a selector that may include combinators.
|
|
||||||
fn parseSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
var s = try p.parseSimpleSelectorSequence(allocator);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
var combinator: Combinator = .empty;
|
|
||||||
if (p.skipWhitespace()) {
|
|
||||||
combinator = .descendant;
|
|
||||||
}
|
|
||||||
if (p.i >= p.s.len) {
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (p.s[p.i]) {
|
|
||||||
'+', '>', '~' => {
|
|
||||||
combinator = try Combinator.parse(p.s[p.i]);
|
|
||||||
p.i += 1;
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
},
|
|
||||||
// These characters can't begin a selector, but they can legally occur after one.
|
|
||||||
',', ')' => {
|
|
||||||
return s;
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
if (combinator == .empty) {
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
const c = try p.parseSimpleSelectorSequence(allocator);
|
|
||||||
|
|
||||||
const first = try allocator.create(Selector);
|
|
||||||
errdefer allocator.destroy(first);
|
|
||||||
first.* = s;
|
|
||||||
|
|
||||||
const second = try allocator.create(Selector);
|
|
||||||
errdefer allocator.destroy(second);
|
|
||||||
second.* = c;
|
|
||||||
|
|
||||||
s = Selector{ .combined = .{
|
|
||||||
.first = first,
|
|
||||||
.second = second,
|
|
||||||
.combinator = combinator,
|
|
||||||
} };
|
|
||||||
}
|
|
||||||
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
// consumeClosingParenthesis consumes a closing parenthesis and any preceding
|
|
||||||
// whitespace. It returns true if there was actually a parenthesis to skip.
|
|
||||||
fn consumeClosingParenthesis(p: *Parser) bool {
|
|
||||||
const i = p.i;
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
if (p.i < p.s.len and p.s[p.i] == ')') {
|
|
||||||
p.i += 1;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
p.i = i;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseInteger parses a decimal integer.
|
|
||||||
fn parseInteger(p: *Parser) ParseError!isize {
|
|
||||||
var i = p.i;
|
|
||||||
const start = i;
|
|
||||||
while (i < p.s.len and '0' <= p.s[i] and p.s[i] <= '9') i += 1;
|
|
||||||
if (i == start) return ParseError.ExpectedInteger;
|
|
||||||
p.i = i;
|
|
||||||
|
|
||||||
return std.fmt.parseUnsigned(isize, p.s[start..i], 10) catch ParseError.ExpectedInteger;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseNthReadN(p: *Parser, a: isize) ParseError![2]isize {
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
|
||||||
|
|
||||||
return switch (p.s[p.i]) {
|
|
||||||
'+' => {
|
|
||||||
p.i += 1;
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
const b = try p.parseInteger();
|
|
||||||
return .{ a, b };
|
|
||||||
},
|
|
||||||
'-' => {
|
|
||||||
p.i += 1;
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
const b = try p.parseInteger();
|
|
||||||
return .{ a, -b };
|
|
||||||
},
|
|
||||||
else => .{ a, 0 },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseNthReadA(p: *Parser, a: isize) ParseError![2]isize {
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
|
||||||
return switch (p.s[p.i]) {
|
|
||||||
'n', 'N' => {
|
|
||||||
p.i += 1;
|
|
||||||
return p.parseNthReadN(a);
|
|
||||||
},
|
|
||||||
else => .{ 0, a },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseNthNegativeA(p: *Parser) ParseError![2]isize {
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
|
||||||
const c = p.s[p.i];
|
|
||||||
if (std.ascii.isDigit(c)) {
|
|
||||||
const a = try p.parseInteger() * -1;
|
|
||||||
return p.parseNthReadA(a);
|
|
||||||
}
|
|
||||||
if (c == 'n' or c == 'N') {
|
|
||||||
p.i += 1;
|
|
||||||
return p.parseNthReadN(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ParseError.InvalidNthExpression;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseNthPositiveA(p: *Parser) ParseError![2]isize {
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
|
||||||
const c = p.s[p.i];
|
|
||||||
if (std.ascii.isDigit(c)) {
|
|
||||||
const a = try p.parseInteger();
|
|
||||||
return p.parseNthReadA(a);
|
|
||||||
}
|
|
||||||
if (c == 'n' or c == 'N') {
|
|
||||||
p.i += 1;
|
|
||||||
return p.parseNthReadN(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ParseError.InvalidNthExpression;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseNth parses the argument for :nth-child (normally of the form an+b).
|
|
||||||
fn parseNth(p: *Parser, allocator: Allocator) ParseError![2]isize {
|
|
||||||
// initial state
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
|
||||||
return switch (p.s[p.i]) {
|
|
||||||
'-' => {
|
|
||||||
p.i += 1;
|
|
||||||
return p.parseNthNegativeA();
|
|
||||||
},
|
|
||||||
'+' => {
|
|
||||||
p.i += 1;
|
|
||||||
return p.parseNthPositiveA();
|
|
||||||
},
|
|
||||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => p.parseNthPositiveA(),
|
|
||||||
'n', 'N' => {
|
|
||||||
p.i += 1;
|
|
||||||
return p.parseNthReadN(1);
|
|
||||||
},
|
|
||||||
'o', 'O', 'e', 'E' => {
|
|
||||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
|
||||||
defer buf.deinit(allocator);
|
|
||||||
|
|
||||||
try p.parseName(buf.writer(allocator));
|
|
||||||
|
|
||||||
if (std.ascii.eqlIgnoreCase("odd", buf.items)) return .{ 2, 1 };
|
|
||||||
if (std.ascii.eqlIgnoreCase("even", buf.items)) return .{ 2, 0 };
|
|
||||||
|
|
||||||
return ParseError.InvalidNthExpression;
|
|
||||||
},
|
|
||||||
else => ParseError.InvalidNthExpression,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// nameStart returns whether c can be the first character of an identifier
|
|
||||||
// (not counting an initial hyphen, or an escape sequence).
|
|
||||||
fn nameStart(c: u8) bool {
|
|
||||||
return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127 or
|
|
||||||
'0' <= c and c <= '9';
|
|
||||||
}
|
|
||||||
|
|
||||||
// nameChar returns whether c can be a character within an identifier
|
|
||||||
// (not counting an escape sequence).
|
|
||||||
fn nameChar(c: u8) bool {
|
|
||||||
return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127 or
|
|
||||||
c == '-' or '0' <= c and c <= '9';
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lowerstr(str: []u8) void {
|
|
||||||
for (str, 0..) |c, i| {
|
|
||||||
str[i] = std.ascii.toLower(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseAttributeOP parses an AttributeOP from a string of 1 or 2 bytes.
|
|
||||||
fn parseAttributeOP(s: []const u8) ParseError!AttributeOP {
|
|
||||||
if (s.len < 1 or s.len > 2) return ParseError.InvalidAttributeOperator;
|
|
||||||
|
|
||||||
// if the first sign is equal, we don't check anything else.
|
|
||||||
if (s[0] == '=') return .eql;
|
|
||||||
|
|
||||||
if (s.len != 2 or s[1] != '=') return ParseError.InvalidAttributeOperator;
|
|
||||||
|
|
||||||
return switch (s[0]) {
|
|
||||||
'=' => .eql,
|
|
||||||
'!' => .not_eql,
|
|
||||||
'~' => .one_of,
|
|
||||||
'|' => .prefix_hyphen,
|
|
||||||
'^' => .prefix,
|
|
||||||
'$' => .suffix,
|
|
||||||
'*' => .contains,
|
|
||||||
'#' => .regexp,
|
|
||||||
else => ParseError.InvalidAttributeOperator,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test "parser.skipWhitespace" {
|
|
||||||
const testcases = [_]struct {
|
|
||||||
s: []const u8,
|
|
||||||
i: usize,
|
|
||||||
r: bool,
|
|
||||||
}{
|
|
||||||
.{ .s = "", .i = 0, .r = false },
|
|
||||||
.{ .s = "foo", .i = 0, .r = false },
|
|
||||||
.{ .s = " ", .i = 1, .r = true },
|
|
||||||
.{ .s = " foo", .i = 1, .r = true },
|
|
||||||
.{ .s = "/* foo */ bar", .i = 10, .r = true },
|
|
||||||
.{ .s = "/* foo", .i = 0, .r = false },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
|
||||||
const res = p.skipWhitespace();
|
|
||||||
try std.testing.expectEqual(tc.r, res);
|
|
||||||
try std.testing.expectEqual(tc.i, p.i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "parser.parseIdentifier" {
|
|
||||||
const allocator = std.testing.allocator;
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
s: []const u8, // given value
|
|
||||||
exp: []const u8, // expected value
|
|
||||||
err: bool = false,
|
|
||||||
}{
|
|
||||||
.{ .s = "x", .exp = "x" },
|
|
||||||
.{ .s = "96", .exp = "96", .err = false },
|
|
||||||
.{ .s = "-x", .exp = "-x" },
|
|
||||||
.{ .s = "r\\e9 sumé", .exp = "résumé" },
|
|
||||||
.{ .s = "r\\0000e9 sumé", .exp = "résumé" },
|
|
||||||
.{ .s = "r\\0000e9sumé", .exp = "résumé" },
|
|
||||||
.{ .s = "a\\\"b", .exp = "a\"b" },
|
|
||||||
};
|
|
||||||
|
|
||||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
|
||||||
defer buf.deinit(allocator);
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
buf.clearRetainingCapacity();
|
|
||||||
|
|
||||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
|
||||||
p.parseIdentifier(buf.writer(allocator)) catch |e| {
|
|
||||||
// if error was expected, continue.
|
|
||||||
if (tc.err) continue;
|
|
||||||
|
|
||||||
std.debug.print("test case {s}\n", .{tc.s});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
std.testing.expectEqualDeep(tc.exp, buf.items) catch |e| {
|
|
||||||
std.debug.print("test case {s} : {s}\n", .{ tc.s, buf.items });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "parser.parseString" {
|
|
||||||
const allocator = std.testing.allocator;
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
s: []const u8, // given value
|
|
||||||
exp: []const u8, // expected value
|
|
||||||
err: bool = false,
|
|
||||||
}{
|
|
||||||
.{ .s = "\"x\"", .exp = "x" },
|
|
||||||
.{ .s = "'x'", .exp = "x" },
|
|
||||||
.{ .s = "'x", .exp = "", .err = true },
|
|
||||||
.{ .s = "'x\\\r\nx'", .exp = "xx" },
|
|
||||||
.{ .s = "\"r\\e9 sumé\"", .exp = "résumé" },
|
|
||||||
.{ .s = "\"r\\0000e9 sumé\"", .exp = "résumé" },
|
|
||||||
.{ .s = "\"r\\0000e9sumé\"", .exp = "résumé" },
|
|
||||||
.{ .s = "\"a\\\"b\"", .exp = "a\"b" },
|
|
||||||
.{ .s = "\"\\\n\"", .exp = "" },
|
|
||||||
.{ .s = "\"hello world\"", .exp = "hello world" },
|
|
||||||
};
|
|
||||||
|
|
||||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
|
||||||
defer buf.deinit(allocator);
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
buf.clearRetainingCapacity();
|
|
||||||
|
|
||||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
|
||||||
p.parseString(buf.writer(allocator)) catch |e| {
|
|
||||||
// if error was expected, continue.
|
|
||||||
if (tc.err) continue;
|
|
||||||
|
|
||||||
std.debug.print("test case {s}\n", .{tc.s});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
std.testing.expectEqualDeep(tc.exp, buf.items) catch |e| {
|
|
||||||
std.debug.print("test case {s} : {s}\n", .{ tc.s, buf.items });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "parser.parse" {
|
|
||||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
||||||
defer arena.deinit();
|
|
||||||
const allocator = arena.allocator();
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
s: []const u8, // given value
|
|
||||||
exp: Selector, // expected value
|
|
||||||
err: bool = false,
|
|
||||||
}{
|
|
||||||
.{ .s = "root", .exp = .{ .tag = "root" } },
|
|
||||||
.{ .s = ".root", .exp = .{ .class = "root" } },
|
|
||||||
.{ .s = ":root", .exp = .{ .pseudo_class = .root } },
|
|
||||||
.{ .s = ".\\:bar", .exp = .{ .class = ":bar" } },
|
|
||||||
.{ .s = ".foo\\:bar", .exp = .{ .class = "foo:bar" } },
|
|
||||||
.{ .s = "[class=75c0fa18a94b9e3a6b8e14d6cbe688a27f5da10a]", .exp = .{ .attribute = .{ .key = "class", .val = "75c0fa18a94b9e3a6b8e14d6cbe688a27f5da10a", .op = .eql } } },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
|
||||||
const sel = p.parse(allocator) catch |e| {
|
|
||||||
// if error was expected, continue.
|
|
||||||
if (tc.err) continue;
|
|
||||||
|
|
||||||
std.debug.print("test case {s}\n", .{tc.s});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
std.testing.expectEqualDeep(tc.exp, sel) catch |e| {
|
|
||||||
std.debug.print("test case {s} : {}\n", .{ tc.s, sel });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,289 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const CSSConstants = struct {
|
|
||||||
const IMPORTANT = "!important";
|
|
||||||
const URL_PREFIX = "url(";
|
|
||||||
};
|
|
||||||
|
|
||||||
const CSSParserState = enum {
|
|
||||||
seek_name,
|
|
||||||
in_name,
|
|
||||||
seek_colon,
|
|
||||||
seek_value,
|
|
||||||
in_value,
|
|
||||||
in_quoted_value,
|
|
||||||
in_single_quoted_value,
|
|
||||||
in_url,
|
|
||||||
in_important,
|
|
||||||
};
|
|
||||||
|
|
||||||
const CSSDeclaration = struct {
|
|
||||||
name: []const u8,
|
|
||||||
value: []const u8,
|
|
||||||
is_important: bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
const CSSParser = @This();
|
|
||||||
state: CSSParserState,
|
|
||||||
name_start: usize,
|
|
||||||
name_end: usize,
|
|
||||||
value_start: usize,
|
|
||||||
position: usize,
|
|
||||||
paren_depth: usize,
|
|
||||||
escape_next: bool,
|
|
||||||
|
|
||||||
pub fn init() CSSParser {
|
|
||||||
return .{
|
|
||||||
.state = .seek_name,
|
|
||||||
.name_start = 0,
|
|
||||||
.name_end = 0,
|
|
||||||
.value_start = 0,
|
|
||||||
.position = 0,
|
|
||||||
.paren_depth = 0,
|
|
||||||
.escape_next = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parseDeclarations(arena: Allocator, text: []const u8) ![]CSSDeclaration {
|
|
||||||
var parser = init();
|
|
||||||
var declarations: std.ArrayListUnmanaged(CSSDeclaration) = .empty;
|
|
||||||
|
|
||||||
while (parser.position < text.len) {
|
|
||||||
const c = text[parser.position];
|
|
||||||
|
|
||||||
switch (parser.state) {
|
|
||||||
.seek_name => {
|
|
||||||
if (!std.ascii.isWhitespace(c)) {
|
|
||||||
parser.name_start = parser.position;
|
|
||||||
parser.state = .in_name;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.in_name => {
|
|
||||||
if (c == ':') {
|
|
||||||
parser.name_end = parser.position;
|
|
||||||
parser.state = .seek_value;
|
|
||||||
} else if (std.ascii.isWhitespace(c)) {
|
|
||||||
parser.name_end = parser.position;
|
|
||||||
parser.state = .seek_colon;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.seek_colon => {
|
|
||||||
if (c == ':') {
|
|
||||||
parser.state = .seek_value;
|
|
||||||
} else if (!std.ascii.isWhitespace(c)) {
|
|
||||||
parser.state = .seek_name;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.seek_value => {
|
|
||||||
if (!std.ascii.isWhitespace(c)) {
|
|
||||||
parser.value_start = parser.position;
|
|
||||||
if (c == '"') {
|
|
||||||
parser.state = .in_quoted_value;
|
|
||||||
} else if (c == '\'') {
|
|
||||||
parser.state = .in_single_quoted_value;
|
|
||||||
} else if (c == 'u' and parser.position + CSSConstants.URL_PREFIX.len <= text.len and std.mem.startsWith(u8, text[parser.position..], CSSConstants.URL_PREFIX)) {
|
|
||||||
parser.state = .in_url;
|
|
||||||
parser.paren_depth = 1;
|
|
||||||
parser.position += 3;
|
|
||||||
} else {
|
|
||||||
parser.state = .in_value;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.in_value => {
|
|
||||||
if (parser.escape_next) {
|
|
||||||
parser.escape_next = false;
|
|
||||||
} else if (c == '\\') {
|
|
||||||
parser.escape_next = true;
|
|
||||||
} else if (c == '(') {
|
|
||||||
parser.paren_depth += 1;
|
|
||||||
} else if (c == ')' and parser.paren_depth > 0) {
|
|
||||||
parser.paren_depth -= 1;
|
|
||||||
} else if (c == ';' and parser.paren_depth == 0) {
|
|
||||||
try parser.finishDeclaration(arena, &declarations, text);
|
|
||||||
parser.state = .seek_name;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.in_quoted_value => {
|
|
||||||
if (parser.escape_next) {
|
|
||||||
parser.escape_next = false;
|
|
||||||
} else if (c == '\\') {
|
|
||||||
parser.escape_next = true;
|
|
||||||
} else if (c == '"') {
|
|
||||||
parser.state = .in_value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.in_single_quoted_value => {
|
|
||||||
if (parser.escape_next) {
|
|
||||||
parser.escape_next = false;
|
|
||||||
} else if (c == '\\') {
|
|
||||||
parser.escape_next = true;
|
|
||||||
} else if (c == '\'') {
|
|
||||||
parser.state = .in_value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.in_url => {
|
|
||||||
if (parser.escape_next) {
|
|
||||||
parser.escape_next = false;
|
|
||||||
} else if (c == '\\') {
|
|
||||||
parser.escape_next = true;
|
|
||||||
} else if (c == '(') {
|
|
||||||
parser.paren_depth += 1;
|
|
||||||
} else if (c == ')') {
|
|
||||||
parser.paren_depth -= 1;
|
|
||||||
if (parser.paren_depth == 0) {
|
|
||||||
parser.state = .in_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.in_important => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
parser.position += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
try parser.finalize(arena, &declarations, text);
|
|
||||||
|
|
||||||
return declarations.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finishDeclaration(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
|
|
||||||
const name = std.mem.trim(u8, text[self.name_start..self.name_end], &std.ascii.whitespace);
|
|
||||||
if (name.len == 0) return;
|
|
||||||
|
|
||||||
const raw_value = text[self.value_start..self.position];
|
|
||||||
const value = std.mem.trim(u8, raw_value, &std.ascii.whitespace);
|
|
||||||
|
|
||||||
var final_value = value;
|
|
||||||
var is_important = false;
|
|
||||||
|
|
||||||
if (std.mem.endsWith(u8, value, CSSConstants.IMPORTANT)) {
|
|
||||||
is_important = true;
|
|
||||||
final_value = std.mem.trimRight(u8, value[0 .. value.len - CSSConstants.IMPORTANT.len], &std.ascii.whitespace);
|
|
||||||
}
|
|
||||||
|
|
||||||
try declarations.append(arena, .{
|
|
||||||
.name = name,
|
|
||||||
.value = final_value,
|
|
||||||
.is_important = is_important,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finalize(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
|
|
||||||
if (self.state != .in_value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return self.finishDeclaration(arena, declarations, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: CSS.Parser - Simple property" {
|
|
||||||
defer testing.reset();
|
|
||||||
|
|
||||||
const text = "color: red;";
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
|
||||||
|
|
||||||
try testing.expectEqual(1, declarations.len);
|
|
||||||
try testing.expectEqual("color", declarations[0].name);
|
|
||||||
try testing.expectEqual("red", declarations[0].value);
|
|
||||||
try testing.expectEqual(false, declarations[0].is_important);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.Parser - Property with !important" {
|
|
||||||
defer testing.reset();
|
|
||||||
const text = "margin: 10px !important;";
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
|
||||||
|
|
||||||
try testing.expectEqual(1, declarations.len);
|
|
||||||
try testing.expectEqual("margin", declarations[0].name);
|
|
||||||
try testing.expectEqual("10px", declarations[0].value);
|
|
||||||
try testing.expectEqual(true, declarations[0].is_important);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.Parser - Multiple properties" {
|
|
||||||
defer testing.reset();
|
|
||||||
const text = "color: red; font-size: 12px; margin: 5px !important;";
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
|
||||||
|
|
||||||
try testing.expect(declarations.len == 3);
|
|
||||||
|
|
||||||
try testing.expectEqual("color", declarations[0].name);
|
|
||||||
try testing.expectEqual("red", declarations[0].value);
|
|
||||||
try testing.expectEqual(false, declarations[0].is_important);
|
|
||||||
|
|
||||||
try testing.expectEqual("font-size", declarations[1].name);
|
|
||||||
try testing.expectEqual("12px", declarations[1].value);
|
|
||||||
try testing.expectEqual(false, declarations[1].is_important);
|
|
||||||
|
|
||||||
try testing.expectEqual("margin", declarations[2].name);
|
|
||||||
try testing.expectEqual("5px", declarations[2].value);
|
|
||||||
try testing.expectEqual(true, declarations[2].is_important);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.Parser - Quoted value with semicolon" {
|
|
||||||
defer testing.reset();
|
|
||||||
const text = "content: \"Hello; world!\";";
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
|
||||||
|
|
||||||
try testing.expectEqual(1, declarations.len);
|
|
||||||
try testing.expectEqual("content", declarations[0].name);
|
|
||||||
try testing.expectEqual("\"Hello; world!\"", declarations[0].value);
|
|
||||||
try testing.expectEqual(false, declarations[0].is_important);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.Parser - URL value" {
|
|
||||||
defer testing.reset();
|
|
||||||
const text = "background-image: url(\"test.png\");";
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
|
||||||
|
|
||||||
try testing.expectEqual(1, declarations.len);
|
|
||||||
try testing.expectEqual("background-image", declarations[0].name);
|
|
||||||
try testing.expectEqual("url(\"test.png\")", declarations[0].value);
|
|
||||||
try testing.expectEqual(false, declarations[0].is_important);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.Parser - Whitespace handling" {
|
|
||||||
defer testing.reset();
|
|
||||||
const text = " color : purple ; margin : 10px ; ";
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
|
||||||
|
|
||||||
try testing.expectEqual(2, declarations.len);
|
|
||||||
try testing.expectEqual("color", declarations[0].name);
|
|
||||||
try testing.expectEqual("purple", declarations[0].value);
|
|
||||||
try testing.expectEqual("margin", declarations[1].name);
|
|
||||||
try testing.expectEqual("10px", declarations[1].value);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const CSSRule = @import("CSSRule.zig");
|
|
||||||
|
|
||||||
const CSSImportRule = CSSRule.CSSImportRule;
|
|
||||||
|
|
||||||
const CSSRuleList = @This();
|
|
||||||
list: std.ArrayListUnmanaged([]const u8),
|
|
||||||
|
|
||||||
pub fn constructor() CSSRuleList {
|
|
||||||
return .{ .list = .empty };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _item(self: *CSSRuleList, _index: u32) ?CSSRule {
|
|
||||||
const index: usize = @intCast(_index);
|
|
||||||
|
|
||||||
if (index > self.list.items.len) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: for now, just return null.
|
|
||||||
// this depends on properly parsing CSSRule
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_length(self: *CSSRuleList) u32 {
|
|
||||||
return @intCast(self.list.items.len);
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: CSS.CSSRuleList" {
|
|
||||||
try testing.htmlRunner("cssom/css_rule_list.html");
|
|
||||||
}
|
|
||||||
@@ -1,958 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const CSSRule = @import("CSSRule.zig");
|
|
||||||
const CSSParser = @import("CSSParser.zig");
|
|
||||||
|
|
||||||
const Property = struct {
|
|
||||||
value: []const u8,
|
|
||||||
priority: bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
const CSSStyleDeclaration = @This();
|
|
||||||
|
|
||||||
properties: std.StringArrayHashMapUnmanaged(Property),
|
|
||||||
|
|
||||||
pub const empty: CSSStyleDeclaration = .{
|
|
||||||
.properties = .empty,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn get_cssFloat(self: *const CSSStyleDeclaration) []const u8 {
|
|
||||||
return self._getPropertyValue("float");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_cssFloat(self: *CSSStyleDeclaration, value: ?[]const u8, page: *Page) !void {
|
|
||||||
const final_value = value orelse "";
|
|
||||||
return self._setProperty("float", final_value, null, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_cssText(self: *const CSSStyleDeclaration, page: *Page) ![]const u8 {
|
|
||||||
var buffer: std.ArrayListUnmanaged(u8) = .empty;
|
|
||||||
const writer = buffer.writer(page.call_arena);
|
|
||||||
var it = self.properties.iterator();
|
|
||||||
while (it.next()) |entry| {
|
|
||||||
const name = entry.key_ptr.*;
|
|
||||||
const property = entry.value_ptr;
|
|
||||||
const escaped = try escapeCSSValue(page.call_arena, property.value);
|
|
||||||
try writer.print("{s}: {s}", .{ name, escaped });
|
|
||||||
if (property.priority) {
|
|
||||||
try writer.writeAll(" !important; ");
|
|
||||||
} else {
|
|
||||||
try writer.writeAll("; ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buffer.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Propagate also upward to parent node
|
|
||||||
pub fn set_cssText(self: *CSSStyleDeclaration, text: []const u8, page: *Page) !void {
|
|
||||||
self.properties.clearRetainingCapacity();
|
|
||||||
|
|
||||||
// call_arena is safe here, because _setProperty will dupe the name
|
|
||||||
// using the page's longer-living arena.
|
|
||||||
const declarations = try CSSParser.parseDeclarations(page.call_arena, text);
|
|
||||||
|
|
||||||
for (declarations) |decl| {
|
|
||||||
if (!isValidPropertyName(decl.name)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const priority: ?[]const u8 = if (decl.is_important) "important" else null;
|
|
||||||
try self._setProperty(decl.name, decl.value, priority, page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_length(self: *const CSSStyleDeclaration) usize {
|
|
||||||
return self.properties.count();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_parentRule(_: *const CSSStyleDeclaration) ?CSSRule {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getPropertyPriority(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
|
|
||||||
const property = self.properties.getPtr(name) orelse return "";
|
|
||||||
return if (property.priority) "important" else "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO should handle properly shorthand properties and canonical forms
|
|
||||||
pub fn _getPropertyValue(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
|
|
||||||
if (self.properties.getPtr(name)) |property| {
|
|
||||||
return property.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// default to everything being visible (unless it's been explicitly set)
|
|
||||||
if (std.mem.eql(u8, name, "visibility")) {
|
|
||||||
return "visible";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _item(self: *const CSSStyleDeclaration, index: usize) []const u8 {
|
|
||||||
const values = self.properties.entries.items(.key);
|
|
||||||
if (index >= values.len) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return values[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _removeProperty(self: *CSSStyleDeclaration, name: []const u8) ![]const u8 {
|
|
||||||
const property = self.properties.fetchOrderedRemove(name) orelse return "";
|
|
||||||
return property.value.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setProperty(self: *CSSStyleDeclaration, name: []const u8, value: []const u8, priority: ?[]const u8, page: *Page) !void {
|
|
||||||
const gop = try self.properties.getOrPut(page.arena, name);
|
|
||||||
if (!gop.found_existing) {
|
|
||||||
const owned_name = try page.arena.dupe(u8, name);
|
|
||||||
gop.key_ptr.* = owned_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const owned_value = try page.arena.dupe(u8, value);
|
|
||||||
const is_important = priority != null and std.ascii.eqlIgnoreCase(priority.?, "important");
|
|
||||||
gop.value_ptr.* = .{ .value = owned_value, .priority = is_important };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn named_get(self: *const CSSStyleDeclaration, name: []const u8, _: *bool) []const u8 {
|
|
||||||
return self._getPropertyValue(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn named_set(self: *CSSStyleDeclaration, name: []const u8, value: []const u8, _: *bool, page: *Page) !void {
|
|
||||||
return self._setProperty(name, value, null, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isNumericWithUnit(value: []const u8) bool {
|
|
||||||
if (value.len == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const first = value[0];
|
|
||||||
|
|
||||||
if (!std.ascii.isDigit(first) and first != '+' and first != '-' and first != '.') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
var has_digit = false;
|
|
||||||
var decimal_point = false;
|
|
||||||
|
|
||||||
while (i < value.len) : (i += 1) {
|
|
||||||
const c = value[i];
|
|
||||||
if (std.ascii.isDigit(c)) {
|
|
||||||
has_digit = true;
|
|
||||||
} else if (c == '.' and !decimal_point) {
|
|
||||||
decimal_point = true;
|
|
||||||
} else if ((c == 'e' or c == 'E') and has_digit) {
|
|
||||||
if (i + 1 >= value.len) return false;
|
|
||||||
if (value[i + 1] != '+' and value[i + 1] != '-' and !std.ascii.isDigit(value[i + 1])) break;
|
|
||||||
i += 1;
|
|
||||||
if (value[i] == '+' or value[i] == '-') {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
var has_exp_digits = false;
|
|
||||||
while (i < value.len and std.ascii.isDigit(value[i])) : (i += 1) {
|
|
||||||
has_exp_digits = true;
|
|
||||||
}
|
|
||||||
if (!has_exp_digits) return false;
|
|
||||||
break;
|
|
||||||
} else if (c != '-' and c != '+') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!has_digit) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i == value.len) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const unit = value[i..];
|
|
||||||
return CSSKeywords.isValidUnit(unit);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isHexColor(value: []const u8) bool {
|
|
||||||
if (value.len == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (value[0] != '#') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hex_part = value[1..];
|
|
||||||
if (hex_part.len != 3 and hex_part.len != 6 and hex_part.len != 8) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (hex_part) |c| {
|
|
||||||
if (!std.ascii.isHex(c)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isMultiValueProperty(value: []const u8) bool {
|
|
||||||
var parts = std.mem.splitAny(u8, value, " ");
|
|
||||||
var multi_value_parts: usize = 0;
|
|
||||||
var all_parts_valid = true;
|
|
||||||
|
|
||||||
while (parts.next()) |part| {
|
|
||||||
if (part.len == 0) continue;
|
|
||||||
multi_value_parts += 1;
|
|
||||||
|
|
||||||
if (isNumericWithUnit(part)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (isHexColor(part)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (CSSKeywords.isKnownKeyword(part)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (CSSKeywords.startsWithFunction(part)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
all_parts_valid = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return multi_value_parts >= 2 and all_parts_valid;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isAlreadyQuoted(value: []const u8) bool {
|
|
||||||
return value.len >= 2 and ((value[0] == '"' and value[value.len - 1] == '"') or
|
|
||||||
(value[0] == '\'' and value[value.len - 1] == '\''));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isValidPropertyName(name: []const u8) bool {
|
|
||||||
if (name.len == 0) return false;
|
|
||||||
|
|
||||||
if (std.mem.startsWith(u8, name, "--")) {
|
|
||||||
if (name.len == 2) return false;
|
|
||||||
for (name[2..]) |c| {
|
|
||||||
if (!std.ascii.isAlphanumeric(c) and c != '-' and c != '_') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const first_char = name[0];
|
|
||||||
if (!std.ascii.isAlphabetic(first_char) and first_char != '-') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (first_char == '-') {
|
|
||||||
if (name.len < 2) return false;
|
|
||||||
|
|
||||||
if (!std.ascii.isAlphabetic(name[1])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (name[2..]) |c| {
|
|
||||||
if (!std.ascii.isAlphanumeric(c) and c != '-') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (name[1..]) |c| {
|
|
||||||
if (!std.ascii.isAlphanumeric(c) and c != '-') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extractImportant(value: []const u8) struct { value: []const u8, is_important: bool } {
|
|
||||||
const trimmed = std.mem.trim(u8, value, &std.ascii.whitespace);
|
|
||||||
|
|
||||||
if (std.mem.endsWith(u8, trimmed, "!important")) {
|
|
||||||
const clean_value = std.mem.trimRight(u8, trimmed[0 .. trimmed.len - 10], &std.ascii.whitespace);
|
|
||||||
return .{ .value = clean_value, .is_important = true };
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{ .value = trimmed, .is_important = false };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn needsQuotes(value: []const u8) bool {
|
|
||||||
if (value.len == 0) return true;
|
|
||||||
if (isAlreadyQuoted(value)) return false;
|
|
||||||
|
|
||||||
if (CSSKeywords.containsSpecialChar(value)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.indexOfScalar(u8, value, ' ') == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const is_url = std.mem.startsWith(u8, value, "url(");
|
|
||||||
const is_function = CSSKeywords.startsWithFunction(value);
|
|
||||||
|
|
||||||
return !isMultiValueProperty(value) and
|
|
||||||
!is_url and
|
|
||||||
!is_function;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn escapeCSSValue(arena: std.mem.Allocator, value: []const u8) ![]const u8 {
|
|
||||||
if (!needsQuotes(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
var out: std.ArrayListUnmanaged(u8) = .empty;
|
|
||||||
|
|
||||||
// We'll need at least this much space, +2 for the quotes
|
|
||||||
try out.ensureTotalCapacity(arena, value.len + 2);
|
|
||||||
const writer = out.writer(arena);
|
|
||||||
|
|
||||||
try writer.writeByte('"');
|
|
||||||
|
|
||||||
for (value, 0..) |c, i| {
|
|
||||||
switch (c) {
|
|
||||||
'"' => try writer.writeAll("\\\""),
|
|
||||||
'\\' => try writer.writeAll("\\\\"),
|
|
||||||
'\n' => try writer.writeAll("\\A "),
|
|
||||||
'\r' => try writer.writeAll("\\D "),
|
|
||||||
'\t' => try writer.writeAll("\\9 "),
|
|
||||||
0...8, 11, 12, 14...31, 127 => {
|
|
||||||
try writer.print("\\{x}", .{c});
|
|
||||||
if (i + 1 < value.len and std.ascii.isHex(value[i + 1])) {
|
|
||||||
try writer.writeByte(' ');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => try writer.writeByte(c),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try writer.writeByte('"');
|
|
||||||
return out.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isKnownKeyword(value: []const u8) bool {
|
|
||||||
return CSSKeywords.isKnownKeyword(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn containsSpecialChar(value: []const u8) bool {
|
|
||||||
return CSSKeywords.containsSpecialChar(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const CSSKeywords = struct {
|
|
||||||
const BORDER_STYLES = [_][]const u8{
|
|
||||||
"none", "solid", "dotted", "dashed", "double", "groove", "ridge", "inset", "outset",
|
|
||||||
};
|
|
||||||
|
|
||||||
const COLOR_NAMES = [_][]const u8{
|
|
||||||
"black", "white", "red", "green", "blue", "yellow", "purple", "gray", "transparent",
|
|
||||||
"currentColor", "inherit",
|
|
||||||
};
|
|
||||||
|
|
||||||
const POSITION_KEYWORDS = [_][]const u8{
|
|
||||||
"auto", "center", "left", "right", "top", "bottom",
|
|
||||||
};
|
|
||||||
|
|
||||||
const BACKGROUND_REPEAT = [_][]const u8{
|
|
||||||
"repeat", "no-repeat", "repeat-x", "repeat-y", "space", "round",
|
|
||||||
};
|
|
||||||
|
|
||||||
const FONT_STYLES = [_][]const u8{
|
|
||||||
"normal", "italic", "oblique", "bold", "bolder", "lighter",
|
|
||||||
};
|
|
||||||
|
|
||||||
const FONT_SIZES = [_][]const u8{
|
|
||||||
"xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large",
|
|
||||||
"smaller", "larger",
|
|
||||||
};
|
|
||||||
|
|
||||||
const FONT_FAMILIES = [_][]const u8{
|
|
||||||
"serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui",
|
|
||||||
};
|
|
||||||
|
|
||||||
const CSS_GLOBAL = [_][]const u8{
|
|
||||||
"initial", "inherit", "unset", "revert",
|
|
||||||
};
|
|
||||||
|
|
||||||
const DISPLAY_VALUES = [_][]const u8{
|
|
||||||
"block", "inline", "inline-block", "flex", "grid", "none",
|
|
||||||
};
|
|
||||||
|
|
||||||
const UNITS = [_][]const u8{
|
|
||||||
// LENGTH
|
|
||||||
"px", "em", "rem", "vw", "vh", "vmin", "vmax", "%", "pt", "pc", "in", "cm", "mm",
|
|
||||||
"ex", "ch", "fr",
|
|
||||||
|
|
||||||
// ANGLE
|
|
||||||
"deg", "rad", "grad", "turn",
|
|
||||||
|
|
||||||
// TIME
|
|
||||||
"s", "ms",
|
|
||||||
|
|
||||||
// FREQUENCY
|
|
||||||
"hz", "khz",
|
|
||||||
|
|
||||||
// RESOLUTION
|
|
||||||
"dpi", "dpcm",
|
|
||||||
"dppx",
|
|
||||||
};
|
|
||||||
|
|
||||||
const SPECIAL_CHARS = [_]u8{
|
|
||||||
'"', '\'', ';', '{', '}', '\\', '<', '>', '/', '\n', '\t', '\r', '\x00', '\x7F',
|
|
||||||
};
|
|
||||||
|
|
||||||
const FUNCTIONS = [_][]const u8{
|
|
||||||
"rgb(", "rgba(", "hsl(", "hsla(", "url(", "calc(", "var(", "attr(",
|
|
||||||
"linear-gradient(", "radial-gradient(", "conic-gradient(", "translate(", "rotate(", "scale(", "skew(", "matrix(",
|
|
||||||
};
|
|
||||||
|
|
||||||
const KEYWORDS = BORDER_STYLES ++ COLOR_NAMES ++ POSITION_KEYWORDS ++
|
|
||||||
BACKGROUND_REPEAT ++ FONT_STYLES ++ FONT_SIZES ++ FONT_FAMILIES ++
|
|
||||||
CSS_GLOBAL ++ DISPLAY_VALUES;
|
|
||||||
|
|
||||||
const MAX_KEYWORD_LEN = lengthOfLongestValue(&KEYWORDS);
|
|
||||||
|
|
||||||
pub fn isKnownKeyword(value: []const u8) bool {
|
|
||||||
if (value.len > MAX_KEYWORD_LEN) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var buf: [MAX_KEYWORD_LEN]u8 = undefined;
|
|
||||||
const normalized = std.ascii.lowerString(&buf, value);
|
|
||||||
|
|
||||||
for (KEYWORDS) |keyword| {
|
|
||||||
if (std.ascii.eqlIgnoreCase(normalized, keyword)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn containsSpecialChar(value: []const u8) bool {
|
|
||||||
return std.mem.indexOfAny(u8, value, &SPECIAL_CHARS) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_UNIT_LEN = lengthOfLongestValue(&UNITS);
|
|
||||||
|
|
||||||
pub fn isValidUnit(unit: []const u8) bool {
|
|
||||||
if (unit.len > MAX_UNIT_LEN) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var buf: [MAX_UNIT_LEN]u8 = undefined;
|
|
||||||
const normalized = std.ascii.lowerString(&buf, unit);
|
|
||||||
|
|
||||||
for (UNITS) |u| {
|
|
||||||
if (std.mem.eql(u8, normalized, u)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn startsWithFunction(value: []const u8) bool {
|
|
||||||
const pos = std.mem.indexOfScalar(u8, value, '(') orelse return false;
|
|
||||||
if (pos == 0) return false;
|
|
||||||
|
|
||||||
if (std.mem.indexOfScalarPos(u8, value, pos, ')') == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const function_name = value[0..pos];
|
|
||||||
return isValidFunctionName(function_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isValidFunctionName(name: []const u8) bool {
|
|
||||||
if (name.len == 0) return false;
|
|
||||||
|
|
||||||
const first = name[0];
|
|
||||||
if (!std.ascii.isAlphabetic(first) and first != '_' and first != '-') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (name[1..]) |c| {
|
|
||||||
if (!std.ascii.isAlphanumeric(c) and c != '_' and c != '-') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn lengthOfLongestValue(values: []const []const u8) usize {
|
|
||||||
var max: usize = 0;
|
|
||||||
for (values) |v| {
|
|
||||||
max = @max(v.len, max);
|
|
||||||
}
|
|
||||||
return max;
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: CSS.StyleDeclaration" {
|
|
||||||
try testing.htmlRunner("cssom/css_style_declaration.html");
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - valid numbers with units" {
|
|
||||||
try testing.expect(isNumericWithUnit("10px"));
|
|
||||||
try testing.expect(isNumericWithUnit("3.14em"));
|
|
||||||
try testing.expect(isNumericWithUnit("-5rem"));
|
|
||||||
try testing.expect(isNumericWithUnit("+12.5%"));
|
|
||||||
try testing.expect(isNumericWithUnit("0vh"));
|
|
||||||
try testing.expect(isNumericWithUnit(".5vw"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - scientific notation" {
|
|
||||||
try testing.expect(isNumericWithUnit("1e5px"));
|
|
||||||
try testing.expect(isNumericWithUnit("2.5E-3em"));
|
|
||||||
try testing.expect(isNumericWithUnit("1e+2rem"));
|
|
||||||
try testing.expect(isNumericWithUnit("-3.14e10px"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - edge cases and invalid inputs" {
|
|
||||||
try testing.expect(!isNumericWithUnit(""));
|
|
||||||
|
|
||||||
try testing.expect(!isNumericWithUnit("px"));
|
|
||||||
try testing.expect(!isNumericWithUnit("--px"));
|
|
||||||
try testing.expect(!isNumericWithUnit(".px"));
|
|
||||||
|
|
||||||
try testing.expect(!isNumericWithUnit("1e"));
|
|
||||||
try testing.expect(!isNumericWithUnit("1epx"));
|
|
||||||
try testing.expect(!isNumericWithUnit("1e+"));
|
|
||||||
try testing.expect(!isNumericWithUnit("1e+px"));
|
|
||||||
|
|
||||||
try testing.expect(!isNumericWithUnit("1.2.3px"));
|
|
||||||
|
|
||||||
try testing.expect(!isNumericWithUnit("10xyz"));
|
|
||||||
try testing.expect(!isNumericWithUnit("5invalid"));
|
|
||||||
|
|
||||||
try testing.expect(isNumericWithUnit("10"));
|
|
||||||
try testing.expect(isNumericWithUnit("3.14"));
|
|
||||||
try testing.expect(isNumericWithUnit("-5"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: isHexColor - valid hex colors" {
|
|
||||||
try testing.expect(isHexColor("#000"));
|
|
||||||
try testing.expect(isHexColor("#fff"));
|
|
||||||
try testing.expect(isHexColor("#123456"));
|
|
||||||
try testing.expect(isHexColor("#abcdef"));
|
|
||||||
try testing.expect(isHexColor("#ABCDEF"));
|
|
||||||
try testing.expect(isHexColor("#12345678"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: isHexColor - invalid hex colors" {
|
|
||||||
try testing.expect(!isHexColor(""));
|
|
||||||
try testing.expect(!isHexColor("#"));
|
|
||||||
try testing.expect(!isHexColor("000"));
|
|
||||||
try testing.expect(!isHexColor("#00"));
|
|
||||||
try testing.expect(!isHexColor("#0000"));
|
|
||||||
try testing.expect(!isHexColor("#00000"));
|
|
||||||
try testing.expect(!isHexColor("#0000000"));
|
|
||||||
try testing.expect(!isHexColor("#000000000"));
|
|
||||||
try testing.expect(!isHexColor("#gggggg"));
|
|
||||||
try testing.expect(!isHexColor("#123xyz"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: isMultiValueProperty - valid multi-value properties" {
|
|
||||||
try testing.expect(isMultiValueProperty("10px 20px"));
|
|
||||||
try testing.expect(isMultiValueProperty("solid red"));
|
|
||||||
try testing.expect(isMultiValueProperty("#fff black"));
|
|
||||||
try testing.expect(isMultiValueProperty("1em 2em 3em 4em"));
|
|
||||||
try testing.expect(isMultiValueProperty("rgb(255,0,0) solid"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: isMultiValueProperty - invalid multi-value properties" {
|
|
||||||
try testing.expect(!isMultiValueProperty(""));
|
|
||||||
try testing.expect(!isMultiValueProperty("10px"));
|
|
||||||
try testing.expect(!isMultiValueProperty("invalid unknown"));
|
|
||||||
try testing.expect(!isMultiValueProperty("10px invalid"));
|
|
||||||
try testing.expect(!isMultiValueProperty(" "));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: isAlreadyQuoted - various quoting scenarios" {
|
|
||||||
try testing.expect(isAlreadyQuoted("\"hello\""));
|
|
||||||
try testing.expect(isAlreadyQuoted("'world'"));
|
|
||||||
try testing.expect(isAlreadyQuoted("\"\""));
|
|
||||||
try testing.expect(isAlreadyQuoted("''"));
|
|
||||||
|
|
||||||
try testing.expect(!isAlreadyQuoted(""));
|
|
||||||
try testing.expect(!isAlreadyQuoted("hello"));
|
|
||||||
try testing.expect(!isAlreadyQuoted("\""));
|
|
||||||
try testing.expect(!isAlreadyQuoted("'"));
|
|
||||||
try testing.expect(!isAlreadyQuoted("\"hello'"));
|
|
||||||
try testing.expect(!isAlreadyQuoted("'hello\""));
|
|
||||||
try testing.expect(!isAlreadyQuoted("\"hello"));
|
|
||||||
try testing.expect(!isAlreadyQuoted("hello\""));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: isValidPropertyName - valid property names" {
|
|
||||||
try testing.expect(isValidPropertyName("color"));
|
|
||||||
try testing.expect(isValidPropertyName("background-color"));
|
|
||||||
try testing.expect(isValidPropertyName("-webkit-transform"));
|
|
||||||
try testing.expect(isValidPropertyName("font-size"));
|
|
||||||
try testing.expect(isValidPropertyName("margin-top"));
|
|
||||||
try testing.expect(isValidPropertyName("z-index"));
|
|
||||||
try testing.expect(isValidPropertyName("line-height"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: isValidPropertyName - invalid property names" {
|
|
||||||
try testing.expect(!isValidPropertyName(""));
|
|
||||||
try testing.expect(!isValidPropertyName("123color"));
|
|
||||||
try testing.expect(!isValidPropertyName("color!"));
|
|
||||||
try testing.expect(!isValidPropertyName("color space"));
|
|
||||||
try testing.expect(!isValidPropertyName("@color"));
|
|
||||||
try testing.expect(!isValidPropertyName("color.test"));
|
|
||||||
try testing.expect(!isValidPropertyName("color_test"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: extractImportant - with and without !important" {
|
|
||||||
var result = extractImportant("red !important");
|
|
||||||
try testing.expect(result.is_important);
|
|
||||||
try testing.expectEqual("red", result.value);
|
|
||||||
|
|
||||||
result = extractImportant("blue");
|
|
||||||
try testing.expect(!result.is_important);
|
|
||||||
try testing.expectEqual("blue", result.value);
|
|
||||||
|
|
||||||
result = extractImportant(" green !important ");
|
|
||||||
try testing.expect(result.is_important);
|
|
||||||
try testing.expectEqual("green", result.value);
|
|
||||||
|
|
||||||
result = extractImportant("!important");
|
|
||||||
try testing.expect(result.is_important);
|
|
||||||
try testing.expectEqual("", result.value);
|
|
||||||
|
|
||||||
result = extractImportant("important");
|
|
||||||
try testing.expect(!result.is_important);
|
|
||||||
try testing.expectEqual("important", result.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: needsQuotes - various scenarios" {
|
|
||||||
try testing.expect(needsQuotes(""));
|
|
||||||
try testing.expect(needsQuotes("hello world"));
|
|
||||||
try testing.expect(needsQuotes("test;"));
|
|
||||||
try testing.expect(needsQuotes("a{b}"));
|
|
||||||
try testing.expect(needsQuotes("test\"quote"));
|
|
||||||
|
|
||||||
try testing.expect(!needsQuotes("\"already quoted\""));
|
|
||||||
try testing.expect(!needsQuotes("'already quoted'"));
|
|
||||||
try testing.expect(!needsQuotes("url(image.png)"));
|
|
||||||
try testing.expect(!needsQuotes("rgb(255, 0, 0)"));
|
|
||||||
try testing.expect(!needsQuotes("10px 20px"));
|
|
||||||
try testing.expect(!needsQuotes("simple"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: escapeCSSValue - escaping various characters" {
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
var result = try escapeCSSValue(allocator, "simple");
|
|
||||||
try testing.expectEqual("simple", result);
|
|
||||||
|
|
||||||
result = try escapeCSSValue(allocator, "\"already quoted\"");
|
|
||||||
try testing.expectEqual("\"already quoted\"", result);
|
|
||||||
|
|
||||||
result = try escapeCSSValue(allocator, "test\"quote");
|
|
||||||
try testing.expectEqual("\"test\\\"quote\"", result);
|
|
||||||
|
|
||||||
result = try escapeCSSValue(allocator, "test\nline");
|
|
||||||
try testing.expectEqual("\"test\\A line\"", result);
|
|
||||||
|
|
||||||
result = try escapeCSSValue(allocator, "test\\back");
|
|
||||||
try testing.expectEqual("\"test\\\\back\"", result);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: CSSKeywords.isKnownKeyword - case sensitivity" {
|
|
||||||
try testing.expect(CSSKeywords.isKnownKeyword("red"));
|
|
||||||
try testing.expect(CSSKeywords.isKnownKeyword("solid"));
|
|
||||||
try testing.expect(CSSKeywords.isKnownKeyword("center"));
|
|
||||||
try testing.expect(CSSKeywords.isKnownKeyword("inherit"));
|
|
||||||
|
|
||||||
try testing.expect(CSSKeywords.isKnownKeyword("RED"));
|
|
||||||
try testing.expect(CSSKeywords.isKnownKeyword("Red"));
|
|
||||||
try testing.expect(CSSKeywords.isKnownKeyword("SOLID"));
|
|
||||||
try testing.expect(CSSKeywords.isKnownKeyword("Center"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSKeywords.isKnownKeyword("invalid"));
|
|
||||||
try testing.expect(!CSSKeywords.isKnownKeyword("unknown"));
|
|
||||||
try testing.expect(!CSSKeywords.isKnownKeyword(""));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: CSSKeywords.containsSpecialChar - various special characters" {
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test\"quote"));
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test'quote"));
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test;end"));
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test{brace"));
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test}brace"));
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test\\back"));
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test<angle"));
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test>angle"));
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test/slash"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSKeywords.containsSpecialChar("normal-text"));
|
|
||||||
try testing.expect(!CSSKeywords.containsSpecialChar("text123"));
|
|
||||||
try testing.expect(!CSSKeywords.containsSpecialChar(""));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: CSSKeywords.isValidUnit - various units" {
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("px"));
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("em"));
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("rem"));
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("%"));
|
|
||||||
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("deg"));
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("rad"));
|
|
||||||
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("s"));
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("ms"));
|
|
||||||
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("PX"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSKeywords.isValidUnit("invalid"));
|
|
||||||
try testing.expect(!CSSKeywords.isValidUnit(""));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: CSSKeywords.startsWithFunction - function detection" {
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("rgb(255, 0, 0)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("rgba(255, 0, 0, 0.5)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("url(image.png)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("calc(100% - 20px)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("var(--custom-property)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("linear-gradient(to right, red, blue)"));
|
|
||||||
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("custom-function(args)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("unknown(test)"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSKeywords.startsWithFunction("not-a-function"));
|
|
||||||
try testing.expect(!CSSKeywords.startsWithFunction("missing-paren)"));
|
|
||||||
try testing.expect(!CSSKeywords.startsWithFunction("missing-close("));
|
|
||||||
try testing.expect(!CSSKeywords.startsWithFunction(""));
|
|
||||||
try testing.expect(!CSSKeywords.startsWithFunction("rgb"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - whitespace handling" {
|
|
||||||
try testing.expect(!isNumericWithUnit(" 10px"));
|
|
||||||
try testing.expect(!isNumericWithUnit("10 px"));
|
|
||||||
try testing.expect(!isNumericWithUnit("10px "));
|
|
||||||
try testing.expect(!isNumericWithUnit(" 10 px "));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: extractImportant - whitespace edge cases" {
|
|
||||||
var result = extractImportant(" ");
|
|
||||||
try testing.expect(!result.is_important);
|
|
||||||
try testing.expectEqual("", result.value);
|
|
||||||
|
|
||||||
result = extractImportant("\t\n\r !important\t\n");
|
|
||||||
try testing.expect(result.is_important);
|
|
||||||
try testing.expectEqual("", result.value);
|
|
||||||
|
|
||||||
result = extractImportant("red\t!important");
|
|
||||||
try testing.expect(result.is_important);
|
|
||||||
try testing.expectEqual("red", result.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: isHexColor - mixed case handling" {
|
|
||||||
try testing.expect(isHexColor("#AbC"));
|
|
||||||
try testing.expect(isHexColor("#123aBc"));
|
|
||||||
try testing.expect(isHexColor("#FFffFF"));
|
|
||||||
try testing.expect(isHexColor("#000FFF"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: edge case - very long inputs" {
|
|
||||||
const long_valid = "a" ** 1000 ++ "px";
|
|
||||||
try testing.expect(!isNumericWithUnit(long_valid)); // not numeric
|
|
||||||
|
|
||||||
const long_property = "a-" ** 100 ++ "property";
|
|
||||||
try testing.expect(isValidPropertyName(long_property));
|
|
||||||
|
|
||||||
const long_hex = "#" ++ "a" ** 20;
|
|
||||||
try testing.expect(!isHexColor(long_hex));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: boundary conditions - numeric parsing" {
|
|
||||||
try testing.expect(isNumericWithUnit("0px"));
|
|
||||||
try testing.expect(isNumericWithUnit("0.0px"));
|
|
||||||
try testing.expect(isNumericWithUnit(".0px"));
|
|
||||||
try testing.expect(isNumericWithUnit("0.px"));
|
|
||||||
|
|
||||||
try testing.expect(isNumericWithUnit("999999999px"));
|
|
||||||
try testing.expect(isNumericWithUnit("1.7976931348623157e+308px"));
|
|
||||||
|
|
||||||
try testing.expect(isNumericWithUnit("0.000000001px"));
|
|
||||||
try testing.expect(isNumericWithUnit("1e-100px"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: extractImportant - malformed important declarations" {
|
|
||||||
var result = extractImportant("red ! important");
|
|
||||||
try testing.expect(!result.is_important);
|
|
||||||
try testing.expectEqual("red ! important", result.value);
|
|
||||||
|
|
||||||
result = extractImportant("red !Important");
|
|
||||||
try testing.expect(!result.is_important);
|
|
||||||
try testing.expectEqual("red !Important", result.value);
|
|
||||||
|
|
||||||
result = extractImportant("red !IMPORTANT");
|
|
||||||
try testing.expect(!result.is_important);
|
|
||||||
try testing.expectEqual("red !IMPORTANT", result.value);
|
|
||||||
|
|
||||||
result = extractImportant("!importantred");
|
|
||||||
try testing.expect(!result.is_important);
|
|
||||||
try testing.expectEqual("!importantred", result.value);
|
|
||||||
|
|
||||||
result = extractImportant("red !important !important");
|
|
||||||
try testing.expect(result.is_important);
|
|
||||||
try testing.expectEqual("red !important", result.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: isMultiValueProperty - complex spacing scenarios" {
|
|
||||||
try testing.expect(isMultiValueProperty("10px 20px"));
|
|
||||||
try testing.expect(isMultiValueProperty("solid red"));
|
|
||||||
|
|
||||||
try testing.expect(isMultiValueProperty(" 10px 20px "));
|
|
||||||
|
|
||||||
try testing.expect(!isMultiValueProperty("10px\t20px"));
|
|
||||||
try testing.expect(!isMultiValueProperty("10px\n20px"));
|
|
||||||
|
|
||||||
try testing.expect(isMultiValueProperty("10px 20px 30px"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: isAlreadyQuoted - edge cases with quotes" {
|
|
||||||
try testing.expect(isAlreadyQuoted("\"'hello'\""));
|
|
||||||
try testing.expect(isAlreadyQuoted("'\"hello\"'"));
|
|
||||||
|
|
||||||
try testing.expect(isAlreadyQuoted("\"hello\\\"world\""));
|
|
||||||
try testing.expect(isAlreadyQuoted("'hello\\'world'"));
|
|
||||||
|
|
||||||
try testing.expect(!isAlreadyQuoted("\"hello"));
|
|
||||||
try testing.expect(!isAlreadyQuoted("hello\""));
|
|
||||||
try testing.expect(!isAlreadyQuoted("'hello"));
|
|
||||||
try testing.expect(!isAlreadyQuoted("hello'"));
|
|
||||||
|
|
||||||
try testing.expect(isAlreadyQuoted("\"a\""));
|
|
||||||
try testing.expect(isAlreadyQuoted("'b'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: needsQuotes - function and URL edge cases" {
|
|
||||||
try testing.expect(!needsQuotes("rgb(255, 0, 0)"));
|
|
||||||
try testing.expect(!needsQuotes("calc(100% - 20px)"));
|
|
||||||
|
|
||||||
try testing.expect(!needsQuotes("url(path with spaces.jpg)"));
|
|
||||||
|
|
||||||
try testing.expect(!needsQuotes("linear-gradient(to right, red, blue)"));
|
|
||||||
|
|
||||||
try testing.expect(needsQuotes("rgb(255, 0, 0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: escapeCSSValue - control characters and Unicode" {
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
var result = try escapeCSSValue(allocator, "test\ttab");
|
|
||||||
try testing.expectEqual("\"test\\9 tab\"", result);
|
|
||||||
|
|
||||||
result = try escapeCSSValue(allocator, "test\rreturn");
|
|
||||||
try testing.expectEqual("\"test\\D return\"", result);
|
|
||||||
|
|
||||||
result = try escapeCSSValue(allocator, "test\x00null");
|
|
||||||
try testing.expectEqual("\"test\\0null\"", result);
|
|
||||||
|
|
||||||
result = try escapeCSSValue(allocator, "test\x7Fdel");
|
|
||||||
try testing.expectEqual("\"test\\7f del\"", result);
|
|
||||||
|
|
||||||
result = try escapeCSSValue(allocator, "test\"quote\nline\\back");
|
|
||||||
try testing.expectEqual("\"test\\\"quote\\A line\\\\back\"", result);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: isValidPropertyName - CSS custom properties and vendor prefixes" {
|
|
||||||
try testing.expect(isValidPropertyName("--custom-color"));
|
|
||||||
try testing.expect(isValidPropertyName("--my-variable"));
|
|
||||||
try testing.expect(isValidPropertyName("--123"));
|
|
||||||
|
|
||||||
try testing.expect(isValidPropertyName("-webkit-transform"));
|
|
||||||
try testing.expect(isValidPropertyName("-moz-border-radius"));
|
|
||||||
try testing.expect(isValidPropertyName("-ms-filter"));
|
|
||||||
try testing.expect(isValidPropertyName("-o-transition"));
|
|
||||||
|
|
||||||
try testing.expect(!isValidPropertyName("-123invalid"));
|
|
||||||
try testing.expect(!isValidPropertyName("--"));
|
|
||||||
try testing.expect(!isValidPropertyName("-"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: startsWithFunction - case sensitivity and partial matches" {
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("RGB(255, 0, 0)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("Rgb(255, 0, 0)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("URL(image.png)"));
|
|
||||||
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("rg(something)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("ur(something)"));
|
|
||||||
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("rgb(1,2,3)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("rgba(1,2,3,4)"));
|
|
||||||
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("my-custom-function(args)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("function-with-dashes(test)"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSKeywords.startsWithFunction("123function(test)"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: isHexColor - Unicode and invalid characters" {
|
|
||||||
try testing.expect(!isHexColor("#ghijkl"));
|
|
||||||
try testing.expect(!isHexColor("#12345g"));
|
|
||||||
try testing.expect(!isHexColor("#xyz"));
|
|
||||||
|
|
||||||
try testing.expect(!isHexColor("#АВС"));
|
|
||||||
|
|
||||||
try testing.expect(!isHexColor("#1234567g"));
|
|
||||||
try testing.expect(!isHexColor("#g2345678"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: complex integration scenarios" {
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
try testing.expect(isMultiValueProperty("rgb(255,0,0) url(bg.jpg)"));
|
|
||||||
|
|
||||||
try testing.expect(!needsQuotes("calc(100% - 20px)"));
|
|
||||||
|
|
||||||
const result = try escapeCSSValue(allocator, "fake(function with spaces");
|
|
||||||
try testing.expectEqual("\"fake(function with spaces\"", result);
|
|
||||||
|
|
||||||
const important_result = extractImportant("rgb(255,0,0) !important");
|
|
||||||
try testing.expect(important_result.is_important);
|
|
||||||
try testing.expectEqual("rgb(255,0,0)", important_result.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: performance edge cases - empty and minimal inputs" {
|
|
||||||
try testing.expect(!isNumericWithUnit(""));
|
|
||||||
try testing.expect(!isHexColor(""));
|
|
||||||
try testing.expect(!isMultiValueProperty(""));
|
|
||||||
try testing.expect(!isAlreadyQuoted(""));
|
|
||||||
try testing.expect(!isValidPropertyName(""));
|
|
||||||
try testing.expect(needsQuotes(""));
|
|
||||||
try testing.expect(!CSSKeywords.isKnownKeyword(""));
|
|
||||||
try testing.expect(!CSSKeywords.containsSpecialChar(""));
|
|
||||||
try testing.expect(!CSSKeywords.isValidUnit(""));
|
|
||||||
try testing.expect(!CSSKeywords.startsWithFunction(""));
|
|
||||||
|
|
||||||
try testing.expect(!isNumericWithUnit("a"));
|
|
||||||
try testing.expect(!isHexColor("a"));
|
|
||||||
try testing.expect(!isMultiValueProperty("a"));
|
|
||||||
try testing.expect(!isAlreadyQuoted("a"));
|
|
||||||
try testing.expect(isValidPropertyName("a"));
|
|
||||||
try testing.expect(!needsQuotes("a"));
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const StyleSheet = @import("StyleSheet.zig");
|
|
||||||
const CSSRuleList = @import("CSSRuleList.zig");
|
|
||||||
const CSSImportRule = @import("CSSRule.zig").CSSImportRule;
|
|
||||||
|
|
||||||
const CSSStyleSheet = @This();
|
|
||||||
pub const prototype = *StyleSheet;
|
|
||||||
|
|
||||||
proto: StyleSheet,
|
|
||||||
css_rules: CSSRuleList,
|
|
||||||
owner_rule: ?*CSSImportRule,
|
|
||||||
|
|
||||||
const CSSStyleSheetOpts = struct {
|
|
||||||
base_url: ?[]const u8 = null,
|
|
||||||
// TODO: Suupport media
|
|
||||||
disabled: bool = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn constructor(_opts: ?CSSStyleSheetOpts) !CSSStyleSheet {
|
|
||||||
const opts = _opts orelse CSSStyleSheetOpts{};
|
|
||||||
return .{
|
|
||||||
.proto = .{ .disabled = opts.disabled },
|
|
||||||
.css_rules = .constructor(),
|
|
||||||
.owner_rule = null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_ownerRule(_: *CSSStyleSheet) ?*CSSImportRule {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_cssRules(self: *CSSStyleSheet) *CSSRuleList {
|
|
||||||
return &self.css_rules;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _insertRule(self: *CSSStyleSheet, rule: []const u8, _index: ?usize, page: *Page) !usize {
|
|
||||||
const index = _index orelse 0;
|
|
||||||
if (index > self.css_rules.list.items.len) {
|
|
||||||
return error.IndexSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
const arena = page.arena;
|
|
||||||
try self.css_rules.list.insert(arena, index, try arena.dupe(u8, rule));
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _deleteRule(self: *CSSStyleSheet, index: usize) !void {
|
|
||||||
if (index > self.css_rules.list.items.len) {
|
|
||||||
return error.IndexSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = self.css_rules.list.orderedRemove(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise {
|
|
||||||
_ = self;
|
|
||||||
_ = text;
|
|
||||||
// TODO: clear self.css_rules
|
|
||||||
// parse text and re-populate self.css_rules
|
|
||||||
|
|
||||||
return page.js.resolvePromise({});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _replaceSync(self: *CSSStyleSheet, text: []const u8) !void {
|
|
||||||
_ = self;
|
|
||||||
_ = text;
|
|
||||||
// TODO: clear self.css_rules
|
|
||||||
// parse text and re-populate self.css_rules
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: CSS.StyleSheet" {
|
|
||||||
try testing.htmlRunner("cssom/css_stylesheet.html");
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/StyleSheet#specifications
|
|
||||||
const StyleSheet = @This();
|
|
||||||
|
|
||||||
disabled: bool = false,
|
|
||||||
href: []const u8 = "",
|
|
||||||
owner_node: ?*parser.Node = null,
|
|
||||||
parent_stylesheet: ?*StyleSheet = null,
|
|
||||||
title: []const u8 = "",
|
|
||||||
type: []const u8 = "text/css",
|
|
||||||
|
|
||||||
pub fn get_disabled(self: *const StyleSheet) bool {
|
|
||||||
return self.disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_href(self: *const StyleSheet) []const u8 {
|
|
||||||
return self.href;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: media
|
|
||||||
|
|
||||||
pub fn get_ownerNode(self: *const StyleSheet) ?*parser.Node {
|
|
||||||
return self.owner_node;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_parentStyleSheet(self: *const StyleSheet) ?*StyleSheet {
|
|
||||||
return self.parent_stylesheet;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_title(self: *const StyleSheet) []const u8 {
|
|
||||||
return self.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_type(self: *const StyleSheet) []const u8 {
|
|
||||||
return self.type;
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
@import("StyleSheet.zig"),
|
|
||||||
@import("CSSStyleSheet.zig"),
|
|
||||||
@import("CSSStyleDeclaration.zig"),
|
|
||||||
@import("CSSRuleList.zig"),
|
|
||||||
@import("CSSRule.zig").Interfaces,
|
|
||||||
};
|
|
||||||
@@ -1,107 +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 js = @import("../js/js.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const Animation = @This();
|
|
||||||
|
|
||||||
effect: ?js.Object,
|
|
||||||
timeline: ?js.Object,
|
|
||||||
ready_resolver: ?js.PromiseResolver,
|
|
||||||
finished_resolver: ?js.PromiseResolver,
|
|
||||||
|
|
||||||
pub fn constructor(effect: ?js.Object, timeline: ?js.Object) !Animation {
|
|
||||||
return .{
|
|
||||||
.effect = if (effect) |eo| try eo.persist() else null,
|
|
||||||
.timeline = if (timeline) |to| try to.persist() else null,
|
|
||||||
.ready_resolver = null,
|
|
||||||
.finished_resolver = null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_playState(self: *const Animation) []const u8 {
|
|
||||||
_ = self;
|
|
||||||
return "finished";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_pending(self: *const Animation) bool {
|
|
||||||
_ = self;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_finished(self: *Animation, page: *Page) !js.Promise {
|
|
||||||
if (self.finished_resolver == null) {
|
|
||||||
const resolver = page.js.createPromiseResolver(.none);
|
|
||||||
try resolver.resolve(self);
|
|
||||||
self.finished_resolver = resolver;
|
|
||||||
}
|
|
||||||
return self.finished_resolver.?.promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_ready(self: *Animation, page: *Page) !js.Promise {
|
|
||||||
// never resolved, because we're always "finished"
|
|
||||||
if (self.ready_resolver == null) {
|
|
||||||
const resolver = page.js.createPromiseResolver(.none);
|
|
||||||
self.ready_resolver = resolver;
|
|
||||||
}
|
|
||||||
return self.ready_resolver.?.promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_effect(self: *const Animation) ?js.Object {
|
|
||||||
return self.effect;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_effect(self: *Animation, effect: js.Object) !void {
|
|
||||||
self.effect = try effect.persist();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_timeline(self: *const Animation) ?js.Object {
|
|
||||||
return self.timeline;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_timeline(self: *Animation, timeline: js.Object) !void {
|
|
||||||
self.timeline = try timeline.persist();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _play(self: *const Animation) void {
|
|
||||||
_ = self;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _pause(self: *const Animation) void {
|
|
||||||
_ = self;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _cancel(self: *const Animation) void {
|
|
||||||
_ = self;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _finish(self: *const Animation) void {
|
|
||||||
_ = self;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _reverse(self: *const Animation) void {
|
|
||||||
_ = self;
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.Animation" {
|
|
||||||
try testing.htmlRunner("dom/animation.html");
|
|
||||||
}
|
|
||||||
@@ -1,329 +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 js = @import("../js/js.zig");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
const Element = @import("element.zig").Element;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
IntersectionObserver,
|
|
||||||
Entry,
|
|
||||||
};
|
|
||||||
|
|
||||||
// This implementation attempts to be as less wrong as possible. Since we don't
|
|
||||||
// render, or know how things are positioned, our best guess isn't very good.
|
|
||||||
const IntersectionObserver = @This();
|
|
||||||
page: *Page,
|
|
||||||
root: *parser.Node,
|
|
||||||
callback: js.Function,
|
|
||||||
event_node: parser.EventNode,
|
|
||||||
observed_entries: std.ArrayList(Entry),
|
|
||||||
pending_elements: std.ArrayList(*parser.Element),
|
|
||||||
ready_elements: std.ArrayList(*parser.Element),
|
|
||||||
|
|
||||||
pub fn constructor(callback: js.Function, opts_: ?IntersectionObserverOptions, page: *Page) !*IntersectionObserver {
|
|
||||||
const opts = opts_ orelse IntersectionObserverOptions{};
|
|
||||||
|
|
||||||
const self = try page.arena.create(IntersectionObserver);
|
|
||||||
self.* = .{
|
|
||||||
.page = page,
|
|
||||||
.callback = callback,
|
|
||||||
.ready_elements = .{},
|
|
||||||
.observed_entries = .{},
|
|
||||||
.pending_elements = .{},
|
|
||||||
.event_node = .{ .func = mutationCallback },
|
|
||||||
.root = opts.root orelse parser.documentToNode(parser.documentHTMLToDocument(page.window.document)),
|
|
||||||
};
|
|
||||||
|
|
||||||
_ = try parser.eventTargetAddEventListener(
|
|
||||||
parser.toEventTarget(parser.Node, self.root),
|
|
||||||
"DOMNodeInserted",
|
|
||||||
&self.event_node,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
_ = try parser.eventTargetAddEventListener(
|
|
||||||
parser.toEventTarget(parser.Node, self.root),
|
|
||||||
"DOMNodeRemoved",
|
|
||||||
&self.event_node,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _disconnect(self: *IntersectionObserver) !void {
|
|
||||||
// We don't free as it is on an arena
|
|
||||||
self.ready_elements = .{};
|
|
||||||
self.observed_entries = .{};
|
|
||||||
self.pending_elements = .{};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _observe(self: *IntersectionObserver, target_element: *parser.Element, page: *Page) !void {
|
|
||||||
for (self.observed_entries.items) |*observer| {
|
|
||||||
if (observer.target == target_element) {
|
|
||||||
return; // Already observed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.isPending(target_element)) {
|
|
||||||
return; // Already pending
|
|
||||||
}
|
|
||||||
|
|
||||||
for (self.ready_elements.items) |element| {
|
|
||||||
if (element == target_element) {
|
|
||||||
return; // Already primed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can never fire callbacks synchronously. Code like React expects any
|
|
||||||
// callback to fire in the future (e.g. via microtasks).
|
|
||||||
try self.ready_elements.append(self.page.arena, target_element);
|
|
||||||
if (self.ready_elements.items.len == 1) {
|
|
||||||
// this is our first ready entry, schedule a callback
|
|
||||||
try page.scheduler.add(self, processReady, 0, .{
|
|
||||||
.name = "intersection ready",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _unobserve(self: *IntersectionObserver, target: *parser.Element) !void {
|
|
||||||
if (self.removeObserved(target)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (self.ready_elements.items, 0..) |el, index| {
|
|
||||||
if (el == target) {
|
|
||||||
_ = self.ready_elements.swapRemove(index);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (self.pending_elements.items, 0..) |el, index| {
|
|
||||||
if (el == target) {
|
|
||||||
_ = self.pending_elements.swapRemove(index);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _takeRecords(self: *IntersectionObserver) []Entry {
|
|
||||||
return self.observed_entries.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn processReady(ctx: *anyopaque) ?u32 {
|
|
||||||
const self: *IntersectionObserver = @ptrCast(@alignCast(ctx));
|
|
||||||
self._processReady() catch |err| {
|
|
||||||
log.err(.web_api, "intersection ready", .{ .err = err });
|
|
||||||
};
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _processReady(self: *IntersectionObserver) !void {
|
|
||||||
defer self.ready_elements.clearRetainingCapacity();
|
|
||||||
for (self.ready_elements.items) |element| {
|
|
||||||
// IntersectionObserver probably doesn't work like what your intuition
|
|
||||||
// thinks. As long as a node has a parent, even if that parent isn't
|
|
||||||
// connected and even if the two nodes don't intersect, it'll fire the
|
|
||||||
// callback once.
|
|
||||||
if (try Node.get_parentNode(@ptrCast(element)) == null) {
|
|
||||||
if (!self.isPending(element)) {
|
|
||||||
try self.pending_elements.append(self.page.arena, element);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try self.forceObserve(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isPending(self: *IntersectionObserver, element: *parser.Element) bool {
|
|
||||||
for (self.pending_elements.items) |el| {
|
|
||||||
if (el == element) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mutationCallback(en: *parser.EventNode, event: *parser.Event) void {
|
|
||||||
const mutation_event = parser.eventToMutationEvent(event);
|
|
||||||
const self: *IntersectionObserver = @fieldParentPtr("event_node", en);
|
|
||||||
self._mutationCallback(mutation_event) catch |err| {
|
|
||||||
log.err(.web_api, "mutation callback", .{ .err = err, .source = "intersection observer" });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _mutationCallback(self: *IntersectionObserver, event: *parser.MutationEvent) !void {
|
|
||||||
const event_type = parser.eventType(@ptrCast(event));
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, event_type, "DOMNodeInserted")) {
|
|
||||||
const node = parser.mutationEventRelatedNode(event) catch return orelse return;
|
|
||||||
if (parser.nodeType(node) != .element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const el: *parser.Element = @ptrCast(node);
|
|
||||||
if (self.removePending(el)) {
|
|
||||||
// It was pending (because it wasn't in the root), but now it is
|
|
||||||
// we should observe it.
|
|
||||||
try self.forceObserve(el);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, event_type, "DOMNodeRemoved")) {
|
|
||||||
const node = parser.mutationEventRelatedNode(event) catch return orelse return;
|
|
||||||
if (parser.nodeType(node) != .element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const el: *parser.Element = @ptrCast(node);
|
|
||||||
if (self.removeObserved(el)) {
|
|
||||||
// It _was_ observed, it no longer is in our root, but if it was
|
|
||||||
// to get re-added, it should be observed again (I think), so
|
|
||||||
// we add it to our pending list
|
|
||||||
try self.pending_elements.append(self.page.arena, el);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// impossible event type
|
|
||||||
unreachable;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exists to skip the checks made _observe when called from a DOMNodeInserted
|
|
||||||
// event. In such events, the event handler has alread done the necessary
|
|
||||||
// checks.
|
|
||||||
fn forceObserve(self: *IntersectionObserver, target: *parser.Element) !void {
|
|
||||||
try self.observed_entries.append(self.page.arena, .{
|
|
||||||
.page = self.page,
|
|
||||||
.root = self.root,
|
|
||||||
.target = target,
|
|
||||||
});
|
|
||||||
|
|
||||||
var result: js.Function.Result = undefined;
|
|
||||||
self.callback.tryCall(void, .{self.observed_entries.items}, &result) catch {
|
|
||||||
log.debug(.user_script, "callback error", .{
|
|
||||||
.err = result.exception,
|
|
||||||
.stack = result.stack,
|
|
||||||
.source = "intersection observer",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn removeObserved(self: *IntersectionObserver, target: *parser.Element) bool {
|
|
||||||
for (self.observed_entries.items, 0..) |*observer, index| {
|
|
||||||
if (observer.target == target) {
|
|
||||||
_ = self.observed_entries.swapRemove(index);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn removePending(self: *IntersectionObserver, target: *parser.Element) bool {
|
|
||||||
for (self.pending_elements.items, 0..) |el, index| {
|
|
||||||
if (el == target) {
|
|
||||||
_ = self.pending_elements.swapRemove(index);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const IntersectionObserverOptions = struct {
|
|
||||||
root: ?*parser.Node = null, // Element or Document
|
|
||||||
rootMargin: ?[]const u8 = "0px 0px 0px 0px",
|
|
||||||
threshold: ?Threshold = .{ .single = 0.0 },
|
|
||||||
|
|
||||||
const Threshold = union(enum) {
|
|
||||||
single: f32,
|
|
||||||
list: []const f32,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Entry
|
|
||||||
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
|
|
||||||
pub const Entry = struct {
|
|
||||||
page: *Page,
|
|
||||||
root: *parser.Node,
|
|
||||||
target: *parser.Element,
|
|
||||||
|
|
||||||
// Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect().
|
|
||||||
pub fn get_boundingClientRect(self: *const Entry) !Element.DOMRect {
|
|
||||||
return Element._getBoundingClientRect(self.target, self.page);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the ratio of the intersectionRect to the boundingClientRect.
|
|
||||||
pub fn get_intersectionRatio(_: *const Entry) f32 {
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a DOMRectReadOnly representing the target's visible area.
|
|
||||||
pub fn get_intersectionRect(self: *const Entry) !Element.DOMRect {
|
|
||||||
return Element._getBoundingClientRect(self.target, self.page);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A Boolean value which is true if the target element intersects with the
|
|
||||||
// intersection observer's root. If this is true, then, the
|
|
||||||
// Entry describes a transition into a state of
|
|
||||||
// intersection; if it's false, then you know the transition is from
|
|
||||||
// intersecting to not-intersecting.
|
|
||||||
pub fn get_isIntersecting(_: *const Entry) bool {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a DOMRectReadOnly for the intersection observer's root.
|
|
||||||
pub fn get_rootBounds(self: *const Entry) !Element.DOMRect {
|
|
||||||
const root = self.root;
|
|
||||||
if (@intFromPtr(root) == @intFromPtr(self.page.window.document)) {
|
|
||||||
return self.page.renderer.boundingRect();
|
|
||||||
}
|
|
||||||
|
|
||||||
const root_type = parser.nodeType(root);
|
|
||||||
|
|
||||||
var element: *parser.Element = undefined;
|
|
||||||
switch (root_type) {
|
|
||||||
.element => element = parser.nodeToElement(root),
|
|
||||||
.document => {
|
|
||||||
const doc = parser.nodeToDocument(root);
|
|
||||||
element = (try parser.documentGetDocumentElement(doc)).?;
|
|
||||||
},
|
|
||||||
else => return error.InvalidState,
|
|
||||||
}
|
|
||||||
|
|
||||||
return Element._getBoundingClientRect(element, self.page);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Element whose intersection with the root changed.
|
|
||||||
pub fn get_target(self: *const Entry) *parser.Element {
|
|
||||||
return self.target;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: pub fn get_time(self: *const Entry)
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.IntersectionObserver" {
|
|
||||||
try testing.htmlRunner("dom/intersection_observer.html");
|
|
||||||
}
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
|
||||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const MAX_QUEUE_SIZE = 10;
|
|
||||||
|
|
||||||
pub const Interfaces = .{ MessageChannel, MessagePort };
|
|
||||||
|
|
||||||
const MessageChannel = @This();
|
|
||||||
|
|
||||||
port1: *MessagePort,
|
|
||||||
port2: *MessagePort,
|
|
||||||
|
|
||||||
pub fn constructor(page: *Page) !MessageChannel {
|
|
||||||
// Why do we allocate this rather than storing directly in the struct?
|
|
||||||
// https://github.com/lightpanda-io/project/discussions/165
|
|
||||||
const port1 = try page.arena.create(MessagePort);
|
|
||||||
const port2 = try page.arena.create(MessagePort);
|
|
||||||
port1.* = .{
|
|
||||||
.pair = port2,
|
|
||||||
};
|
|
||||||
port2.* = .{
|
|
||||||
.pair = port1,
|
|
||||||
};
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.port1 = port1,
|
|
||||||
.port2 = port2,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_port1(self: *const MessageChannel) *MessagePort {
|
|
||||||
return self.port1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_port2(self: *const MessageChannel) *MessagePort {
|
|
||||||
return self.port2;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const MessagePort = struct {
|
|
||||||
pub const prototype = *EventTarget;
|
|
||||||
|
|
||||||
proto: parser.EventTargetTBase = .{ .internal_target_type = .message_port },
|
|
||||||
|
|
||||||
pair: *MessagePort,
|
|
||||||
closed: bool = false,
|
|
||||||
started: bool = false,
|
|
||||||
onmessage_cbk: ?js.Function = null,
|
|
||||||
onmessageerror_cbk: ?js.Function = null,
|
|
||||||
// This is the queue of messages to dispatch to THIS MessagePort when the
|
|
||||||
// MessagePort is started.
|
|
||||||
queue: std.ArrayListUnmanaged(js.Object) = .empty,
|
|
||||||
|
|
||||||
pub const PostMessageOption = union(enum) {
|
|
||||||
transfer: js.Object,
|
|
||||||
options: Opts,
|
|
||||||
|
|
||||||
pub const Opts = struct {
|
|
||||||
transfer: js.Object,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn _postMessage(self: *MessagePort, obj: js.Object, opts_: ?PostMessageOption, page: *Page) !void {
|
|
||||||
if (self.closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts_ != null) {
|
|
||||||
log.warn(.web_api, "not implemented", .{ .feature = "MessagePort postMessage options" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.pair.dispatchOrQueue(obj, page.arena);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start impacts the ability to receive a message.
|
|
||||||
// Given pair1 (started) and pair2 (not started), then:
|
|
||||||
// pair2.postMessage('x'); //will be dispatched to pair1.onmessage
|
|
||||||
// pair1.postMessage('x'); // will be queued until pair2 is started
|
|
||||||
pub fn _start(self: *MessagePort) !void {
|
|
||||||
if (self.started) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.started = true;
|
|
||||||
for (self.queue.items) |data| {
|
|
||||||
try self.dispatch(data);
|
|
||||||
}
|
|
||||||
// we'll never use this queue again, but it's allocated with an arena
|
|
||||||
// we don't even need to clear it, but it seems a bit safer to do at
|
|
||||||
// least that
|
|
||||||
self.queue.clearRetainingCapacity();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Closing seems to stop both the publishing and receiving of messages,
|
|
||||||
// effectively rendering the channel useless. It cannot be reversed.
|
|
||||||
pub fn _close(self: *MessagePort) void {
|
|
||||||
self.closed = true;
|
|
||||||
self.pair.closed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_onmessage(self: *MessagePort) ?js.Function {
|
|
||||||
return self.onmessage_cbk;
|
|
||||||
}
|
|
||||||
pub fn get_onmessageerror(self: *MessagePort) ?js.Function {
|
|
||||||
return self.onmessageerror_cbk;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_onmessage(self: *MessagePort, listener: EventHandler.Listener, page: *Page) !void {
|
|
||||||
if (self.onmessage_cbk) |cbk| {
|
|
||||||
try self.unregister("message", cbk.id);
|
|
||||||
}
|
|
||||||
self.onmessage_cbk = try self.register(page.arena, "message", listener);
|
|
||||||
|
|
||||||
// When onmessage is set directly, then it's like start() was called.
|
|
||||||
// If addEventListener('message') is used, the app has to call start()
|
|
||||||
// explicitly.
|
|
||||||
try self._start();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_onmessageerror(self: *MessagePort, listener: EventHandler.Listener, page: *Page) !void {
|
|
||||||
if (self.onmessageerror_cbk) |cbk| {
|
|
||||||
try self.unregister("messageerror", cbk.id);
|
|
||||||
}
|
|
||||||
self.onmessageerror_cbk = try self.register(page.arena, "messageerror", listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
// called from our pair. If port1.postMessage("x") is called, then this
|
|
||||||
// will be called on port2.
|
|
||||||
fn dispatchOrQueue(self: *MessagePort, obj: js.Object, arena: Allocator) !void {
|
|
||||||
// our pair should have checked this already
|
|
||||||
std.debug.assert(self.closed == false);
|
|
||||||
|
|
||||||
if (self.started) {
|
|
||||||
return self.dispatch(try obj.persist());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.queue.items.len > MAX_QUEUE_SIZE) {
|
|
||||||
// This isn't part of the spec, but not putting a limit is reckless
|
|
||||||
return error.MessageQueueLimit;
|
|
||||||
}
|
|
||||||
return self.queue.append(arena, try obj.persist());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dispatch(self: *MessagePort, obj: js.Object) !void {
|
|
||||||
// obj is already persisted, don't use `MessageEvent.constructor`, but
|
|
||||||
// go directly to `init`, which assumes persisted objects.
|
|
||||||
var evt = try MessageEvent.init(.{ .data = obj });
|
|
||||||
_ = try parser.eventTargetDispatchEvent(
|
|
||||||
parser.toEventTarget(MessagePort, self),
|
|
||||||
@as(*parser.Event, @ptrCast(&evt)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register(
|
|
||||||
self: *MessagePort,
|
|
||||||
alloc: Allocator,
|
|
||||||
typ: []const u8,
|
|
||||||
listener: EventHandler.Listener,
|
|
||||||
) !?js.Function {
|
|
||||||
const target = @as(*parser.EventTarget, @ptrCast(self));
|
|
||||||
const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable;
|
|
||||||
return eh.callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unregister(self: *MessagePort, typ: []const u8, cbk_id: usize) !void {
|
|
||||||
const et = @as(*parser.EventTarget, @ptrCast(self));
|
|
||||||
const lst = try parser.eventTargetHasListener(et, typ, false, cbk_id);
|
|
||||||
if (lst == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try parser.eventTargetRemoveEventListener(et, typ, lst.?, false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const MessageEvent = struct {
|
|
||||||
const Event = @import("../events/event.zig").Event;
|
|
||||||
const DOMException = @import("exceptions.zig").DOMException;
|
|
||||||
|
|
||||||
pub const prototype = *Event;
|
|
||||||
pub const Exception = DOMException;
|
|
||||||
pub const union_make_copy = true;
|
|
||||||
|
|
||||||
proto: parser.Event,
|
|
||||||
data: ?js.Object,
|
|
||||||
|
|
||||||
// You would think if port1 sends to port2, the source would be port2
|
|
||||||
// (which is how I read the documentation), but it appears to always be
|
|
||||||
// null. It can always be set explicitly via the constructor;
|
|
||||||
source: ?js.Object,
|
|
||||||
|
|
||||||
origin: []const u8,
|
|
||||||
|
|
||||||
// This is used for Server-Sent events. Appears to always be an empty
|
|
||||||
// string for MessagePort messages.
|
|
||||||
last_event_id: []const u8,
|
|
||||||
|
|
||||||
// This might be related to the "transfer" option of postMessage which
|
|
||||||
// we don't yet support. For "normal" message, it's always an empty array.
|
|
||||||
// Though it could be set explicitly via the constructor
|
|
||||||
ports: []*MessagePort,
|
|
||||||
|
|
||||||
const Options = struct {
|
|
||||||
data: ?js.Object = null,
|
|
||||||
source: ?js.Object = null,
|
|
||||||
origin: []const u8 = "",
|
|
||||||
lastEventId: []const u8 = "",
|
|
||||||
ports: []*MessagePort = &.{},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn constructor(opts: Options) !MessageEvent {
|
|
||||||
return init(.{
|
|
||||||
.data = if (opts.data) |obj| try obj.persist() else null,
|
|
||||||
.source = if (opts.source) |obj| try obj.persist() else null,
|
|
||||||
.ports = opts.ports,
|
|
||||||
.origin = opts.origin,
|
|
||||||
.lastEventId = opts.lastEventId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is like "constructor", but it assumes js.Objects have already been
|
|
||||||
// persisted. Necessary because this `new MessageEvent()` can be called
|
|
||||||
// directly from JS OR from a port.postMessage. In the latter case, data
|
|
||||||
// may have already been persisted (as it might need to be queued);
|
|
||||||
fn init(opts: Options) !MessageEvent {
|
|
||||||
const event = try parser.eventCreate();
|
|
||||||
defer parser.eventDestroy(event);
|
|
||||||
try parser.eventInit(event, "message", .{});
|
|
||||||
parser.eventSetInternalType(event, .message_event);
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.proto = event.*,
|
|
||||||
.data = opts.data,
|
|
||||||
.source = opts.source,
|
|
||||||
.ports = opts.ports,
|
|
||||||
.origin = opts.origin,
|
|
||||||
.last_event_id = opts.lastEventId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_data(self: *const MessageEvent) !?js.Object {
|
|
||||||
return self.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_origin(self: *const MessageEvent) []const u8 {
|
|
||||||
return self.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_source(self: *const MessageEvent) ?js.Object {
|
|
||||||
return self.source;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_ports(self: *const MessageEvent) []*MessagePort {
|
|
||||||
return self.ports;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_lastEventId(self: *const MessageEvent) []const u8 {
|
|
||||||
return self.last_event_id;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.MessageChannel" {
|
|
||||||
try testing.htmlRunner("dom/message_channel.html");
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#attr
|
|
||||||
pub const Attr = struct {
|
|
||||||
pub const Self = parser.Attribute;
|
|
||||||
pub const prototype = *Node;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub fn get_namespaceURI(self: *parser.Attribute) ?[]const u8 {
|
|
||||||
return parser.nodeGetNamespace(parser.attributeToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_prefix(self: *parser.Attribute) ?[]const u8 {
|
|
||||||
return parser.nodeGetPrefix(parser.attributeToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_localName(self: *parser.Attribute) ![]const u8 {
|
|
||||||
return parser.nodeLocalName(parser.attributeToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_name(self: *parser.Attribute) ![]const u8 {
|
|
||||||
return parser.attributeGetName(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_value(self: *parser.Attribute) !?[]const u8 {
|
|
||||||
return parser.attributeGetValue(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_value(self: *parser.Attribute, v: []const u8) !?[]const u8 {
|
|
||||||
if (try parser.attributeGetOwnerElement(self)) |el| {
|
|
||||||
// if possible, go through the element, as that triggers a
|
|
||||||
// DOMAttrModified event (which MutationObserver cares about)
|
|
||||||
const name = try parser.attributeGetName(self);
|
|
||||||
try parser.elementSetAttribute(el, name, v);
|
|
||||||
} else {
|
|
||||||
try parser.attributeSetValue(self, v);
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_ownerElement(self: *parser.Attribute) !?*parser.Element {
|
|
||||||
return try parser.attributeGetOwnerElement(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_specified(_: *parser.Attribute) bool {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.Attribute" {
|
|
||||||
try testing.htmlRunner("dom/attribute.html");
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const Text = @import("text.zig").Text;
|
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#cdatasection
|
|
||||||
pub const CDATASection = struct {
|
|
||||||
pub const Self = parser.CDATASection;
|
|
||||||
pub const prototype = *Text;
|
|
||||||
pub const subtype = .node;
|
|
||||||
};
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
const Comment = @import("comment.zig").Comment;
|
|
||||||
const Text = @import("text.zig");
|
|
||||||
const ProcessingInstruction = @import("processing_instruction.zig").ProcessingInstruction;
|
|
||||||
const Element = @import("element.zig").Element;
|
|
||||||
const ElementUnion = @import("element.zig").Union;
|
|
||||||
|
|
||||||
// CharacterData interfaces
|
|
||||||
pub const Interfaces = .{
|
|
||||||
Comment,
|
|
||||||
Text.Text,
|
|
||||||
Text.Interfaces,
|
|
||||||
ProcessingInstruction,
|
|
||||||
};
|
|
||||||
|
|
||||||
// CharacterData implementation
|
|
||||||
pub const CharacterData = struct {
|
|
||||||
pub const Self = parser.CharacterData;
|
|
||||||
pub const prototype = *Node;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
// JS funcs
|
|
||||||
// --------
|
|
||||||
|
|
||||||
// Read attributes
|
|
||||||
|
|
||||||
pub fn get_length(self: *parser.CharacterData) !u32 {
|
|
||||||
return try parser.characterDataLength(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_nextElementSibling(self: *parser.CharacterData) !?ElementUnion {
|
|
||||||
const res = parser.nodeNextElementSibling(parser.characterDataToNode(self));
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try Element.toInterface(res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_previousElementSibling(self: *parser.CharacterData) !?ElementUnion {
|
|
||||||
const res = parser.nodePreviousElementSibling(parser.characterDataToNode(self));
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try Element.toInterface(res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read/Write attributes
|
|
||||||
|
|
||||||
pub fn get_data(self: *parser.CharacterData) []const u8 {
|
|
||||||
return parser.characterDataData(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_data(self: *parser.CharacterData, data: []const u8) !void {
|
|
||||||
return try parser.characterDataSetData(self, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// JS methods
|
|
||||||
// ----------
|
|
||||||
|
|
||||||
pub fn _appendData(self: *parser.CharacterData, data: []const u8) !void {
|
|
||||||
return try parser.characterDataAppendData(self, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _deleteData(self: *parser.CharacterData, offset: u32, count: u32) !void {
|
|
||||||
return try parser.characterDataDeleteData(self, offset, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _insertData(self: *parser.CharacterData, offset: u32, data: []const u8) !void {
|
|
||||||
return try parser.characterDataInsertData(self, offset, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _replaceData(self: *parser.CharacterData, offset: u32, count: u32, data: []const u8) !void {
|
|
||||||
return try parser.characterDataReplaceData(self, offset, count, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _substringData(self: *parser.CharacterData, offset: u32, count: u32) ![]const u8 {
|
|
||||||
return parser.characterDataSubstringData(self, offset, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// netsurf's CharacterData (text, comment) doesn't implement the
|
|
||||||
// dom_node_get_attributes and thus will crash if we try to call nodeIsEqualNode.
|
|
||||||
pub fn _isEqualNode(self: *parser.CharacterData, other_node: *parser.Node) bool {
|
|
||||||
if (parser.nodeType(@ptrCast(@alignCast(self))) != parser.nodeType(other_node)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const other: *parser.CharacterData = @ptrCast(other_node);
|
|
||||||
if (std.mem.eql(u8, get_data(self), get_data(other)) == false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _before(self: *parser.CharacterData, nodes: []const Node.NodeOrText) !void {
|
|
||||||
const ref_node = parser.characterDataToNode(self);
|
|
||||||
return Node.before(ref_node, nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _after(self: *parser.CharacterData, nodes: []const Node.NodeOrText) !void {
|
|
||||||
const ref_node = parser.characterDataToNode(self);
|
|
||||||
return Node.after(ref_node, nodes);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.CharacterData" {
|
|
||||||
try testing.htmlRunner("dom/character_data.html");
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const CharacterData = @import("character_data.zig").CharacterData;
|
|
||||||
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#interface-comment
|
|
||||||
pub const Comment = struct {
|
|
||||||
pub const Self = parser.Comment;
|
|
||||||
pub const prototype = *CharacterData;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub fn constructor(data: ?[]const u8, page: *const Page) !*parser.Comment {
|
|
||||||
return parser.documentCreateComment(
|
|
||||||
parser.documentHTMLToDocument(page.window.document),
|
|
||||||
data orelse "",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.Comment" {
|
|
||||||
try testing.htmlRunner("dom/comment.html");
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const css = @import("../css/css.zig");
|
|
||||||
const Node = @import("../css/libdom.zig").Node;
|
|
||||||
const NodeList = @import("nodelist.zig").NodeList;
|
|
||||||
|
|
||||||
const MatchFirst = struct {
|
|
||||||
n: ?*parser.Node = null,
|
|
||||||
|
|
||||||
pub fn match(m: *MatchFirst, n: Node) !void {
|
|
||||||
m.n = n.node;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn querySelector(alloc: std.mem.Allocator, n: *parser.Node, selector: []const u8) !?*parser.Node {
|
|
||||||
const ps = try css.parse(alloc, selector, .{ .accept_pseudo_elts = true });
|
|
||||||
defer ps.deinit(alloc);
|
|
||||||
|
|
||||||
var m = MatchFirst{};
|
|
||||||
|
|
||||||
_ = try css.matchFirst(&ps, Node{ .node = n }, &m);
|
|
||||||
return m.n;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MatchAll = struct {
|
|
||||||
alloc: std.mem.Allocator,
|
|
||||||
nl: NodeList,
|
|
||||||
|
|
||||||
fn init(alloc: std.mem.Allocator) MatchAll {
|
|
||||||
return .{
|
|
||||||
.alloc = alloc,
|
|
||||||
.nl = .{},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(m: *MatchAll) void {
|
|
||||||
m.nl.deinit(m.alloc);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match(m: *MatchAll, n: Node) !void {
|
|
||||||
try m.nl.append(m.alloc, n.node);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toOwnedList(m: *MatchAll) NodeList {
|
|
||||||
// reset it.
|
|
||||||
defer m.nl = .{};
|
|
||||||
return m.nl;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn querySelectorAll(alloc: std.mem.Allocator, n: *parser.Node, selector: []const u8) !NodeList {
|
|
||||||
const ps = try css.parse(alloc, selector, .{ .accept_pseudo_elts = true });
|
|
||||||
defer ps.deinit(alloc);
|
|
||||||
|
|
||||||
var m = MatchAll.init(alloc);
|
|
||||||
defer m.deinit();
|
|
||||||
|
|
||||||
try css.matchAll(&ps, Node{ .node = n }, &m);
|
|
||||||
return m.toOwnedList();
|
|
||||||
}
|
|
||||||
@@ -1,321 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
const NodeList = @import("nodelist.zig").NodeList;
|
|
||||||
const NodeUnion = @import("node.zig").Union;
|
|
||||||
|
|
||||||
const collection = @import("html_collection.zig");
|
|
||||||
const css = @import("css.zig");
|
|
||||||
|
|
||||||
const Element = @import("element.zig").Element;
|
|
||||||
const ElementUnion = @import("element.zig").Union;
|
|
||||||
const TreeWalker = @import("tree_walker.zig").TreeWalker;
|
|
||||||
const CSSStyleSheet = @import("../cssom/CSSStyleSheet.zig");
|
|
||||||
const NodeIterator = @import("node_iterator.zig").NodeIterator;
|
|
||||||
const Range = @import("range.zig").Range;
|
|
||||||
|
|
||||||
const CustomEvent = @import("../events/custom_event.zig").CustomEvent;
|
|
||||||
|
|
||||||
const DOMImplementation = @import("implementation.zig").DOMImplementation;
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#document
|
|
||||||
pub const Document = struct {
|
|
||||||
pub const Self = parser.Document;
|
|
||||||
pub const prototype = *Node;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub fn constructor(page: *const Page) !*parser.DocumentHTML {
|
|
||||||
const doc = try parser.documentCreateDocument(
|
|
||||||
try parser.documentHTMLGetTitle(page.window.document),
|
|
||||||
);
|
|
||||||
|
|
||||||
// we have to work w/ document instead of html document.
|
|
||||||
const ddoc = parser.documentHTMLToDocument(doc);
|
|
||||||
const ccur = parser.documentHTMLToDocument(page.window.document);
|
|
||||||
try parser.documentSetDocumentURI(ddoc, try parser.documentGetDocumentURI(ccur));
|
|
||||||
try parser.documentSetInputEncoding(ddoc, try parser.documentGetInputEncoding(ccur));
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// JS funcs
|
|
||||||
// --------
|
|
||||||
pub fn get_implementation(_: *parser.Document) DOMImplementation {
|
|
||||||
return DOMImplementation{};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_documentElement(self: *parser.Document) !?ElementUnion {
|
|
||||||
const e = try parser.documentGetDocumentElement(self);
|
|
||||||
if (e == null) return null;
|
|
||||||
return try Element.toInterface(e.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_documentURI(self: *parser.Document) ![]const u8 {
|
|
||||||
return try parser.documentGetDocumentURI(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_URL(self: *parser.Document) ![]const u8 {
|
|
||||||
return try get_documentURI(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement contentType
|
|
||||||
pub fn get_contentType(self: *parser.Document) []const u8 {
|
|
||||||
_ = self;
|
|
||||||
return "text/html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement compactMode
|
|
||||||
pub fn get_compatMode(self: *parser.Document) []const u8 {
|
|
||||||
_ = self;
|
|
||||||
return "CSS1Compat";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_characterSet(self: *parser.Document) ![]const u8 {
|
|
||||||
return try parser.documentGetInputEncoding(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// alias of get_characterSet
|
|
||||||
pub fn get_charset(self: *parser.Document) ![]const u8 {
|
|
||||||
return try get_characterSet(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// alias of get_characterSet
|
|
||||||
pub fn get_inputEncoding(self: *parser.Document) ![]const u8 {
|
|
||||||
return try get_characterSet(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_doctype(self: *parser.Document) !?*parser.DocumentType {
|
|
||||||
return try parser.documentGetDoctype(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createEvent(_: *parser.Document, eventCstr: []const u8) !union(enum) {
|
|
||||||
base: *parser.Event,
|
|
||||||
custom: CustomEvent,
|
|
||||||
} {
|
|
||||||
const eqlIgnoreCase = std.ascii.eqlIgnoreCase;
|
|
||||||
|
|
||||||
if (eqlIgnoreCase(eventCstr, "Event") or eqlIgnoreCase(eventCstr, "Events") or eqlIgnoreCase(eventCstr, "HTMLEvents")) {
|
|
||||||
return .{ .base = try parser.eventCreate() };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not documented in MDN but supported in Chrome.
|
|
||||||
// This is actually both instance of `Event` and `CustomEvent`.
|
|
||||||
if (std.ascii.eqlIgnoreCase(eventCstr, "CustomEvent")) {
|
|
||||||
return .{ .custom = try CustomEvent.constructor(eventCstr, null) };
|
|
||||||
}
|
|
||||||
|
|
||||||
return error.NotSupported;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getElementById(self: *parser.Document, id: []const u8) !?ElementUnion {
|
|
||||||
const e = try parser.documentGetElementById(self, id) orelse return null;
|
|
||||||
return try Element.toInterface(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createElement(self: *parser.Document, tag_name: []const u8) !ElementUnion {
|
|
||||||
// The element’s namespace is the HTML namespace when document is an HTML document
|
|
||||||
// https://dom.spec.whatwg.org/#ref-for-dom-document-createelement%E2%91%A0
|
|
||||||
const e = try parser.documentCreateElementNS(self, "http://www.w3.org/1999/xhtml", tag_name);
|
|
||||||
return Element.toInterface(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion {
|
|
||||||
const e = try parser.documentCreateElementNS(self, ns, tag_name);
|
|
||||||
return try Element.toInterface(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can't simply use libdom dom_document_get_elements_by_tag_name here.
|
|
||||||
// Indeed, netsurf implemented a previous dom spec when
|
|
||||||
// getElementsByTagName returned a NodeList.
|
|
||||||
// But since
|
|
||||||
// https://github.com/whatwg/dom/commit/190700b7c12ecfd3b5ebdb359ab1d6ea9cbf7749
|
|
||||||
// the spec changed to return an HTMLCollection instead.
|
|
||||||
// That's why we reimplemented getElementsByTagName by using an
|
|
||||||
// HTMLCollection in zig here.
|
|
||||||
pub fn _getElementsByTagName(self: *parser.Document, tag_name: js.String) !collection.HTMLCollection {
|
|
||||||
return collection.HTMLCollectionByTagName(parser.documentToNode(self), tag_name.string, .{
|
|
||||||
.include_root = true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getElementsByClassName(self: *parser.Document, class_names: js.String) !collection.HTMLCollection {
|
|
||||||
return collection.HTMLCollectionByClassName(parser.documentToNode(self), class_names.string, .{
|
|
||||||
.include_root = true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createDocumentFragment(self: *parser.Document) !*parser.DocumentFragment {
|
|
||||||
return try parser.documentCreateDocumentFragment(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createTextNode(self: *parser.Document, data: []const u8) !*parser.Text {
|
|
||||||
return try parser.documentCreateTextNode(self, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createCDATASection(self: *parser.Document, data: []const u8) !*parser.CDATASection {
|
|
||||||
return try parser.documentCreateCDATASection(self, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createComment(self: *parser.Document, data: []const u8) !*parser.Comment {
|
|
||||||
return try parser.documentCreateComment(self, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createProcessingInstruction(self: *parser.Document, target: []const u8, data: []const u8) !*parser.ProcessingInstruction {
|
|
||||||
return try parser.documentCreateProcessingInstruction(self, target, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _importNode(self: *parser.Document, node: *parser.Node, deep: ?bool) !NodeUnion {
|
|
||||||
const n = try parser.documentImportNode(self, node, deep orelse false);
|
|
||||||
return try Node.toInterface(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _adoptNode(self: *parser.Document, node: *parser.Node) !NodeUnion {
|
|
||||||
const n = try parser.documentAdoptNode(self, node);
|
|
||||||
return try Node.toInterface(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createAttribute(self: *parser.Document, name: []const u8) !*parser.Attribute {
|
|
||||||
return try parser.documentCreateAttribute(self, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createAttributeNS(self: *parser.Document, ns: []const u8, qname: []const u8) !*parser.Attribute {
|
|
||||||
return try parser.documentCreateAttributeNS(self, ns, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParentNode
|
|
||||||
// https://dom.spec.whatwg.org/#parentnode
|
|
||||||
pub fn get_children(self: *parser.Document) !collection.HTMLCollection {
|
|
||||||
return collection.HTMLCollectionChildren(parser.documentToNode(self), .{
|
|
||||||
.include_root = false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_firstElementChild(self: *parser.Document) !?ElementUnion {
|
|
||||||
const elt = try parser.documentGetDocumentElement(self) orelse return null;
|
|
||||||
return try Element.toInterface(elt);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_lastElementChild(self: *parser.Document) !?ElementUnion {
|
|
||||||
const elt = try parser.documentGetDocumentElement(self) orelse return null;
|
|
||||||
return try Element.toInterface(elt);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_childElementCount(self: *parser.Document) !u32 {
|
|
||||||
_ = try parser.documentGetDocumentElement(self) orelse return 0;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _querySelector(self: *parser.Document, selector: []const u8, page: *Page) !?ElementUnion {
|
|
||||||
if (selector.len == 0) return null;
|
|
||||||
|
|
||||||
const n = try css.querySelector(page.call_arena, parser.documentToNode(self), selector);
|
|
||||||
|
|
||||||
if (n == null) return null;
|
|
||||||
|
|
||||||
return try Element.toInterface(parser.nodeToElement(n.?));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _querySelectorAll(self: *parser.Document, selector: []const u8, page: *Page) !NodeList {
|
|
||||||
return css.querySelectorAll(page.arena, parser.documentToNode(self), selector);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _prepend(self: *parser.Document, nodes: []const Node.NodeOrText) !void {
|
|
||||||
return Node.prepend(parser.documentToNode(self), nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _append(self: *parser.Document, nodes: []const Node.NodeOrText) !void {
|
|
||||||
return Node.append(parser.documentToNode(self), nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _replaceChildren(self: *parser.Document, nodes: []const Node.NodeOrText) !void {
|
|
||||||
return Node.replaceChildren(parser.documentToNode(self), nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createTreeWalker(_: *parser.Document, root: *parser.Node, what_to_show: ?TreeWalker.WhatToShow, filter: ?TreeWalker.TreeWalkerOpts) !TreeWalker {
|
|
||||||
return TreeWalker.init(root, what_to_show, filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createNodeIterator(_: *parser.Document, root: *parser.Node, what_to_show: ?NodeIterator.WhatToShow, filter: ?NodeIterator.NodeIteratorOpts) !NodeIterator {
|
|
||||||
return NodeIterator.init(root, what_to_show, filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getActiveElement(self: *parser.Document, page: *Page) !?*parser.Element {
|
|
||||||
if (page.getNodeState(@ptrCast(@alignCast(self)))) |state| {
|
|
||||||
if (state.active_element) |ae| {
|
|
||||||
return ae;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try parser.documentHTMLBody(page.window.document)) |body| {
|
|
||||||
return @ptrCast(@alignCast(body));
|
|
||||||
}
|
|
||||||
|
|
||||||
return try parser.documentGetDocumentElement(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion {
|
|
||||||
const ae = (try getActiveElement(self, page)) orelse return null;
|
|
||||||
return try Element.toInterface(ae);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: some elements can't be focused, like if they're disabled
|
|
||||||
// but there doesn't seem to be a generic way to check this. For example
|
|
||||||
// we could look for the "disabled" attribute, but that's only meaningful
|
|
||||||
// on certain types, and libdom's vtable doesn't seem to expose this.
|
|
||||||
pub fn setFocus(self: *parser.Document, e: *parser.ElementHTML, page: *Page) !void {
|
|
||||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
|
||||||
state.active_element = @ptrCast(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createRange(_: *parser.Document, page: *Page) Range {
|
|
||||||
return Range.constructor(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: dummy implementation
|
|
||||||
pub fn get_styleSheets(_: *parser.Document) []CSSStyleSheet {
|
|
||||||
return &.{};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !js.Object {
|
|
||||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
|
||||||
if (state.adopted_style_sheets) |obj| {
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
const obj = try page.js.createArray(0).persist();
|
|
||||||
state.adopted_style_sheets = obj;
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: js.Object, page: *Page) !void {
|
|
||||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
|
||||||
state.adopted_style_sheets = try sheets.persist();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.Document" {
|
|
||||||
try testing.htmlRunner("dom/document.html");
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const css = @import("css.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const NodeList = @import("nodelist.zig").NodeList;
|
|
||||||
const Element = @import("element.zig").Element;
|
|
||||||
const ElementUnion = @import("element.zig").Union;
|
|
||||||
const collection = @import("html_collection.zig");
|
|
||||||
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#documentfragment
|
|
||||||
pub const DocumentFragment = struct {
|
|
||||||
pub const Self = parser.DocumentFragment;
|
|
||||||
pub const prototype = *Node;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub fn constructor(page: *const Page) !*parser.DocumentFragment {
|
|
||||||
return parser.documentCreateDocumentFragment(
|
|
||||||
parser.documentHTMLToDocument(page.window.document),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _isEqualNode(self: *parser.DocumentFragment, other_node: *parser.Node) bool {
|
|
||||||
const other_type = parser.nodeType(other_node);
|
|
||||||
if (other_type != .document_fragment) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_ = self;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _prepend(self: *parser.DocumentFragment, nodes: []const Node.NodeOrText) !void {
|
|
||||||
return Node.prepend(parser.documentFragmentToNode(self), nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _append(self: *parser.DocumentFragment, nodes: []const Node.NodeOrText) !void {
|
|
||||||
return Node.append(parser.documentFragmentToNode(self), nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _replaceChildren(self: *parser.DocumentFragment, nodes: []const Node.NodeOrText) !void {
|
|
||||||
return Node.replaceChildren(parser.documentFragmentToNode(self), nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _querySelector(self: *parser.DocumentFragment, selector: []const u8, page: *Page) !?ElementUnion {
|
|
||||||
if (selector.len == 0) return null;
|
|
||||||
|
|
||||||
const n = try css.querySelector(page.call_arena, parser.documentFragmentToNode(self), selector);
|
|
||||||
|
|
||||||
if (n == null) return null;
|
|
||||||
|
|
||||||
return try Element.toInterface(parser.nodeToElement(n.?));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _querySelectorAll(self: *parser.DocumentFragment, selector: []const u8, page: *Page) !NodeList {
|
|
||||||
return css.querySelectorAll(page.arena, parser.documentFragmentToNode(self), selector);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_childElementCount(self: *parser.DocumentFragment) !u32 {
|
|
||||||
var children = try get_children(self);
|
|
||||||
return children.get_length();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_children(self: *parser.DocumentFragment) !collection.HTMLCollection {
|
|
||||||
return collection.HTMLCollectionChildren(parser.documentFragmentToNode(self), .{
|
|
||||||
.include_root = false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getElementById(self: *parser.DocumentFragment, id: []const u8) !?ElementUnion {
|
|
||||||
const e = try parser.nodeGetElementById(@ptrCast(@alignCast(self)), id) orelse return null;
|
|
||||||
return try Element.toInterface(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.DocumentFragment" {
|
|
||||||
try testing.htmlRunner("dom/document_fragment.html");
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#documenttype
|
|
||||||
pub const DocumentType = struct {
|
|
||||||
pub const Self = parser.DocumentType;
|
|
||||||
pub const prototype = *Node;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub fn get_name(self: *parser.DocumentType) ![]const u8 {
|
|
||||||
return parser.documentTypeGetName(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_publicId(self: *parser.DocumentType) []const u8 {
|
|
||||||
return parser.documentTypeGetPublicId(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_systemId(self: *parser.DocumentType) []const u8 {
|
|
||||||
return parser.documentTypeGetSystemId(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// netsurf's DocumentType doesn't implement the dom_node_get_attributes
|
|
||||||
// and thus will crash if we try to call nodeIsEqualNode.
|
|
||||||
pub fn _isEqualNode(self: *parser.DocumentType, other_node: *parser.Node) !bool {
|
|
||||||
if (parser.nodeType(other_node) != .document_type) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const other: *parser.DocumentType = @ptrCast(other_node);
|
|
||||||
if (std.mem.eql(u8, try get_name(self), try get_name(other)) == false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (std.mem.eql(u8, get_publicId(self), get_publicId(other)) == false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (std.mem.eql(u8, get_systemId(self), get_systemId(other)) == false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.DocumentType" {
|
|
||||||
try testing.htmlRunner("dom/document_type.html");
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const DOMException = @import("exceptions.zig").DOMException;
|
|
||||||
const EventTarget = @import("event_target.zig").EventTarget;
|
|
||||||
const DOMImplementation = @import("implementation.zig").DOMImplementation;
|
|
||||||
const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap;
|
|
||||||
const DOMTokenList = @import("token_list.zig");
|
|
||||||
const NodeList = @import("nodelist.zig");
|
|
||||||
const Node = @import("node.zig");
|
|
||||||
const ResizeObserver = @import("resize_observer.zig");
|
|
||||||
const MutationObserver = @import("mutation_observer.zig");
|
|
||||||
const DOMParser = @import("dom_parser.zig").DOMParser;
|
|
||||||
const TreeWalker = @import("tree_walker.zig").TreeWalker;
|
|
||||||
const NodeIterator = @import("node_iterator.zig").NodeIterator;
|
|
||||||
const NodeFilter = @import("node_filter.zig").NodeFilter;
|
|
||||||
const PerformanceObserver = @import("performance_observer.zig").PerformanceObserver;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
DOMException,
|
|
||||||
EventTarget,
|
|
||||||
DOMImplementation,
|
|
||||||
NamedNodeMap,
|
|
||||||
NamedNodeMap.Iterator,
|
|
||||||
DOMTokenList.Interfaces,
|
|
||||||
NodeList.Interfaces,
|
|
||||||
Node.Node,
|
|
||||||
Node.Interfaces,
|
|
||||||
ResizeObserver.Interfaces,
|
|
||||||
MutationObserver.Interfaces,
|
|
||||||
DOMParser,
|
|
||||||
TreeWalker,
|
|
||||||
NodeIterator,
|
|
||||||
NodeFilter,
|
|
||||||
@import("performance.zig").Interfaces,
|
|
||||||
PerformanceObserver,
|
|
||||||
@import("range.zig").Interfaces,
|
|
||||||
@import("Animation.zig"),
|
|
||||||
@import("MessageChannel.zig").Interfaces,
|
|
||||||
@import("IntersectionObserver.zig").Interfaces,
|
|
||||||
};
|
|
||||||
@@ -1,686 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const css = @import("css.zig");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const dump = @import("../dump.zig");
|
|
||||||
const collection = @import("html_collection.zig");
|
|
||||||
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
const Walker = @import("walker.zig").WalkerDepthFirst;
|
|
||||||
const NodeList = @import("nodelist.zig").NodeList;
|
|
||||||
const HTMLElem = @import("../html/elements.zig");
|
|
||||||
const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot;
|
|
||||||
|
|
||||||
const Animation = @import("Animation.zig");
|
|
||||||
|
|
||||||
pub const Union = @import("../html/elements.zig").Union;
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#element
|
|
||||||
pub const Element = struct {
|
|
||||||
pub const Self = parser.Element;
|
|
||||||
pub const prototype = *Node;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub const DOMRect = struct {
|
|
||||||
x: f64,
|
|
||||||
y: f64,
|
|
||||||
width: f64,
|
|
||||||
height: f64,
|
|
||||||
bottom: f64,
|
|
||||||
right: f64,
|
|
||||||
top: f64,
|
|
||||||
left: f64,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn toInterface(e: *parser.Element) !Union {
|
|
||||||
return toInterfaceT(Union, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toInterfaceT(comptime T: type, e: *parser.Element) !T {
|
|
||||||
const tagname = try parser.elementGetTagName(e) orelse {
|
|
||||||
// If the owner's document is HTML, assume we have an HTMLElement.
|
|
||||||
const doc = parser.nodeOwnerDocument(parser.elementToNode(e));
|
|
||||||
if (doc != null and !doc.?.is_html) {
|
|
||||||
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{ .Element = e };
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO SVGElement and MathML are not supported yet.
|
|
||||||
|
|
||||||
const tag = parser.Tag.fromString(tagname) catch {
|
|
||||||
// If the owner's document is HTML, assume we have an HTMLElement.
|
|
||||||
const doc = parser.nodeOwnerDocument(parser.elementToNode(e));
|
|
||||||
if (doc != null and doc.?.is_html) {
|
|
||||||
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{ .Element = e };
|
|
||||||
};
|
|
||||||
|
|
||||||
return HTMLElem.toInterfaceFromTag(T, e, tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
// JS funcs
|
|
||||||
// --------
|
|
||||||
|
|
||||||
pub fn get_namespaceURI(self: *parser.Element) ?[]const u8 {
|
|
||||||
return parser.nodeGetNamespace(parser.elementToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_prefix(self: *parser.Element) ?[]const u8 {
|
|
||||||
return parser.nodeGetPrefix(parser.elementToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_localName(self: *parser.Element) ![]const u8 {
|
|
||||||
return try parser.nodeLocalName(parser.elementToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_tagName(self: *parser.Element) ![]const u8 {
|
|
||||||
return try parser.nodeName(parser.elementToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_dir(self: *parser.Element) ![]const u8 {
|
|
||||||
return try parser.elementGetAttribute(self, "dir") orelse "";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_dir(self: *parser.Element, dir: []const u8) !void {
|
|
||||||
return parser.elementSetAttribute(self, "dir", dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_id(self: *parser.Element) ![]const u8 {
|
|
||||||
return try parser.elementGetAttribute(self, "id") orelse "";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_id(self: *parser.Element, id: []const u8) !void {
|
|
||||||
return try parser.elementSetAttribute(self, "id", id);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_className(self: *parser.Element) ![]const u8 {
|
|
||||||
return try parser.elementGetAttribute(self, "class") orelse "";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_className(self: *parser.Element, class: []const u8) !void {
|
|
||||||
return try parser.elementSetAttribute(self, "class", class);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_slot(self: *parser.Element) ![]const u8 {
|
|
||||||
return try parser.elementGetAttribute(self, "slot") orelse "";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_slot(self: *parser.Element, slot: []const u8) !void {
|
|
||||||
return try parser.elementSetAttribute(self, "slot", slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_assignedSlot(self: *parser.Element, page: *const Page) !?*parser.Slot {
|
|
||||||
return @import("../SlotChangeMonitor.zig").findSlot(self, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_classList(self: *parser.Element) !*parser.TokenList {
|
|
||||||
return try parser.tokenListCreate(self, "class");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_attributes(self: *parser.Element) !*parser.NamedNodeMap {
|
|
||||||
// An element must have non-nil attributes.
|
|
||||||
return try parser.nodeGetAttributes(parser.elementToNode(self)) orelse unreachable;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_innerHTML(self: *parser.Element, page: *Page) ![]const u8 {
|
|
||||||
var aw = std.Io.Writer.Allocating.init(page.call_arena);
|
|
||||||
try dump.writeChildren(parser.elementToNode(self), .{}, &aw.writer);
|
|
||||||
return aw.written();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_outerHTML(self: *parser.Element, page: *Page) ![]const u8 {
|
|
||||||
var aw = std.Io.Writer.Allocating.init(page.call_arena);
|
|
||||||
try dump.writeNode(parser.elementToNode(self), .{}, &aw.writer);
|
|
||||||
return aw.written();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_innerHTML(self: *parser.Element, str: []const u8, page: *Page) !void {
|
|
||||||
const node = parser.elementToNode(self);
|
|
||||||
const doc = parser.nodeOwnerDocument(node) orelse return parser.DOMError.WrongDocument;
|
|
||||||
// parse the fragment
|
|
||||||
const fragment = try parser.documentParseFragmentFromStr(doc, str);
|
|
||||||
|
|
||||||
// remove existing children
|
|
||||||
try Node.removeChildren(node);
|
|
||||||
|
|
||||||
const fragment_node = parser.documentFragmentToNode(fragment);
|
|
||||||
|
|
||||||
// I'm not sure what the exact behavior is supposed to be. Initially,
|
|
||||||
// we were only copying the body of the document fragment. But it seems
|
|
||||||
// like head elements should be copied too. Specifically, some sites
|
|
||||||
// create script tags via innerHTML, which we need to capture.
|
|
||||||
// If you play with this in a browser, you should notice that the
|
|
||||||
// behavior is different depending on whether you're in a blank page
|
|
||||||
// or an actual document. In a blank page, something like:
|
|
||||||
// x.innerHTML = '<script></script>';
|
|
||||||
// does _not_ create an empty script, but in a real page, it does. Weird.
|
|
||||||
const html = parser.nodeFirstChild(fragment_node) orelse return;
|
|
||||||
const head = parser.nodeFirstChild(html) orelse return;
|
|
||||||
const body = parser.nodeNextSibling(head) orelse return;
|
|
||||||
|
|
||||||
if (try parser.elementTag(self) == .template) {
|
|
||||||
// HTMLElementTemplate is special. We don't append these as children
|
|
||||||
// of the template, but instead set its content as the body of the
|
|
||||||
// fragment. Simpler to do this by copying the body children into
|
|
||||||
// a new fragment
|
|
||||||
const clean = try parser.documentCreateDocumentFragment(doc);
|
|
||||||
const children = try parser.nodeGetChildNodes(body);
|
|
||||||
// always index 0, because nodeAppendChild moves the node out of
|
|
||||||
// the nodeList and into the new tree
|
|
||||||
while (parser.nodeListItem(children, 0)) |child| {
|
|
||||||
_ = try parser.nodeAppendChild(@ptrCast(@alignCast(clean)), child);
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = try page.getOrCreateNodeState(node);
|
|
||||||
state.template_content = clean;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For any node other than a template, we copy the head and body elements
|
|
||||||
// as child nodes of the element
|
|
||||||
{
|
|
||||||
// First, copy some of the head element
|
|
||||||
const children = try parser.nodeGetChildNodes(head);
|
|
||||||
// always index 0, because nodeAppendChild moves the node out of
|
|
||||||
// the nodeList and into the new tree
|
|
||||||
while (parser.nodeListItem(children, 0)) |child| {
|
|
||||||
_ = try parser.nodeAppendChild(node, child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const children = try parser.nodeGetChildNodes(body);
|
|
||||||
// always index 0, because nodeAppendChild moves the node out of
|
|
||||||
// the nodeList and into the new tree
|
|
||||||
while (parser.nodeListItem(children, 0)) |child| {
|
|
||||||
_ = try parser.nodeAppendChild(node, child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses the given `input` string and inserts its children to an element at given `position`.
|
|
||||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
|
|
||||||
///
|
|
||||||
/// TODO: Support for XML parsing and `TrustedHTML` instances.
|
|
||||||
pub fn _insertAdjacentHTML(self: *parser.Element, position: []const u8, input: []const u8) !void {
|
|
||||||
const self_node = parser.elementToNode(self);
|
|
||||||
const doc = parser.nodeOwnerDocument(self_node) orelse {
|
|
||||||
return parser.DOMError.WrongDocument;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Parse the fragment.
|
|
||||||
// Should return error.Syntax on fail?
|
|
||||||
const fragment = try parser.documentParseFragmentFromStr(doc, input);
|
|
||||||
const fragment_node = parser.documentFragmentToNode(fragment);
|
|
||||||
|
|
||||||
// We always get it wrapped like so:
|
|
||||||
// <html><head></head><body>{ ... }</body></html>
|
|
||||||
// None of the following can be null.
|
|
||||||
const maybe_html = parser.nodeFirstChild(fragment_node);
|
|
||||||
std.debug.assert(maybe_html != null);
|
|
||||||
const html = maybe_html orelse return;
|
|
||||||
|
|
||||||
const maybe_body = parser.nodeLastChild(html);
|
|
||||||
std.debug.assert(maybe_body != null);
|
|
||||||
const body = maybe_body orelse return;
|
|
||||||
|
|
||||||
const children = try parser.nodeGetChildNodes(body);
|
|
||||||
|
|
||||||
// * `target_node` is `*Node` (where we actually insert),
|
|
||||||
// * `prev_node` is `?*Node`.
|
|
||||||
const target_node, const prev_node = blk: {
|
|
||||||
// Prefer case-sensitive match.
|
|
||||||
// "beforeend" was the most common case in my tests; we might adjust the order
|
|
||||||
// depending on which ones websites prefer most.
|
|
||||||
if (std.mem.eql(u8, position, "beforeend")) {
|
|
||||||
break :blk .{ self_node, null };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, position, "afterbegin")) {
|
|
||||||
// Get the first child; null indicates there are no children.
|
|
||||||
const first_child = parser.nodeFirstChild(self_node);
|
|
||||||
break :blk .{ self_node, first_child };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, position, "beforebegin")) {
|
|
||||||
// The node must have a parent node in order to use this variant.
|
|
||||||
const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed;
|
|
||||||
// Parent cannot be Document.
|
|
||||||
// Should have checks for document_fragment and document_type?
|
|
||||||
if (parser.nodeType(parent) == .document) {
|
|
||||||
return error.NoModificationAllowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
break :blk .{ parent, self_node };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, position, "afterend")) {
|
|
||||||
// The node must have a parent node in order to use this variant.
|
|
||||||
const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed;
|
|
||||||
// Parent cannot be Document.
|
|
||||||
if (parser.nodeType(parent) == .document) {
|
|
||||||
return error.NoModificationAllowed;
|
|
||||||
}
|
|
||||||
// Get the next sibling or null; null indicates our node is the only one.
|
|
||||||
const sibling = parser.nodeNextSibling(self_node);
|
|
||||||
break :blk .{ parent, sibling };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thrown if:
|
|
||||||
// * position is not one of the four listed values.
|
|
||||||
// * The input is XML that is not well-formed.
|
|
||||||
return error.Syntax;
|
|
||||||
};
|
|
||||||
|
|
||||||
while (parser.nodeListItem(children, 0)) |child| {
|
|
||||||
_ = try parser.nodeInsertBefore(target_node, child, prev_node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The closest() method of the Element interface traverses the element and its parents (heading toward the document root) until it finds a node that matches the specified CSS selector.
|
|
||||||
// Returns the closest ancestor Element or itself, which matches the selectors. If there are no such element, null.
|
|
||||||
pub fn _closest(self: *parser.Element, selector: []const u8, page: *Page) !?*parser.Element {
|
|
||||||
const cssParse = @import("../css/css.zig").parse;
|
|
||||||
const CssNodeWrap = @import("../css/libdom.zig").Node;
|
|
||||||
const select = try cssParse(page.call_arena, selector, .{});
|
|
||||||
|
|
||||||
var current: CssNodeWrap = .{ .node = parser.elementToNode(self) };
|
|
||||||
while (true) {
|
|
||||||
if (try select.match(current)) {
|
|
||||||
if (!current.isElement()) {
|
|
||||||
log.err(.browser, "closest invalid type", .{ .type = try current.tag() });
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return parser.nodeToElement(current.node);
|
|
||||||
}
|
|
||||||
current = current.parent() orelse return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't use parser.nodeHasAttributes(...) because that returns true/false
|
|
||||||
// based on the type, e.g. a node never as attributes, an element always has
|
|
||||||
// attributes. But, Element.hasAttributes is supposed to return true only
|
|
||||||
// if the element has at least 1 attribute.
|
|
||||||
pub fn _hasAttributes(self: *parser.Element) !bool {
|
|
||||||
// an element _must_ have at least an empty attribute
|
|
||||||
const node_map = try parser.nodeGetAttributes(parser.elementToNode(self)) orelse unreachable;
|
|
||||||
return try parser.namedNodeMapGetLength(node_map) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getAttribute(self: *parser.Element, qname: []const u8) !?[]const u8 {
|
|
||||||
return try parser.elementGetAttribute(self, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getAttributeNS(self: *parser.Element, ns: []const u8, qname: []const u8) !?[]const u8 {
|
|
||||||
return try parser.elementGetAttributeNS(self, ns, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setAttribute(self: *parser.Element, qname: []const u8, value: []const u8) !void {
|
|
||||||
return try parser.elementSetAttribute(self, qname, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setAttributeNS(self: *parser.Element, ns: []const u8, qname: []const u8, value: []const u8) !void {
|
|
||||||
return try parser.elementSetAttributeNS(self, ns, qname, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _removeAttribute(self: *parser.Element, qname: []const u8) !void {
|
|
||||||
return try parser.elementRemoveAttribute(self, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _removeAttributeNS(self: *parser.Element, ns: []const u8, qname: []const u8) !void {
|
|
||||||
return try parser.elementRemoveAttributeNS(self, ns, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _hasAttribute(self: *parser.Element, qname: []const u8) !bool {
|
|
||||||
return try parser.elementHasAttribute(self, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _hasAttributeNS(self: *parser.Element, ns: []const u8, qname: []const u8) !bool {
|
|
||||||
return try parser.elementHasAttributeNS(self, ns, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#dom-element-toggleattribute
|
|
||||||
pub fn _toggleAttribute(self: *parser.Element, qname: []u8, force: ?bool) !bool {
|
|
||||||
_ = std.ascii.lowerString(qname, qname);
|
|
||||||
const exists = try parser.elementHasAttribute(self, qname);
|
|
||||||
|
|
||||||
// If attribute is null, then:
|
|
||||||
if (!exists) {
|
|
||||||
// If force is not given or is true, create an attribute whose
|
|
||||||
// local name is qualifiedName, value is the empty string and node
|
|
||||||
// document is this’s node document, then append this attribute to
|
|
||||||
// this, and then return true.
|
|
||||||
if (force == null or force.?) {
|
|
||||||
try parser.elementSetAttribute(self, qname, "");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (try parser.validateName(qname) == false) {
|
|
||||||
return parser.DOMError.InvalidCharacter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return false.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, if force is not given or is false, remove an attribute
|
|
||||||
// given qualifiedName and this, and then return false.
|
|
||||||
if (force == null or !force.?) {
|
|
||||||
try parser.elementRemoveAttribute(self, qname);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return true.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getAttributeNames(self: *parser.Element, page: *Page) ![]const []const u8 {
|
|
||||||
const attributes = try parser.nodeGetAttributes(@ptrCast(self)) orelse return &.{};
|
|
||||||
const ln = try parser.namedNodeMapGetLength(attributes);
|
|
||||||
|
|
||||||
const names = try page.call_arena.alloc([]const u8, ln);
|
|
||||||
var at: usize = 0;
|
|
||||||
|
|
||||||
for (0..ln) |i| {
|
|
||||||
const attribute = try parser.namedNodeMapItem(attributes, @intCast(i)) orelse break;
|
|
||||||
names[at] = try parser.attributeGetName(attribute);
|
|
||||||
at += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return names[0..at];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getAttributeNode(self: *parser.Element, name: []const u8) !?*parser.Attribute {
|
|
||||||
return try parser.elementGetAttributeNode(self, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getAttributeNodeNS(self: *parser.Element, ns: []const u8, name: []const u8) !?*parser.Attribute {
|
|
||||||
return try parser.elementGetAttributeNodeNS(self, ns, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setAttributeNode(self: *parser.Element, attr: *parser.Attribute) !?*parser.Attribute {
|
|
||||||
return try parser.elementSetAttributeNode(self, attr);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setAttributeNodeNS(self: *parser.Element, attr: *parser.Attribute) !?*parser.Attribute {
|
|
||||||
return try parser.elementSetAttributeNodeNS(self, attr);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _removeAttributeNode(self: *parser.Element, attr: *parser.Attribute) !*parser.Attribute {
|
|
||||||
return try parser.elementRemoveAttributeNode(self, attr);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getElementsByTagName(self: *parser.Element, tag_name: js.String) !collection.HTMLCollection {
|
|
||||||
return collection.HTMLCollectionByTagName(
|
|
||||||
parser.elementToNode(self),
|
|
||||||
tag_name.string,
|
|
||||||
.{ .include_root = false },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getElementsByClassName(self: *parser.Element, class_names: js.String) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionByClassName(
|
|
||||||
parser.elementToNode(self),
|
|
||||||
class_names.string,
|
|
||||||
.{ .include_root = false },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParentNode
|
|
||||||
// https://dom.spec.whatwg.org/#parentnode
|
|
||||||
pub fn get_children(self: *parser.Element) !collection.HTMLCollection {
|
|
||||||
return collection.HTMLCollectionChildren(parser.elementToNode(self), .{
|
|
||||||
.include_root = false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_firstElementChild(self: *parser.Element) !?Union {
|
|
||||||
var children = try get_children(self);
|
|
||||||
return try children._item(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_lastElementChild(self: *parser.Element) !?Union {
|
|
||||||
// TODO we could check the last child node first, if it's an element,
|
|
||||||
// we can return it directly instead of looping twice over the
|
|
||||||
// children.
|
|
||||||
var children = try get_children(self);
|
|
||||||
const ln = try children.get_length();
|
|
||||||
if (ln == 0) return null;
|
|
||||||
return try children._item(ln - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_childElementCount(self: *parser.Element) !u32 {
|
|
||||||
var children = try get_children(self);
|
|
||||||
return try children.get_length();
|
|
||||||
}
|
|
||||||
|
|
||||||
// NonDocumentTypeChildNode
|
|
||||||
// https://dom.spec.whatwg.org/#interface-nondocumenttypechildnode
|
|
||||||
pub fn get_previousElementSibling(self: *parser.Element) !?Union {
|
|
||||||
const res = parser.nodePreviousElementSibling(parser.elementToNode(self));
|
|
||||||
if (res == null) return null;
|
|
||||||
return try toInterface(res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_nextElementSibling(self: *parser.Element) !?Union {
|
|
||||||
const res = parser.nodeNextElementSibling(parser.elementToNode(self));
|
|
||||||
if (res == null) return null;
|
|
||||||
return try toInterface(res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getElementById(self: *parser.Element, id: []const u8) !?*parser.Node {
|
|
||||||
// walk over the node tree fo find the node by id.
|
|
||||||
const root = parser.elementToNode(self);
|
|
||||||
const walker = Walker{};
|
|
||||||
var next: ?*parser.Node = null;
|
|
||||||
while (true) {
|
|
||||||
next = try walker.get_next(root, next) orelse return null;
|
|
||||||
// ignore non-element nodes.
|
|
||||||
if (parser.nodeType(next.?) != .element) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const e = parser.nodeToElement(next.?);
|
|
||||||
if (std.mem.eql(u8, id, try get_id(e))) return next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _querySelector(self: *parser.Element, selector: []const u8, page: *Page) !?Union {
|
|
||||||
if (selector.len == 0) return null;
|
|
||||||
|
|
||||||
const n = try css.querySelector(page.call_arena, parser.elementToNode(self), selector);
|
|
||||||
|
|
||||||
if (n == null) return null;
|
|
||||||
|
|
||||||
return try toInterface(parser.nodeToElement(n.?));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _querySelectorAll(self: *parser.Element, selector: []const u8, page: *Page) !NodeList {
|
|
||||||
return css.querySelectorAll(page.arena, parser.elementToNode(self), selector);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _prepend(self: *parser.Element, nodes: []const Node.NodeOrText) !void {
|
|
||||||
return Node.prepend(parser.elementToNode(self), nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _append(self: *parser.Element, nodes: []const Node.NodeOrText) !void {
|
|
||||||
return Node.append(parser.elementToNode(self), nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _before(self: *parser.Element, nodes: []const Node.NodeOrText) !void {
|
|
||||||
const ref_node = parser.elementToNode(self);
|
|
||||||
return Node.before(ref_node, nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _after(self: *parser.Element, nodes: []const Node.NodeOrText) !void {
|
|
||||||
const ref_node = parser.elementToNode(self);
|
|
||||||
return Node.after(ref_node, nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _replaceChildren(self: *parser.Element, nodes: []const Node.NodeOrText) !void {
|
|
||||||
return Node.replaceChildren(parser.elementToNode(self), nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A DOMRect object providing information about the size of an element and its position relative to the viewport.
|
|
||||||
// Returns a 0 DOMRect object if the element is eventually detached from the main window
|
|
||||||
pub fn _getBoundingClientRect(self: *parser.Element, page: *Page) !DOMRect {
|
|
||||||
// Since we are lazy rendering we need to do this check. We could store the renderer in a viewport such that it could cache these, but it would require tracking changes.
|
|
||||||
if (!page.isNodeAttached(parser.elementToNode(self))) {
|
|
||||||
return DOMRect{
|
|
||||||
.x = 0,
|
|
||||||
.y = 0,
|
|
||||||
.width = 0,
|
|
||||||
.height = 0,
|
|
||||||
.bottom = 0,
|
|
||||||
.right = 0,
|
|
||||||
.top = 0,
|
|
||||||
.left = 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return page.renderer.getRect(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a collection of DOMRect objects that indicate the bounding rectangles for each CSS border box in a client.
|
|
||||||
// We do not render so it only always return the element's bounding rect.
|
|
||||||
// Returns an empty array if the element is eventually detached from the main window
|
|
||||||
pub fn _getClientRects(self: *parser.Element, page: *Page) ![]DOMRect {
|
|
||||||
if (!page.isNodeAttached(parser.elementToNode(self))) {
|
|
||||||
return &.{};
|
|
||||||
}
|
|
||||||
const heap_ptr = try page.call_arena.create(DOMRect);
|
|
||||||
heap_ptr.* = try page.renderer.getRect(self);
|
|
||||||
return heap_ptr[0..1];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_clientWidth(_: *parser.Element, page: *Page) u32 {
|
|
||||||
return page.renderer.width();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_clientHeight(_: *parser.Element, page: *Page) u32 {
|
|
||||||
return page.renderer.height();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _matches(self: *parser.Element, selectors: []const u8, page: *Page) !bool {
|
|
||||||
const cssParse = @import("../css/css.zig").parse;
|
|
||||||
const CssNodeWrap = @import("../css/libdom.zig").Node;
|
|
||||||
const s = try cssParse(page.call_arena, selectors, .{});
|
|
||||||
return s.match(CssNodeWrap{ .node = parser.elementToNode(self) });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _scrollIntoViewIfNeeded(_: *parser.Element, center_if_needed: ?bool) void {
|
|
||||||
_ = center_if_needed;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CheckVisibilityOpts = struct {
|
|
||||||
contentVisibilityAuto: bool,
|
|
||||||
opacityProperty: bool,
|
|
||||||
visibilityProperty: bool,
|
|
||||||
checkVisibilityCSS: bool,
|
|
||||||
checkOpacity: bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn _checkVisibility(self: *parser.Element, opts: ?CheckVisibilityOpts) bool {
|
|
||||||
_ = self;
|
|
||||||
_ = opts;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AttachShadowOpts = struct {
|
|
||||||
mode: []const u8, // must be specified
|
|
||||||
};
|
|
||||||
pub fn _attachShadow(self: *parser.Element, opts: AttachShadowOpts, page: *Page) !*ShadowRoot {
|
|
||||||
const mode = std.meta.stringToEnum(ShadowRoot.Mode, opts.mode) orelse return error.InvalidArgument;
|
|
||||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
|
||||||
if (state.shadow_root) |sr| {
|
|
||||||
if (mode != sr.mode) {
|
|
||||||
// this is the behavior per the spec
|
|
||||||
return error.NotSupportedError;
|
|
||||||
}
|
|
||||||
|
|
||||||
try Node.removeChildren(@ptrCast(@alignCast(sr.proto)));
|
|
||||||
return sr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not sure what to do if there is no owner document
|
|
||||||
const doc = parser.nodeOwnerDocument(@ptrCast(self)) orelse return error.InvalidArgument;
|
|
||||||
const fragment = try parser.documentCreateDocumentFragment(doc);
|
|
||||||
const sr = try page.arena.create(ShadowRoot);
|
|
||||||
sr.* = .{
|
|
||||||
.host = self,
|
|
||||||
.mode = mode,
|
|
||||||
.proto = fragment,
|
|
||||||
};
|
|
||||||
state.shadow_root = sr;
|
|
||||||
parser.documentFragmentSetHost(sr.proto, @ptrCast(@alignCast(self)));
|
|
||||||
|
|
||||||
// Storing the ShadowRoot on the element makes sense, it's the ShadowRoot's
|
|
||||||
// parent. When we render, we go top-down, so we'll have the element, get
|
|
||||||
// its shadowroot, and go on. that's what the above code does.
|
|
||||||
// But we sometimes need to go bottom-up, e.g when we have a slot element
|
|
||||||
// and want to find the containing parent. Unforatunately , we don't have
|
|
||||||
// that link, so we need to create it. In the DOM, the ShadowRoot is
|
|
||||||
// represented by this DocumentFragment (it's the ShadowRoot's base prototype)
|
|
||||||
// So we can also store the ShadowRoot in the DocumentFragment's state.
|
|
||||||
const fragment_state = try page.getOrCreateNodeState(@ptrCast(@alignCast(fragment)));
|
|
||||||
fragment_state.shadow_root = sr;
|
|
||||||
|
|
||||||
return sr;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_shadowRoot(self: *parser.Element, page: *Page) ?*ShadowRoot {
|
|
||||||
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
|
|
||||||
const sr = state.shadow_root orelse return null;
|
|
||||||
if (sr.mode == .closed) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return sr;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _animate(self: *parser.Element, effect: js.Object, opts: js.Object) !Animation {
|
|
||||||
_ = self;
|
|
||||||
_ = opts;
|
|
||||||
return Animation.constructor(effect, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _remove(self: *parser.Element) !void {
|
|
||||||
// TODO: This hasn't been tested to make sure all references to this
|
|
||||||
// node are properly updated. A lot of libdom is lazy and will look
|
|
||||||
// for related elements JIT by walking the tree, but there could be
|
|
||||||
// cases in libdom or the Zig WebAPI where this reference is kept
|
|
||||||
const as_node: *parser.Node = @ptrCast(self);
|
|
||||||
const parent = parser.nodeParentNode(as_node) orelse return;
|
|
||||||
_ = try Node._removeChild(parent, as_node);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.Element" {
|
|
||||||
try testing.htmlRunner("dom/element.html");
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
|
||||||
|
|
||||||
const DOMException = @import("exceptions.zig").DOMException;
|
|
||||||
const nod = @import("node.zig");
|
|
||||||
|
|
||||||
pub const Union = union(enum) {
|
|
||||||
node: nod.Union,
|
|
||||||
xhr: *@import("../xhr/xhr.zig").XMLHttpRequest,
|
|
||||||
plain: *parser.EventTarget,
|
|
||||||
message_port: *@import("MessageChannel.zig").MessagePort,
|
|
||||||
screen: *@import("../html/screen.zig").Screen,
|
|
||||||
screen_orientation: *@import("../html/screen.zig").ScreenOrientation,
|
|
||||||
performance: *@import("performance.zig").Performance,
|
|
||||||
media_query_list: *@import("../html/media_query_list.zig").MediaQueryList,
|
|
||||||
};
|
|
||||||
|
|
||||||
// EventTarget implementation
|
|
||||||
pub const EventTarget = struct {
|
|
||||||
pub const Self = parser.EventTarget;
|
|
||||||
pub const Exception = DOMException;
|
|
||||||
|
|
||||||
// Extend libdom event target for pure zig struct.
|
|
||||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .plain },
|
|
||||||
|
|
||||||
pub fn toInterface(et: *parser.EventTarget, page: *Page) !Union {
|
|
||||||
// libdom assumes that all event targets are libdom nodes. They are not.
|
|
||||||
|
|
||||||
switch (parser.eventTargetInternalType(et)) {
|
|
||||||
.libdom_node => {
|
|
||||||
return .{ .node = try nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))) };
|
|
||||||
},
|
|
||||||
.plain => return .{ .plain = et },
|
|
||||||
.abort_signal => {
|
|
||||||
// AbortSignal is a special case, it has its own internal type.
|
|
||||||
// We return it as a node, but we need to handle it differently.
|
|
||||||
return .{ .node = .{ .AbortSignal = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) } };
|
|
||||||
},
|
|
||||||
.window => {
|
|
||||||
// The window is a common non-node target, but it's easy to handle as its a singleton.
|
|
||||||
std.debug.assert(@intFromPtr(et) == @intFromPtr(&page.window.base));
|
|
||||||
return .{ .node = .{ .Window = &page.window } };
|
|
||||||
},
|
|
||||||
.xhr => {
|
|
||||||
const XMLHttpRequestEventTarget = @import("../xhr/event_target.zig").XMLHttpRequestEventTarget;
|
|
||||||
const base: *XMLHttpRequestEventTarget = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et)));
|
|
||||||
return .{ .xhr = @fieldParentPtr("proto", base) };
|
|
||||||
},
|
|
||||||
.message_port => {
|
|
||||||
return .{ .message_port = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
|
||||||
},
|
|
||||||
.screen => {
|
|
||||||
return .{ .screen = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
|
||||||
},
|
|
||||||
.screen_orientation => {
|
|
||||||
return .{ .screen_orientation = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
|
||||||
},
|
|
||||||
.performance => {
|
|
||||||
return .{ .performance = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
|
||||||
},
|
|
||||||
.media_query_list => {
|
|
||||||
return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// JS funcs
|
|
||||||
// --------
|
|
||||||
pub fn constructor(page: *Page) !*parser.EventTarget {
|
|
||||||
const et = try page.arena.create(EventTarget);
|
|
||||||
return @ptrCast(&et.base);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _addEventListener(
|
|
||||||
self: *parser.EventTarget,
|
|
||||||
typ: []const u8,
|
|
||||||
listener: EventHandler.Listener,
|
|
||||||
opts: ?EventHandler.Opts,
|
|
||||||
page: *Page,
|
|
||||||
) !void {
|
|
||||||
_ = try EventHandler.register(page.arena, self, typ, listener, opts);
|
|
||||||
if (std.mem.eql(u8, typ, "slotchange")) {
|
|
||||||
try page.registerSlotChangeMonitor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const RemoveEventListenerOpts = union(enum) {
|
|
||||||
opts: Opts,
|
|
||||||
capture: bool,
|
|
||||||
|
|
||||||
const Opts = struct {
|
|
||||||
capture: ?bool,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn _removeEventListener(
|
|
||||||
self: *parser.EventTarget,
|
|
||||||
typ: []const u8,
|
|
||||||
listener: EventHandler.Listener,
|
|
||||||
opts_: ?RemoveEventListenerOpts,
|
|
||||||
) !void {
|
|
||||||
var capture = false;
|
|
||||||
if (opts_) |opts| {
|
|
||||||
capture = switch (opts) {
|
|
||||||
.capture => |c| c,
|
|
||||||
.opts => |o| o.capture orelse false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const cbk = (try listener.callback(self)) orelse return;
|
|
||||||
|
|
||||||
// check if event target has already this listener
|
|
||||||
const lst = try parser.eventTargetHasListener(
|
|
||||||
self,
|
|
||||||
typ,
|
|
||||||
capture,
|
|
||||||
cbk.id,
|
|
||||||
);
|
|
||||||
if (lst == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove listener
|
|
||||||
try parser.eventTargetRemoveEventListener(
|
|
||||||
self,
|
|
||||||
typ,
|
|
||||||
lst.?,
|
|
||||||
capture,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event, page: *Page) !bool {
|
|
||||||
const res = try parser.eventTargetDispatchEvent(self, event);
|
|
||||||
|
|
||||||
if (!parser.eventBubbles(event) or parser.eventIsStopped(event)) {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
try page.window.dispatchForDocumentTarget(event);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.EventTarget" {
|
|
||||||
try testing.htmlRunner("dom/event_target.html");
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const allocPrint = std.fmt.allocPrint;
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
// https://webidl.spec.whatwg.org/#idl-DOMException
|
|
||||||
pub const DOMException = struct {
|
|
||||||
err: ?parser.DOMError,
|
|
||||||
str: []const u8,
|
|
||||||
|
|
||||||
pub const ErrorSet = parser.DOMError;
|
|
||||||
|
|
||||||
// static attributes
|
|
||||||
pub const _INDEX_SIZE_ERR = 1;
|
|
||||||
pub const _DOMSTRING_SIZE_ERR = 2;
|
|
||||||
pub const _HIERARCHY_REQUEST_ERR = 3;
|
|
||||||
pub const _WRONG_DOCUMENT_ERR = 4;
|
|
||||||
pub const _INVALID_CHARACTER_ERR = 5;
|
|
||||||
pub const _NO_DATA_ALLOWED_ERR = 6;
|
|
||||||
pub const _NO_MODIFICATION_ALLOWED_ERR = 7;
|
|
||||||
pub const _NOT_FOUND_ERR = 8;
|
|
||||||
pub const _NOT_SUPPORTED_ERR = 9;
|
|
||||||
pub const _INUSE_ATTRIBUTE_ERR = 10;
|
|
||||||
pub const _INVALID_STATE_ERR = 11;
|
|
||||||
pub const _SYNTAX_ERR = 12;
|
|
||||||
pub const _INVALID_MODIFICATION_ERR = 13;
|
|
||||||
pub const _NAMESPACE_ERR = 14;
|
|
||||||
pub const _INVALID_ACCESS_ERR = 15;
|
|
||||||
pub const _VALIDATION_ERR = 16;
|
|
||||||
pub const _TYPE_MISMATCH_ERR = 17;
|
|
||||||
pub const _SECURITY_ERR = 18;
|
|
||||||
pub const _NETWORK_ERR = 19;
|
|
||||||
pub const _ABORT_ERR = 20;
|
|
||||||
pub const _URL_MISMATCH_ERR = 21;
|
|
||||||
pub const _QUOTA_EXCEEDED_ERR = 22;
|
|
||||||
pub const _TIMEOUT_ERR = 23;
|
|
||||||
pub const _INVALID_NODE_TYPE_ERR = 24;
|
|
||||||
pub const _DATA_CLONE_ERR = 25;
|
|
||||||
|
|
||||||
pub fn constructor(message_: ?[]const u8, name_: ?[]const u8, page: *const Page) !DOMException {
|
|
||||||
const message = message_ orelse "";
|
|
||||||
const err = if (name_) |n| error_from_str(n) else null;
|
|
||||||
const fixed_name = name(err);
|
|
||||||
|
|
||||||
if (message.len == 0) return .{ .err = err, .str = fixed_name };
|
|
||||||
|
|
||||||
const str = try allocPrint(page.arena, "{s}: {s}", .{ fixed_name, message });
|
|
||||||
return .{ .err = err, .str = str };
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: deinit
|
|
||||||
pub fn init(alloc: std.mem.Allocator, err: anyerror, caller_name: []const u8) !DOMException {
|
|
||||||
const dom_error = @as(parser.DOMError, @errorCast(err));
|
|
||||||
const error_name = DOMException.name(dom_error);
|
|
||||||
const str = switch (dom_error) {
|
|
||||||
error.HierarchyRequest => try allocPrint(
|
|
||||||
alloc,
|
|
||||||
"{s}: Failed to execute '{s}' on 'Node': The new child element contains the parent.",
|
|
||||||
.{ error_name, caller_name },
|
|
||||||
),
|
|
||||||
// todo add more custom error messages
|
|
||||||
else => try allocPrint(
|
|
||||||
alloc,
|
|
||||||
"{s}: Failed to execute '{s}' : {s}",
|
|
||||||
.{ error_name, caller_name, error_name },
|
|
||||||
),
|
|
||||||
error.NoError => unreachable,
|
|
||||||
};
|
|
||||||
return .{ .err = dom_error, .str = str };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn error_from_str(name_: []const u8) ?parser.DOMError {
|
|
||||||
// @speed: Consider length first, left as is for maintainability, awaiting switch on string support
|
|
||||||
if (std.mem.eql(u8, name_, "IndexSizeError")) return error.IndexSize;
|
|
||||||
if (std.mem.eql(u8, name_, "StringSizeError")) return error.StringSize;
|
|
||||||
if (std.mem.eql(u8, name_, "HierarchyRequestError")) return error.HierarchyRequest;
|
|
||||||
if (std.mem.eql(u8, name_, "WrongDocumentError")) return error.WrongDocument;
|
|
||||||
if (std.mem.eql(u8, name_, "InvalidCharacterError")) return error.InvalidCharacter;
|
|
||||||
if (std.mem.eql(u8, name_, "NoDataAllowedError")) return error.NoDataAllowed;
|
|
||||||
if (std.mem.eql(u8, name_, "NoModificationAllowedError")) return error.NoModificationAllowed;
|
|
||||||
if (std.mem.eql(u8, name_, "NotFoundError")) return error.NotFound;
|
|
||||||
if (std.mem.eql(u8, name_, "NotSupportedError")) return error.NotSupported;
|
|
||||||
if (std.mem.eql(u8, name_, "InuseAttributeError")) return error.InuseAttribute;
|
|
||||||
if (std.mem.eql(u8, name_, "InvalidStateError")) return error.InvalidState;
|
|
||||||
if (std.mem.eql(u8, name_, "SyntaxError")) return error.Syntax;
|
|
||||||
if (std.mem.eql(u8, name_, "InvalidModificationError")) return error.InvalidModification;
|
|
||||||
if (std.mem.eql(u8, name_, "NamespaceError")) return error.Namespace;
|
|
||||||
if (std.mem.eql(u8, name_, "InvalidAccessError")) return error.InvalidAccess;
|
|
||||||
if (std.mem.eql(u8, name_, "ValidationError")) return error.Validation;
|
|
||||||
if (std.mem.eql(u8, name_, "TypeMismatchError")) return error.TypeMismatch;
|
|
||||||
if (std.mem.eql(u8, name_, "SecurityError")) return error.Security;
|
|
||||||
if (std.mem.eql(u8, name_, "NetworkError")) return error.Network;
|
|
||||||
if (std.mem.eql(u8, name_, "AbortError")) return error.Abort;
|
|
||||||
if (std.mem.eql(u8, name_, "URLismatchError")) return error.URLismatch;
|
|
||||||
if (std.mem.eql(u8, name_, "QuotaExceededError")) return error.QuotaExceeded;
|
|
||||||
if (std.mem.eql(u8, name_, "TimeoutError")) return error.Timeout;
|
|
||||||
if (std.mem.eql(u8, name_, "InvalidNodeTypeError")) return error.InvalidNodeType;
|
|
||||||
if (std.mem.eql(u8, name_, "DataCloneError")) return error.DataClone;
|
|
||||||
|
|
||||||
// custom netsurf error
|
|
||||||
if (std.mem.eql(u8, name_, "UnspecifiedEventTypeError")) return error.UnspecifiedEventType;
|
|
||||||
if (std.mem.eql(u8, name_, "DispatchRequestError")) return error.DispatchRequest;
|
|
||||||
if (std.mem.eql(u8, name_, "NoMemoryError")) return error.NoMemory;
|
|
||||||
if (std.mem.eql(u8, name_, "AttributeWrongTypeError")) return error.AttributeWrongType;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(err_: ?parser.DOMError) []const u8 {
|
|
||||||
const err = err_ orelse return "Error";
|
|
||||||
|
|
||||||
return switch (err) {
|
|
||||||
error.IndexSize => "IndexSizeError",
|
|
||||||
error.StringSize => "StringSizeError", // Legacy: DOMSTRING_SIZE_ERR
|
|
||||||
error.HierarchyRequest => "HierarchyRequestError",
|
|
||||||
error.WrongDocument => "WrongDocumentError",
|
|
||||||
error.InvalidCharacter => "InvalidCharacterError",
|
|
||||||
error.NoDataAllowed => "NoDataAllowedError", // Legacy: NO_DATA_ALLOWED_ERR
|
|
||||||
error.NoModificationAllowed => "NoModificationAllowedError",
|
|
||||||
error.NotFound => "NotFoundError",
|
|
||||||
error.NotSupported => "NotSupportedError",
|
|
||||||
error.InuseAttribute => "InuseAttributeError",
|
|
||||||
error.InvalidState => "InvalidStateError",
|
|
||||||
error.Syntax => "SyntaxError",
|
|
||||||
error.InvalidModification => "InvalidModificationError",
|
|
||||||
error.Namespace => "NamespaceError",
|
|
||||||
error.InvalidAccess => "InvalidAccessError",
|
|
||||||
error.Validation => "ValidationError", // Legacy: VALIDATION_ERR
|
|
||||||
error.TypeMismatch => "TypeMismatchError",
|
|
||||||
error.Security => "SecurityError",
|
|
||||||
error.Network => "NetworkError",
|
|
||||||
error.Abort => "AbortError",
|
|
||||||
error.URLismatch => "URLismatchError",
|
|
||||||
error.QuotaExceeded => "QuotaExceededError",
|
|
||||||
error.Timeout => "TimeoutError",
|
|
||||||
error.InvalidNodeType => "InvalidNodeTypeError",
|
|
||||||
error.DataClone => "DataCloneError",
|
|
||||||
error.NoError => unreachable,
|
|
||||||
|
|
||||||
// custom netsurf error
|
|
||||||
error.UnspecifiedEventType => "UnspecifiedEventTypeError",
|
|
||||||
error.DispatchRequest => "DispatchRequestError",
|
|
||||||
error.NoMemory => "NoMemoryError",
|
|
||||||
error.AttributeWrongType => "AttributeWrongTypeError",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// JS properties and methods
|
|
||||||
|
|
||||||
pub fn get_code(self: *const DOMException) u8 {
|
|
||||||
const err = self.err orelse return 0;
|
|
||||||
return switch (err) {
|
|
||||||
error.IndexSize => 1,
|
|
||||||
error.StringSize => 2,
|
|
||||||
error.HierarchyRequest => 3,
|
|
||||||
error.WrongDocument => 4,
|
|
||||||
error.InvalidCharacter => 5,
|
|
||||||
error.NoDataAllowed => 6,
|
|
||||||
error.NoModificationAllowed => 7,
|
|
||||||
error.NotFound => 8,
|
|
||||||
error.NotSupported => 9,
|
|
||||||
error.InuseAttribute => 10,
|
|
||||||
error.InvalidState => 11,
|
|
||||||
error.Syntax => 12,
|
|
||||||
error.InvalidModification => 13,
|
|
||||||
error.Namespace => 14,
|
|
||||||
error.InvalidAccess => 15,
|
|
||||||
error.Validation => 16,
|
|
||||||
error.TypeMismatch => 17,
|
|
||||||
error.Security => 18,
|
|
||||||
error.Network => 19,
|
|
||||||
error.Abort => 20,
|
|
||||||
error.URLismatch => 21,
|
|
||||||
error.QuotaExceeded => 22,
|
|
||||||
error.Timeout => 23,
|
|
||||||
error.InvalidNodeType => 24,
|
|
||||||
error.DataClone => 25,
|
|
||||||
error.NoError => unreachable,
|
|
||||||
|
|
||||||
// custom netsurf error
|
|
||||||
error.UnspecifiedEventType => 128,
|
|
||||||
error.DispatchRequest => 129,
|
|
||||||
error.NoMemory => 130,
|
|
||||||
error.AttributeWrongType => 131,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_name(self: *const DOMException) []const u8 {
|
|
||||||
return DOMException.name(self.err);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_message(self: *const DOMException) []const u8 {
|
|
||||||
const errName = DOMException.name(self.err);
|
|
||||||
if (self.str.len <= errName.len + 2) return "";
|
|
||||||
return self.str[errName.len + 2 ..]; // ! Requires str is formatted as "{name}: {message}"
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _toString(self: *const DOMException) []const u8 {
|
|
||||||
return self.str;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.Exceptions" {
|
|
||||||
try testing.htmlRunner("dom/exceptions.html");
|
|
||||||
}
|
|
||||||
@@ -1,454 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const Element = @import("element.zig").Element;
|
|
||||||
const Union = @import("element.zig").Union;
|
|
||||||
const Walker = @import("walker.zig").Walker;
|
|
||||||
|
|
||||||
const Matcher = union(enum) {
|
|
||||||
matchByName: MatchByName,
|
|
||||||
matchByTagName: MatchByTagName,
|
|
||||||
matchByClassName: MatchByClassName,
|
|
||||||
matchByLinks: MatchByLinks,
|
|
||||||
matchByAnchors: MatchByAnchors,
|
|
||||||
matchTrue: struct {},
|
|
||||||
matchFalse: struct {},
|
|
||||||
|
|
||||||
pub fn match(self: Matcher, node: *parser.Node) !bool {
|
|
||||||
switch (self) {
|
|
||||||
.matchTrue => return true,
|
|
||||||
.matchFalse => return false,
|
|
||||||
.matchByLinks => return MatchByLinks.match(node),
|
|
||||||
.matchByAnchors => return MatchByAnchors.match(node),
|
|
||||||
inline else => |m| return m.match(node),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const MatchByTagName = struct {
|
|
||||||
// tag is used to select node against their name.
|
|
||||||
// tag comparison is case insensitive.
|
|
||||||
tag: []const u8,
|
|
||||||
is_wildcard: bool,
|
|
||||||
|
|
||||||
fn init(tag_name: []const u8) MatchByTagName {
|
|
||||||
if (std.mem.eql(u8, tag_name, "*")) {
|
|
||||||
return .{ .tag = "*", .is_wildcard = true };
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.tag = tag_name,
|
|
||||||
.is_wildcard = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match(self: MatchByTagName, node: *parser.Node) !bool {
|
|
||||||
return self.is_wildcard or std.ascii.eqlIgnoreCase(self.tag, try parser.nodeName(node));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn HTMLCollectionByTagName(
|
|
||||||
root: ?*parser.Node,
|
|
||||||
tag_name: []const u8,
|
|
||||||
opts: Opts,
|
|
||||||
) HTMLCollection {
|
|
||||||
return .{
|
|
||||||
.root = root,
|
|
||||||
.walker = .{ .walkerDepthFirst = .{} },
|
|
||||||
.matcher = .{ .matchByTagName = MatchByTagName.init(tag_name) },
|
|
||||||
.mutable = opts.mutable,
|
|
||||||
.include_root = opts.include_root,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const MatchByClassName = struct {
|
|
||||||
class_names: []const u8,
|
|
||||||
|
|
||||||
fn init(class_names: []const u8) !MatchByClassName {
|
|
||||||
return .{
|
|
||||||
.class_names = class_names,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match(self: MatchByClassName, node: *parser.Node) !bool {
|
|
||||||
const e = parser.nodeToElement(node);
|
|
||||||
|
|
||||||
var it = std.mem.splitScalar(u8, self.class_names, ' ');
|
|
||||||
while (it.next()) |c| {
|
|
||||||
if (!try parser.elementHasClass(e, c)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn HTMLCollectionByClassName(
|
|
||||||
root: ?*parser.Node,
|
|
||||||
class_names: []const u8,
|
|
||||||
opts: Opts,
|
|
||||||
) !HTMLCollection {
|
|
||||||
return HTMLCollection{
|
|
||||||
.root = root,
|
|
||||||
.walker = .{ .walkerDepthFirst = .{} },
|
|
||||||
.matcher = .{ .matchByClassName = try MatchByClassName.init(class_names) },
|
|
||||||
.mutable = opts.mutable,
|
|
||||||
.include_root = opts.include_root,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const MatchByName = struct {
|
|
||||||
name: []const u8,
|
|
||||||
|
|
||||||
fn init(name: []const u8) !MatchByName {
|
|
||||||
return .{ .name = name };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match(self: MatchByName, node: *parser.Node) !bool {
|
|
||||||
const e = parser.nodeToElement(node);
|
|
||||||
const nname = try parser.elementGetAttribute(e, "name") orelse return false;
|
|
||||||
return std.mem.eql(u8, self.name, nname);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn HTMLCollectionByName(
|
|
||||||
root: ?*parser.Node,
|
|
||||||
name: []const u8,
|
|
||||||
opts: Opts,
|
|
||||||
) !HTMLCollection {
|
|
||||||
return HTMLCollection{
|
|
||||||
.root = root,
|
|
||||||
.walker = .{ .walkerDepthFirst = .{} },
|
|
||||||
.matcher = .{ .matchByName = try MatchByName.init(name) },
|
|
||||||
.mutable = opts.mutable,
|
|
||||||
.include_root = opts.include_root,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTMLAllCollection is a special type: instances of it are falsy. It's the only
|
|
||||||
// object in the WebAPI that behaves like this - in fact, it's even a special
|
|
||||||
// case in the JavaScript spec.
|
|
||||||
// This is important, because a lot of browser detection rely on this behavior
|
|
||||||
// to determine what browser is running.
|
|
||||||
|
|
||||||
// It's also possible to use an instance like a function:
|
|
||||||
// document.all(3)
|
|
||||||
// document.all('some_id')
|
|
||||||
pub const HTMLAllCollection = struct {
|
|
||||||
pub const prototype = *HTMLCollection;
|
|
||||||
|
|
||||||
proto: HTMLCollection,
|
|
||||||
|
|
||||||
pub const mark_as_undetectable = true;
|
|
||||||
|
|
||||||
pub fn init(root: ?*parser.Node) HTMLAllCollection {
|
|
||||||
return .{ .proto = .{
|
|
||||||
.root = root,
|
|
||||||
.walker = .{ .walkerDepthFirst = .{} },
|
|
||||||
.matcher = .{ .matchTrue = .{} },
|
|
||||||
.include_root = true,
|
|
||||||
} };
|
|
||||||
}
|
|
||||||
|
|
||||||
const CAllAsFunctionArg = union(enum) {
|
|
||||||
index: u32,
|
|
||||||
id: []const u8,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn jsCallAsFunction(self: *HTMLAllCollection, arg: CAllAsFunctionArg) !?Union {
|
|
||||||
return switch (arg) {
|
|
||||||
.index => |i| self.proto._item(i),
|
|
||||||
.id => |id| self.proto._namedItem(id),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn HTMLCollectionChildren(
|
|
||||||
root: ?*parser.Node,
|
|
||||||
opts: Opts,
|
|
||||||
) HTMLCollection {
|
|
||||||
return HTMLCollection{
|
|
||||||
.root = root,
|
|
||||||
.walker = .{ .walkerChildren = .{} },
|
|
||||||
.matcher = .{ .matchTrue = .{} },
|
|
||||||
.mutable = opts.mutable,
|
|
||||||
.include_root = opts.include_root,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn HTMLCollectionEmpty() HTMLCollection {
|
|
||||||
return .{
|
|
||||||
.root = null,
|
|
||||||
.walker = .{ .walkerNone = .{} },
|
|
||||||
.matcher = .{ .matchFalse = .{} },
|
|
||||||
.include_root = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// MatchByLinks matches the a and area elements in the Document that have href
|
|
||||||
// attributes.
|
|
||||||
// https://html.spec.whatwg.org/#dom-document-links
|
|
||||||
pub const MatchByLinks = struct {
|
|
||||||
pub fn match(node: *parser.Node) !bool {
|
|
||||||
const tag = try parser.nodeName(node);
|
|
||||||
if (!std.ascii.eqlIgnoreCase(tag, "a") and !std.ascii.eqlIgnoreCase(tag, "area")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const elem = @as(*parser.Element, @ptrCast(node));
|
|
||||||
return parser.elementHasAttribute(elem, "href");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn HTMLCollectionByLinks(root: ?*parser.Node, opts: Opts) HTMLCollection {
|
|
||||||
return .{
|
|
||||||
.root = root,
|
|
||||||
.walker = .{ .walkerDepthFirst = .{} },
|
|
||||||
.matcher = .{ .matchByLinks = .{} },
|
|
||||||
.mutable = opts.mutable,
|
|
||||||
.include_root = opts.include_root,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// MatchByAnchors matches the a elements in the Document that have name
|
|
||||||
// attributes.
|
|
||||||
// https://html.spec.whatwg.org/#dom-document-anchors
|
|
||||||
pub const MatchByAnchors = struct {
|
|
||||||
pub fn match(node: *parser.Node) !bool {
|
|
||||||
const tag = try parser.nodeName(node);
|
|
||||||
if (!std.ascii.eqlIgnoreCase(tag, "a")) return false;
|
|
||||||
|
|
||||||
const elem = @as(*parser.Element, @ptrCast(node));
|
|
||||||
return parser.elementHasAttribute(elem, "name");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn HTMLCollectionByAnchors(root: ?*parser.Node, opts: Opts) HTMLCollection {
|
|
||||||
return .{
|
|
||||||
.root = root,
|
|
||||||
.walker = .{ .walkerDepthFirst = .{} },
|
|
||||||
.matcher = .{ .matchByAnchors = .{} },
|
|
||||||
.mutable = opts.mutable,
|
|
||||||
.include_root = opts.include_root,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const HTMLCollectionIterator = struct {
|
|
||||||
coll: *HTMLCollection,
|
|
||||||
index: u32 = 0,
|
|
||||||
|
|
||||||
pub const Return = struct {
|
|
||||||
value: ?Union,
|
|
||||||
done: bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn _next(self: *HTMLCollectionIterator) !Return {
|
|
||||||
const e = try self.coll._item(self.index);
|
|
||||||
if (e == null) {
|
|
||||||
return Return{
|
|
||||||
.value = null,
|
|
||||||
.done = true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
self.index += 1;
|
|
||||||
return Return{
|
|
||||||
.value = e,
|
|
||||||
.done = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Opts = struct {
|
|
||||||
include_root: bool,
|
|
||||||
mutable: bool = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#htmlcollection
|
|
||||||
// HTMLCollection is re implemented in zig here because libdom
|
|
||||||
// dom_html_collection expects a comparison function callback as arguement.
|
|
||||||
// But we wanted a dynamically comparison here, according to the match tagname.
|
|
||||||
pub const HTMLCollection = struct {
|
|
||||||
matcher: Matcher,
|
|
||||||
walker: Walker,
|
|
||||||
|
|
||||||
root: ?*parser.Node,
|
|
||||||
|
|
||||||
// By default the HTMLCollection walk on the root's descendant only.
|
|
||||||
// But on somes cases, like for dom document, we want to walk over the root
|
|
||||||
// itself.
|
|
||||||
include_root: bool = false,
|
|
||||||
|
|
||||||
mutable: bool = false,
|
|
||||||
|
|
||||||
// save a state for the collection to improve the _item speed.
|
|
||||||
cur_idx: ?u32 = null,
|
|
||||||
cur_node: ?*parser.Node = null,
|
|
||||||
|
|
||||||
// start returns the first node to walk on.
|
|
||||||
fn start(self: *const HTMLCollection) !?*parser.Node {
|
|
||||||
if (self.root == null) return null;
|
|
||||||
|
|
||||||
if (self.include_root) {
|
|
||||||
return self.root.?;
|
|
||||||
}
|
|
||||||
|
|
||||||
return try self.walker.get_next(self.root.?, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _symbol_iterator(self: *HTMLCollection) HTMLCollectionIterator {
|
|
||||||
return HTMLCollectionIterator{
|
|
||||||
.coll = self,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get_length computes the collection's length dynamically according to
|
|
||||||
/// the current root structure.
|
|
||||||
// TODO: nodes retrieved must be de-referenced.
|
|
||||||
pub fn get_length(self: *HTMLCollection) !u32 {
|
|
||||||
if (self.root == null) return 0;
|
|
||||||
|
|
||||||
var len: u32 = 0;
|
|
||||||
var node = try self.start() orelse return 0;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (parser.nodeType(node) == .element) {
|
|
||||||
if (try self.matcher.match(node)) {
|
|
||||||
len += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node = try self.walker.get_next(self.root.?, node) orelse break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn item(self: *HTMLCollection, index: u32) !?*parser.Node {
|
|
||||||
if (self.root == null) return null;
|
|
||||||
|
|
||||||
var i: u32 = 0;
|
|
||||||
var node: *parser.Node = undefined;
|
|
||||||
|
|
||||||
// Use the current state to improve speed if possible.
|
|
||||||
if (self.mutable == false and self.cur_idx != null and index >= self.cur_idx.?) {
|
|
||||||
i = self.cur_idx.?;
|
|
||||||
node = self.cur_node.?;
|
|
||||||
} else {
|
|
||||||
node = try self.start() orelse return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (parser.nodeType(node) == .element) {
|
|
||||||
if (try self.matcher.match(node)) {
|
|
||||||
// check if we found the searched element.
|
|
||||||
if (i == index) {
|
|
||||||
// save the current state
|
|
||||||
self.cur_node = node;
|
|
||||||
self.cur_idx = i;
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node = try self.walker.get_next(self.root.?, node) orelse break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _item(self: *HTMLCollection, index: u32) !?Union {
|
|
||||||
const node = try self.item(index) orelse return null;
|
|
||||||
const e = @as(*parser.Element, @ptrCast(node));
|
|
||||||
return try Element.toInterface(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _namedItem(self: *const HTMLCollection, name: []const u8) !?Union {
|
|
||||||
if (self.root == null) return null;
|
|
||||||
if (name.len == 0) return null;
|
|
||||||
|
|
||||||
var node = try self.start() orelse return null;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (parser.nodeType(node) == .element) {
|
|
||||||
if (try self.matcher.match(node)) {
|
|
||||||
const elem = @as(*parser.Element, @ptrCast(node));
|
|
||||||
|
|
||||||
var attr = try parser.elementGetAttribute(elem, "id");
|
|
||||||
// check if the node id corresponds to the name argument.
|
|
||||||
if (attr != null and std.mem.eql(u8, name, attr.?)) {
|
|
||||||
return try Element.toInterface(elem);
|
|
||||||
}
|
|
||||||
|
|
||||||
attr = try parser.elementGetAttribute(elem, "name");
|
|
||||||
// check if the node id corresponds to the name argument.
|
|
||||||
if (attr != null and std.mem.eql(u8, name, attr.?)) {
|
|
||||||
return try Element.toInterface(elem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node = try self.walker.get_next(self.root.?, node) orelse break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn item_name(elt: *parser.Element) !?[]const u8 {
|
|
||||||
if (try parser.elementGetAttribute(elt, "id")) |v| {
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
if (try parser.elementGetAttribute(elt, "name")) |v| {
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn indexed_get(self: *HTMLCollection, index: u32, has_value: *bool) !?Union {
|
|
||||||
return (try _item(self, index)) orelse {
|
|
||||||
has_value.* = false;
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn named_get(self: *const HTMLCollection, name: []const u8, has_value: *bool) !?Union {
|
|
||||||
// Even though an entry might have an empty id, the spec says
|
|
||||||
// that namedItem("") should always return null
|
|
||||||
if (name.len == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (try _namedItem(self, name)) orelse {
|
|
||||||
has_value.* = false;
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.HTMLCollection" {
|
|
||||||
try testing.htmlRunner("dom/html_collection.html");
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const DOMException = @import("exceptions.zig").DOMException;
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#domimplementation
|
|
||||||
pub const DOMImplementation = struct {
|
|
||||||
pub const Exception = DOMException;
|
|
||||||
|
|
||||||
pub fn _createDocumentType(
|
|
||||||
_: *DOMImplementation,
|
|
||||||
qname: [:0]const u8,
|
|
||||||
publicId: [:0]const u8,
|
|
||||||
systemId: [:0]const u8,
|
|
||||||
) !*parser.DocumentType {
|
|
||||||
return try parser.domImplementationCreateDocumentType(qname, publicId, systemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createDocument(
|
|
||||||
_: *DOMImplementation,
|
|
||||||
namespace: ?[:0]const u8,
|
|
||||||
qname: ?[:0]const u8,
|
|
||||||
doctype: ?*parser.DocumentType,
|
|
||||||
) !*parser.Document {
|
|
||||||
return try parser.domImplementationCreateDocument(namespace, qname, doctype);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createHTMLDocument(_: *DOMImplementation, title: ?[]const u8) !*parser.DocumentHTML {
|
|
||||||
return try parser.domImplementationCreateHTMLDocument(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _hasFeature(_: *DOMImplementation) bool {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.Implementation" {
|
|
||||||
try testing.htmlRunner("dom/implementation.html");
|
|
||||||
}
|
|
||||||
@@ -1,407 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const NodeList = @import("nodelist.zig").NodeList;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
MutationObserver,
|
|
||||||
MutationRecord,
|
|
||||||
};
|
|
||||||
|
|
||||||
const Walker = @import("../dom/walker.zig").WalkerChildren;
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
|
|
||||||
pub const MutationObserver = struct {
|
|
||||||
page: *Page,
|
|
||||||
cbk: js.Function,
|
|
||||||
scheduled: bool,
|
|
||||||
observers: std.ArrayListUnmanaged(*Observer),
|
|
||||||
|
|
||||||
// List of records which were observed. When the call scope ends, we need to
|
|
||||||
// execute our callback with it.
|
|
||||||
observed: std.ArrayListUnmanaged(MutationRecord),
|
|
||||||
|
|
||||||
pub fn constructor(cbk: js.Function, page: *Page) !MutationObserver {
|
|
||||||
return .{
|
|
||||||
.cbk = cbk,
|
|
||||||
.page = page,
|
|
||||||
.observed = .{},
|
|
||||||
.scheduled = false,
|
|
||||||
.observers = .empty,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?Options) !void {
|
|
||||||
const arena = self.page.arena;
|
|
||||||
var options = options_ orelse Options{};
|
|
||||||
if (options.attributeFilter.len > 0) {
|
|
||||||
options.attributeFilter = try arena.dupe([]const u8, options.attributeFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = try arena.create(Observer);
|
|
||||||
observer.* = .{
|
|
||||||
.node = node,
|
|
||||||
.options = options,
|
|
||||||
.mutation_observer = self,
|
|
||||||
.event_node = .{ .id = self.cbk.id, .func = Observer.handle },
|
|
||||||
};
|
|
||||||
|
|
||||||
try self.observers.append(arena, observer);
|
|
||||||
|
|
||||||
// register node's events
|
|
||||||
if (options.childList or options.subtree) {
|
|
||||||
observer.dom_node_inserted_listener = try parser.eventTargetAddEventListener(
|
|
||||||
parser.toEventTarget(parser.Node, node),
|
|
||||||
"DOMNodeInserted",
|
|
||||||
&observer.event_node,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
observer.dom_node_removed_listener = try parser.eventTargetAddEventListener(
|
|
||||||
parser.toEventTarget(parser.Node, node),
|
|
||||||
"DOMNodeRemoved",
|
|
||||||
&observer.event_node,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (options.attr()) {
|
|
||||||
observer.dom_node_attribute_modified_listener = try parser.eventTargetAddEventListener(
|
|
||||||
parser.toEventTarget(parser.Node, node),
|
|
||||||
"DOMAttrModified",
|
|
||||||
&observer.event_node,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (options.cdata()) {
|
|
||||||
observer.dom_cdata_modified_listener = try parser.eventTargetAddEventListener(
|
|
||||||
parser.toEventTarget(parser.Node, node),
|
|
||||||
"DOMCharacterDataModified",
|
|
||||||
&observer.event_node,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (options.subtree) {
|
|
||||||
observer.dom_subtree_modified_listener = try parser.eventTargetAddEventListener(
|
|
||||||
parser.toEventTarget(parser.Node, node),
|
|
||||||
"DOMSubtreeModified",
|
|
||||||
&observer.event_node,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn callback(ctx: *anyopaque) ?u32 {
|
|
||||||
const self: *MutationObserver = @ptrCast(@alignCast(ctx));
|
|
||||||
self.scheduled = false;
|
|
||||||
|
|
||||||
const records = self.observed.items;
|
|
||||||
if (records.len == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
defer self.observed.clearRetainingCapacity();
|
|
||||||
|
|
||||||
var result: js.Function.Result = undefined;
|
|
||||||
self.cbk.tryCallWithThis(void, self, .{records}, &result) catch {
|
|
||||||
log.debug(.user_script, "callback error", .{
|
|
||||||
.err = result.exception,
|
|
||||||
.stack = result.stack,
|
|
||||||
.source = "mutation observer",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _disconnect(self: *MutationObserver) !void {
|
|
||||||
for (self.observers.items) |observer| {
|
|
||||||
const event_target = parser.toEventTarget(parser.Node, observer.node);
|
|
||||||
if (observer.dom_node_inserted_listener) |listener| {
|
|
||||||
try parser.eventTargetRemoveEventListener(
|
|
||||||
event_target,
|
|
||||||
"DOMNodeInserted",
|
|
||||||
listener,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (observer.dom_node_removed_listener) |listener| {
|
|
||||||
try parser.eventTargetRemoveEventListener(
|
|
||||||
event_target,
|
|
||||||
"DOMNodeRemoved",
|
|
||||||
listener,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (observer.dom_node_attribute_modified_listener) |listener| {
|
|
||||||
try parser.eventTargetRemoveEventListener(
|
|
||||||
event_target,
|
|
||||||
"DOMAttrModified",
|
|
||||||
listener,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (observer.dom_cdata_modified_listener) |listener| {
|
|
||||||
try parser.eventTargetRemoveEventListener(
|
|
||||||
event_target,
|
|
||||||
"DOMCharacterDataModified",
|
|
||||||
listener,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (observer.dom_subtree_modified_listener) |listener| {
|
|
||||||
try parser.eventTargetRemoveEventListener(
|
|
||||||
event_target,
|
|
||||||
"DOMSubtreeModified",
|
|
||||||
listener,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.observers.clearRetainingCapacity();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
pub fn _takeRecords(_: *const MutationObserver) ?[]const u8 {
|
|
||||||
return &[_]u8{};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const MutationRecord = struct {
|
|
||||||
type: []const u8,
|
|
||||||
target: *parser.Node,
|
|
||||||
added_nodes: NodeList = .{},
|
|
||||||
removed_nodes: NodeList = .{},
|
|
||||||
previous_sibling: ?*parser.Node = null,
|
|
||||||
next_sibling: ?*parser.Node = null,
|
|
||||||
attribute_name: ?[]const u8 = null,
|
|
||||||
attribute_namespace: ?[]const u8 = null,
|
|
||||||
old_value: ?[]const u8 = null,
|
|
||||||
|
|
||||||
pub fn get_type(self: *const MutationRecord) []const u8 {
|
|
||||||
return self.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_addedNodes(self: *MutationRecord) *NodeList {
|
|
||||||
return &self.added_nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_removedNodes(self: *MutationRecord) *NodeList {
|
|
||||||
return &self.removed_nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_target(self: *const MutationRecord) *parser.Node {
|
|
||||||
return self.target;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_attributeName(self: *const MutationRecord) ?[]const u8 {
|
|
||||||
return self.attribute_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_attributeNamespace(self: *const MutationRecord) ?[]const u8 {
|
|
||||||
return self.attribute_namespace;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_previousSibling(self: *const MutationRecord) ?*parser.Node {
|
|
||||||
return self.previous_sibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_nextSibling(self: *const MutationRecord) ?*parser.Node {
|
|
||||||
return self.next_sibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_oldValue(self: *const MutationRecord) ?[]const u8 {
|
|
||||||
return self.old_value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Options = struct {
|
|
||||||
childList: bool = false,
|
|
||||||
attributes: bool = false,
|
|
||||||
characterData: bool = false,
|
|
||||||
subtree: bool = false,
|
|
||||||
attributeOldValue: bool = false,
|
|
||||||
characterDataOldValue: bool = false,
|
|
||||||
attributeFilter: [][]const u8 = &.{},
|
|
||||||
|
|
||||||
fn attr(self: Options) bool {
|
|
||||||
return self.attributes or self.attributeOldValue or self.attributeFilter.len > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cdata(self: Options) bool {
|
|
||||||
return self.characterData or self.characterDataOldValue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Observer = struct {
|
|
||||||
node: *parser.Node,
|
|
||||||
options: Options,
|
|
||||||
|
|
||||||
// reference back to the MutationObserver so that we can access the arena
|
|
||||||
// and batch the mutation records.
|
|
||||||
mutation_observer: *MutationObserver,
|
|
||||||
|
|
||||||
event_node: parser.EventNode,
|
|
||||||
|
|
||||||
dom_node_inserted_listener: ?*parser.EventListener = null,
|
|
||||||
dom_node_removed_listener: ?*parser.EventListener = null,
|
|
||||||
dom_node_attribute_modified_listener: ?*parser.EventListener = null,
|
|
||||||
dom_cdata_modified_listener: ?*parser.EventListener = null,
|
|
||||||
dom_subtree_modified_listener: ?*parser.EventListener = null,
|
|
||||||
|
|
||||||
fn appliesTo(
|
|
||||||
self: *const Observer,
|
|
||||||
target: *parser.Node,
|
|
||||||
event_type: MutationEventType,
|
|
||||||
event: *parser.MutationEvent,
|
|
||||||
) !bool {
|
|
||||||
if (event_type == .DOMAttrModified and self.options.attributeFilter.len > 0) {
|
|
||||||
const attribute_name = try parser.mutationEventAttributeName(event);
|
|
||||||
for (self.options.attributeFilter) |needle| blk: {
|
|
||||||
if (std.mem.eql(u8, attribute_name, needle)) {
|
|
||||||
break :blk;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// mutation on any target is always ok.
|
|
||||||
if (self.options.subtree) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if target equals node, alway ok.
|
|
||||||
if (target == self.node) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// no subtree, no same target and no childlist, always noky.
|
|
||||||
if (!self.options.childList) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// target must be a child of o.node
|
|
||||||
const walker = Walker{};
|
|
||||||
var next: ?*parser.Node = null;
|
|
||||||
while (true) {
|
|
||||||
next = walker.get_next(self.node, next) catch break orelse break;
|
|
||||||
if (next.? == target) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle(en: *parser.EventNode, event: *parser.Event) void {
|
|
||||||
const self: *Observer = @fieldParentPtr("event_node", en);
|
|
||||||
self._handle(event) catch |err| {
|
|
||||||
log.err(.web_api, "handle error", .{ .err = err, .source = "mutation observer" });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _handle(self: *Observer, event: *parser.Event) !void {
|
|
||||||
var mutation_observer = self.mutation_observer;
|
|
||||||
|
|
||||||
const node = blk: {
|
|
||||||
const event_target = parser.eventTarget(event) orelse return;
|
|
||||||
break :blk parser.eventTargetToNode(event_target);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mutation_event = parser.eventToMutationEvent(event);
|
|
||||||
const event_type = blk: {
|
|
||||||
const t = parser.eventType(event);
|
|
||||||
break :blk std.meta.stringToEnum(MutationEventType, t) orelse return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (try self.appliesTo(node, event_type, mutation_event) == false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var record = MutationRecord{
|
|
||||||
.target = self.node,
|
|
||||||
.type = event_type.recordType(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const arena = mutation_observer.page.arena;
|
|
||||||
switch (event_type) {
|
|
||||||
.DOMAttrModified => {
|
|
||||||
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
|
|
||||||
if (self.options.attributeOldValue) {
|
|
||||||
record.old_value = parser.mutationEventPrevValue(mutation_event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.DOMCharacterDataModified => {
|
|
||||||
if (self.options.characterDataOldValue) {
|
|
||||||
record.old_value = parser.mutationEventPrevValue(mutation_event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.DOMNodeInserted => {
|
|
||||||
if (parser.mutationEventRelatedNode(mutation_event) catch null) |related_node| {
|
|
||||||
try record.added_nodes.append(arena, related_node);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.DOMNodeRemoved => {
|
|
||||||
if (parser.mutationEventRelatedNode(mutation_event) catch null) |related_node| {
|
|
||||||
try record.removed_nodes.append(arena, related_node);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
try mutation_observer.observed.append(arena, record);
|
|
||||||
|
|
||||||
if (mutation_observer.scheduled == false) {
|
|
||||||
mutation_observer.scheduled = true;
|
|
||||||
try mutation_observer.page.scheduler.add(
|
|
||||||
mutation_observer,
|
|
||||||
MutationObserver.callback,
|
|
||||||
0,
|
|
||||||
.{ .name = "mutation_observer" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const MutationEventType = enum {
|
|
||||||
DOMAttrModified,
|
|
||||||
DOMCharacterDataModified,
|
|
||||||
DOMNodeInserted,
|
|
||||||
DOMNodeRemoved,
|
|
||||||
|
|
||||||
fn recordType(self: MutationEventType) []const u8 {
|
|
||||||
return switch (self) {
|
|
||||||
.DOMAttrModified => "attributes",
|
|
||||||
.DOMCharacterDataModified => "characterData",
|
|
||||||
.DOMNodeInserted => "childList",
|
|
||||||
.DOMNodeRemoved => "childList",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.MutationObserver" {
|
|
||||||
try testing.htmlRunner("dom/mutation_observer.html");
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const DOMException = @import("exceptions.zig").DOMException;
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#namednodemap
|
|
||||||
pub const NamedNodeMap = struct {
|
|
||||||
pub const Self = parser.NamedNodeMap;
|
|
||||||
|
|
||||||
pub const Exception = DOMException;
|
|
||||||
pub const Iterator = NamedNodeMapIterator;
|
|
||||||
|
|
||||||
// TODO implement LegacyUnenumerableNamedProperties.
|
|
||||||
// https://webidl.spec.whatwg.org/#LegacyUnenumerableNamedProperties
|
|
||||||
|
|
||||||
pub fn get_length(self: *parser.NamedNodeMap) !u32 {
|
|
||||||
return try parser.namedNodeMapGetLength(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _item(self: *parser.NamedNodeMap, index: u32) !?*parser.Attribute {
|
|
||||||
return try parser.namedNodeMapItem(self, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getNamedItem(self: *parser.NamedNodeMap, qname: []const u8) !?*parser.Attribute {
|
|
||||||
return try parser.namedNodeMapGetNamedItem(self, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getNamedItemNS(
|
|
||||||
self: *parser.NamedNodeMap,
|
|
||||||
namespace: []const u8,
|
|
||||||
localname: []const u8,
|
|
||||||
) !?*parser.Attribute {
|
|
||||||
return try parser.namedNodeMapGetNamedItemNS(self, namespace, localname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setNamedItem(self: *parser.NamedNodeMap, attr: *parser.Attribute) !?*parser.Attribute {
|
|
||||||
return try parser.namedNodeMapSetNamedItem(self, attr);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setNamedItemNS(self: *parser.NamedNodeMap, attr: *parser.Attribute) !?*parser.Attribute {
|
|
||||||
return try parser.namedNodeMapSetNamedItemNS(self, attr);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _removeNamedItem(self: *parser.NamedNodeMap, qname: []const u8) !*parser.Attribute {
|
|
||||||
return try parser.namedNodeMapRemoveNamedItem(self, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _removeNamedItemNS(
|
|
||||||
self: *parser.NamedNodeMap,
|
|
||||||
namespace: []const u8,
|
|
||||||
localname: []const u8,
|
|
||||||
) !*parser.Attribute {
|
|
||||||
return try parser.namedNodeMapRemoveNamedItemNS(self, namespace, localname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn indexed_get(self: *parser.NamedNodeMap, index: u32, has_value: *bool) !*parser.Attribute {
|
|
||||||
return (try _item(self, index)) orelse {
|
|
||||||
has_value.* = false;
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn named_get(self: *parser.NamedNodeMap, name: []const u8, has_value: *bool) !*parser.Attribute {
|
|
||||||
return (try _getNamedItem(self, name)) orelse {
|
|
||||||
has_value.* = false;
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _symbol_iterator(self: *parser.NamedNodeMap) NamedNodeMapIterator {
|
|
||||||
return .{ .map = self };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const NamedNodeMapIterator = struct {
|
|
||||||
index: u32 = 0,
|
|
||||||
map: *parser.NamedNodeMap,
|
|
||||||
|
|
||||||
pub const Return = struct {
|
|
||||||
done: bool,
|
|
||||||
value: ?*parser.Attribute,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn _next(self: *NamedNodeMapIterator) !Return {
|
|
||||||
const e = try NamedNodeMap._item(self.map, self.index);
|
|
||||||
if (e == null) {
|
|
||||||
return .{
|
|
||||||
.value = null,
|
|
||||||
.done = true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
self.index += 1;
|
|
||||||
return .{
|
|
||||||
.value = e,
|
|
||||||
.done = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.NamedNodeMap" {
|
|
||||||
try testing.htmlRunner("dom/named_node_map.html");
|
|
||||||
}
|
|
||||||
@@ -1,637 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const generate = @import("../js/generate.zig");
|
|
||||||
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const EventTarget = @import("event_target.zig").EventTarget;
|
|
||||||
|
|
||||||
// DOM
|
|
||||||
const Attr = @import("attribute.zig").Attr;
|
|
||||||
const CData = @import("character_data.zig");
|
|
||||||
const Element = @import("element.zig").Element;
|
|
||||||
const ElementUnion = @import("element.zig").Union;
|
|
||||||
const NodeList = @import("nodelist.zig").NodeList;
|
|
||||||
const Document = @import("document.zig").Document;
|
|
||||||
const DocumentType = @import("document_type.zig").DocumentType;
|
|
||||||
const DocumentFragment = @import("document_fragment.zig").DocumentFragment;
|
|
||||||
const HTMLCollection = @import("html_collection.zig").HTMLCollection;
|
|
||||||
const HTMLAllCollection = @import("html_collection.zig").HTMLAllCollection;
|
|
||||||
const HTMLCollectionIterator = @import("html_collection.zig").HTMLCollectionIterator;
|
|
||||||
const ShadowRoot = @import("shadow_root.zig").ShadowRoot;
|
|
||||||
const Walker = @import("walker.zig").WalkerDepthFirst;
|
|
||||||
|
|
||||||
// HTML
|
|
||||||
const HTML = @import("../html/html.zig");
|
|
||||||
|
|
||||||
// Node interfaces
|
|
||||||
pub const Interfaces = .{
|
|
||||||
Attr,
|
|
||||||
CData.CharacterData,
|
|
||||||
CData.Interfaces,
|
|
||||||
Element,
|
|
||||||
Document,
|
|
||||||
DocumentType,
|
|
||||||
DocumentFragment,
|
|
||||||
HTMLCollection,
|
|
||||||
HTMLAllCollection,
|
|
||||||
HTMLCollectionIterator,
|
|
||||||
HTML.Interfaces,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Union = generate.Union(Interfaces);
|
|
||||||
|
|
||||||
// Node implementation
|
|
||||||
pub const Node = struct {
|
|
||||||
pub const Self = parser.Node;
|
|
||||||
pub const prototype = *EventTarget;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub fn toInterface(node: *parser.Node) !Union {
|
|
||||||
return switch (parser.nodeType(node)) {
|
|
||||||
.element => try Element.toInterfaceT(
|
|
||||||
Union,
|
|
||||||
@as(*parser.Element, @ptrCast(node)),
|
|
||||||
),
|
|
||||||
.comment => .{ .Comment = @as(*parser.Comment, @ptrCast(node)) },
|
|
||||||
.text => .{ .Text = @as(*parser.Text, @ptrCast(node)) },
|
|
||||||
.cdata_section => .{ .CDATASection = @as(*parser.CDATASection, @ptrCast(node)) },
|
|
||||||
.processing_instruction => .{ .ProcessingInstruction = @as(*parser.ProcessingInstruction, @ptrCast(node)) },
|
|
||||||
.document => blk: {
|
|
||||||
const doc: *parser.Document = @ptrCast(node);
|
|
||||||
if (doc.is_html) {
|
|
||||||
break :blk .{ .HTMLDocument = @as(*parser.DocumentHTML, @ptrCast(node)) };
|
|
||||||
}
|
|
||||||
|
|
||||||
break :blk .{ .Document = doc };
|
|
||||||
},
|
|
||||||
.document_type => .{ .DocumentType = @as(*parser.DocumentType, @ptrCast(node)) },
|
|
||||||
.attribute => .{ .Attr = @as(*parser.Attribute, @ptrCast(node)) },
|
|
||||||
.document_fragment => .{ .DocumentFragment = @as(*parser.DocumentFragment, @ptrCast(node)) },
|
|
||||||
else => @panic("node type not handled"), // TODO
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// class attributes
|
|
||||||
|
|
||||||
pub const _ELEMENT_NODE = @intFromEnum(parser.NodeType.element);
|
|
||||||
pub const _ATTRIBUTE_NODE = @intFromEnum(parser.NodeType.attribute);
|
|
||||||
pub const _TEXT_NODE = @intFromEnum(parser.NodeType.text);
|
|
||||||
pub const _CDATA_SECTION_NODE = @intFromEnum(parser.NodeType.cdata_section);
|
|
||||||
pub const _PROCESSING_INSTRUCTION_NODE = @intFromEnum(parser.NodeType.processing_instruction);
|
|
||||||
pub const _COMMENT_NODE = @intFromEnum(parser.NodeType.comment);
|
|
||||||
pub const _DOCUMENT_NODE = @intFromEnum(parser.NodeType.document);
|
|
||||||
pub const _DOCUMENT_TYPE_NODE = @intFromEnum(parser.NodeType.document_type);
|
|
||||||
pub const _DOCUMENT_FRAGMENT_NODE = @intFromEnum(parser.NodeType.document_fragment);
|
|
||||||
|
|
||||||
// These 3 are deprecated, but both Chrome and Firefox still expose them
|
|
||||||
pub const _ENTITY_REFERENCE_NODE = @intFromEnum(parser.NodeType.entity_reference);
|
|
||||||
pub const _ENTITY_NODE = @intFromEnum(parser.NodeType.entity);
|
|
||||||
pub const _NOTATION_NODE = @intFromEnum(parser.NodeType.notation);
|
|
||||||
|
|
||||||
pub const _DOCUMENT_POSITION_DISCONNECTED = @intFromEnum(parser.DocumentPosition.disconnected);
|
|
||||||
pub const _DOCUMENT_POSITION_PRECEDING = @intFromEnum(parser.DocumentPosition.preceding);
|
|
||||||
pub const _DOCUMENT_POSITION_FOLLOWING = @intFromEnum(parser.DocumentPosition.following);
|
|
||||||
pub const _DOCUMENT_POSITION_CONTAINS = @intFromEnum(parser.DocumentPosition.contains);
|
|
||||||
pub const _DOCUMENT_POSITION_CONTAINED_BY = @intFromEnum(parser.DocumentPosition.contained_by);
|
|
||||||
pub const _DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = @intFromEnum(parser.DocumentPosition.implementation_specific);
|
|
||||||
|
|
||||||
// JS funcs
|
|
||||||
// --------
|
|
||||||
|
|
||||||
// Read-only attributes
|
|
||||||
pub fn get_baseURI(_: *parser.Node, page: *Page) ![]const u8 {
|
|
||||||
return page.url.raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_firstChild(self: *parser.Node) !?Union {
|
|
||||||
const res = parser.nodeFirstChild(self);
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try Node.toInterface(res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_lastChild(self: *parser.Node) !?Union {
|
|
||||||
const res = parser.nodeLastChild(self);
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try Node.toInterface(res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_nextSibling(self: *parser.Node) !?Union {
|
|
||||||
const res = parser.nodeNextSibling(self);
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try Node.toInterface(res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_previousSibling(self: *parser.Node) !?Union {
|
|
||||||
const res = parser.nodePreviousSibling(self);
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try Node.toInterface(res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_parentNode(self: *parser.Node) !?Union {
|
|
||||||
const res = parser.nodeParentNode(self);
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try Node.toInterface(res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_parentElement(self: *parser.Node) !?ElementUnion {
|
|
||||||
const res = parser.nodeParentElement(self);
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try Element.toInterface(res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_nodeName(self: *parser.Node) ![]const u8 {
|
|
||||||
return try parser.nodeName(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_nodeType(self: *parser.Node) !u8 {
|
|
||||||
return @intFromEnum(parser.nodeType(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_ownerDocument(self: *parser.Node) !?*parser.DocumentHTML {
|
|
||||||
const res = parser.nodeOwnerDocument(self);
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return @as(*parser.DocumentHTML, @ptrCast(res.?));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_isConnected(self: *parser.Node) !bool {
|
|
||||||
var node = self;
|
|
||||||
while (true) {
|
|
||||||
const node_type = parser.nodeType(node);
|
|
||||||
if (node_type == .document) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parser.nodeParentNode(node)) |parent| {
|
|
||||||
// didn't find a document, but node has a parent, let's see
|
|
||||||
// if it's connected;
|
|
||||||
node = parent;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node_type != .document_fragment) {
|
|
||||||
// doesn't have a parent and isn't a document_fragment
|
|
||||||
// can't be connected
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parser.documentFragmentGetHost(@ptrCast(node))) |host| {
|
|
||||||
// node doesn't have a parent, but it's a document fragment
|
|
||||||
// with a host. The host is like the parent, but we only want to
|
|
||||||
// traverse up (or down) to it in specific cases, like isConnected.
|
|
||||||
node = host;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read/Write attributes
|
|
||||||
|
|
||||||
pub fn get_nodeValue(self: *parser.Node) !?[]const u8 {
|
|
||||||
return parser.nodeValue(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_nodeValue(self: *parser.Node, data: []u8) !void {
|
|
||||||
try parser.nodeSetValue(self, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_textContent(self: *parser.Node) ?[]const u8 {
|
|
||||||
return parser.nodeTextContent(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_textContent(self: *parser.Node, data: []u8) !void {
|
|
||||||
return try parser.nodeSetTextContent(self, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
|
|
||||||
pub fn _appendChild(self: *parser.Node, child: *parser.Node) !Union {
|
|
||||||
const self_owner = parser.nodeOwnerDocument(self);
|
|
||||||
const child_owner = parser.nodeOwnerDocument(child);
|
|
||||||
|
|
||||||
// If the node to be inserted has a different ownerDocument than the parent node,
|
|
||||||
// modern browsers automatically adopt the node and its descendants into
|
|
||||||
// the parent's ownerDocument.
|
|
||||||
// This process is known as adoption.
|
|
||||||
// (7.1) https://dom.spec.whatwg.org/#concept-node-insert
|
|
||||||
if (child_owner == null or (self_owner != null and child_owner.? != self_owner.?)) {
|
|
||||||
const w = Walker{};
|
|
||||||
var current = child;
|
|
||||||
while (true) {
|
|
||||||
current.owner = self_owner;
|
|
||||||
current = try w.get_next(child, current) orelse break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: DocumentFragment special case
|
|
||||||
const res = try parser.nodeAppendChild(self, child);
|
|
||||||
return try Node.toInterface(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _cloneNode(self: *parser.Node, deep: ?bool) !Union {
|
|
||||||
const clone = try parser.nodeCloneNode(self, deep orelse false);
|
|
||||||
return try Node.toInterface(clone);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _compareDocumentPosition(self: *parser.Node, other: *parser.Node) !u32 {
|
|
||||||
if (self == other) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const docself = parser.nodeOwnerDocument(self) orelse blk: {
|
|
||||||
if (parser.nodeType(self) == .document) {
|
|
||||||
break :blk @as(*parser.Document, @ptrCast(self));
|
|
||||||
}
|
|
||||||
break :blk null;
|
|
||||||
};
|
|
||||||
const docother = parser.nodeOwnerDocument(other) orelse blk: {
|
|
||||||
if (parser.nodeType(other) == .document) {
|
|
||||||
break :blk @as(*parser.Document, @ptrCast(other));
|
|
||||||
}
|
|
||||||
break :blk null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Both are in different document.
|
|
||||||
if (docself == null or docother == null or docself.? != docother.?) {
|
|
||||||
return @intFromEnum(parser.DocumentPosition.disconnected) +
|
|
||||||
@intFromEnum(parser.DocumentPosition.implementation_specific) +
|
|
||||||
@intFromEnum(parser.DocumentPosition.preceding);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (@intFromPtr(self) == @intFromPtr(docself.?)) {
|
|
||||||
// if self is the document, and we already know other is in the
|
|
||||||
// document, then other is contained by and following self.
|
|
||||||
return @intFromEnum(parser.DocumentPosition.following) +
|
|
||||||
@intFromEnum(parser.DocumentPosition.contained_by);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootself = parser.nodeGetRootNode(self);
|
|
||||||
const rootother = parser.nodeGetRootNode(other);
|
|
||||||
if (rootself != rootother) {
|
|
||||||
return @intFromEnum(parser.DocumentPosition.disconnected) +
|
|
||||||
@intFromEnum(parser.DocumentPosition.implementation_specific) +
|
|
||||||
@intFromEnum(parser.DocumentPosition.preceding);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Both are in a different trees in the same document.
|
|
||||||
|
|
||||||
const w = Walker{};
|
|
||||||
var next: ?*parser.Node = null;
|
|
||||||
|
|
||||||
// Is other a descendant of self?
|
|
||||||
while (true) {
|
|
||||||
next = try w.get_next(self, next) orelse break;
|
|
||||||
if (other == next) {
|
|
||||||
return @intFromEnum(parser.DocumentPosition.following) +
|
|
||||||
@intFromEnum(parser.DocumentPosition.contained_by);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is self a descendant of other?
|
|
||||||
next = null;
|
|
||||||
while (true) {
|
|
||||||
next = try w.get_next(other, next) orelse break;
|
|
||||||
if (self == next) {
|
|
||||||
return @intFromEnum(parser.DocumentPosition.contains) +
|
|
||||||
@intFromEnum(parser.DocumentPosition.preceding);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
next = null;
|
|
||||||
while (true) {
|
|
||||||
next = try w.get_next(parser.documentToNode(docself.?), next) orelse break;
|
|
||||||
if (other == next) {
|
|
||||||
// other precedes self.
|
|
||||||
return @intFromEnum(parser.DocumentPosition.preceding);
|
|
||||||
}
|
|
||||||
if (self == next) {
|
|
||||||
// other follows self.
|
|
||||||
return @intFromEnum(parser.DocumentPosition.following);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _contains(self: *parser.Node, other: *parser.Node) bool {
|
|
||||||
return parser.nodeContains(self, other);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns itself or ancestor object inheriting from Node.
|
|
||||||
// - An Element inside a standard web page will return an HTMLDocument object representing the entire page (or <iframe>).
|
|
||||||
// - An Element inside a shadow DOM will return the associated ShadowRoot.
|
|
||||||
// - An Element that is not attached to a document or a shadow tree will return the root of the DOM tree it belongs to
|
|
||||||
const GetRootNodeResult = union(enum) {
|
|
||||||
shadow_root: *ShadowRoot,
|
|
||||||
node: Union,
|
|
||||||
};
|
|
||||||
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult {
|
|
||||||
if (options) |options_| if (options_.composed) {
|
|
||||||
log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const root = parser.nodeGetRootNode(self);
|
|
||||||
if (page.getNodeState(root)) |state| {
|
|
||||||
if (state.shadow_root) |sr| {
|
|
||||||
return .{ .shadow_root = sr };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{ .node = try Node.toInterface(root) };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _hasChildNodes(self: *parser.Node) bool {
|
|
||||||
return parser.nodeHasChildNodes(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList {
|
|
||||||
const allocator = page.arena;
|
|
||||||
var list: NodeList = .{};
|
|
||||||
|
|
||||||
var n = parser.nodeFirstChild(self) orelse return list;
|
|
||||||
while (true) {
|
|
||||||
try list.append(allocator, n);
|
|
||||||
n = parser.nodeNextSibling(n) orelse return list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _insertBefore(self: *parser.Node, new_node: *parser.Node, ref_node_: ?*parser.Node) !Union {
|
|
||||||
if (ref_node_ == null) {
|
|
||||||
return _appendChild(self, new_node);
|
|
||||||
}
|
|
||||||
|
|
||||||
const self_owner = parser.nodeOwnerDocument(self);
|
|
||||||
const new_node_owner = parser.nodeOwnerDocument(new_node);
|
|
||||||
|
|
||||||
// If the node to be inserted has a different ownerDocument than the parent node,
|
|
||||||
// modern browsers automatically adopt the node and its descendants into
|
|
||||||
// the parent's ownerDocument.
|
|
||||||
// This process is known as adoption.
|
|
||||||
// (7.1) https://dom.spec.whatwg.org/#concept-node-insert
|
|
||||||
if (new_node_owner == null or (self_owner != null and new_node_owner.? != self_owner.?)) {
|
|
||||||
const w = Walker{};
|
|
||||||
var current = new_node;
|
|
||||||
while (true) {
|
|
||||||
current.owner = self_owner;
|
|
||||||
current = try w.get_next(new_node, current) orelse break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Node.toInterface(try parser.nodeInsertBefore(self, new_node, ref_node_.?));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _isDefaultNamespace(self: *parser.Node, namespace: ?[]const u8) !bool {
|
|
||||||
return parser.nodeIsDefaultNamespace(self, namespace);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _isEqualNode(self: *parser.Node, other: *parser.Node) !bool {
|
|
||||||
// TODO: other is not an optional parameter, but can be null.
|
|
||||||
return try parser.nodeIsEqualNode(self, other);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _isSameNode(self: *parser.Node, other: *parser.Node) bool {
|
|
||||||
// TODO: other is not an optional parameter, but can be null.
|
|
||||||
// NOTE: there is no need to use isSameNode(); instead use the === strict equality operator
|
|
||||||
return parser.nodeIsSameNode(self, other);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _lookupPrefix(self: *parser.Node, namespace: ?[]const u8) !?[]const u8 {
|
|
||||||
// TODO: other is not an optional parameter, but can be null.
|
|
||||||
if (namespace == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (std.mem.eql(u8, namespace.?, "")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try parser.nodeLookupPrefix(self, namespace.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _lookupNamespaceURI(self: *parser.Node, prefix: ?[]const u8) !?[]const u8 {
|
|
||||||
// TODO: other is not an optional parameter, but can be null.
|
|
||||||
return try parser.nodeLookupNamespaceURI(self, prefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _normalize(self: *parser.Node) !void {
|
|
||||||
return try parser.nodeNormalize(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _removeChild(self: *parser.Node, child: *parser.Node) !Union {
|
|
||||||
const res = try parser.nodeRemoveChild(self, child);
|
|
||||||
return try Node.toInterface(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _replaceChild(self: *parser.Node, new_child: *parser.Node, old_child: *parser.Node) !Union {
|
|
||||||
const res = try parser.nodeReplaceChild(self, new_child, old_child);
|
|
||||||
return try Node.toInterface(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the hierarchy node tree constraints are respected.
|
|
||||||
// For now, it checks only if new nodes are not self.
|
|
||||||
// TODO implements the others contraints.
|
|
||||||
// see https://dom.spec.whatwg.org/#concept-node-tree
|
|
||||||
pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool {
|
|
||||||
for (nodes) |n| {
|
|
||||||
if (n.is(self)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prepend(self: *parser.Node, nodes: []const NodeOrText) !void {
|
|
||||||
if (nodes.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check hierarchy
|
|
||||||
if (!hierarchy(self, nodes)) {
|
|
||||||
return parser.DOMError.HierarchyRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
const doc = (parser.nodeOwnerDocument(self)) orelse return;
|
|
||||||
|
|
||||||
if (parser.nodeFirstChild(self)) |first| {
|
|
||||||
for (nodes) |node| {
|
|
||||||
_ = try parser.nodeInsertBefore(self, try node.toNode(doc), first);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (nodes) |node| {
|
|
||||||
_ = try parser.nodeAppendChild(self, try node.toNode(doc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn append(self: *parser.Node, nodes: []const NodeOrText) !void {
|
|
||||||
if (nodes.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check hierarchy
|
|
||||||
if (!hierarchy(self, nodes)) {
|
|
||||||
return parser.DOMError.HierarchyRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
const doc = (parser.nodeOwnerDocument(self)) orelse return;
|
|
||||||
for (nodes) |node| {
|
|
||||||
_ = try parser.nodeAppendChild(self, try node.toNode(doc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn replaceChildren(self: *parser.Node, nodes: []const NodeOrText) !void {
|
|
||||||
if (nodes.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check hierarchy
|
|
||||||
if (!hierarchy(self, nodes)) {
|
|
||||||
return parser.DOMError.HierarchyRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove existing children
|
|
||||||
try removeChildren(self);
|
|
||||||
|
|
||||||
const doc = (parser.nodeOwnerDocument(self)) orelse return;
|
|
||||||
// add new children
|
|
||||||
for (nodes) |node| {
|
|
||||||
_ = try parser.nodeAppendChild(self, try node.toNode(doc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn removeChildren(self: *parser.Node) !void {
|
|
||||||
if (!parser.nodeHasChildNodes(self)) return;
|
|
||||||
|
|
||||||
const children = try parser.nodeGetChildNodes(self);
|
|
||||||
const ln = parser.nodeListLength(children);
|
|
||||||
var i: u32 = 0;
|
|
||||||
while (i < ln) {
|
|
||||||
defer i += 1;
|
|
||||||
// we always retrieve the 0 index child on purpose: libdom nodelist
|
|
||||||
// are dynamic. So the next child to remove is always as pos 0.
|
|
||||||
const child = parser.nodeListItem(children, 0) orelse continue;
|
|
||||||
_ = try parser.nodeRemoveChild(self, child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn before(self: *parser.Node, nodes: []const NodeOrText) !void {
|
|
||||||
const parent = parser.nodeParentNode(self) orelse return;
|
|
||||||
const doc = (parser.nodeOwnerDocument(parent)) orelse return;
|
|
||||||
|
|
||||||
var sibling: ?*parser.Node = self;
|
|
||||||
// have to find the first sibling that isn't in nodes
|
|
||||||
CHECK: while (sibling) |s| {
|
|
||||||
for (nodes) |n| {
|
|
||||||
if (n.is(s)) {
|
|
||||||
sibling = parser.nodePreviousSibling(s);
|
|
||||||
continue :CHECK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sibling == null) {
|
|
||||||
sibling = parser.nodeFirstChild(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sibling) |ref_node| {
|
|
||||||
for (nodes) |node| {
|
|
||||||
_ = try parser.nodeInsertBefore(parent, try node.toNode(doc), ref_node);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Node.prepend(self, nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn after(self: *parser.Node, nodes: []const NodeOrText) !void {
|
|
||||||
const parent = parser.nodeParentNode(self) orelse return;
|
|
||||||
const doc = (parser.nodeOwnerDocument(parent)) orelse return;
|
|
||||||
|
|
||||||
// have to find the first sibling that isn't in nodes
|
|
||||||
var sibling = parser.nodeNextSibling(self);
|
|
||||||
CHECK: while (sibling) |s| {
|
|
||||||
for (nodes) |n| {
|
|
||||||
if (n.is(s)) {
|
|
||||||
sibling = parser.nodeNextSibling(s);
|
|
||||||
continue :CHECK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sibling) |ref_node| {
|
|
||||||
for (nodes) |node| {
|
|
||||||
_ = try parser.nodeInsertBefore(parent, try node.toNode(doc), ref_node);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (nodes) |node| {
|
|
||||||
_ = try parser.nodeAppendChild(parent, try node.toNode(doc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A lot of functions take either a node or text input.
|
|
||||||
// The text input is to be converted into a Text node.
|
|
||||||
pub const NodeOrText = union(enum) {
|
|
||||||
text: []const u8,
|
|
||||||
node: *parser.Node,
|
|
||||||
|
|
||||||
fn toNode(self: NodeOrText, doc: *parser.Document) !*parser.Node {
|
|
||||||
return switch (self) {
|
|
||||||
.node => |n| n,
|
|
||||||
.text => |txt| @ptrCast(@alignCast(try parser.documentCreateTextNode(doc, txt))),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whether the node represented by the NodeOrText is the same as the
|
|
||||||
// given Node. Always false for text values as these represent as-of-yet
|
|
||||||
// created Text nodes.
|
|
||||||
fn is(self: NodeOrText, other: *parser.Node) bool {
|
|
||||||
return switch (self) {
|
|
||||||
.text => false,
|
|
||||||
.node => |n| n == other,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.Node" {
|
|
||||||
try testing.htmlRunner("dom/node.html");
|
|
||||||
try testing.htmlRunner("dom/node_owner.html");
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
|
|
||||||
pub const NodeFilter = struct {
|
|
||||||
pub const _FILTER_ACCEPT: u16 = 1;
|
|
||||||
pub const _FILTER_REJECT: u16 = 2;
|
|
||||||
pub const _FILTER_SKIP: u16 = 3;
|
|
||||||
|
|
||||||
pub const _SHOW_ALL: u32 = std.math.maxInt(u32);
|
|
||||||
pub const _SHOW_ELEMENT: u32 = 0b1;
|
|
||||||
pub const _SHOW_ATTRIBUTE: u32 = 0b10;
|
|
||||||
pub const _SHOW_TEXT: u32 = 0b100;
|
|
||||||
pub const _SHOW_CDATA_SECTION: u32 = 0b1000;
|
|
||||||
pub const _SHOW_ENTITY_REFERENCE: u32 = 0b10000;
|
|
||||||
pub const _SHOW_ENTITY: u32 = 0b100000;
|
|
||||||
pub const _SHOW_PROCESSING_INSTRUCTION: u32 = 0b1000000;
|
|
||||||
pub const _SHOW_COMMENT: u32 = 0b10000000;
|
|
||||||
pub const _SHOW_DOCUMENT: u32 = 0b100000000;
|
|
||||||
pub const _SHOW_DOCUMENT_TYPE: u32 = 0b1000000000;
|
|
||||||
pub const _SHOW_DOCUMENT_FRAGMENT: u32 = 0b10000000000;
|
|
||||||
pub const _SHOW_NOTATION: u32 = 0b100000000000;
|
|
||||||
};
|
|
||||||
|
|
||||||
const VerifyResult = enum { accept, skip, reject };
|
|
||||||
|
|
||||||
pub fn verify(what_to_show: u32, filter: ?js.Function, node: *parser.Node) !VerifyResult {
|
|
||||||
const node_type = parser.nodeType(node);
|
|
||||||
|
|
||||||
// Verify that we can show this node type.
|
|
||||||
// Per the DOM spec, what_to_show filters which nodes to return, but should
|
|
||||||
// still traverse children. So we return .skip (not .reject) when the node
|
|
||||||
// type doesn't match.
|
|
||||||
if (!switch (node_type) {
|
|
||||||
.attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0,
|
|
||||||
.cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0,
|
|
||||||
.comment => what_to_show & NodeFilter._SHOW_COMMENT != 0,
|
|
||||||
.document => what_to_show & NodeFilter._SHOW_DOCUMENT != 0,
|
|
||||||
.document_fragment => what_to_show & NodeFilter._SHOW_DOCUMENT_FRAGMENT != 0,
|
|
||||||
.document_type => what_to_show & NodeFilter._SHOW_DOCUMENT_TYPE != 0,
|
|
||||||
.element => what_to_show & NodeFilter._SHOW_ELEMENT != 0,
|
|
||||||
.entity => what_to_show & NodeFilter._SHOW_ENTITY != 0,
|
|
||||||
.entity_reference => what_to_show & NodeFilter._SHOW_ENTITY_REFERENCE != 0,
|
|
||||||
.notation => what_to_show & NodeFilter._SHOW_NOTATION != 0,
|
|
||||||
.processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0,
|
|
||||||
.text => what_to_show & NodeFilter._SHOW_TEXT != 0,
|
|
||||||
}) return .skip;
|
|
||||||
|
|
||||||
// Verify that we aren't filtering it out.
|
|
||||||
if (filter) |f| {
|
|
||||||
const acceptance = try f.call(u16, .{try Node.toInterface(node)});
|
|
||||||
return switch (acceptance) {
|
|
||||||
NodeFilter._FILTER_ACCEPT => .accept,
|
|
||||||
NodeFilter._FILTER_REJECT => .reject,
|
|
||||||
NodeFilter._FILTER_SKIP => .skip,
|
|
||||||
else => .reject,
|
|
||||||
};
|
|
||||||
} else return .accept;
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.NodeFilter" {
|
|
||||||
try testing.htmlRunner("dom/node_filter.html");
|
|
||||||
}
|
|
||||||
@@ -1,302 +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 js = @import("../js/js.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const NodeFilter = @import("node_filter.zig");
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
const NodeUnion = @import("node.zig").Union;
|
|
||||||
const DOMException = @import("exceptions.zig").DOMException;
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/NodeIterator
|
|
||||||
// While this is similar to TreeWalker it has its own implementation as there are several subtle differences
|
|
||||||
// For example:
|
|
||||||
// - nextNode returns the reference node, whereas TreeWalker returns the next node
|
|
||||||
// - Skip and reject are equivalent for NodeIterator, for TreeWalker they are different
|
|
||||||
pub const NodeIterator = struct {
|
|
||||||
pub const Exception = DOMException;
|
|
||||||
|
|
||||||
root: *parser.Node,
|
|
||||||
reference_node: *parser.Node,
|
|
||||||
what_to_show: u32,
|
|
||||||
filter: ?NodeIteratorOpts,
|
|
||||||
filter_func: ?js.Function,
|
|
||||||
pointer_before_current: bool = true,
|
|
||||||
// used to track / block recursive filters
|
|
||||||
is_in_callback: bool = false,
|
|
||||||
|
|
||||||
// One of the few cases where null and undefined resolve to different default.
|
|
||||||
// We need the raw JsObject so that we can probe the tri state:
|
|
||||||
// null, undefined or i32.
|
|
||||||
pub const WhatToShow = js.Object;
|
|
||||||
|
|
||||||
pub const NodeIteratorOpts = union(enum) {
|
|
||||||
function: js.Function,
|
|
||||||
object: struct { acceptNode: js.Function },
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?NodeIteratorOpts) !NodeIterator {
|
|
||||||
var filter_func: ?js.Function = null;
|
|
||||||
if (filter) |f| {
|
|
||||||
filter_func = switch (f) {
|
|
||||||
.function => |func| func,
|
|
||||||
.object => |o| o.acceptNode,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var what_to_show: u32 = undefined;
|
|
||||||
if (what_to_show_) |wts| {
|
|
||||||
switch (try wts.triState(NodeIterator, "what_to_show", u32)) {
|
|
||||||
.null => what_to_show = 0,
|
|
||||||
.undefined => what_to_show = NodeFilter.NodeFilter._SHOW_ALL,
|
|
||||||
.value => |v| what_to_show = v,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
what_to_show = NodeFilter.NodeFilter._SHOW_ALL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.root = node,
|
|
||||||
.filter = filter,
|
|
||||||
.reference_node = node,
|
|
||||||
.filter_func = filter_func,
|
|
||||||
.what_to_show = what_to_show,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_filter(self: *const NodeIterator) ?NodeIteratorOpts {
|
|
||||||
return self.filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_pointerBeforeReferenceNode(self: *const NodeIterator) bool {
|
|
||||||
return self.pointer_before_current;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_referenceNode(self: *const NodeIterator) !NodeUnion {
|
|
||||||
return try Node.toInterface(self.reference_node);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_root(self: *const NodeIterator) !NodeUnion {
|
|
||||||
return try Node.toInterface(self.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_whatToShow(self: *const NodeIterator) u32 {
|
|
||||||
return self.what_to_show;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _nextNode(self: *NodeIterator) !?NodeUnion {
|
|
||||||
try self.callbackStart();
|
|
||||||
defer self.callbackEnd();
|
|
||||||
|
|
||||||
if (self.pointer_before_current) {
|
|
||||||
self.pointer_before_current = false;
|
|
||||||
// Unlike TreeWalker, NodeIterator starts at the first node
|
|
||||||
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
|
|
||||||
self.pointer_before_current = false;
|
|
||||||
return try Node.toInterface(self.reference_node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try self.firstChild(self.reference_node)) |child| {
|
|
||||||
self.reference_node = child;
|
|
||||||
return try Node.toInterface(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
var current = self.reference_node;
|
|
||||||
while (current != self.root) {
|
|
||||||
// Try to get next sibling (including .skip/.reject nodes we need to descend into)
|
|
||||||
if (try self.nextSiblingOrSkipReject(current)) |result| {
|
|
||||||
if (result.should_descend) {
|
|
||||||
// This is a .skip/.reject node - try to find acceptable children within it
|
|
||||||
if (try self.firstChild(result.node)) |child| {
|
|
||||||
self.reference_node = child;
|
|
||||||
return try Node.toInterface(child);
|
|
||||||
}
|
|
||||||
// No acceptable children, continue looking at this node's siblings
|
|
||||||
current = result.node;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// This is an .accept node - return it
|
|
||||||
self.reference_node = result.node;
|
|
||||||
return try Node.toInterface(result.node);
|
|
||||||
}
|
|
||||||
|
|
||||||
current = (parser.nodeParentNode(current)) orelse break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _previousNode(self: *NodeIterator) !?NodeUnion {
|
|
||||||
try self.callbackStart();
|
|
||||||
defer self.callbackEnd();
|
|
||||||
|
|
||||||
if (!self.pointer_before_current) {
|
|
||||||
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
|
|
||||||
self.pointer_before_current = true;
|
|
||||||
// Still need to verify as last may be first as well
|
|
||||||
return try Node.toInterface(self.reference_node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (self.reference_node == self.root) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var current = self.reference_node;
|
|
||||||
while (parser.nodePreviousSibling(current)) |previous| {
|
|
||||||
current = previous;
|
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
|
||||||
.accept => {
|
|
||||||
// Get last child if it has one.
|
|
||||||
if (try self.lastChild(current)) |child| {
|
|
||||||
self.reference_node = child;
|
|
||||||
return try Node.toInterface(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, this node is our previous one.
|
|
||||||
self.reference_node = current;
|
|
||||||
return try Node.toInterface(current);
|
|
||||||
},
|
|
||||||
.reject, .skip => {
|
|
||||||
// Get last child if it has one.
|
|
||||||
if (try self.lastChild(current)) |child| {
|
|
||||||
self.reference_node = child;
|
|
||||||
return try Node.toInterface(child);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current != self.root) {
|
|
||||||
if (try self.parentNode(current)) |parent| {
|
|
||||||
self.reference_node = parent;
|
|
||||||
return try Node.toInterface(parent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _detach(self: *const NodeIterator) void {
|
|
||||||
// no-op as per spec
|
|
||||||
_ = self;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn firstChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
|
||||||
const children = try parser.nodeGetChildNodes(node);
|
|
||||||
const child_count = parser.nodeListLength(children);
|
|
||||||
|
|
||||||
for (0..child_count) |i| {
|
|
||||||
const index: u32 = @intCast(i);
|
|
||||||
const child = (parser.nodeListItem(children, index)) orelse return null;
|
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
|
||||||
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
|
|
||||||
.reject, .skip => if (try self.firstChild(child)) |gchild| return gchild,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lastChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
|
||||||
const children = try parser.nodeGetChildNodes(node);
|
|
||||||
const child_count = parser.nodeListLength(children);
|
|
||||||
|
|
||||||
var index: u32 = child_count;
|
|
||||||
while (index > 0) {
|
|
||||||
index -= 1;
|
|
||||||
const child = (parser.nodeListItem(children, index)) orelse return null;
|
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
|
||||||
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
|
|
||||||
.reject, .skip => if (try self.lastChild(child)) |gchild| return gchild,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This implementation is actually the same as :TreeWalker
|
|
||||||
fn parentNode(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
|
||||||
if (self.root == node) return null;
|
|
||||||
|
|
||||||
var current = node;
|
|
||||||
while (true) {
|
|
||||||
if (current == self.root) return null;
|
|
||||||
current = (parser.nodeParentNode(current)) orelse return null;
|
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
|
||||||
.accept => return current,
|
|
||||||
.reject, .skip => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This implementation is actually the same as :TreeWalker
|
|
||||||
fn nextSibling(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
|
||||||
var current = node;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
current = (parser.nodeNextSibling(current)) orelse return null;
|
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
|
||||||
.accept => return current,
|
|
||||||
.skip, .reject => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the next sibling that is either acceptable or should be descended into (skip/reject)
|
|
||||||
fn nextSiblingOrSkipReject(self: *const NodeIterator, node: *parser.Node) !?struct { node: *parser.Node, should_descend: bool } {
|
|
||||||
var current = node;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
current = (parser.nodeNextSibling(current)) orelse return null;
|
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
|
||||||
.accept => return .{ .node = current, .should_descend = false },
|
|
||||||
.skip, .reject => return .{ .node = current, .should_descend = true },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn callbackStart(self: *NodeIterator) !void {
|
|
||||||
if (self.is_in_callback) {
|
|
||||||
// this is the correct DOMExeption
|
|
||||||
return error.InvalidState;
|
|
||||||
}
|
|
||||||
self.is_in_callback = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn callbackEnd(self: *NodeIterator) void {
|
|
||||||
self.is_in_callback = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.NodeIterator" {
|
|
||||||
try testing.htmlRunner("dom/node_iterator.html");
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const NodeUnion = @import("node.zig").Union;
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
|
|
||||||
const U32Iterator = @import("../iterator/iterator.zig").U32Iterator;
|
|
||||||
|
|
||||||
const DOMException = @import("exceptions.zig").DOMException;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
NodeListIterator,
|
|
||||||
NodeList,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const NodeListIterator = struct {
|
|
||||||
coll: *NodeList,
|
|
||||||
index: u32 = 0,
|
|
||||||
|
|
||||||
pub const Return = struct {
|
|
||||||
value: ?NodeUnion,
|
|
||||||
done: bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn _next(self: *NodeListIterator) !Return {
|
|
||||||
const e = try self.coll._item(self.index);
|
|
||||||
if (e == null) {
|
|
||||||
return Return{
|
|
||||||
.value = null,
|
|
||||||
.done = true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
self.index += 1;
|
|
||||||
return Return{
|
|
||||||
.value = e,
|
|
||||||
.done = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const NodeListEntriesIterator = struct {
|
|
||||||
coll: *NodeList,
|
|
||||||
index: u32 = 0,
|
|
||||||
|
|
||||||
pub const Return = struct {
|
|
||||||
value: ?NodeUnion,
|
|
||||||
done: bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn _next(self: *NodeListEntriesIterator) !Return {
|
|
||||||
const e = try self.coll._item(self.index);
|
|
||||||
if (e == null) {
|
|
||||||
return Return{
|
|
||||||
.value = null,
|
|
||||||
.done = true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
self.index += 1;
|
|
||||||
return Return{
|
|
||||||
.value = e,
|
|
||||||
.done = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Nodelist is implemented in pure Zig b/c libdom's NodeList doesn't allow to
|
|
||||||
// append nodes.
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#nodelist
|
|
||||||
//
|
|
||||||
// TODO: a Nodelist can be either static or live. But the current
|
|
||||||
// implementation allows only static nodelist.
|
|
||||||
// see https://dom.spec.whatwg.org/#old-style-collections
|
|
||||||
pub const NodeList = struct {
|
|
||||||
pub const Exception = DOMException;
|
|
||||||
const NodesArrayList = std.ArrayListUnmanaged(*parser.Node);
|
|
||||||
|
|
||||||
nodes: NodesArrayList = .{},
|
|
||||||
|
|
||||||
pub fn deinit(self: *NodeList, allocator: Allocator) void {
|
|
||||||
self.nodes.deinit(allocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ensureTotalCapacity(self: *NodeList, allocator: Allocator, n: usize) !void {
|
|
||||||
return self.nodes.ensureTotalCapacity(allocator, n);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn append(self: *NodeList, allocator: Allocator, node: *parser.Node) !void {
|
|
||||||
try self.nodes.append(allocator, node);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn appendAssumeCapacity(self: *NodeList, node: *parser.Node) void {
|
|
||||||
self.nodes.appendAssumeCapacity(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_length(self: *const NodeList) u32 {
|
|
||||||
return @intCast(self.nodes.items.len);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _item(self: *const NodeList, index: u32) !?NodeUnion {
|
|
||||||
if (index >= self.nodes.items.len) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const n = self.nodes.items[index];
|
|
||||||
return try Node.toInterface(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This code works, but it's _MUCH_ slower than using postAttach. The benefit
|
|
||||||
// of this version, is that it's "live"..but we're talking many orders of
|
|
||||||
// magnitude slower.
|
|
||||||
//
|
|
||||||
// You can test it by commenting out `postAttach`, uncommenting this and
|
|
||||||
// running:
|
|
||||||
// zig build wpt -- tests/wpt/dom/nodes/NodeList-static-length-getter-tampered-indexOf-1.html
|
|
||||||
//
|
|
||||||
// I think this _is_ the right way to do it, but I must be doing something
|
|
||||||
// wrong to make it so slow.
|
|
||||||
// pub fn indexed_get(self: *const NodeList, index: u32, has_value: *bool) !?NodeUnion {
|
|
||||||
// return (try self._item(index)) orelse {
|
|
||||||
// has_value.* = false;
|
|
||||||
// return null;
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub fn _forEach(self: *NodeList, cbk: js.Function) !void { // TODO handle thisArg
|
|
||||||
for (self.nodes.items, 0..) |n, i| {
|
|
||||||
const ii: u32 = @intCast(i);
|
|
||||||
var result: js.Function.Result = undefined;
|
|
||||||
cbk.tryCall(void, .{ n, ii, self }, &result) catch {
|
|
||||||
log.debug(.user_script, "forEach callback", .{ .err = result.exception, .stack = result.stack });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _keys(self: *NodeList) U32Iterator {
|
|
||||||
return .{
|
|
||||||
.length = self.get_length(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _values(self: *NodeList) NodeListIterator {
|
|
||||||
return .{
|
|
||||||
.coll = self,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _symbol_iterator(self: *NodeList) NodeListIterator {
|
|
||||||
return self._values();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
|
|
||||||
pub fn postAttach(self: *NodeList, js_this: js.This) !void {
|
|
||||||
const len = self.get_length();
|
|
||||||
for (0..len) |i| {
|
|
||||||
const node = try self._item(@intCast(i)) orelse unreachable;
|
|
||||||
try js_this.setIndex(@intCast(i), node, .{});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.NodeList" {
|
|
||||||
try testing.htmlRunner("dom/node_list.html");
|
|
||||||
}
|
|
||||||
@@ -1,206 +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 js = @import("../js/js.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
Performance,
|
|
||||||
PerformanceEntry,
|
|
||||||
PerformanceMark,
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Performance
|
|
||||||
pub const Performance = struct {
|
|
||||||
pub const prototype = *EventTarget;
|
|
||||||
|
|
||||||
// Extend libdom event target for pure zig struct.
|
|
||||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .performance },
|
|
||||||
|
|
||||||
time_origin: u64,
|
|
||||||
// if (Window.crossOriginIsolated) -> Resolution in isolated contexts: 5 microseconds
|
|
||||||
// else -> Resolution in non-isolated contexts: 100 microseconds
|
|
||||||
const ms_resolution = 100;
|
|
||||||
|
|
||||||
pub fn init() Performance {
|
|
||||||
return .{
|
|
||||||
.time_origin = milliTimestamp(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_timeOrigin(self: *const Performance) u64 {
|
|
||||||
return self.time_origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(self: *Performance) void {
|
|
||||||
self.time_origin = milliTimestamp();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _now(self: *const Performance) u64 {
|
|
||||||
return milliTimestamp() - self.time_origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _mark(_: *Performance, name: js.String, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark {
|
|
||||||
const mark: PerformanceMark = try .constructor(name, _options, page);
|
|
||||||
// TODO: Should store this in an entries list
|
|
||||||
return mark;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: fn _mark should record the marks in a lookup
|
|
||||||
pub fn _clearMarks(_: *Performance, name: ?[]const u8) void {
|
|
||||||
_ = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: fn _measures should record the marks in a lookup
|
|
||||||
pub fn _clearMeasures(_: *Performance, name: ?[]const u8) void {
|
|
||||||
_ = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: fn _measures should record the marks in a lookup
|
|
||||||
pub fn _getEntriesByName(_: *Performance, name: []const u8, typ: ?[]const u8) []PerformanceEntry {
|
|
||||||
_ = name;
|
|
||||||
_ = typ;
|
|
||||||
return &.{};
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: fn _measures should record the marks in a lookup
|
|
||||||
pub fn _getEntriesByType(_: *Performance, typ: []const u8) []PerformanceEntry {
|
|
||||||
_ = typ;
|
|
||||||
return &.{};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry
|
|
||||||
pub const PerformanceEntry = struct {
|
|
||||||
const PerformanceEntryType = enum {
|
|
||||||
element,
|
|
||||||
event,
|
|
||||||
first_input,
|
|
||||||
largest_contentful_paint,
|
|
||||||
layout_shift,
|
|
||||||
long_animation_frame,
|
|
||||||
longtask,
|
|
||||||
mark,
|
|
||||||
measure,
|
|
||||||
navigation,
|
|
||||||
paint,
|
|
||||||
resource,
|
|
||||||
taskattribution,
|
|
||||||
visibility_state,
|
|
||||||
|
|
||||||
pub fn toString(self: PerformanceEntryType) []const u8 {
|
|
||||||
return switch (self) {
|
|
||||||
.first_input => "first-input",
|
|
||||||
.largest_contentful_paint => "largest-contentful-paint",
|
|
||||||
.layout_shift => "layout-shift",
|
|
||||||
.long_animation_frame => "long-animation-frame",
|
|
||||||
.visibility_state => "visibility-state",
|
|
||||||
else => @tagName(self),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
duration: f64 = 0.0,
|
|
||||||
entry_type: PerformanceEntryType,
|
|
||||||
name: []const u8,
|
|
||||||
start_time: f64 = 0.0,
|
|
||||||
|
|
||||||
pub fn get_duration(self: *const PerformanceEntry) f64 {
|
|
||||||
return self.duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_entryType(self: *const PerformanceEntry) PerformanceEntryType {
|
|
||||||
return self.entry_type;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_name(self: *const PerformanceEntry) []const u8 {
|
|
||||||
return self.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_startTime(self: *const PerformanceEntry) f64 {
|
|
||||||
return self.start_time;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMark
|
|
||||||
pub const PerformanceMark = struct {
|
|
||||||
pub const prototype = *PerformanceEntry;
|
|
||||||
|
|
||||||
proto: PerformanceEntry,
|
|
||||||
detail: ?js.Object,
|
|
||||||
|
|
||||||
const Options = struct {
|
|
||||||
detail: ?js.Object = null,
|
|
||||||
startTime: ?f64 = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn constructor(name: js.String, _options: ?Options, page: *Page) !PerformanceMark {
|
|
||||||
const perf = &page.window.performance;
|
|
||||||
|
|
||||||
const options = _options orelse Options{};
|
|
||||||
const start_time = options.startTime orelse @as(f64, @floatFromInt(perf._now()));
|
|
||||||
|
|
||||||
if (start_time < 0.0) {
|
|
||||||
return error.TypeError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const detail = if (options.detail) |d| try d.persist() else null;
|
|
||||||
const proto = PerformanceEntry{ .name = name.string, .entry_type = .mark, .start_time = start_time };
|
|
||||||
|
|
||||||
return .{ .proto = proto, .detail = detail };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_detail(self: *const PerformanceMark) ?js.Object {
|
|
||||||
return self.detail;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("./../../testing.zig");
|
|
||||||
|
|
||||||
test "Performance: get_timeOrigin" {
|
|
||||||
var perf = Performance.init();
|
|
||||||
const time_origin = perf.get_timeOrigin();
|
|
||||||
try testing.expect(time_origin >= 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Performance: now" {
|
|
||||||
var perf = Performance.init();
|
|
||||||
|
|
||||||
// Monotonically increasing
|
|
||||||
var now = perf._now();
|
|
||||||
while (now <= 0) { // Loop for now to not be 0
|
|
||||||
try testing.expectEqual(now, 0);
|
|
||||||
now = perf._now();
|
|
||||||
}
|
|
||||||
|
|
||||||
var after = perf._now();
|
|
||||||
while (after <= now) { // Loop untill after > now
|
|
||||||
try testing.expectEqual(after, now);
|
|
||||||
after = perf._now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser: Performance.Mark" {
|
|
||||||
try testing.htmlRunner("dom/performance.html");
|
|
||||||
}
|
|
||||||
@@ -1,58 +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 js = @import("../js/js.zig");
|
|
||||||
|
|
||||||
const PerformanceEntry = @import("performance.zig").PerformanceEntry;
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver
|
|
||||||
pub const PerformanceObserver = struct {
|
|
||||||
pub const _supportedEntryTypes = [0][]const u8{};
|
|
||||||
|
|
||||||
pub fn constructor(cbk: js.Function) PerformanceObserver {
|
|
||||||
_ = cbk;
|
|
||||||
return .{};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _observe(self: *const PerformanceObserver, options_: ?Options) void {
|
|
||||||
_ = self;
|
|
||||||
_ = options_;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _disconnect(self: *PerformanceObserver) void {
|
|
||||||
_ = self;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _takeRecords(_: *const PerformanceObserver) []PerformanceEntry {
|
|
||||||
return &[_]PerformanceEntry{};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Options = struct {
|
|
||||||
buffered: ?bool = null,
|
|
||||||
durationThreshold: ?f64 = null,
|
|
||||||
entryTypes: ?[]const []const u8 = null,
|
|
||||||
type: ?[]const u8 = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.PerformanceObserver" {
|
|
||||||
try testing.htmlRunner("dom/performance_observer.html");
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#processinginstruction
|
|
||||||
pub const ProcessingInstruction = struct {
|
|
||||||
pub const Self = parser.ProcessingInstruction;
|
|
||||||
|
|
||||||
// TODO for libdom processing instruction inherit from node.
|
|
||||||
// But the spec says it must inherit from CDATA.
|
|
||||||
pub const prototype = *Node;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub fn get_target(self: *parser.ProcessingInstruction) ![]const u8 {
|
|
||||||
// libdom stores the ProcessingInstruction target in the node's name.
|
|
||||||
return try parser.nodeName(parser.processingInstructionToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
// There's something wrong when we try to clone a ProcessInstruction normally.
|
|
||||||
// The resulting object can't be cast back into a node (it crashes). This is
|
|
||||||
// a simple workaround.
|
|
||||||
pub fn _cloneNode(self: *parser.ProcessingInstruction, _: ?bool, page: *Page) !*parser.ProcessingInstruction {
|
|
||||||
return try parser.documentCreateProcessingInstruction(
|
|
||||||
@ptrCast(page.window.document),
|
|
||||||
try get_target(self),
|
|
||||||
(try get_data(self)) orelse "",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_data(self: *parser.ProcessingInstruction) !?[]const u8 {
|
|
||||||
return parser.nodeValue(parser.processingInstructionToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_data(self: *parser.ProcessingInstruction, data: []u8) !void {
|
|
||||||
try parser.nodeSetValue(parser.processingInstructionToNode(self), data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// netsurf's ProcessInstruction doesn't implement the dom_node_get_attributes
|
|
||||||
// and thus will crash if we try to call nodeIsEqualNode.
|
|
||||||
pub fn _isEqualNode(self: *parser.ProcessingInstruction, other_node: *parser.Node) !bool {
|
|
||||||
if (parser.nodeType(other_node) != .processing_instruction) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const other: *parser.ProcessingInstruction = @ptrCast(other_node);
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, try get_target(self), try get_target(other)) == false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const self_data = try get_data(self);
|
|
||||||
const other_data = try get_data(other);
|
|
||||||
if (self_data == null and other_data != null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (self_data != null and other_data == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (std.mem.eql(u8, self_data.?, other_data.?) == false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.ProcessingInstruction" {
|
|
||||||
try testing.htmlRunner("dom/processing_instruction.html");
|
|
||||||
}
|
|
||||||
@@ -1,390 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
const NodeUnion = @import("node.zig").Union;
|
|
||||||
const DOMException = @import("exceptions.zig").DOMException;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
AbstractRange,
|
|
||||||
Range,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const AbstractRange = struct {
|
|
||||||
collapsed: bool,
|
|
||||||
end_node: *parser.Node,
|
|
||||||
end_offset: u32,
|
|
||||||
start_node: *parser.Node,
|
|
||||||
start_offset: u32,
|
|
||||||
|
|
||||||
pub fn updateCollapsed(self: *AbstractRange) void {
|
|
||||||
// TODO: Eventually, compare properly.
|
|
||||||
self.collapsed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_collapsed(self: *const AbstractRange) bool {
|
|
||||||
return self.collapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_endContainer(self: *const AbstractRange) !NodeUnion {
|
|
||||||
return Node.toInterface(self.end_node);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_endOffset(self: *const AbstractRange) u32 {
|
|
||||||
return self.end_offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_startContainer(self: *const AbstractRange) !NodeUnion {
|
|
||||||
return Node.toInterface(self.start_node);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_startOffset(self: *const AbstractRange) u32 {
|
|
||||||
return self.start_offset;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Range = struct {
|
|
||||||
pub const Exception = DOMException;
|
|
||||||
pub const prototype = *AbstractRange;
|
|
||||||
|
|
||||||
proto: AbstractRange,
|
|
||||||
|
|
||||||
pub const _START_TO_START = 0;
|
|
||||||
pub const _START_TO_END = 1;
|
|
||||||
pub const _END_TO_END = 2;
|
|
||||||
pub const _END_TO_START = 3;
|
|
||||||
|
|
||||||
// The Range() constructor returns a newly created Range object whose start
|
|
||||||
// and end is the global Document object.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Range/Range
|
|
||||||
pub fn constructor(page: *Page) Range {
|
|
||||||
const proto: AbstractRange = .{
|
|
||||||
.collapsed = true,
|
|
||||||
.end_node = parser.documentHTMLToNode(page.window.document),
|
|
||||||
.end_offset = 0,
|
|
||||||
.start_node = parser.documentHTMLToNode(page.window.document),
|
|
||||||
.start_offset = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
return .{ .proto = proto };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
|
|
||||||
try ensureValidOffset(node, offset_);
|
|
||||||
const offset: u32 = @intCast(offset_);
|
|
||||||
const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) {
|
|
||||||
error.WrongDocument => blk: {
|
|
||||||
// allow a node with a different root than the current, or
|
|
||||||
// a disconnected one. Treat it as if it's "after", so that
|
|
||||||
// we also update the end_offset and end_node.
|
|
||||||
break :blk 1;
|
|
||||||
},
|
|
||||||
else => return err,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (position == 1) {
|
|
||||||
// if we're setting the node after the current start, the end must
|
|
||||||
// be set too.
|
|
||||||
self.proto.end_offset = offset;
|
|
||||||
self.proto.end_node = node;
|
|
||||||
}
|
|
||||||
self.proto.start_node = node;
|
|
||||||
self.proto.start_offset = offset;
|
|
||||||
self.proto.updateCollapsed();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setStartBefore(self: *Range, node: *parser.Node) !void {
|
|
||||||
const parent, const index = try getParentAndIndex(node);
|
|
||||||
self.proto.start_node = parent;
|
|
||||||
self.proto.start_offset = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setStartAfter(self: *Range, node: *parser.Node) !void {
|
|
||||||
const parent, const index = try getParentAndIndex(node);
|
|
||||||
self.proto.start_node = parent;
|
|
||||||
self.proto.start_offset = index + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setEnd(self: *Range, node: *parser.Node, offset_: i32) !void {
|
|
||||||
try ensureValidOffset(node, offset_);
|
|
||||||
const offset: u32 = @intCast(offset_);
|
|
||||||
|
|
||||||
const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) {
|
|
||||||
error.WrongDocument => blk: {
|
|
||||||
// allow a node with a different root than the current, or
|
|
||||||
// a disconnected one. Treat it as if it's "before", so that
|
|
||||||
// we also update the end_offset and end_node.
|
|
||||||
break :blk -1;
|
|
||||||
},
|
|
||||||
else => return err,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (position == -1) {
|
|
||||||
// if we're setting the node before the current start, the start
|
|
||||||
// must be set too.
|
|
||||||
self.proto.start_offset = offset;
|
|
||||||
self.proto.start_node = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.proto.end_node = node;
|
|
||||||
self.proto.end_offset = offset;
|
|
||||||
self.proto.updateCollapsed();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setEndBefore(self: *Range, node: *parser.Node) !void {
|
|
||||||
const parent, const index = try getParentAndIndex(node);
|
|
||||||
self.proto.end_node = parent;
|
|
||||||
self.proto.end_offset = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setEndAfter(self: *Range, node: *parser.Node) !void {
|
|
||||||
const parent, const index = try getParentAndIndex(node);
|
|
||||||
self.proto.end_node = parent;
|
|
||||||
self.proto.end_offset = index + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createContextualFragment(_: *Range, fragment: []const u8, page: *Page) !*parser.DocumentFragment {
|
|
||||||
const document_html = page.window.document;
|
|
||||||
const document = parser.documentHTMLToDocument(document_html);
|
|
||||||
const doc_frag = try parser.documentParseFragmentFromStr(document, fragment);
|
|
||||||
return doc_frag;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _selectNodeContents(self: *Range, node: *parser.Node) !void {
|
|
||||||
self.proto.start_node = node;
|
|
||||||
self.proto.start_offset = 0;
|
|
||||||
self.proto.end_node = node;
|
|
||||||
|
|
||||||
// Set end_offset
|
|
||||||
switch (parser.nodeType(node)) {
|
|
||||||
.text, .cdata_section, .comment, .processing_instruction => {
|
|
||||||
// For text-like nodes, end_offset should be the length of the text data
|
|
||||||
if (parser.nodeValue(node)) |text_data| {
|
|
||||||
self.proto.end_offset = @intCast(text_data.len);
|
|
||||||
} else {
|
|
||||||
self.proto.end_offset = 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => {
|
|
||||||
// For element and other nodes, end_offset is the number of children
|
|
||||||
const child_nodes = try parser.nodeGetChildNodes(node);
|
|
||||||
const child_count = parser.nodeListLength(child_nodes);
|
|
||||||
self.proto.end_offset = @intCast(child_count);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
self.proto.updateCollapsed();
|
|
||||||
}
|
|
||||||
|
|
||||||
// creates a copy
|
|
||||||
pub fn _cloneRange(self: *const Range) Range {
|
|
||||||
return .{
|
|
||||||
.proto = .{
|
|
||||||
.collapsed = self.proto.collapsed,
|
|
||||||
.end_node = self.proto.end_node,
|
|
||||||
.end_offset = self.proto.end_offset,
|
|
||||||
.start_node = self.proto.start_node,
|
|
||||||
.start_offset = self.proto.start_offset,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _comparePoint(self: *const Range, node: *parser.Node, offset_: i32) !i32 {
|
|
||||||
const start = self.proto.start_node;
|
|
||||||
if (parser.nodeGetRootNode(start) != parser.nodeGetRootNode(node)) {
|
|
||||||
// WPT really wants this error to be first. Later, when we check
|
|
||||||
// if the relative position is 'disconnected', it'll also catch this
|
|
||||||
// case, but WPT will complain because it sometimes also sends
|
|
||||||
// invalid offsets, and it wants WrongDocument to be raised.
|
|
||||||
return error.WrongDocument;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parser.nodeType(node) == .document_type) {
|
|
||||||
return error.InvalidNodeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
try ensureValidOffset(node, offset_);
|
|
||||||
|
|
||||||
const offset: u32 = @intCast(offset_);
|
|
||||||
if (try compare(node, offset, start, self.proto.start_offset) == -1) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try compare(node, offset, self.proto.end_node, self.proto.end_offset) == 1) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _isPointInRange(self: *const Range, node: *parser.Node, offset_: i32) !bool {
|
|
||||||
return self._comparePoint(node, offset_) catch |err| switch (err) {
|
|
||||||
error.WrongDocument => return false,
|
|
||||||
else => return err,
|
|
||||||
} == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _intersectsNode(self: *const Range, node: *parser.Node) !bool {
|
|
||||||
const start_root = parser.nodeGetRootNode(self.proto.start_node);
|
|
||||||
const node_root = parser.nodeGetRootNode(node);
|
|
||||||
if (start_root != node_root) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parent, const index = getParentAndIndex(node) catch |err| switch (err) {
|
|
||||||
error.InvalidNodeType => return true, // if node has no parent, we return true.
|
|
||||||
else => return err,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (try compare(parent, index + 1, self.proto.start_node, self.proto.start_offset) != 1) {
|
|
||||||
// node isn't after start, can't intersect
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try compare(parent, index, self.proto.end_node, self.proto.end_offset) != -1) {
|
|
||||||
// node isn't before end, can't intersect
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _compareBoundaryPoints(self: *const Range, how: i32, other: *const Range) !i32 {
|
|
||||||
return switch (how) {
|
|
||||||
_START_TO_START => compare(self.proto.start_node, self.proto.start_offset, other.proto.start_node, other.proto.start_offset),
|
|
||||||
_START_TO_END => compare(self.proto.start_node, self.proto.start_offset, other.proto.end_node, other.proto.end_offset),
|
|
||||||
_END_TO_END => compare(self.proto.end_node, self.proto.end_offset, other.proto.end_node, other.proto.end_offset),
|
|
||||||
_END_TO_START => compare(self.proto.end_node, self.proto.end_offset, other.proto.start_node, other.proto.start_offset),
|
|
||||||
else => error.NotSupported, // this is the correct DOM Exception to return
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Range.detach() method does nothing. It used to disable the Range
|
|
||||||
// object and enable the browser to release associated resources. The
|
|
||||||
// method has been kept for compatibility.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Range/detach
|
|
||||||
pub fn _detach(_: *Range) void {}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn ensureValidOffset(node: *parser.Node, offset: i32) !void {
|
|
||||||
if (offset < 0) {
|
|
||||||
return error.IndexSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
// not >= because 0 seems to represent the node itself.
|
|
||||||
if (offset > try nodeLength(node)) {
|
|
||||||
return error.IndexSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nodeLength(node: *parser.Node) !usize {
|
|
||||||
switch (try isTextual(node)) {
|
|
||||||
true => return ((parser.nodeTextContent(node)) orelse "").len,
|
|
||||||
false => {
|
|
||||||
const children = try parser.nodeGetChildNodes(node);
|
|
||||||
return @intCast(parser.nodeListLength(children));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isTextual(node: *parser.Node) !bool {
|
|
||||||
return switch (parser.nodeType(node)) {
|
|
||||||
.text, .comment, .cdata_section => true,
|
|
||||||
else => false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getParentAndIndex(child: *parser.Node) !struct { *parser.Node, u32 } {
|
|
||||||
const parent = (parser.nodeParentNode(child)) orelse return error.InvalidNodeType;
|
|
||||||
const children = try parser.nodeGetChildNodes(parent);
|
|
||||||
const ln = parser.nodeListLength(children);
|
|
||||||
var i: u32 = 0;
|
|
||||||
while (i < ln) {
|
|
||||||
defer i += 1;
|
|
||||||
const c = parser.nodeListItem(children, i) orelse continue;
|
|
||||||
if (c == child) {
|
|
||||||
return .{ parent, i };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// should not be possible to reach this point
|
|
||||||
return error.InvalidNodeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
// implementation is largely copied from the WPT helper called getPosition in
|
|
||||||
// the common.js of the dom folder.
|
|
||||||
fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b: u32) !i32 {
|
|
||||||
if (node_a == node_b) {
|
|
||||||
// This is a simple and common case, where the two nodes are the same
|
|
||||||
// We just need to compare their offsets
|
|
||||||
if (offset_a == offset_b) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return if (offset_a < offset_b) -1 else 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're probably comparing two different nodes. "Probably", because the
|
|
||||||
// above case on considered the offset if the two nodes were the same
|
|
||||||
// as-is. They could still be the same here, if we first consider the
|
|
||||||
// offset.
|
|
||||||
const position = try Node._compareDocumentPosition(node_b, node_a);
|
|
||||||
if (position & @intFromEnum(parser.DocumentPosition.disconnected) == @intFromEnum(parser.DocumentPosition.disconnected)) {
|
|
||||||
return error.WrongDocument;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position & @intFromEnum(parser.DocumentPosition.following) == @intFromEnum(parser.DocumentPosition.following)) {
|
|
||||||
return switch (try compare(node_b, offset_b, node_a, offset_a)) {
|
|
||||||
-1 => 1,
|
|
||||||
1 => -1,
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position & @intFromEnum(parser.DocumentPosition.contains) == @intFromEnum(parser.DocumentPosition.contains)) {
|
|
||||||
// node_a contains node_b
|
|
||||||
var child = node_b;
|
|
||||||
while (parser.nodeParentNode(child)) |parent| {
|
|
||||||
if (parent == node_a) {
|
|
||||||
// child.parentNode == node_a
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
child = parent;
|
|
||||||
} else {
|
|
||||||
// this should not happen, because Node._compareDocumentPosition
|
|
||||||
// has told us that node_a contains node_b, so one of node_b's
|
|
||||||
// parent's MUST be node_a. But somehow we do end up here sometimes.
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const child_parent, const child_index = try getParentAndIndex(child);
|
|
||||||
std.debug.assert(node_a == child_parent);
|
|
||||||
return if (child_index < offset_a) -1 else 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: Range" {
|
|
||||||
try testing.htmlRunner("dom/range.html");
|
|
||||||
}
|
|
||||||
@@ -1,101 +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 dump = @import("../dump.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const js = @import(".././js/js.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
const Element = @import("element.zig").Element;
|
|
||||||
const ElementUnion = @import("element.zig").Union;
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#interface-shadowroot
|
|
||||||
pub const ShadowRoot = struct {
|
|
||||||
pub const prototype = *parser.DocumentFragment;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
mode: Mode,
|
|
||||||
host: *parser.Element,
|
|
||||||
proto: *parser.DocumentFragment,
|
|
||||||
adopted_style_sheets: ?js.Object = null,
|
|
||||||
|
|
||||||
pub const Mode = enum {
|
|
||||||
open,
|
|
||||||
closed,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn get_host(self: *const ShadowRoot) !ElementUnion {
|
|
||||||
return Element.toInterface(self.host);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !js.Object {
|
|
||||||
if (self.adopted_style_sheets) |obj| {
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
const obj = try page.js.createArray(0).persist();
|
|
||||||
self.adopted_style_sheets = obj;
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: js.Object) !void {
|
|
||||||
self.adopted_style_sheets = try sheets.persist();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_innerHTML(self: *ShadowRoot, page: *Page) ![]const u8 {
|
|
||||||
var aw = std.Io.Writer.Allocating.init(page.call_arena);
|
|
||||||
try dump.writeChildren(parser.documentFragmentToNode(self.proto), .{}, &aw.writer);
|
|
||||||
return aw.written();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_innerHTML(self: *ShadowRoot, str_: ?[]const u8) !void {
|
|
||||||
const sr_doc = parser.documentFragmentToNode(self.proto);
|
|
||||||
const doc = parser.nodeOwnerDocument(sr_doc) orelse return parser.DOMError.WrongDocument;
|
|
||||||
try Node.removeChildren(sr_doc);
|
|
||||||
const str = str_ orelse return;
|
|
||||||
|
|
||||||
const fragment = try parser.documentParseFragmentFromStr(doc, str);
|
|
||||||
const fragment_node = parser.documentFragmentToNode(fragment);
|
|
||||||
|
|
||||||
// Element.set_innerHTML also has some weirdness here. It isn't clear
|
|
||||||
// what should and shouldn't be set. Whatever string you pass to libdom,
|
|
||||||
// it always creates a full HTML document, with an html, head and body
|
|
||||||
// element.
|
|
||||||
// For ShadowRoot, it appears the only the children within the body should
|
|
||||||
// be set.
|
|
||||||
const html = parser.nodeFirstChild(fragment_node) orelse return;
|
|
||||||
const head = parser.nodeFirstChild(html) orelse return;
|
|
||||||
const body = parser.nodeNextSibling(head) orelse return;
|
|
||||||
|
|
||||||
const children = try parser.nodeGetChildNodes(body);
|
|
||||||
const ln = parser.nodeListLength(children);
|
|
||||||
for (0..ln) |_| {
|
|
||||||
// always index 0, because nodeAppendChild moves the node out of
|
|
||||||
// the nodeList and into the new tree
|
|
||||||
const child = parser.nodeListItem(children, 0) orelse continue;
|
|
||||||
_ = try parser.nodeAppendChild(sr_doc, child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.ShadowRoot" {
|
|
||||||
try testing.htmlRunner("dom/shadow_root.html");
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const CharacterData = @import("character_data.zig").CharacterData;
|
|
||||||
const CDATASection = @import("cdata_section.zig").CDATASection;
|
|
||||||
|
|
||||||
// Text interfaces
|
|
||||||
pub const Interfaces = .{
|
|
||||||
CDATASection,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Text = struct {
|
|
||||||
pub const Self = parser.Text;
|
|
||||||
pub const prototype = *CharacterData;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub fn constructor(data: ?[]const u8, page: *const Page) !*parser.Text {
|
|
||||||
return parser.documentCreateTextNode(
|
|
||||||
parser.documentHTMLToDocument(page.window.document),
|
|
||||||
data orelse "",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// JS funcs
|
|
||||||
// --------
|
|
||||||
|
|
||||||
// Read attributes
|
|
||||||
|
|
||||||
pub fn get_wholeText(self: *parser.Text) ![]const u8 {
|
|
||||||
return try parser.textWholdeText(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// JS methods
|
|
||||||
// ----------
|
|
||||||
|
|
||||||
pub fn _splitText(self: *parser.Text, offset: u32) !*parser.Text {
|
|
||||||
return try parser.textSplitText(self, offset);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.Text" {
|
|
||||||
try testing.htmlRunner("dom/text.html");
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const iterator = @import("../iterator/iterator.zig");
|
|
||||||
|
|
||||||
const DOMException = @import("exceptions.zig").DOMException;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
DOMTokenList,
|
|
||||||
DOMTokenListIterable,
|
|
||||||
TokenListEntriesIterator,
|
|
||||||
TokenListEntriesIterator.Iterable,
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#domtokenlist
|
|
||||||
pub const DOMTokenList = struct {
|
|
||||||
pub const Self = parser.TokenList;
|
|
||||||
pub const Exception = DOMException;
|
|
||||||
|
|
||||||
pub fn get_length(self: *parser.TokenList) !u32 {
|
|
||||||
return parser.tokenListGetLength(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _item(self: *parser.TokenList, index: u32) !?[]const u8 {
|
|
||||||
return parser.tokenListItem(self, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _contains(self: *parser.TokenList, token: []const u8) !bool {
|
|
||||||
return parser.tokenListContains(self, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _add(self: *parser.TokenList, tokens: []const []const u8) !void {
|
|
||||||
for (tokens) |token| {
|
|
||||||
try parser.tokenListAdd(self, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _remove(self: *parser.TokenList, tokens: []const []const u8) !void {
|
|
||||||
for (tokens) |token| {
|
|
||||||
try parser.tokenListRemove(self, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If token is the empty string, then throw a "SyntaxError" DOMException.
|
|
||||||
/// If token contains any ASCII whitespace, then throw an
|
|
||||||
/// "InvalidCharacterError" DOMException.
|
|
||||||
fn validateToken(token: []const u8) !void {
|
|
||||||
if (token.len == 0) {
|
|
||||||
return parser.DOMError.Syntax;
|
|
||||||
}
|
|
||||||
for (token) |c| {
|
|
||||||
if (std.ascii.isWhitespace(c)) return parser.DOMError.InvalidCharacter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _toggle(self: *parser.TokenList, token: []const u8, force: ?bool) !bool {
|
|
||||||
try validateToken(token);
|
|
||||||
const exists = try parser.tokenListContains(self, token);
|
|
||||||
if (exists) {
|
|
||||||
if (force == null or force.? == false) {
|
|
||||||
try parser.tokenListRemove(self, token);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (force == null or force.? == true) {
|
|
||||||
try parser.tokenListAdd(self, token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _replace(self: *parser.TokenList, token: []const u8, new: []const u8) !bool {
|
|
||||||
try validateToken(token);
|
|
||||||
try validateToken(new);
|
|
||||||
const exists = try parser.tokenListContains(self, token);
|
|
||||||
if (!exists) return false;
|
|
||||||
try parser.tokenListRemove(self, token);
|
|
||||||
try parser.tokenListAdd(self, new);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO to implement.
|
|
||||||
pub fn _supports(_: *parser.TokenList, token: []const u8) !bool {
|
|
||||||
try validateToken(token);
|
|
||||||
return error.TypeError;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_value(self: *parser.TokenList) !?[]const u8 {
|
|
||||||
return (try parser.tokenListGetValue(self)) orelse "";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_value(self: *parser.TokenList, value: []const u8) !void {
|
|
||||||
return parser.tokenListSetValue(self, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _toString(self: *parser.TokenList) ![]const u8 {
|
|
||||||
return (try get_value(self)) orelse "";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _keys(self: *parser.TokenList) !iterator.U32Iterator {
|
|
||||||
return .{ .length = try get_length(self) };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _values(self: *parser.TokenList) DOMTokenListIterable {
|
|
||||||
return DOMTokenListIterable.init(.{ .token_list = self });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _entries(self: *parser.TokenList) TokenListEntriesIterator {
|
|
||||||
return TokenListEntriesIterator.init(.{ .token_list = self });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _symbol_iterator(self: *parser.TokenList) DOMTokenListIterable {
|
|
||||||
return _values(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO handle thisArg
|
|
||||||
pub fn _forEach(self: *parser.TokenList, cbk: js.Function, this_arg: js.Object) !void {
|
|
||||||
var entries = _entries(self);
|
|
||||||
while (try entries._next()) |entry| {
|
|
||||||
var result: js.Function.Result = undefined;
|
|
||||||
cbk.tryCallWithThis(void, this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch {
|
|
||||||
log.debug(.user_script, "callback error", .{
|
|
||||||
.err = result.exception,
|
|
||||||
.stack = result.stack,
|
|
||||||
.soure = "tokenList foreach",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const DOMTokenListIterable = iterator.Iterable(Iterator, "DOMTokenListIterable");
|
|
||||||
const TokenListEntriesIterator = iterator.NumericEntries(Iterator, "TokenListEntriesIterator");
|
|
||||||
|
|
||||||
pub const Iterator = struct {
|
|
||||||
index: u32 = 0,
|
|
||||||
token_list: *parser.TokenList,
|
|
||||||
|
|
||||||
// used when wrapped in an iterator.NumericEntries
|
|
||||||
pub const Error = parser.DOMError;
|
|
||||||
|
|
||||||
pub fn _next(self: *Iterator) !?[]const u8 {
|
|
||||||
const index = self.index;
|
|
||||||
self.index = index + 1;
|
|
||||||
return DOMTokenList._item(self.token_list, index);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.TokenList" {
|
|
||||||
try testing.htmlRunner("dom/token_list.html");
|
|
||||||
}
|
|
||||||
@@ -1,315 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const NodeFilter = @import("node_filter.zig");
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
const NodeUnion = @import("node.zig").Union;
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
|
|
||||||
pub const TreeWalker = struct {
|
|
||||||
root: *parser.Node,
|
|
||||||
current_node: *parser.Node,
|
|
||||||
what_to_show: u32,
|
|
||||||
filter: ?TreeWalkerOpts,
|
|
||||||
filter_func: ?js.Function,
|
|
||||||
|
|
||||||
// One of the few cases where null and undefined resolve to different default.
|
|
||||||
// We need the raw JsObject so that we can probe the tri state:
|
|
||||||
// null, undefined or i32.
|
|
||||||
pub const WhatToShow = js.Object;
|
|
||||||
|
|
||||||
pub const TreeWalkerOpts = union(enum) {
|
|
||||||
function: js.Function,
|
|
||||||
object: struct { acceptNode: js.Function },
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?TreeWalkerOpts) !TreeWalker {
|
|
||||||
var filter_func: ?js.Function = null;
|
|
||||||
|
|
||||||
if (filter) |f| {
|
|
||||||
filter_func = switch (f) {
|
|
||||||
.function => |func| func,
|
|
||||||
.object => |o| o.acceptNode,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var what_to_show: u32 = undefined;
|
|
||||||
if (what_to_show_) |wts| {
|
|
||||||
switch (try wts.triState(TreeWalker, "what_to_show", u32)) {
|
|
||||||
.null => what_to_show = 0,
|
|
||||||
.undefined => what_to_show = NodeFilter.NodeFilter._SHOW_ALL,
|
|
||||||
.value => |v| what_to_show = v,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
what_to_show = NodeFilter.NodeFilter._SHOW_ALL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.root = node,
|
|
||||||
.current_node = node,
|
|
||||||
.what_to_show = what_to_show,
|
|
||||||
.filter = filter,
|
|
||||||
.filter_func = filter_func,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_root(self: *TreeWalker) !NodeUnion {
|
|
||||||
return try Node.toInterface(self.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_currentNode(self: *TreeWalker) !NodeUnion {
|
|
||||||
return try Node.toInterface(self.current_node);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_whatToShow(self: *TreeWalker) u32 {
|
|
||||||
return self.what_to_show;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_filter(self: *TreeWalker) ?TreeWalkerOpts {
|
|
||||||
return self.filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_currentNode(self: *TreeWalker, node: *parser.Node) !void {
|
|
||||||
self.current_node = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn firstChild(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
|
||||||
const children = try parser.nodeGetChildNodes(node);
|
|
||||||
const child_count = parser.nodeListLength(children);
|
|
||||||
|
|
||||||
for (0..child_count) |i| {
|
|
||||||
const index: u32 = @intCast(i);
|
|
||||||
const child = (parser.nodeListItem(children, index)) orelse return null;
|
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
|
||||||
.accept => return child,
|
|
||||||
.reject => continue,
|
|
||||||
.skip => if (try self.firstChild(child)) |gchild| return gchild,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lastChild(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
|
||||||
const children = try parser.nodeGetChildNodes(node);
|
|
||||||
const child_count = parser.nodeListLength(children);
|
|
||||||
|
|
||||||
var index: u32 = child_count;
|
|
||||||
while (index > 0) {
|
|
||||||
index -= 1;
|
|
||||||
const child = (parser.nodeListItem(children, index)) orelse return null;
|
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
|
||||||
.accept => return child,
|
|
||||||
.reject => continue,
|
|
||||||
.skip => if (try self.lastChild(child)) |gchild| return gchild,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nextSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
|
||||||
var current = node;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
current = (parser.nodeNextSibling(current)) orelse return null;
|
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
|
||||||
.accept => return current,
|
|
||||||
.skip, .reject => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the next sibling that is either acceptable or should be descended into (skip)
|
|
||||||
fn nextSiblingOrSkip(self: *const TreeWalker, node: *parser.Node) !?struct { node: *parser.Node, should_descend: bool } {
|
|
||||||
var current = node;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
current = (parser.nodeNextSibling(current)) orelse return null;
|
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
|
||||||
.accept => return .{ .node = current, .should_descend = false },
|
|
||||||
.skip => return .{ .node = current, .should_descend = true },
|
|
||||||
.reject => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn previousSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
|
||||||
var current = node;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
current = (parser.nodePreviousSibling(current)) orelse return null;
|
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
|
||||||
.accept => return current,
|
|
||||||
.skip, .reject => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parentNode(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
|
||||||
if (self.root == node) return null;
|
|
||||||
|
|
||||||
var current = node;
|
|
||||||
while (true) {
|
|
||||||
if (current == self.root) return null;
|
|
||||||
current = (parser.nodeParentNode(current)) orelse return null;
|
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
|
||||||
.accept => return current,
|
|
||||||
.reject, .skip => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _firstChild(self: *TreeWalker) !?NodeUnion {
|
|
||||||
if (try self.firstChild(self.current_node)) |child| {
|
|
||||||
self.current_node = child;
|
|
||||||
return try Node.toInterface(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _lastChild(self: *TreeWalker) !?NodeUnion {
|
|
||||||
if (try self.lastChild(self.current_node)) |child| {
|
|
||||||
self.current_node = child;
|
|
||||||
return try Node.toInterface(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _nextNode(self: *TreeWalker) !?NodeUnion {
|
|
||||||
var current = self.current_node;
|
|
||||||
|
|
||||||
// First, try to go to first child of current node
|
|
||||||
if (try self.firstChild(current)) |child| {
|
|
||||||
self.current_node = child;
|
|
||||||
return try Node.toInterface(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
// No acceptable children, move to next node in tree
|
|
||||||
while (current != self.root) {
|
|
||||||
const result = try self.nextSiblingOrSkip(current) orelse {
|
|
||||||
// No next sibling, go up to parent and continue
|
|
||||||
// or, if there is no parent, we're done
|
|
||||||
current = (parser.nodeParentNode(current)) orelse break;
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
if (!result.should_descend) {
|
|
||||||
// This is an .accept node - return it
|
|
||||||
self.current_node = result.node;
|
|
||||||
return try Node.toInterface(result.node);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a .skip node - try to find acceptable children within it
|
|
||||||
if (try self.firstChild(result.node)) |child| {
|
|
||||||
self.current_node = child;
|
|
||||||
return try Node.toInterface(child);
|
|
||||||
}
|
|
||||||
// No acceptable children, continue looking at this node's siblings
|
|
||||||
current = result.node;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _nextSibling(self: *TreeWalker) !?NodeUnion {
|
|
||||||
if (try self.nextSibling(self.current_node)) |sibling| {
|
|
||||||
self.current_node = sibling;
|
|
||||||
return try Node.toInterface(sibling);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _parentNode(self: *TreeWalker) !?NodeUnion {
|
|
||||||
if (try self.parentNode(self.current_node)) |parent| {
|
|
||||||
self.current_node = parent;
|
|
||||||
return try Node.toInterface(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _previousNode(self: *TreeWalker) !?NodeUnion {
|
|
||||||
if (self.current_node == self.root) return null;
|
|
||||||
|
|
||||||
var current = self.current_node;
|
|
||||||
while (parser.nodePreviousSibling(current)) |previous| {
|
|
||||||
current = previous;
|
|
||||||
|
|
||||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
|
||||||
.accept => {
|
|
||||||
// Get last child if it has one.
|
|
||||||
if (try self.lastChild(current)) |child| {
|
|
||||||
self.current_node = child;
|
|
||||||
return try Node.toInterface(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, this node is our previous one.
|
|
||||||
self.current_node = current;
|
|
||||||
return try Node.toInterface(current);
|
|
||||||
},
|
|
||||||
.reject => continue,
|
|
||||||
.skip => {
|
|
||||||
// Get last child if it has one.
|
|
||||||
if (try self.lastChild(current)) |child| {
|
|
||||||
self.current_node = child;
|
|
||||||
return try Node.toInterface(child);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current != self.root) {
|
|
||||||
if (try self.parentNode(current)) |parent| {
|
|
||||||
self.current_node = parent;
|
|
||||||
return try Node.toInterface(parent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _previousSibling(self: *TreeWalker) !?NodeUnion {
|
|
||||||
if (try self.previousSibling(self.current_node)) |sibling| {
|
|
||||||
self.current_node = sibling;
|
|
||||||
return try Node.toInterface(sibling);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
pub const Walker = union(enum) {
|
|
||||||
walkerDepthFirst: WalkerDepthFirst,
|
|
||||||
walkerChildren: WalkerChildren,
|
|
||||||
walkerNone: WalkerNone,
|
|
||||||
|
|
||||||
pub fn get_next(self: Walker, root: *parser.Node, cur: ?*parser.Node) !?*parser.Node {
|
|
||||||
switch (self) {
|
|
||||||
inline else => |case| return case.get_next(root, cur),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// WalkerDepthFirst iterates over the DOM tree to return the next following
|
|
||||||
// node or null at the end.
|
|
||||||
//
|
|
||||||
// This implementation is a zig version of Netsurf code.
|
|
||||||
// http://source.netsurf-browser.org/libdom.git/tree/src/html/html_collection.c#n177
|
|
||||||
//
|
|
||||||
// The iteration is a depth first as required by the specification.
|
|
||||||
// https://dom.spec.whatwg.org/#htmlcollection
|
|
||||||
// https://dom.spec.whatwg.org/#concept-tree-order
|
|
||||||
pub const WalkerDepthFirst = struct {
|
|
||||||
pub fn get_next(_: WalkerDepthFirst, root: *parser.Node, cur: ?*parser.Node) !?*parser.Node {
|
|
||||||
var n = cur orelse root;
|
|
||||||
|
|
||||||
// TODO deinit next
|
|
||||||
if (parser.nodeFirstChild(n)) |next| {
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO deinit next
|
|
||||||
if (parser.nodeNextSibling(n)) |next| {
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO deinit parent
|
|
||||||
// Back to the parent of cur.
|
|
||||||
// If cur has no parent, then the iteration is over.
|
|
||||||
var parent = parser.nodeParentNode(n) orelse return null;
|
|
||||||
|
|
||||||
// TODO deinit lastchild
|
|
||||||
var lastchild = parser.nodeLastChild(parent);
|
|
||||||
while (n != root and n == lastchild) {
|
|
||||||
n = parent;
|
|
||||||
|
|
||||||
// TODO deinit parent
|
|
||||||
// Back to the prev's parent.
|
|
||||||
// If prev has no parent, then the loop must stop.
|
|
||||||
parent = parser.nodeParentNode(n) orelse break;
|
|
||||||
|
|
||||||
// TODO deinit lastchild
|
|
||||||
lastchild = parser.nodeLastChild(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n == root) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parser.nodeNextSibling(n);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// WalkerChildren iterates over the root's children only.
|
|
||||||
pub const WalkerChildren = struct {
|
|
||||||
pub fn get_next(_: WalkerChildren, root: *parser.Node, cur: ?*parser.Node) !?*parser.Node {
|
|
||||||
// On walk start, we return the first root's child.
|
|
||||||
if (cur == null) return parser.nodeFirstChild(root);
|
|
||||||
|
|
||||||
// If cur is root, then return null.
|
|
||||||
// This is a special case, if the root is included in the walk, we
|
|
||||||
// don't want to go further to find children.
|
|
||||||
if (root == cur.?) return null;
|
|
||||||
|
|
||||||
return parser.nodeNextSibling(cur.?);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const WalkerNone = struct {
|
|
||||||
pub fn get_next(_: WalkerNone, _: *parser.Node, _: ?*parser.Node) !?*parser.Node {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -17,323 +17,344 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
const Slot = @import("webapi/element/html/Slot.zig");
|
||||||
|
const IFrame = @import("webapi/element/html/IFrame.zig");
|
||||||
|
|
||||||
const parser = @import("netsurf.zig");
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
const Page = @import("page.zig").Page;
|
|
||||||
const Walker = @import("dom/walker.zig").WalkerChildren;
|
|
||||||
|
|
||||||
pub const Opts = struct {
|
pub const Opts = struct {
|
||||||
// set to include element shadowroots in the dump
|
with_base: bool = false,
|
||||||
page: ?*const Page = null,
|
with_frames: bool = false,
|
||||||
|
strip: Opts.Strip = .{},
|
||||||
|
shadow: Opts.Shadow = .rendered,
|
||||||
|
|
||||||
strip_mode: StripMode = .{},
|
pub const Strip = struct {
|
||||||
|
|
||||||
pub const StripMode = struct {
|
|
||||||
js: bool = false,
|
js: bool = false,
|
||||||
ui: bool = false,
|
ui: bool = false,
|
||||||
css: bool = false,
|
css: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const Shadow = enum {
|
||||||
|
// Skip shadow DOM entirely (innerHTML/outerHTML)
|
||||||
|
skip,
|
||||||
|
|
||||||
|
// Dump everyhting (like "view source")
|
||||||
|
complete,
|
||||||
|
|
||||||
|
// Resolve slot elements (like what actually gets rendered)
|
||||||
|
rendered,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// writer must be a std.io.Writer
|
pub fn root(doc: *Node.Document, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
pub fn writeHTML(doc: *parser.Document, opts: Opts, writer: *std.Io.Writer) !void {
|
if (doc.is(Node.Document.HTMLDocument)) |html_doc| {
|
||||||
try writer.writeAll("<!DOCTYPE html>\n");
|
blk: {
|
||||||
try writeChildren(parser.documentToNode(doc), opts, writer);
|
// Ideally we just render the doctype which is part of the document
|
||||||
try writer.writeAll("\n");
|
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>");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spec: https://www.w3.org/TR/xml/#sec-prolog-dtd
|
if (opts.with_base) {
|
||||||
pub fn writeDocType(doc_type: *parser.DocumentType, writer: *std.Io.Writer) !void {
|
const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode();
|
||||||
try writer.writeAll("<!DOCTYPE ");
|
const base = try doc.createElement("base", null, page);
|
||||||
try writer.writeAll(try parser.documentTypeGetName(doc_type));
|
try base.setAttributeSafe(comptime .wrap("base"), .wrap(page.base()), page);
|
||||||
|
_ = try parent.insertBefore(base.asNode(), parent.firstChild(), page);
|
||||||
const public_id = parser.documentTypeGetPublicId(doc_type);
|
|
||||||
const system_id = parser.documentTypeGetSystemId(doc_type);
|
|
||||||
if (public_id.len != 0 and system_id.len != 0) {
|
|
||||||
try writer.writeAll(" PUBLIC \"");
|
|
||||||
try writeEscapedAttributeValue(writer, public_id);
|
|
||||||
try writer.writeAll("\" \"");
|
|
||||||
try writeEscapedAttributeValue(writer, system_id);
|
|
||||||
try writer.writeAll("\"");
|
|
||||||
} else if (public_id.len != 0) {
|
|
||||||
try writer.writeAll(" PUBLIC \"");
|
|
||||||
try writeEscapedAttributeValue(writer, public_id);
|
|
||||||
try writer.writeAll("\"");
|
|
||||||
} else if (system_id.len != 0) {
|
|
||||||
try writer.writeAll(" SYSTEM \"");
|
|
||||||
try writeEscapedAttributeValue(writer, system_id);
|
|
||||||
try writer.writeAll("\"");
|
|
||||||
}
|
}
|
||||||
// Internal subset is not implemented
|
|
||||||
try writer.writeAll(">");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerror!void {
|
return deep(doc.asNode(), opts, writer, page);
|
||||||
switch (parser.nodeType(node)) {
|
}
|
||||||
.element => {
|
|
||||||
// open the tag
|
pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {
|
||||||
const tag_type = try parser.nodeHTMLGetTagType(node) orelse .undef;
|
return _deep(node, opts, false, writer, page);
|
||||||
if (try isStripped(tag_type, node, opts.strip_mode)) {
|
}
|
||||||
|
|
||||||
|
fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {
|
||||||
|
switch (node._type) {
|
||||||
|
.cdata => |cd| {
|
||||||
|
if (node.is(Node.CData.Comment)) |_| {
|
||||||
|
try writer.writeAll("<!--");
|
||||||
|
try writer.writeAll(cd.getData().str());
|
||||||
|
try writer.writeAll("-->");
|
||||||
|
} else if (node.is(Node.CData.ProcessingInstruction)) |pi| {
|
||||||
|
try writer.writeAll("<?");
|
||||||
|
try writer.writeAll(pi._target);
|
||||||
|
try writer.writeAll(" ");
|
||||||
|
try writer.writeAll(cd.getData().str());
|
||||||
|
try writer.writeAll("?>");
|
||||||
|
} else {
|
||||||
|
if (shouldEscapeText(node._parent)) {
|
||||||
|
try writeEscapedText(cd.getData().str(), writer);
|
||||||
|
} else {
|
||||||
|
try writer.writeAll(cd.getData().str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.element => |el| {
|
||||||
|
if (shouldStripElement(el, opts)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tag = try parser.nodeLocalName(node);
|
// When opts.shadow == .rendered, we normally skip any element with
|
||||||
try writer.writeAll("<");
|
// a slot attribute. Only the "active" element will get rendered into
|
||||||
try writer.writeAll(tag);
|
// the <slot name="X">. However, the `deep` function is itself used
|
||||||
|
// to render that "active" content, so when we're trying to render
|
||||||
// write the attributes
|
// it, we don't want to skip it.
|
||||||
const _map = try parser.nodeGetAttributes(node);
|
if ((comptime force_slot == false) and opts.shadow == .rendered) {
|
||||||
if (_map) |map| {
|
if (el.getAttributeSafe(comptime .wrap("slot"))) |_| {
|
||||||
const ln = try parser.namedNodeMapGetLength(map);
|
// Skip - will be rendered by the Slot if it's the active container
|
||||||
for (0..ln) |i| {
|
return;
|
||||||
const attr = try parser.namedNodeMapItem(map, @intCast(i)) orelse break;
|
|
||||||
try writer.writeAll(" ");
|
|
||||||
try writer.writeAll(try parser.attributeGetName(attr));
|
|
||||||
try writer.writeAll("=\"");
|
|
||||||
const attribute_value = try parser.attributeGetValue(attr) orelse "";
|
|
||||||
try writeEscapedAttributeValue(writer, attribute_value);
|
|
||||||
try writer.writeAll("\"");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try writer.writeAll(">");
|
try el.format(writer);
|
||||||
|
|
||||||
if (opts.page) |page| {
|
if (opts.shadow == .rendered) {
|
||||||
if (page.getNodeState(node)) |state| {
|
if (el.is(Slot)) |slot| {
|
||||||
if (state.shadow_root) |sr| {
|
try dumpSlotContent(slot, opts, writer, page);
|
||||||
try writeChildren(@ptrCast(@alignCast(sr.proto)), opts, writer);
|
return writer.writeAll("</slot>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (opts.shadow != .skip) {
|
||||||
|
if (page._element_shadow_roots.get(el)) |shadow| {
|
||||||
// void elements can't have any content.
|
try children(shadow.asNode(), opts, writer, page);
|
||||||
if (try isVoid(parser.nodeToElement(node))) return;
|
// In rendered mode, light DOM is only shown through slots, not directly
|
||||||
|
if (opts.shadow == .rendered) {
|
||||||
if (tag_type == .script) {
|
// Skip rendering light DOM children
|
||||||
try writer.writeAll(parser.nodeTextContent(node) orelse "");
|
if (!isVoidElement(el)) {
|
||||||
} else {
|
|
||||||
// write the children
|
|
||||||
// TODO avoid recursion
|
|
||||||
try writeChildren(node, opts, writer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// close the tag
|
|
||||||
try writer.writeAll("</");
|
try writer.writeAll("</");
|
||||||
try writer.writeAll(tag);
|
try writer.writeAll(el.getTagNameDump());
|
||||||
try writer.writeAll(">");
|
try writer.writeByte('>');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isVoidElement(el)) {
|
||||||
|
try writer.writeAll("</");
|
||||||
|
try writer.writeAll(el.getTagNameDump());
|
||||||
|
try writer.writeByte('>');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
.text => {
|
.document => try children(node, opts, writer, page),
|
||||||
const v = parser.nodeValue(node) orelse return;
|
.document_type => |dt| {
|
||||||
try writeEscapedTextNode(writer, v);
|
try writer.writeAll("<!DOCTYPE ");
|
||||||
|
try writer.writeAll(dt.getName());
|
||||||
|
|
||||||
|
const public_id = dt.getPublicId();
|
||||||
|
const system_id = dt.getSystemId();
|
||||||
|
if (public_id.len != 0 and system_id.len != 0) {
|
||||||
|
try writer.writeAll(" PUBLIC \"");
|
||||||
|
try writeEscapedText(public_id, writer);
|
||||||
|
try writer.writeAll("\" \"");
|
||||||
|
try writeEscapedText(system_id, writer);
|
||||||
|
try writer.writeByte('"');
|
||||||
|
} else if (public_id.len != 0) {
|
||||||
|
try writer.writeAll(" PUBLIC \"");
|
||||||
|
try writeEscapedText(public_id, writer);
|
||||||
|
try writer.writeByte('"');
|
||||||
|
} else if (system_id.len != 0) {
|
||||||
|
try writer.writeAll(" SYSTEM \"");
|
||||||
|
try writeEscapedText(system_id, writer);
|
||||||
|
try writer.writeByte('"');
|
||||||
|
}
|
||||||
|
try writer.writeAll(">\n");
|
||||||
},
|
},
|
||||||
.cdata_section => {
|
.document_fragment => try children(node, opts, writer, page),
|
||||||
const v = parser.nodeValue(node) orelse return;
|
.attribute => {
|
||||||
try writer.writeAll("<![CDATA[");
|
// Not called normally, but can be called via XMLSerializer.serializeToString
|
||||||
try writer.writeAll(v);
|
// in which case it should return an empty string
|
||||||
try writer.writeAll("]]>");
|
try writer.writeAll("");
|
||||||
},
|
},
|
||||||
.comment => {
|
}
|
||||||
const v = parser.nodeValue(node) orelse return;
|
}
|
||||||
try writer.writeAll("<!--");
|
|
||||||
try writer.writeAll(v);
|
pub fn children(parent: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
try writer.writeAll("-->");
|
var it = parent.childrenIterator();
|
||||||
|
while (it.next()) |child| {
|
||||||
|
try deep(child, opts, writer, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toJSON(node: *Node, writer: *std.json.Stringify) !void {
|
||||||
|
try writer.beginObject();
|
||||||
|
|
||||||
|
try writer.objectField("type");
|
||||||
|
switch (node.type) {
|
||||||
|
.cdata => {
|
||||||
|
try writer.write("cdata");
|
||||||
},
|
},
|
||||||
// TODO handle processing instruction dump
|
.document => {
|
||||||
.processing_instruction => return,
|
try writer.write("document");
|
||||||
// document fragment is outside of the main document DOM, so we
|
},
|
||||||
// don't output it.
|
.document_type => {
|
||||||
.document_fragment => return,
|
try writer.write("document_type");
|
||||||
// document will never be called, but required for completeness.
|
},
|
||||||
.document => return,
|
.element => |*el| {
|
||||||
// done globally instead, but required for completeness. Only the outer DOCTYPE should be written
|
try writer.write("element");
|
||||||
.document_type => return,
|
try writer.objectField("tag");
|
||||||
// deprecated
|
try writer.write(el.tagName());
|
||||||
.attribute => return,
|
|
||||||
.entity_reference => return,
|
try writer.objectField("attributes");
|
||||||
.entity => return,
|
try writer.beginObject();
|
||||||
.notation => return,
|
var it = el.attributeIterator();
|
||||||
|
while (it.next()) |attr| {
|
||||||
|
try writer.objectField(attr.name);
|
||||||
|
try writer.write(attr.value);
|
||||||
|
}
|
||||||
|
try writer.endObject();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
try writer.objectField("children");
|
||||||
|
try writer.beginArray();
|
||||||
|
var it = node.childrenIterator();
|
||||||
|
while (it.next()) |child| {
|
||||||
|
try toJSON(child, writer);
|
||||||
|
}
|
||||||
|
try writer.endArray();
|
||||||
|
try writer.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dumpSlotContent(slot: *Slot, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
|
const assigned = slot.assignedNodes(null, page) catch return;
|
||||||
|
|
||||||
|
if (assigned.len > 0) {
|
||||||
|
for (assigned) |assigned_node| {
|
||||||
|
try _deep(assigned_node, opts, true, writer, page);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try children(slot.asNode(), opts, writer, page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// writer must be a std.io.Writer
|
fn isVoidElement(el: *const Node.Element) bool {
|
||||||
pub fn writeChildren(root: *parser.Node, opts: Opts, writer: *std.Io.Writer) !void {
|
return switch (el._type) {
|
||||||
const walker = Walker{};
|
.html => |html| switch (html._type) {
|
||||||
var next: ?*parser.Node = null;
|
.br, .hr, .img, .input, .link, .meta => true,
|
||||||
while (true) {
|
|
||||||
next = try walker.get_next(root, next) orelse break;
|
|
||||||
try writeNode(next.?, opts, writer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isStripped(tag_type: parser.Tag, node: *parser.Node, strip_mode: Opts.StripMode) !bool {
|
|
||||||
if (strip_mode.js and try isJsRelated(tag_type, node)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strip_mode.css and try isCssRelated(tag_type, node)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strip_mode.ui and try isUIRelated(tag_type, node)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isJsRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
|
|
||||||
if (tag_type == .script) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (tag_type == .link) {
|
|
||||||
const el = parser.nodeToElement(node);
|
|
||||||
const as = try parser.elementGetAttribute(el, "as") orelse return false;
|
|
||||||
if (!std.ascii.eqlIgnoreCase(as, "script")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rel = try parser.elementGetAttribute(el, "rel") orelse return false;
|
|
||||||
return std.ascii.eqlIgnoreCase(rel, "preload");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isCssRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
|
|
||||||
if (tag_type == .style) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (tag_type == .link) {
|
|
||||||
const el = parser.nodeToElement(node);
|
|
||||||
const rel = try parser.elementGetAttribute(el, "rel") orelse return false;
|
|
||||||
return std.ascii.eqlIgnoreCase(rel, "stylesheet");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isUIRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
|
|
||||||
if (try isCssRelated(tag_type, node)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (tag_type == .img or tag_type == .picture or tag_type == .video) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (tag_type == .undef) {
|
|
||||||
const name = try parser.nodeLocalName(node);
|
|
||||||
if (std.mem.eql(u8, name, "svg")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
|
|
||||||
// https://html.spec.whatwg.org/#void-elements
|
|
||||||
fn isVoid(elem: *parser.Element) !bool {
|
|
||||||
const tag = try parser.elementTag(elem);
|
|
||||||
return switch (tag) {
|
|
||||||
.area, .base, .br, .col, .embed, .hr, .img, .input, .link => true,
|
|
||||||
.meta, .source, .track, .wbr => true,
|
|
||||||
else => false,
|
else => false,
|
||||||
|
},
|
||||||
|
.svg => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn writeEscapedTextNode(writer: *std.Io.Writer, value: []const u8) !void {
|
fn shouldStripElement(el: *const Node.Element, opts: Opts) bool {
|
||||||
var v = value;
|
const tag_name = el.getTagNameDump();
|
||||||
while (v.len > 0) {
|
|
||||||
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', 194 }) orelse {
|
if (opts.strip.js) {
|
||||||
return writer.writeAll(v);
|
if (std.mem.eql(u8, tag_name, "script")) return true;
|
||||||
|
if (std.mem.eql(u8, tag_name, "noscript")) return true;
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, tag_name, "link")) {
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("as"))) |as| {
|
||||||
|
if (std.mem.eql(u8, as, "script")) return true;
|
||||||
|
}
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("rel"))) |rel| {
|
||||||
|
if (std.mem.eql(u8, rel, "modulepreload") or std.mem.eql(u8, rel, "preload")) {
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("as"))) |as| {
|
||||||
|
if (std.mem.eql(u8, as, "script")) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.strip.css or opts.strip.ui) {
|
||||||
|
if (std.mem.eql(u8, tag_name, "style")) return true;
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, tag_name, "link")) {
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("rel"))) |rel| {
|
||||||
|
if (std.mem.eql(u8, rel, "stylesheet")) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.strip.ui) {
|
||||||
|
if (std.mem.eql(u8, tag_name, "img")) return true;
|
||||||
|
if (std.mem.eql(u8, tag_name, "picture")) return true;
|
||||||
|
if (std.mem.eql(u8, tag_name, "video")) return true;
|
||||||
|
if (std.mem.eql(u8, tag_name, "audio")) return true;
|
||||||
|
if (std.mem.eql(u8, tag_name, "svg")) return true;
|
||||||
|
if (std.mem.eql(u8, tag_name, "canvas")) return true;
|
||||||
|
if (std.mem.eql(u8, tag_name, "iframe")) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shouldEscapeText(node_: ?*Node) bool {
|
||||||
|
const node = node_ orelse return true;
|
||||||
|
if (node.is(Node.Element.Html.Script) != null) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
fn writeEscapedText(text: []const u8, writer: *std.Io.Writer) !void {
|
||||||
|
// Fast path: if no special characters, write directly
|
||||||
|
const first_special = std.mem.indexOfAnyPos(u8, text, 0, &.{ '&', '<', '>', 194 }) orelse {
|
||||||
|
return writer.writeAll(text);
|
||||||
};
|
};
|
||||||
try writer.writeAll(v[0..index]);
|
|
||||||
switch (v[index]) {
|
try writer.writeAll(text[0..first_special]);
|
||||||
|
var remaining = try writeEscapedByte(text, first_special, writer);
|
||||||
|
|
||||||
|
while (std.mem.indexOfAnyPos(u8, remaining, 0, &.{ '&', '<', '>', 194 })) |offset| {
|
||||||
|
try writer.writeAll(remaining[0..offset]);
|
||||||
|
remaining = try writeEscapedByte(remaining, offset, writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining.len > 0) {
|
||||||
|
try writer.writeAll(remaining);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeEscapedByte(input: []const u8, index: usize, writer: *std.Io.Writer) ![]const u8 {
|
||||||
|
switch (input[index]) {
|
||||||
'&' => try writer.writeAll("&"),
|
'&' => try writer.writeAll("&"),
|
||||||
'<' => try writer.writeAll("<"),
|
'<' => try writer.writeAll("<"),
|
||||||
'>' => try writer.writeAll(">"),
|
'>' => try writer.writeAll(">"),
|
||||||
194 => {
|
194 => {
|
||||||
// non breaking space
|
// non breaking space
|
||||||
if (v.len > index + 1 and v[index + 1] == 160) {
|
if (input.len > index + 1 and input[index + 1] == 160) {
|
||||||
try writer.writeAll(" ");
|
try writer.writeAll(" ");
|
||||||
v = v[index + 2 ..];
|
return input[index + 2 ..];
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
try writer.writeByte(194);
|
try writer.writeByte(194);
|
||||||
},
|
},
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
}
|
}
|
||||||
v = v[index + 1 ..];
|
return input[index + 1 ..];
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeEscapedAttributeValue(writer: *std.Io.Writer, value: []const u8) !void {
|
|
||||||
var v = value;
|
|
||||||
while (v.len > 0) {
|
|
||||||
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', '"' }) orelse {
|
|
||||||
return writer.writeAll(v);
|
|
||||||
};
|
|
||||||
try writer.writeAll(v[0..index]);
|
|
||||||
switch (v[index]) {
|
|
||||||
'&' => try writer.writeAll("&"),
|
|
||||||
'<' => try writer.writeAll("<"),
|
|
||||||
'>' => try writer.writeAll(">"),
|
|
||||||
'"' => try writer.writeAll("""),
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
v = v[index + 1 ..];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = std.testing;
|
|
||||||
test "dump.writeHTML" {
|
|
||||||
parser.init();
|
|
||||||
defer parser.deinit();
|
|
||||||
|
|
||||||
try testWriteHTML(
|
|
||||||
"<div id=\"content\">Over 9000!</div>",
|
|
||||||
"<div id=\"content\">Over 9000!</div>",
|
|
||||||
);
|
|
||||||
|
|
||||||
try testWriteHTML(
|
|
||||||
"<root><!-- a comment --></root>",
|
|
||||||
"<root><!-- a comment --></root>",
|
|
||||||
);
|
|
||||||
|
|
||||||
try testWriteHTML(
|
|
||||||
"<p>< > &</p>",
|
|
||||||
"<p>< > &</p>",
|
|
||||||
);
|
|
||||||
|
|
||||||
try testWriteHTML(
|
|
||||||
"<p id=\""><&"''\">wat?</p>",
|
|
||||||
"<p id='\"><&"'''>wat?</p>",
|
|
||||||
);
|
|
||||||
|
|
||||||
try testWriteFullHTML(
|
|
||||||
\\<!DOCTYPE html>
|
|
||||||
\\<html><head><title>It's over what?</title><meta name="a" value="b">
|
|
||||||
\\</head><body>9000</body></html>
|
|
||||||
\\
|
|
||||||
, "<html><title>It's over what?</title><meta name=a value=\"b\">\n<body>9000");
|
|
||||||
|
|
||||||
try testWriteHTML(
|
|
||||||
"<p>hi</p><script>alert(power > 9000)</script>",
|
|
||||||
"<p>hi</p><script>alert(power > 9000)</script>",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn testWriteHTML(comptime expected_body: []const u8, src: []const u8) !void {
|
|
||||||
const expected =
|
|
||||||
"<!DOCTYPE html>\n<html><head></head><body>" ++
|
|
||||||
expected_body ++
|
|
||||||
"</body></html>\n";
|
|
||||||
return testWriteFullHTML(expected, src);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn testWriteFullHTML(comptime expected: []const u8, src: []const u8) !void {
|
|
||||||
var aw = std.Io.Writer.Allocating.init(testing.allocator);
|
|
||||||
defer aw.deinit();
|
|
||||||
|
|
||||||
const doc_html = try parser.documentHTMLParseFromStr(src);
|
|
||||||
defer parser.documentHTMLClose(doc_html) catch {};
|
|
||||||
|
|
||||||
const doc = parser.documentHTMLToDocument(doc_html);
|
|
||||||
try writeHTML(doc, .{}, &aw.writer);
|
|
||||||
try testing.expectEqualStrings(expected, aw.written());
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
// https://encoding.spec.whatwg.org/#interface-textdecoder
|
|
||||||
const TextDecoder = @This();
|
|
||||||
|
|
||||||
const SupportedLabels = enum {
|
|
||||||
utf8,
|
|
||||||
@"utf-8",
|
|
||||||
@"unicode-1-1-utf-8",
|
|
||||||
};
|
|
||||||
|
|
||||||
const Options = struct {
|
|
||||||
fatal: bool = false,
|
|
||||||
ignoreBOM: bool = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
fatal: bool,
|
|
||||||
ignore_bom: bool,
|
|
||||||
stream: std.ArrayList(u8),
|
|
||||||
|
|
||||||
pub fn constructor(label_: ?[]const u8, opts_: ?Options) !TextDecoder {
|
|
||||||
if (label_) |l| {
|
|
||||||
_ = std.meta.stringToEnum(SupportedLabels, l) orelse {
|
|
||||||
log.warn(.web_api, "not implemented", .{ .feature = "TextDecoder label", .label = l });
|
|
||||||
return error.NotImplemented;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const opts = opts_ orelse Options{};
|
|
||||||
return .{
|
|
||||||
.stream = .empty,
|
|
||||||
.fatal = opts.fatal,
|
|
||||||
.ignore_bom = opts.ignoreBOM,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_encoding(_: *const TextDecoder) []const u8 {
|
|
||||||
return "utf-8";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_ignoreBOM(self: *const TextDecoder) bool {
|
|
||||||
return self.ignore_bom;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_fatal(self: *const TextDecoder) bool {
|
|
||||||
return self.fatal;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DecodeOptions = struct {
|
|
||||||
stream: bool = false,
|
|
||||||
};
|
|
||||||
pub fn _decode(self: *TextDecoder, str_: ?[]const u8, opts_: ?DecodeOptions, page: *Page) ![]const u8 {
|
|
||||||
var str = str_ orelse return "";
|
|
||||||
const opts: DecodeOptions = opts_ orelse .{};
|
|
||||||
|
|
||||||
if (self.stream.items.len > 0) {
|
|
||||||
try self.stream.appendSlice(page.arena, str);
|
|
||||||
str = self.stream.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.fatal and !std.unicode.utf8ValidateSlice(str)) {
|
|
||||||
if (opts.stream) {
|
|
||||||
if (self.stream.items.len == 0) {
|
|
||||||
try self.stream.appendSlice(page.arena, str);
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return error.InvalidUtf8;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.stream.clearRetainingCapacity();
|
|
||||||
if (self.ignore_bom == false and std.mem.startsWith(u8, str, &.{ 0xEF, 0xBB, 0xBF })) {
|
|
||||||
return str[3..];
|
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: Encoding.TextDecoder" {
|
|
||||||
try testing.htmlRunner("encoding/decoder.html");
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Event = @import("event.zig").Event;
|
|
||||||
|
|
||||||
const netsurf = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#interface-customevent
|
|
||||||
pub const CustomEvent = struct {
|
|
||||||
pub const prototype = *Event;
|
|
||||||
pub const union_make_copy = true;
|
|
||||||
|
|
||||||
proto: parser.Event,
|
|
||||||
detail: ?js.Object,
|
|
||||||
|
|
||||||
const CustomEventInit = struct {
|
|
||||||
bubbles: bool = false,
|
|
||||||
cancelable: bool = false,
|
|
||||||
composed: bool = false,
|
|
||||||
detail: ?js.Object = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn constructor(event_type: []const u8, opts_: ?CustomEventInit) !CustomEvent {
|
|
||||||
const opts = opts_ orelse CustomEventInit{};
|
|
||||||
|
|
||||||
const event = try parser.eventCreate();
|
|
||||||
defer parser.eventDestroy(event);
|
|
||||||
try parser.eventInit(event, event_type, .{
|
|
||||||
.bubbles = opts.bubbles,
|
|
||||||
.cancelable = opts.cancelable,
|
|
||||||
.composed = opts.composed,
|
|
||||||
});
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.proto = event.*,
|
|
||||||
.detail = if (opts.detail) |d| try d.persist() else null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_detail(self: *CustomEvent) ?js.Object {
|
|
||||||
return self.detail;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initializes an already created `CustomEvent`.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/initCustomEvent
|
|
||||||
pub fn _initCustomEvent(
|
|
||||||
self: *CustomEvent,
|
|
||||||
event_type: []const u8,
|
|
||||||
can_bubble: bool,
|
|
||||||
cancelable: bool,
|
|
||||||
maybe_detail: ?js.Object,
|
|
||||||
) !void {
|
|
||||||
// This function can only be called after the constructor has called.
|
|
||||||
// So we assume proto is initialized already by constructor.
|
|
||||||
self.proto.type = try netsurf.strFromData(event_type);
|
|
||||||
self.proto.bubble = can_bubble;
|
|
||||||
self.proto.cancelable = cancelable;
|
|
||||||
self.proto.is_initialised = true;
|
|
||||||
// Detail is stored separately.
|
|
||||||
if (maybe_detail) |detail| {
|
|
||||||
self.detail = try detail.persist();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: Events.Custom" {
|
|
||||||
try testing.htmlRunner("events/custom.html");
|
|
||||||
}
|
|
||||||
@@ -1,402 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const generate = @import("../js/generate.zig");
|
|
||||||
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const Node = @import("../dom/node.zig").Node;
|
|
||||||
const DOMException = @import("../dom/exceptions.zig").DOMException;
|
|
||||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
|
||||||
const EventTargetUnion = @import("../dom/event_target.zig").Union;
|
|
||||||
const AbortSignal = @import("../html/AbortController.zig").AbortSignal;
|
|
||||||
|
|
||||||
const CustomEvent = @import("custom_event.zig").CustomEvent;
|
|
||||||
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
|
|
||||||
const MouseEvent = @import("mouse_event.zig").MouseEvent;
|
|
||||||
const KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent;
|
|
||||||
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
|
|
||||||
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
|
|
||||||
const PopStateEvent = @import("../html/History.zig").PopStateEvent;
|
|
||||||
|
|
||||||
// Event interfaces
|
|
||||||
pub const Interfaces = .{
|
|
||||||
Event,
|
|
||||||
CustomEvent,
|
|
||||||
ProgressEvent,
|
|
||||||
MouseEvent,
|
|
||||||
KeyboardEvent,
|
|
||||||
ErrorEvent,
|
|
||||||
MessageEvent,
|
|
||||||
PopStateEvent,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Union = generate.Union(Interfaces);
|
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#event
|
|
||||||
pub const Event = struct {
|
|
||||||
pub const Self = parser.Event;
|
|
||||||
pub const Exception = DOMException;
|
|
||||||
|
|
||||||
pub const EventInit = parser.EventInit;
|
|
||||||
|
|
||||||
// JS
|
|
||||||
// --
|
|
||||||
|
|
||||||
pub const _CAPTURING_PHASE = 1;
|
|
||||||
pub const _AT_TARGET = 2;
|
|
||||||
pub const _BUBBLING_PHASE = 3;
|
|
||||||
|
|
||||||
pub fn toInterface(evt: *parser.Event) Union {
|
|
||||||
return switch (parser.eventGetInternalType(evt)) {
|
|
||||||
.event, .abort_signal, .xhr_event => .{ .Event = evt },
|
|
||||||
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
|
|
||||||
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
|
|
||||||
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
|
|
||||||
.error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* },
|
|
||||||
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
|
|
||||||
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
|
|
||||||
.pop_state => .{ .PopStateEvent = @as(*PopStateEvent, @ptrCast(evt)).* },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn constructor(event_type: []const u8, opts: ?EventInit) !*parser.Event {
|
|
||||||
const event = try parser.eventCreate();
|
|
||||||
try parser.eventInit(event, event_type, opts orelse EventInit{});
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
pub fn get_type(self: *parser.Event) []const u8 {
|
|
||||||
return parser.eventType(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_target(self: *parser.Event, page: *Page) !?EventTargetUnion {
|
|
||||||
const et = parser.eventTarget(self);
|
|
||||||
if (et == null) return null;
|
|
||||||
return try EventTarget.toInterface(et.?, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_currentTarget(self: *parser.Event, page: *Page) !?EventTargetUnion {
|
|
||||||
const et = parser.eventCurrentTarget(self);
|
|
||||||
if (et == null) return null;
|
|
||||||
return try EventTarget.toInterface(et.?, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_eventPhase(self: *parser.Event) u8 {
|
|
||||||
return parser.eventPhase(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_bubbles(self: *parser.Event) bool {
|
|
||||||
return parser.eventBubbles(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_cancelable(self: *parser.Event) bool {
|
|
||||||
return parser.eventCancelable(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_defaultPrevented(self: *parser.Event) bool {
|
|
||||||
return parser.eventDefaultPrevented(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_isTrusted(self: *parser.Event) bool {
|
|
||||||
return parser.eventIsTrusted(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Even though this is supposed to to provide microsecond resolution, browser
|
|
||||||
// return coarser values to protect against fingerprinting. libdom returns
|
|
||||||
// seconds, which is good enough.
|
|
||||||
pub fn get_timeStamp(self: *parser.Event) u64 {
|
|
||||||
return parser.eventTimestamp(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
|
|
||||||
pub fn _initEvent(
|
|
||||||
self: *parser.Event,
|
|
||||||
eventType: []const u8,
|
|
||||||
bubbles: ?bool,
|
|
||||||
cancelable: ?bool,
|
|
||||||
) !void {
|
|
||||||
const opts = EventInit{
|
|
||||||
.bubbles = bubbles orelse false,
|
|
||||||
.cancelable = cancelable orelse false,
|
|
||||||
};
|
|
||||||
return try parser.eventInit(self, eventType, opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _stopPropagation(self: *parser.Event) !void {
|
|
||||||
return parser.eventStopPropagation(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _stopImmediatePropagation(self: *parser.Event) !void {
|
|
||||||
return parser.eventStopImmediatePropagation(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _preventDefault(self: *parser.Event) !void {
|
|
||||||
return parser.eventPreventDefault(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _composedPath(self: *parser.Event, page: *Page) ![]const EventTargetUnion {
|
|
||||||
const et_ = parser.eventTarget(self);
|
|
||||||
const et = et_ orelse return &.{};
|
|
||||||
|
|
||||||
var node: ?*parser.Node = switch (parser.eventTargetInternalType(et)) {
|
|
||||||
.libdom_node => @as(*parser.Node, @ptrCast(et)),
|
|
||||||
.plain => parser.eventTargetToNode(et),
|
|
||||||
else => {
|
|
||||||
// Window, XHR, MessagePort, etc...no path beyond the event itself
|
|
||||||
return &.{try EventTarget.toInterface(et, page)};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const arena = page.call_arena;
|
|
||||||
var path: std.ArrayListUnmanaged(EventTargetUnion) = .empty;
|
|
||||||
while (node) |n| {
|
|
||||||
try path.append(arena, .{
|
|
||||||
.node = try Node.toInterface(n),
|
|
||||||
});
|
|
||||||
|
|
||||||
node = parser.nodeParentNode(n);
|
|
||||||
if (node == null and parser.nodeType(n) == .document_fragment) {
|
|
||||||
// we have a non-continuous hook from a shadowroot to its host (
|
|
||||||
// it's parent element). libdom doesn't really support ShdowRoots
|
|
||||||
// and, for the most part, that works out well since it naturally
|
|
||||||
// provides isolation. But events don't follow the same
|
|
||||||
// shadowroot isolation as most other things, so, if this is
|
|
||||||
// a parent-less document fragment, we need to check if it has
|
|
||||||
// a host.
|
|
||||||
if (parser.documentFragmentGetHost(@ptrCast(n))) |host| {
|
|
||||||
node = host;
|
|
||||||
|
|
||||||
// If a document fragment has a host, then that host
|
|
||||||
// _has_ to have a state and that state _has_ to have
|
|
||||||
// a shadow_root field. All of this is set in Element._attachShadow
|
|
||||||
if (page.getNodeState(host).?.shadow_root.?.mode == .closed) {
|
|
||||||
// if the shadow root is closed, then the composedPath
|
|
||||||
// starts at the host element.
|
|
||||||
path.clearRetainingCapacity();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Our document fragement has no parent and no host, we
|
|
||||||
// can break out of the loop.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (path.getLastOrNull()) |last| {
|
|
||||||
// the Window isn't part of the DOM hierarchy, but for events, it
|
|
||||||
// is, so we need to glue it on.
|
|
||||||
if (last.node == .HTMLDocument and last.node.HTMLDocument == page.window.document) {
|
|
||||||
try path.append(arena, .{ .node = .{ .Window = &page.window } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path.items;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const EventHandler = struct {
|
|
||||||
once: bool,
|
|
||||||
capture: bool,
|
|
||||||
callback: js.Function,
|
|
||||||
node: parser.EventNode,
|
|
||||||
listener: *parser.EventListener,
|
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
|
|
||||||
pub const Listener = union(enum) {
|
|
||||||
function: js.Function,
|
|
||||||
object: js.Object,
|
|
||||||
|
|
||||||
pub fn callback(self: Listener, target: *parser.EventTarget) !?js.Function {
|
|
||||||
return switch (self) {
|
|
||||||
.function => |func| try func.withThis(target),
|
|
||||||
.object => |obj| blk: {
|
|
||||||
const func = (try obj.getFunction("handleEvent")) orelse return null;
|
|
||||||
break :blk try func.withThis(try obj.persist());
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Opts = union(enum) {
|
|
||||||
flags: Flags,
|
|
||||||
capture: bool,
|
|
||||||
|
|
||||||
const Flags = struct {
|
|
||||||
once: ?bool,
|
|
||||||
capture: ?bool,
|
|
||||||
// We ignore this property. It seems to be largely used to help the
|
|
||||||
// browser make certain performance tweaks (i.e. the browser knows
|
|
||||||
// that the listener won't call preventDefault() and thus can safely
|
|
||||||
// run the default as needed).
|
|
||||||
passive: ?bool,
|
|
||||||
signal: ?*AbortSignal, // currently does nothing
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn register(
|
|
||||||
allocator: Allocator,
|
|
||||||
target: *parser.EventTarget,
|
|
||||||
typ: []const u8,
|
|
||||||
listener: Listener,
|
|
||||||
opts_: ?Opts,
|
|
||||||
) !?*EventHandler {
|
|
||||||
var once = false;
|
|
||||||
var capture = false;
|
|
||||||
var signal: ?*AbortSignal = null;
|
|
||||||
|
|
||||||
if (opts_) |opts| {
|
|
||||||
switch (opts) {
|
|
||||||
.capture => |c| capture = c,
|
|
||||||
.flags => |f| {
|
|
||||||
once = f.once orelse false;
|
|
||||||
signal = f.signal orelse null;
|
|
||||||
capture = f.capture orelse false;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const callback = (try listener.callback(target)) orelse return null;
|
|
||||||
|
|
||||||
if (signal) |s| {
|
|
||||||
const signal_target = parser.toEventTarget(AbortSignal, s);
|
|
||||||
|
|
||||||
const scb = try allocator.create(SignalCallback);
|
|
||||||
scb.* = .{
|
|
||||||
.target = target,
|
|
||||||
.capture = capture,
|
|
||||||
.callback_id = callback.id,
|
|
||||||
.typ = try allocator.dupe(u8, typ),
|
|
||||||
.signal_target = signal_target,
|
|
||||||
.signal_listener = undefined,
|
|
||||||
.node = .{ .func = SignalCallback.handle },
|
|
||||||
};
|
|
||||||
|
|
||||||
scb.signal_listener = try parser.eventTargetAddEventListener(
|
|
||||||
signal_target,
|
|
||||||
"abort",
|
|
||||||
&scb.node,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if event target has already this listener
|
|
||||||
if (try parser.eventTargetHasListener(target, typ, capture, callback.id) != null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const eh = try allocator.create(EventHandler);
|
|
||||||
eh.* = .{
|
|
||||||
.once = once,
|
|
||||||
.capture = capture,
|
|
||||||
.callback = callback,
|
|
||||||
.node = .{
|
|
||||||
.id = callback.id,
|
|
||||||
.func = handle,
|
|
||||||
},
|
|
||||||
.listener = undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
eh.listener = try parser.eventTargetAddEventListener(
|
|
||||||
target,
|
|
||||||
typ,
|
|
||||||
&eh.node,
|
|
||||||
capture,
|
|
||||||
);
|
|
||||||
return eh;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle(node: *parser.EventNode, event: *parser.Event) void {
|
|
||||||
const ievent = Event.toInterface(event);
|
|
||||||
const self: *EventHandler = @fieldParentPtr("node", node);
|
|
||||||
var result: js.Function.Result = undefined;
|
|
||||||
self.callback.tryCall(void, .{ievent}, &result) catch {
|
|
||||||
log.debug(.user_script, "callback error", .{
|
|
||||||
.err = result.exception,
|
|
||||||
.stack = result.stack,
|
|
||||||
.source = "event handler",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (self.once) {
|
|
||||||
const target = parser.eventTarget(event).?;
|
|
||||||
const typ = parser.eventType(event);
|
|
||||||
parser.eventTargetRemoveEventListener(
|
|
||||||
target,
|
|
||||||
typ,
|
|
||||||
self.listener,
|
|
||||||
self.capture,
|
|
||||||
) catch {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const SignalCallback = struct {
|
|
||||||
typ: []const u8,
|
|
||||||
capture: bool,
|
|
||||||
callback_id: usize,
|
|
||||||
node: parser.EventNode,
|
|
||||||
target: *parser.EventTarget,
|
|
||||||
signal_target: *parser.EventTarget,
|
|
||||||
signal_listener: *parser.EventListener,
|
|
||||||
|
|
||||||
fn handle(node: *parser.EventNode, _: *parser.Event) void {
|
|
||||||
const self: *SignalCallback = @fieldParentPtr("node", node);
|
|
||||||
self._handle() catch |err| {
|
|
||||||
log.err(.app, "event signal handler", .{ .err = err });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _handle(self: *SignalCallback) !void {
|
|
||||||
const lst = try parser.eventTargetHasListener(
|
|
||||||
self.target,
|
|
||||||
self.typ,
|
|
||||||
self.capture,
|
|
||||||
self.callback_id,
|
|
||||||
);
|
|
||||||
if (lst == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try parser.eventTargetRemoveEventListener(
|
|
||||||
self.target,
|
|
||||||
self.typ,
|
|
||||||
lst.?,
|
|
||||||
self.capture,
|
|
||||||
);
|
|
||||||
|
|
||||||
// remove the abort signal listener itself
|
|
||||||
try parser.eventTargetRemoveEventListener(
|
|
||||||
self.signal_target,
|
|
||||||
"abort",
|
|
||||||
self.signal_listener,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: Event" {
|
|
||||||
try testing.htmlRunner("events/event.html");
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const builtin = @import("builtin");
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Event = @import("event.zig").Event;
|
|
||||||
|
|
||||||
// TODO: We currently don't have a UIEvent interface so we skip it in the prototype chain.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
|
|
||||||
const UIEvent = Event;
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
|
|
||||||
pub const KeyboardEvent = struct {
|
|
||||||
pub const Self = parser.KeyboardEvent;
|
|
||||||
pub const prototype = *UIEvent;
|
|
||||||
|
|
||||||
pub const ConstructorOptions = struct {
|
|
||||||
key: []const u8 = "",
|
|
||||||
code: []const u8 = "",
|
|
||||||
location: parser.KeyboardEventOpts.LocationCode = .standard,
|
|
||||||
repeat: bool = false,
|
|
||||||
isComposing: bool = false,
|
|
||||||
// Currently not supported but we take as argument.
|
|
||||||
charCode: u32 = 0,
|
|
||||||
// Currently not supported but we take as argument.
|
|
||||||
keyCode: u32 = 0,
|
|
||||||
// Currently not supported but we take as argument.
|
|
||||||
which: u32 = 0,
|
|
||||||
ctrlKey: bool = false,
|
|
||||||
shiftKey: bool = false,
|
|
||||||
altKey: bool = false,
|
|
||||||
metaKey: bool = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn constructor(event_type: []const u8, maybe_options: ?ConstructorOptions) !*parser.KeyboardEvent {
|
|
||||||
const options: ConstructorOptions = maybe_options orelse .{};
|
|
||||||
|
|
||||||
const event = try parser.keyboardEventCreate();
|
|
||||||
parser.eventSetInternalType(@ptrCast(event), .keyboard_event);
|
|
||||||
|
|
||||||
try parser.keyboardEventInit(
|
|
||||||
event,
|
|
||||||
event_type,
|
|
||||||
.{
|
|
||||||
.key = options.key,
|
|
||||||
.code = options.code,
|
|
||||||
.location = options.location,
|
|
||||||
.repeat = options.repeat,
|
|
||||||
.is_composing = options.isComposing,
|
|
||||||
.ctrl_key = options.ctrlKey,
|
|
||||||
.shift_key = options.shiftKey,
|
|
||||||
.alt_key = options.altKey,
|
|
||||||
.meta_key = options.metaKey,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the modifier state for given modifier key.
|
|
||||||
pub fn _getModifierState(self: *Self, key: []const u8) bool {
|
|
||||||
// Chrome and Firefox do case-sensitive match, here we prefer the same.
|
|
||||||
if (std.mem.eql(u8, key, "Alt")) {
|
|
||||||
return get_altKey(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, key, "AltGraph")) {
|
|
||||||
return (get_altKey(self) and get_ctrlKey(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, key, "Control")) {
|
|
||||||
return get_ctrlKey(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, key, "Shift")) {
|
|
||||||
return get_shiftKey(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, key, "Meta") or std.mem.eql(u8, key, "OS")) {
|
|
||||||
return get_metaKey(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case for IE.
|
|
||||||
if (comptime builtin.os.tag == .windows) {
|
|
||||||
if (std.mem.eql(u8, key, "Win")) {
|
|
||||||
return get_metaKey(self);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getModifierState() also accepts a deprecated virtual modifier named "Accel".
|
|
||||||
// event.getModifierState("Accel") returns true when at least one of
|
|
||||||
// KeyboardEvent.ctrlKey or KeyboardEvent.metaKey is true.
|
|
||||||
//
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState#accel_virtual_modifier
|
|
||||||
if (std.mem.eql(u8, key, "Accel")) {
|
|
||||||
return (get_ctrlKey(self) or get_metaKey(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add support for "CapsLock", "ScrollLock".
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters.
|
|
||||||
|
|
||||||
pub fn get_altKey(self: *Self) bool {
|
|
||||||
return parser.keyboardEventKeyIsSet(self, .alt);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_ctrlKey(self: *Self) bool {
|
|
||||||
return parser.keyboardEventKeyIsSet(self, .ctrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_metaKey(self: *Self) bool {
|
|
||||||
return parser.keyboardEventKeyIsSet(self, .meta);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_shiftKey(self: *Self) bool {
|
|
||||||
return parser.keyboardEventKeyIsSet(self, .shift);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_isComposing(self: *Self) bool {
|
|
||||||
return self.is_composing;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_location(self: *Self) u32 {
|
|
||||||
return self.location;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_key(self: *Self) ![]const u8 {
|
|
||||||
return parser.keyboardEventGetKey(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_repeat(self: *Self) bool {
|
|
||||||
return self.repeat;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: Events.Keyboard" {
|
|
||||||
try testing.htmlRunner("events/keyboard.html");
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Event = @import("event.zig").Event;
|
|
||||||
|
|
||||||
// TODO: We currently don't have a UIEvent interface so we skip it in the prototype chain.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
|
|
||||||
const UIEvent = Event;
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
|
|
||||||
pub const MouseEvent = struct {
|
|
||||||
pub const Self = parser.MouseEvent;
|
|
||||||
pub const prototype = *UIEvent;
|
|
||||||
|
|
||||||
const MouseButton = enum(u16) {
|
|
||||||
main_button = 0,
|
|
||||||
auxillary_button = 1,
|
|
||||||
secondary_button = 2,
|
|
||||||
fourth_button = 3,
|
|
||||||
fifth_button = 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MouseEventInit = struct {
|
|
||||||
screenX: i32 = 0,
|
|
||||||
screenY: i32 = 0,
|
|
||||||
clientX: i32 = 0,
|
|
||||||
clientY: i32 = 0,
|
|
||||||
ctrlKey: bool = false,
|
|
||||||
shiftKey: bool = false,
|
|
||||||
altKey: bool = false,
|
|
||||||
metaKey: bool = false,
|
|
||||||
button: MouseButton = .main_button,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn constructor(event_type: []const u8, opts_: ?MouseEventInit) !*parser.MouseEvent {
|
|
||||||
const opts = opts_ orelse MouseEventInit{};
|
|
||||||
|
|
||||||
const mouse_event = try parser.mouseEventCreate();
|
|
||||||
parser.eventSetInternalType(@ptrCast(mouse_event), .mouse_event);
|
|
||||||
|
|
||||||
try parser.mouseEventInit(mouse_event, event_type, .{
|
|
||||||
.x = opts.clientX,
|
|
||||||
.y = opts.clientY,
|
|
||||||
.ctrl = opts.ctrlKey,
|
|
||||||
.shift = opts.shiftKey,
|
|
||||||
.alt = opts.altKey,
|
|
||||||
.meta = opts.metaKey,
|
|
||||||
.button = @intFromEnum(opts.button),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!std.mem.eql(u8, event_type, "click")) {
|
|
||||||
log.warn(.browser, "unsupported mouse event", .{ .event = event_type });
|
|
||||||
}
|
|
||||||
|
|
||||||
return mouse_event;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_button(self: *parser.MouseEvent) u16 {
|
|
||||||
return self.button;
|
|
||||||
}
|
|
||||||
|
|
||||||
// These is just an alias for clientX.
|
|
||||||
pub fn get_x(self: *parser.MouseEvent) i32 {
|
|
||||||
return self.cx;
|
|
||||||
}
|
|
||||||
|
|
||||||
// These is just an alias for clientY.
|
|
||||||
pub fn get_y(self: *parser.MouseEvent) i32 {
|
|
||||||
return self.cy;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_clientX(self: *parser.MouseEvent) i32 {
|
|
||||||
return self.cx;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_clientY(self: *parser.MouseEvent) i32 {
|
|
||||||
return self.cy;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_screenX(self: *parser.MouseEvent) i32 {
|
|
||||||
return self.sx;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_screenY(self: *parser.MouseEvent) i32 {
|
|
||||||
return self.sy;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: Events.Mouse" {
|
|
||||||
try testing.htmlRunner("events/mouse.html");
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const URL = @import("../../url.zig").URL;
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const iterator = @import("../iterator/iterator.zig");
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Headers
|
|
||||||
const Headers = @This();
|
|
||||||
|
|
||||||
// Case-Insensitive String HashMap.
|
|
||||||
// This allows us to avoid having to allocate lowercase keys all the time.
|
|
||||||
const HeaderHashMap = std.HashMapUnmanaged([]const u8, []const u8, struct {
|
|
||||||
pub fn hash(_: @This(), s: []const u8) u64 {
|
|
||||||
var buf: [64]u8 = undefined;
|
|
||||||
var hasher = std.hash.Wyhash.init(s.len);
|
|
||||||
|
|
||||||
var key = s;
|
|
||||||
while (key.len >= 64) {
|
|
||||||
const lower = std.ascii.lowerString(buf[0..], key[0..64]);
|
|
||||||
hasher.update(lower);
|
|
||||||
key = key[64..];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key.len > 0) {
|
|
||||||
const lower = std.ascii.lowerString(buf[0..key.len], key);
|
|
||||||
hasher.update(lower);
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasher.final();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eql(_: @This(), a: []const u8, b: []const u8) bool {
|
|
||||||
return std.ascii.eqlIgnoreCase(a, b);
|
|
||||||
}
|
|
||||||
}, 80);
|
|
||||||
|
|
||||||
headers: HeaderHashMap = .empty,
|
|
||||||
|
|
||||||
// They can either be:
|
|
||||||
//
|
|
||||||
// 1. An array of string pairs.
|
|
||||||
// 2. An object with string keys to string values.
|
|
||||||
// 3. Another Headers object.
|
|
||||||
pub const HeadersInit = union(enum) {
|
|
||||||
// List of Pairs of []const u8
|
|
||||||
strings: []const [2][]const u8,
|
|
||||||
// Headers
|
|
||||||
headers: *Headers,
|
|
||||||
// Mappings
|
|
||||||
object: js.Object,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn constructor(_init: ?HeadersInit, page: *Page) !Headers {
|
|
||||||
const arena = page.arena;
|
|
||||||
var headers: HeaderHashMap = .empty;
|
|
||||||
|
|
||||||
if (_init) |init| {
|
|
||||||
switch (init) {
|
|
||||||
.strings => |kvs| {
|
|
||||||
for (kvs) |pair| {
|
|
||||||
const key = try arena.dupe(u8, pair[0]);
|
|
||||||
const value = try arena.dupe(u8, pair[1]);
|
|
||||||
|
|
||||||
try headers.put(arena, key, value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.headers => |hdrs| {
|
|
||||||
var iter = hdrs.headers.iterator();
|
|
||||||
while (iter.next()) |entry| {
|
|
||||||
try headers.put(arena, entry.key_ptr.*, entry.value_ptr.*);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.object => |obj| {
|
|
||||||
var iter = obj.nameIterator();
|
|
||||||
while (try iter.next()) |name_value| {
|
|
||||||
const name = try name_value.toString(arena);
|
|
||||||
const value = try obj.get(name);
|
|
||||||
const value_string = try value.toString(arena);
|
|
||||||
|
|
||||||
try headers.put(arena, name, value_string);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.headers = headers,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn append(self: *Headers, name: []const u8, value: []const u8, allocator: std.mem.Allocator) !void {
|
|
||||||
const key = try allocator.dupe(u8, name);
|
|
||||||
const gop = try self.headers.getOrPut(allocator, key);
|
|
||||||
|
|
||||||
if (gop.found_existing) {
|
|
||||||
// If we found it, append the value.
|
|
||||||
const new_value = try std.fmt.allocPrint(allocator, "{s}, {s}", .{ gop.value_ptr.*, value });
|
|
||||||
gop.value_ptr.* = new_value;
|
|
||||||
} else {
|
|
||||||
// Otherwise, we should just put it in.
|
|
||||||
gop.value_ptr.* = try allocator.dupe(u8, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
|
|
||||||
const arena = page.arena;
|
|
||||||
try self.append(name, value, arena);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _delete(self: *Headers, name: []const u8) void {
|
|
||||||
_ = self.headers.remove(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const HeadersEntryIterator = struct {
|
|
||||||
slot: [2][]const u8,
|
|
||||||
iter: HeaderHashMap.Iterator,
|
|
||||||
|
|
||||||
// TODO: these SHOULD be in lexigraphical order but I'm not sure how actually
|
|
||||||
// important that is.
|
|
||||||
pub fn _next(self: *HeadersEntryIterator) ?[2][]const u8 {
|
|
||||||
if (self.iter.next()) |entry| {
|
|
||||||
self.slot[0] = entry.key_ptr.*;
|
|
||||||
self.slot[1] = entry.value_ptr.*;
|
|
||||||
return self.slot;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn _entries(self: *const Headers) HeadersEntryIterable {
|
|
||||||
return .{
|
|
||||||
.inner = .{
|
|
||||||
.slot = undefined,
|
|
||||||
.iter = self.headers.iterator(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _forEach(self: *Headers, callback_fn: js.Function, this_arg: ?js.Object) !void {
|
|
||||||
var iter = self.headers.iterator();
|
|
||||||
|
|
||||||
const cb = if (this_arg) |this| try callback_fn.withThis(this) else callback_fn;
|
|
||||||
|
|
||||||
while (iter.next()) |entry| {
|
|
||||||
try cb.call(void, .{ entry.key_ptr.*, entry.value_ptr.*, self });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _get(self: *const Headers, name: []const u8) ?[]const u8 {
|
|
||||||
return self.headers.get(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _has(self: *const Headers, name: []const u8) bool {
|
|
||||||
return self.headers.contains(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const HeadersKeyIterator = struct {
|
|
||||||
iter: HeaderHashMap.KeyIterator,
|
|
||||||
|
|
||||||
pub fn _next(self: *HeadersKeyIterator) ?[]const u8 {
|
|
||||||
if (self.iter.next()) |key| {
|
|
||||||
return key.*;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn _keys(self: *const Headers) HeadersKeyIterable {
|
|
||||||
return .{ .inner = .{ .iter = self.headers.keyIterator() } };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _set(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
|
|
||||||
const arena = page.arena;
|
|
||||||
|
|
||||||
const key = try arena.dupe(u8, name);
|
|
||||||
const gop = try self.headers.getOrPut(arena, key);
|
|
||||||
gop.value_ptr.* = try arena.dupe(u8, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const HeadersValueIterator = struct {
|
|
||||||
iter: HeaderHashMap.ValueIterator,
|
|
||||||
|
|
||||||
pub fn _next(self: *HeadersValueIterator) ?[]const u8 {
|
|
||||||
if (self.iter.next()) |value| {
|
|
||||||
return value.*;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn _values(self: *const Headers) HeadersValueIterable {
|
|
||||||
return .{ .inner = .{ .iter = self.headers.valueIterator() } };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const HeadersKeyIterable = iterator.Iterable(HeadersKeyIterator, "HeadersKeyIterator");
|
|
||||||
pub const HeadersValueIterable = iterator.Iterable(HeadersValueIterator, "HeadersValueIterator");
|
|
||||||
pub const HeadersEntryIterable = iterator.Iterable(HeadersEntryIterator, "HeadersEntryIterator");
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "fetch: Headers" {
|
|
||||||
try testing.htmlRunner("fetch/headers.html");
|
|
||||||
}
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
|
|
||||||
const URL = @import("../../url.zig").URL;
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const Response = @import("./Response.zig");
|
|
||||||
const Http = @import("../../http/Http.zig");
|
|
||||||
const ReadableStream = @import("../streams/ReadableStream.zig");
|
|
||||||
|
|
||||||
const Headers = @import("Headers.zig");
|
|
||||||
const HeadersInit = @import("Headers.zig").HeadersInit;
|
|
||||||
|
|
||||||
pub const RequestInput = union(enum) {
|
|
||||||
string: []const u8,
|
|
||||||
request: *Request,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const RequestCache = enum {
|
|
||||||
default,
|
|
||||||
@"no-store",
|
|
||||||
reload,
|
|
||||||
@"no-cache",
|
|
||||||
@"force-cache",
|
|
||||||
@"only-if-cached",
|
|
||||||
|
|
||||||
pub fn fromString(str: []const u8) ?RequestCache {
|
|
||||||
for (std.enums.values(RequestCache)) |cache| {
|
|
||||||
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toString(self: RequestCache) []const u8 {
|
|
||||||
return @tagName(self);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const RequestCredentials = enum {
|
|
||||||
omit,
|
|
||||||
@"same-origin",
|
|
||||||
include,
|
|
||||||
|
|
||||||
pub fn fromString(str: []const u8) ?RequestCredentials {
|
|
||||||
for (std.enums.values(RequestCredentials)) |cache| {
|
|
||||||
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toString(self: RequestCredentials) []const u8 {
|
|
||||||
return @tagName(self);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const RequestMode = enum {
|
|
||||||
cors,
|
|
||||||
@"no-cors",
|
|
||||||
@"same-origin",
|
|
||||||
navigate,
|
|
||||||
|
|
||||||
pub fn fromString(str: []const u8) ?RequestMode {
|
|
||||||
for (std.enums.values(RequestMode)) |cache| {
|
|
||||||
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toString(self: RequestMode) []const u8 {
|
|
||||||
return @tagName(self);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
|
|
||||||
pub const RequestInit = struct {
|
|
||||||
body: ?[]const u8 = null,
|
|
||||||
cache: ?[]const u8 = null,
|
|
||||||
credentials: ?[]const u8 = null,
|
|
||||||
headers: ?HeadersInit = null,
|
|
||||||
integrity: ?[]const u8 = null,
|
|
||||||
method: ?[]const u8 = null,
|
|
||||||
mode: ?[]const u8 = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
|
|
||||||
const Request = @This();
|
|
||||||
|
|
||||||
method: Http.Method,
|
|
||||||
url: [:0]const u8,
|
|
||||||
cache: RequestCache,
|
|
||||||
credentials: RequestCredentials,
|
|
||||||
// no-cors is default is not built with constructor.
|
|
||||||
mode: RequestMode = .@"no-cors",
|
|
||||||
headers: Headers,
|
|
||||||
body: ?[]const u8,
|
|
||||||
body_used: bool = false,
|
|
||||||
integrity: []const u8,
|
|
||||||
|
|
||||||
pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Request {
|
|
||||||
const arena = page.arena;
|
|
||||||
const options: RequestInit = _options orelse .{};
|
|
||||||
|
|
||||||
const url: [:0]const u8 = blk: switch (input) {
|
|
||||||
.string => |str| {
|
|
||||||
break :blk try URL.stitch(arena, str, page.url.raw, .{ .null_terminated = true });
|
|
||||||
},
|
|
||||||
.request => |req| {
|
|
||||||
break :blk try arena.dupeZ(u8, req.url);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const cache = (if (options.cache) |cache| RequestCache.fromString(cache) else null) orelse RequestCache.default;
|
|
||||||
const credentials = (if (options.credentials) |creds| RequestCredentials.fromString(creds) else null) orelse RequestCredentials.@"same-origin";
|
|
||||||
const integrity = if (options.integrity) |integ| try arena.dupe(u8, integ) else "";
|
|
||||||
const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{};
|
|
||||||
const mode = (if (options.mode) |mode| RequestMode.fromString(mode) else null) orelse RequestMode.cors;
|
|
||||||
|
|
||||||
const method: Http.Method = blk: {
|
|
||||||
if (options.method) |given_method| {
|
|
||||||
for (std.enums.values(Http.Method)) |method| {
|
|
||||||
if (std.ascii.eqlIgnoreCase(given_method, @tagName(method))) {
|
|
||||||
break :blk method;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return error.TypeError;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break :blk Http.Method.GET;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Can't have a body on .GET or .HEAD.
|
|
||||||
const body: ?[]const u8 = blk: {
|
|
||||||
if (method == .GET or method == .HEAD) {
|
|
||||||
break :blk null;
|
|
||||||
} else break :blk if (options.body) |body| try arena.dupe(u8, body) else null;
|
|
||||||
};
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.method = method,
|
|
||||||
.url = url,
|
|
||||||
.cache = cache,
|
|
||||||
.credentials = credentials,
|
|
||||||
.mode = mode,
|
|
||||||
.headers = headers,
|
|
||||||
.body = body,
|
|
||||||
.integrity = integrity,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_body(self: *const Request, page: *Page) !?*ReadableStream {
|
|
||||||
if (self.body) |body| {
|
|
||||||
const stream = try ReadableStream.constructor(null, null, page);
|
|
||||||
try stream.queue.append(page.arena, .{ .string = body });
|
|
||||||
return stream;
|
|
||||||
} else return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_bodyUsed(self: *const Request) bool {
|
|
||||||
return self.body_used;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_cache(self: *const Request) RequestCache {
|
|
||||||
return self.cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_credentials(self: *const Request) RequestCredentials {
|
|
||||||
return self.credentials;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_headers(self: *Request) *Headers {
|
|
||||||
return &self.headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_integrity(self: *const Request) []const u8 {
|
|
||||||
return self.integrity;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: If we ever support the Navigation API, we need isHistoryNavigation
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Request/isHistoryNavigation
|
|
||||||
|
|
||||||
pub fn get_method(self: *const Request) []const u8 {
|
|
||||||
return @tagName(self.method);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_mode(self: *const Request) RequestMode {
|
|
||||||
return self.mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_url(self: *const Request) []const u8 {
|
|
||||||
return self.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _clone(self: *Request) !Request {
|
|
||||||
// Not allowed to clone if the body was used.
|
|
||||||
if (self.body_used) {
|
|
||||||
return error.TypeError;
|
|
||||||
}
|
|
||||||
|
|
||||||
// OK to just return the same fields BECAUSE
|
|
||||||
// all of these fields are read-only and can't be modified.
|
|
||||||
return Request{
|
|
||||||
.body = self.body,
|
|
||||||
.body_used = self.body_used,
|
|
||||||
.cache = self.cache,
|
|
||||||
.credentials = self.credentials,
|
|
||||||
.headers = self.headers,
|
|
||||||
.method = self.method,
|
|
||||||
.integrity = self.integrity,
|
|
||||||
.url = self.url,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _bytes(self: *Response, page: *Page) !js.Promise {
|
|
||||||
if (self.body_used) {
|
|
||||||
return error.TypeError;
|
|
||||||
}
|
|
||||||
self.body_used = true;
|
|
||||||
return page.js.resolvePromise(self.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _json(self: *Response, page: *Page) !js.Promise {
|
|
||||||
if (self.body_used) {
|
|
||||||
return error.TypeError;
|
|
||||||
}
|
|
||||||
self.body_used = true;
|
|
||||||
|
|
||||||
if (self.body) |body| {
|
|
||||||
const p = std.json.parseFromSliceLeaky(
|
|
||||||
std.json.Value,
|
|
||||||
page.call_arena,
|
|
||||||
body,
|
|
||||||
.{},
|
|
||||||
) catch |e| {
|
|
||||||
log.info(.browser, "invalid json", .{ .err = e, .source = "Request" });
|
|
||||||
return error.SyntaxError;
|
|
||||||
};
|
|
||||||
|
|
||||||
return page.js.resolvePromise(p);
|
|
||||||
}
|
|
||||||
return page.js.resolvePromise(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _text(self: *Response, page: *Page) !js.Promise {
|
|
||||||
if (self.body_used) {
|
|
||||||
return error.TypeError;
|
|
||||||
}
|
|
||||||
self.body_used = true;
|
|
||||||
return page.js.resolvePromise(self.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "fetch: Request" {
|
|
||||||
try testing.htmlRunner("fetch/request.html");
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
|
|
||||||
const HttpClient = @import("../../http/Client.zig");
|
|
||||||
const Http = @import("../../http/Http.zig");
|
|
||||||
const URL = @import("../../url.zig").URL;
|
|
||||||
|
|
||||||
const ReadableStream = @import("../streams/ReadableStream.zig");
|
|
||||||
const Headers = @import("Headers.zig");
|
|
||||||
const HeadersInit = @import("Headers.zig").HeadersInit;
|
|
||||||
|
|
||||||
const Mime = @import("../mime.zig").Mime;
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Response
|
|
||||||
const Response = @This();
|
|
||||||
|
|
||||||
status: u16 = 200,
|
|
||||||
status_text: []const u8 = "",
|
|
||||||
headers: Headers,
|
|
||||||
mime: ?Mime = null,
|
|
||||||
url: []const u8 = "",
|
|
||||||
body: ?[]const u8 = null,
|
|
||||||
body_used: bool = false,
|
|
||||||
redirected: bool = false,
|
|
||||||
type: ResponseType = .basic,
|
|
||||||
|
|
||||||
const ResponseBody = union(enum) {
|
|
||||||
string: []const u8,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ResponseOptions = struct {
|
|
||||||
status: u16 = 200,
|
|
||||||
statusText: ?[]const u8 = null,
|
|
||||||
headers: ?HeadersInit = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const ResponseType = enum {
|
|
||||||
basic,
|
|
||||||
cors,
|
|
||||||
@"error",
|
|
||||||
@"opaque",
|
|
||||||
opaqueredirect,
|
|
||||||
|
|
||||||
pub fn fromString(str: []const u8) ?ResponseType {
|
|
||||||
for (std.enums.values(ResponseType)) |cache| {
|
|
||||||
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toString(self: ResponseType) []const u8 {
|
|
||||||
return @tagName(self);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Page) !Response {
|
|
||||||
const arena = page.arena;
|
|
||||||
|
|
||||||
const options: ResponseOptions = _options orelse .{};
|
|
||||||
|
|
||||||
const body = blk: {
|
|
||||||
if (_input) |input| {
|
|
||||||
switch (input) {
|
|
||||||
.string => |str| {
|
|
||||||
break :blk try arena.dupe(u8, str);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break :blk null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{};
|
|
||||||
const status_text = if (options.statusText) |st| try arena.dupe(u8, st) else "";
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.body = body,
|
|
||||||
.headers = headers,
|
|
||||||
.status = options.status,
|
|
||||||
.status_text = status_text,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_body(self: *const Response, page: *Page) !*ReadableStream {
|
|
||||||
const stream = try ReadableStream.constructor(null, null, page);
|
|
||||||
if (self.body) |body| {
|
|
||||||
try stream.queue.append(page.arena, .{ .string = body });
|
|
||||||
}
|
|
||||||
return stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_bodyUsed(self: *const Response) bool {
|
|
||||||
return self.body_used;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_headers(self: *Response) *Headers {
|
|
||||||
return &self.headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_ok(self: *const Response) bool {
|
|
||||||
return self.status >= 200 and self.status <= 299;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_redirected(self: *const Response) bool {
|
|
||||||
return self.redirected;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_status(self: *const Response) u16 {
|
|
||||||
return self.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_statusText(self: *const Response) []const u8 {
|
|
||||||
return self.status_text;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_type(self: *const Response) ResponseType {
|
|
||||||
return self.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_url(self: *const Response) []const u8 {
|
|
||||||
return self.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _clone(self: *const Response) !Response {
|
|
||||||
if (self.body_used) {
|
|
||||||
return error.TypeError;
|
|
||||||
}
|
|
||||||
|
|
||||||
// OK to just return the same fields BECAUSE
|
|
||||||
// all of these fields are read-only and can't be modified.
|
|
||||||
return Response{
|
|
||||||
.body = self.body,
|
|
||||||
.body_used = self.body_used,
|
|
||||||
.mime = self.mime,
|
|
||||||
.headers = self.headers,
|
|
||||||
.redirected = self.redirected,
|
|
||||||
.status = self.status,
|
|
||||||
.url = self.url,
|
|
||||||
.type = self.type,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _bytes(self: *Response, page: *Page) !js.Promise {
|
|
||||||
if (self.body_used) {
|
|
||||||
return error.TypeError;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.body_used = true;
|
|
||||||
return page.js.resolvePromise(self.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _json(self: *Response, page: *Page) !js.Promise {
|
|
||||||
if (self.body_used) {
|
|
||||||
return error.TypeError;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.body) |body| {
|
|
||||||
self.body_used = true;
|
|
||||||
const p = std.json.parseFromSliceLeaky(
|
|
||||||
std.json.Value,
|
|
||||||
page.call_arena,
|
|
||||||
body,
|
|
||||||
.{},
|
|
||||||
) catch |e| {
|
|
||||||
log.info(.browser, "invalid json", .{ .err = e, .source = "Response" });
|
|
||||||
return error.SyntaxError;
|
|
||||||
};
|
|
||||||
|
|
||||||
return page.js.resolvePromise(p);
|
|
||||||
}
|
|
||||||
return page.js.resolvePromise(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _text(self: *Response, page: *Page) !js.Promise {
|
|
||||||
if (self.body_used) {
|
|
||||||
return error.TypeError;
|
|
||||||
}
|
|
||||||
self.body_used = true;
|
|
||||||
|
|
||||||
return page.js.resolvePromise(self.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "fetch: Response" {
|
|
||||||
try testing.htmlRunner("fetch/response.html");
|
|
||||||
}
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const Http = @import("../../http/Http.zig");
|
|
||||||
const HttpClient = @import("../../http/Client.zig");
|
|
||||||
const Mime = @import("../mime.zig").Mime;
|
|
||||||
|
|
||||||
const Headers = @import("Headers.zig");
|
|
||||||
|
|
||||||
const RequestInput = @import("Request.zig").RequestInput;
|
|
||||||
const RequestInit = @import("Request.zig").RequestInit;
|
|
||||||
const Request = @import("Request.zig");
|
|
||||||
const Response = @import("Response.zig");
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
@import("Headers.zig"),
|
|
||||||
@import("Headers.zig").HeadersEntryIterable,
|
|
||||||
@import("Headers.zig").HeadersKeyIterable,
|
|
||||||
@import("Headers.zig").HeadersValueIterable,
|
|
||||||
@import("Request.zig"),
|
|
||||||
@import("Response.zig"),
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const FetchContext = struct {
|
|
||||||
page: *Page,
|
|
||||||
arena: std.mem.Allocator,
|
|
||||||
promise_resolver: js.PersistentPromiseResolver,
|
|
||||||
|
|
||||||
method: Http.Method,
|
|
||||||
url: []const u8,
|
|
||||||
body: std.ArrayListUnmanaged(u8) = .empty,
|
|
||||||
headers: std.ArrayListUnmanaged([]const u8) = .empty,
|
|
||||||
status: u16 = 0,
|
|
||||||
mime: ?Mime = null,
|
|
||||||
mode: Request.RequestMode,
|
|
||||||
transfer: ?*HttpClient.Transfer = null,
|
|
||||||
|
|
||||||
/// This effectively takes ownership of the FetchContext.
|
|
||||||
///
|
|
||||||
/// We just return the underlying slices used for `headers`
|
|
||||||
/// and for `body` here to avoid an allocation.
|
|
||||||
pub fn toResponse(self: *const FetchContext) !Response {
|
|
||||||
var headers: Headers = .{};
|
|
||||||
|
|
||||||
// seems to be the highest priority
|
|
||||||
const same_origin = try self.page.isSameOrigin(self.url);
|
|
||||||
|
|
||||||
// If the mode is "no-cors", we need to return this opaque/stripped Response.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Response/type
|
|
||||||
if (!same_origin and self.mode == .@"no-cors") {
|
|
||||||
return Response{
|
|
||||||
.status = 0,
|
|
||||||
.headers = headers,
|
|
||||||
.mime = self.mime,
|
|
||||||
.body = null,
|
|
||||||
.url = self.url,
|
|
||||||
.type = .@"opaque",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert into Headers
|
|
||||||
for (self.headers.items) |hdr| {
|
|
||||||
var iter = std.mem.splitScalar(u8, hdr, ':');
|
|
||||||
const name = iter.next() orelse "";
|
|
||||||
const value = iter.next() orelse "";
|
|
||||||
try headers.append(name, value, self.arena);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resp_type: Response.ResponseType = blk: {
|
|
||||||
if (same_origin or std.mem.startsWith(u8, self.url, "data:")) {
|
|
||||||
break :blk .basic;
|
|
||||||
}
|
|
||||||
|
|
||||||
break :blk switch (self.mode) {
|
|
||||||
.cors => .cors,
|
|
||||||
.@"same-origin", .navigate => .basic,
|
|
||||||
.@"no-cors" => unreachable,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return Response{
|
|
||||||
.status = self.status,
|
|
||||||
.headers = headers,
|
|
||||||
.mime = self.mime,
|
|
||||||
.body = self.body.items,
|
|
||||||
.url = self.url,
|
|
||||||
.type = resp_type,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
|
|
||||||
pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !js.Promise {
|
|
||||||
const arena = page.arena;
|
|
||||||
|
|
||||||
const req = try Request.constructor(input, options, page);
|
|
||||||
var headers = try page.http_client.newHeaders();
|
|
||||||
|
|
||||||
// Copy our headers into the HTTP headers.
|
|
||||||
var header_iter = req.headers.headers.iterator();
|
|
||||||
while (header_iter.next()) |entry| {
|
|
||||||
const combined = try std.fmt.allocPrintSentinel(
|
|
||||||
page.arena,
|
|
||||||
"{s}: {s}",
|
|
||||||
.{ entry.key_ptr.*, entry.value_ptr.* },
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
try headers.add(combined.ptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
try page.requestCookie(.{}).headersForRequest(arena, req.url, &headers);
|
|
||||||
|
|
||||||
const resolver = try page.js.createPromiseResolver(.page);
|
|
||||||
|
|
||||||
const fetch_ctx = try arena.create(FetchContext);
|
|
||||||
fetch_ctx.* = .{
|
|
||||||
.page = page,
|
|
||||||
.arena = arena,
|
|
||||||
.promise_resolver = resolver,
|
|
||||||
.method = req.method,
|
|
||||||
.url = req.url,
|
|
||||||
.mode = req.mode,
|
|
||||||
};
|
|
||||||
|
|
||||||
try page.http_client.request(.{
|
|
||||||
.ctx = @ptrCast(fetch_ctx),
|
|
||||||
.url = req.url,
|
|
||||||
.method = req.method,
|
|
||||||
.headers = headers,
|
|
||||||
.body = req.body,
|
|
||||||
.cookie_jar = page.cookie_jar,
|
|
||||||
.resource_type = .fetch,
|
|
||||||
|
|
||||||
.start_callback = struct {
|
|
||||||
fn startCallback(transfer: *HttpClient.Transfer) !void {
|
|
||||||
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
|
|
||||||
log.debug(.fetch, "request start", .{ .method = self.method, .url = self.url, .source = "fetch" });
|
|
||||||
|
|
||||||
self.transfer = transfer;
|
|
||||||
}
|
|
||||||
}.startCallback,
|
|
||||||
.header_callback = struct {
|
|
||||||
fn headerCallback(transfer: *HttpClient.Transfer) !void {
|
|
||||||
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
|
|
||||||
|
|
||||||
const header = &transfer.response_header.?;
|
|
||||||
|
|
||||||
log.debug(.fetch, "request header", .{
|
|
||||||
.source = "fetch",
|
|
||||||
.method = self.method,
|
|
||||||
.url = self.url,
|
|
||||||
.status = header.status,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (header.contentType()) |ct| {
|
|
||||||
self.mime = Mime.parse(ct) catch {
|
|
||||||
return error.MimeParsing;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transfer.getContentLength()) |cl| {
|
|
||||||
try self.body.ensureTotalCapacity(self.arena, cl);
|
|
||||||
}
|
|
||||||
|
|
||||||
var it = transfer.responseHeaderIterator();
|
|
||||||
while (it.next()) |hdr| {
|
|
||||||
const joined = try std.fmt.allocPrint(self.arena, "{s}: {s}", .{ hdr.name, hdr.value });
|
|
||||||
try self.headers.append(self.arena, joined);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.status = header.status;
|
|
||||||
}
|
|
||||||
}.headerCallback,
|
|
||||||
.data_callback = struct {
|
|
||||||
fn dataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
|
|
||||||
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
|
|
||||||
try self.body.appendSlice(self.arena, data);
|
|
||||||
}
|
|
||||||
}.dataCallback,
|
|
||||||
.done_callback = struct {
|
|
||||||
fn doneCallback(ctx: *anyopaque) !void {
|
|
||||||
const self: *FetchContext = @ptrCast(@alignCast(ctx));
|
|
||||||
self.transfer = null;
|
|
||||||
|
|
||||||
log.info(.fetch, "request complete", .{
|
|
||||||
.source = "fetch",
|
|
||||||
.method = self.method,
|
|
||||||
.url = self.url,
|
|
||||||
.status = self.status,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = try self.toResponse();
|
|
||||||
try self.promise_resolver.resolve(response);
|
|
||||||
}
|
|
||||||
}.doneCallback,
|
|
||||||
.error_callback = struct {
|
|
||||||
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
|
||||||
const self: *FetchContext = @ptrCast(@alignCast(ctx));
|
|
||||||
self.transfer = null;
|
|
||||||
|
|
||||||
log.err(.fetch, "error", .{
|
|
||||||
.url = self.url,
|
|
||||||
.err = err,
|
|
||||||
.source = "fetch error",
|
|
||||||
});
|
|
||||||
|
|
||||||
// We throw an Abort error when the page is getting closed so,
|
|
||||||
// in this case, we don't need to reject the promise.
|
|
||||||
if (err != error.Abort) {
|
|
||||||
self.promise_resolver.reject(@errorName(err)) catch unreachable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.errorCallback,
|
|
||||||
});
|
|
||||||
|
|
||||||
return resolver.promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "fetch: fetch" {
|
|
||||||
try testing.htmlRunner("fetch/fetch.html");
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
AbortController,
|
|
||||||
AbortSignal,
|
|
||||||
};
|
|
||||||
|
|
||||||
const AbortController = @This();
|
|
||||||
|
|
||||||
signal: *AbortSignal,
|
|
||||||
|
|
||||||
pub fn constructor(page: *Page) !AbortController {
|
|
||||||
// Why do we allocate this rather than storing directly in the struct?
|
|
||||||
// https://github.com/lightpanda-io/project/discussions/165
|
|
||||||
const signal = try page.arena.create(AbortSignal);
|
|
||||||
signal.* = .init;
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.signal = signal,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_signal(self: *AbortController) *AbortSignal {
|
|
||||||
return self.signal;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _abort(self: *AbortController, reason_: ?[]const u8) !void {
|
|
||||||
return self.signal.abort(reason_);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const AbortSignal = struct {
|
|
||||||
const DEFAULT_REASON = "AbortError";
|
|
||||||
|
|
||||||
pub const prototype = *EventTarget;
|
|
||||||
proto: parser.EventTargetTBase = .{ .internal_target_type = .abort_signal },
|
|
||||||
|
|
||||||
aborted: bool,
|
|
||||||
reason: ?[]const u8,
|
|
||||||
|
|
||||||
pub const init: AbortSignal = .{
|
|
||||||
.reason = null,
|
|
||||||
.aborted = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn static_abort(reason_: ?[]const u8) AbortSignal {
|
|
||||||
return .{
|
|
||||||
.aborted = true,
|
|
||||||
.reason = reason_ orelse DEFAULT_REASON,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn static_timeout(delay: u32, page: *Page) !*AbortSignal {
|
|
||||||
const callback = try page.arena.create(TimeoutCallback);
|
|
||||||
callback.* = .{
|
|
||||||
.signal = .init,
|
|
||||||
};
|
|
||||||
|
|
||||||
try page.scheduler.add(callback, TimeoutCallback.run, delay, .{ .name = "abort_signal" });
|
|
||||||
return &callback.signal;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_aborted(self: *const AbortSignal) bool {
|
|
||||||
return self.aborted;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn abort(self: *AbortSignal, reason_: ?[]const u8) !void {
|
|
||||||
self.aborted = true;
|
|
||||||
self.reason = reason_ orelse DEFAULT_REASON;
|
|
||||||
|
|
||||||
const abort_event = try parser.eventCreate();
|
|
||||||
parser.eventSetInternalType(abort_event, .abort_signal);
|
|
||||||
|
|
||||||
defer parser.eventDestroy(abort_event);
|
|
||||||
try parser.eventInit(abort_event, "abort", .{});
|
|
||||||
_ = try parser.eventTargetDispatchEvent(
|
|
||||||
parser.toEventTarget(AbortSignal, self),
|
|
||||||
abort_event,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Reason = union(enum) {
|
|
||||||
reason: []const u8,
|
|
||||||
undefined: void,
|
|
||||||
};
|
|
||||||
pub fn get_reason(self: *const AbortSignal) Reason {
|
|
||||||
if (self.reason) |r| {
|
|
||||||
return .{ .reason = r };
|
|
||||||
}
|
|
||||||
return .{ .undefined = {} };
|
|
||||||
}
|
|
||||||
|
|
||||||
const ThrowIfAborted = union(enum) {
|
|
||||||
exception: js.Exception,
|
|
||||||
undefined: void,
|
|
||||||
};
|
|
||||||
pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted {
|
|
||||||
if (self.aborted) {
|
|
||||||
const ex = page.js.throw(self.reason orelse DEFAULT_REASON);
|
|
||||||
return .{ .exception = ex };
|
|
||||||
}
|
|
||||||
return .{ .undefined = {} };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const TimeoutCallback = struct {
|
|
||||||
signal: AbortSignal,
|
|
||||||
|
|
||||||
fn run(ctx: *anyopaque) ?u32 {
|
|
||||||
const self: *TimeoutCallback = @ptrCast(@alignCast(ctx));
|
|
||||||
self.signal.abort("TimeoutError") catch |err| {
|
|
||||||
log.warn(.app, "abort signal timeout", .{ .err = err });
|
|
||||||
};
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: HTML.AbortController" {
|
|
||||||
try testing.htmlRunner("html/abort_controller.html");
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
const std = @import("std");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const DataSet = @This();
|
|
||||||
|
|
||||||
element: *parser.Element,
|
|
||||||
|
|
||||||
pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !js.UndefinedOr([]const u8) {
|
|
||||||
const normalized_name = try normalize(page.call_arena, name);
|
|
||||||
if (try parser.elementGetAttribute(self.element, normalized_name)) |value| {
|
|
||||||
return .{ .value = value };
|
|
||||||
}
|
|
||||||
return .undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn named_set(self: *DataSet, name: []const u8, value: []const u8, _: *bool, page: *Page) !void {
|
|
||||||
const normalized_name = try normalize(page.call_arena, name);
|
|
||||||
try parser.elementSetAttribute(self.element, normalized_name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn named_delete(self: *DataSet, name: []const u8, _: *bool, page: *Page) !void {
|
|
||||||
const normalized_name = try normalize(page.call_arena, name);
|
|
||||||
try parser.elementRemoveAttribute(self.element, normalized_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalize(allocator: Allocator, name: []const u8) ![]const u8 {
|
|
||||||
var upper_count: usize = 0;
|
|
||||||
for (name) |c| {
|
|
||||||
if (std.ascii.isUpper(c)) {
|
|
||||||
upper_count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// for every upper-case letter, we'll probably need a dash before it
|
|
||||||
// and we need the 'data-' prefix
|
|
||||||
var normalized = try allocator.alloc(u8, name.len + upper_count + 5);
|
|
||||||
|
|
||||||
@memcpy(normalized[0..5], "data-");
|
|
||||||
if (upper_count == 0) {
|
|
||||||
@memcpy(normalized[5..], name);
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pos: usize = 5;
|
|
||||||
for (name) |c| {
|
|
||||||
if (std.ascii.isUpper(c)) {
|
|
||||||
normalized[pos] = '-';
|
|
||||||
pos += 1;
|
|
||||||
normalized[pos] = c + 32;
|
|
||||||
} else {
|
|
||||||
normalized[pos] = c;
|
|
||||||
}
|
|
||||||
pos += 1;
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: HTML.DataSet" {
|
|
||||||
try testing.htmlRunner("html/dataset.html");
|
|
||||||
}
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
|
|
||||||
const History = @This();
|
|
||||||
|
|
||||||
const HistoryEntry = struct {
|
|
||||||
url: []const u8,
|
|
||||||
// This is serialized as JSON because
|
|
||||||
// History must survive a JsContext.
|
|
||||||
state: ?[]u8,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ScrollRestorationMode = enum {
|
|
||||||
auto,
|
|
||||||
manual,
|
|
||||||
|
|
||||||
pub fn fromString(str: []const u8) ?ScrollRestorationMode {
|
|
||||||
for (std.enums.values(ScrollRestorationMode)) |mode| {
|
|
||||||
if (std.ascii.eqlIgnoreCase(str, @tagName(mode))) {
|
|
||||||
return mode;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toString(self: ScrollRestorationMode) []const u8 {
|
|
||||||
return @tagName(self);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
scroll_restoration: ScrollRestorationMode = .auto,
|
|
||||||
stack: std.ArrayListUnmanaged(HistoryEntry) = .empty,
|
|
||||||
current: ?usize = null,
|
|
||||||
|
|
||||||
pub fn get_length(self: *History) u32 {
|
|
||||||
return @intCast(self.stack.items.len);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_scrollRestoration(self: *History) ScrollRestorationMode {
|
|
||||||
return self.scroll_restoration;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
|
|
||||||
self.scroll_restoration = ScrollRestorationMode.fromString(mode) orelse self.scroll_restoration;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_state(self: *History, page: *Page) !?js.Value {
|
|
||||||
if (self.current) |curr| {
|
|
||||||
const entry = self.stack.items[curr];
|
|
||||||
if (entry.state) |state| {
|
|
||||||
const value = try js.Value.fromJson(page.js, state);
|
|
||||||
return value;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pushNavigation(self: *History, _url: []const u8, page: *Page) !void {
|
|
||||||
const arena = page.session.arena;
|
|
||||||
const url = try arena.dupe(u8, _url);
|
|
||||||
|
|
||||||
const entry = HistoryEntry{ .state = null, .url = url };
|
|
||||||
try self.stack.append(arena, entry);
|
|
||||||
self.current = self.stack.items.len - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dispatchPopStateEvent(state: ?[]const u8, page: *Page) void {
|
|
||||||
log.debug(.script_event, "dispatch popstate event", .{
|
|
||||||
.type = "popstate",
|
|
||||||
.source = "history",
|
|
||||||
});
|
|
||||||
History._dispatchPopStateEvent(state, page) catch |err| {
|
|
||||||
log.err(.app, "dispatch popstate event error", .{
|
|
||||||
.err = err,
|
|
||||||
.type = "popstate",
|
|
||||||
.source = "history",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _dispatchPopStateEvent(state: ?[]const u8, page: *Page) !void {
|
|
||||||
var evt = try PopStateEvent.constructor("popstate", .{ .state = state });
|
|
||||||
|
|
||||||
_ = try parser.eventTargetDispatchEvent(
|
|
||||||
@as(*parser.EventTarget, @ptrCast(&page.window)),
|
|
||||||
&evt.proto,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _pushState(self: *History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
|
|
||||||
const arena = page.session.arena;
|
|
||||||
|
|
||||||
const json = try state.toJson(arena);
|
|
||||||
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
|
|
||||||
const entry = HistoryEntry{ .state = json, .url = url };
|
|
||||||
try self.stack.append(arena, entry);
|
|
||||||
self.current = self.stack.items.len - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _replaceState(self: *History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
|
|
||||||
const arena = page.session.arena;
|
|
||||||
|
|
||||||
if (self.current) |curr| {
|
|
||||||
const entry = &self.stack.items[curr];
|
|
||||||
const json = try state.toJson(arena);
|
|
||||||
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
|
|
||||||
entry.* = HistoryEntry{ .state = json, .url = url };
|
|
||||||
} else {
|
|
||||||
try self._pushState(state, "", _url, page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn go(self: *History, delta: i32, page: *Page) !void {
|
|
||||||
// 0 behaves the same as no argument, both reloading the page.
|
|
||||||
// If this is getting called, there SHOULD be an entry, atleast from pushNavigation.
|
|
||||||
const current = self.current.?;
|
|
||||||
|
|
||||||
const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta)));
|
|
||||||
if (index_s < 0 or index_s > self.stack.items.len - 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = @as(usize, @intCast(index_s));
|
|
||||||
const entry = self.stack.items[index];
|
|
||||||
self.current = index;
|
|
||||||
|
|
||||||
if (try page.isSameOrigin(entry.url)) {
|
|
||||||
History.dispatchPopStateEvent(entry.state, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
try page.navigateFromWebAPI(entry.url, .{ .reason = .history });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _go(self: *History, _delta: ?i32, page: *Page) !void {
|
|
||||||
try self.go(_delta orelse 0, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _back(self: *History, page: *Page) !void {
|
|
||||||
try self.go(-1, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _forward(self: *History, page: *Page) !void {
|
|
||||||
try self.go(1, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Event = @import("../events/event.zig").Event;
|
|
||||||
|
|
||||||
pub const PopStateEvent = struct {
|
|
||||||
pub const prototype = *Event;
|
|
||||||
pub const union_make_copy = true;
|
|
||||||
|
|
||||||
pub const EventInit = struct {
|
|
||||||
state: ?[]const u8 = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
proto: parser.Event,
|
|
||||||
state: ?[]const u8,
|
|
||||||
|
|
||||||
pub fn constructor(event_type: []const u8, opts: ?EventInit) !PopStateEvent {
|
|
||||||
const event = try parser.eventCreate();
|
|
||||||
defer parser.eventDestroy(event);
|
|
||||||
try parser.eventInit(event, event_type, .{});
|
|
||||||
parser.eventSetInternalType(event, .pop_state);
|
|
||||||
|
|
||||||
const o = opts orelse EventInit{};
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.proto = event.*,
|
|
||||||
.state = o.state,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// `hasUAVisualTransition` is not implemented. It isn't baseline so this is okay.
|
|
||||||
|
|
||||||
pub fn get_state(self: *const PopStateEvent, page: *Page) !?js.Value {
|
|
||||||
if (self.state) |state| {
|
|
||||||
const value = try js.Value.fromJson(page.js, state);
|
|
||||||
return value;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: HTML.History" {
|
|
||||||
try testing.htmlRunner("html/history.html");
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user